deviseを利用してRailsアプリにLINE連携ログインを導入する

# 初めに
初学者です。

個人開発でRailsアプリにLINE連携ログインを導入した際の記録です。

アプリでLINEBOTと合わせてLINE連携ログインを利用したかったので導入しました。
deviseを使用して実装しています。

この記事で行なっている作業はこちらの記事で構築した環境で実施しています。
https://qiita.com/tkhero555/items/a1811369c59021077d62

アプリ「Puri.log」はこちら(2024/5/3時点でMVPリリース状態)
https://purilog-2a85943c9f18.herokuapp.com/


## 技術構成とバージョン
| カテゴリ | 技術 |
|---|---|
| フロントエンド | javascript/Hotwire/bootstrap　|
| バックエンド | ruby3.3.0/rails7.1.3.2 |
| データベース | PostgreSQL16.2 |
| 認証 | devise |
| 環境構築 | Docker / docker-compose |
| CI/CD | Github Actions |
| インフラ　| heroku |


## deviseの導入と初期設定
gem 'devise'を導入する。
```
gem 'devise'
```
バンドルインストールした後、deviseを実行して必要なファイルを作る。
```
docker compose run web bundle install
docker compose run web rails g devise:install
docker compose run web rails g devise User
```

生成されるマイグレーションファイルを確認してnameカラムを追加する。
```
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :name
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end
```
db:migrateする。

## LINEとdeviseを連携する
LINE developersでLINEログインの新しいチャネルを作成しておく。

gem 'omniauth-line'を導入する。
```
gem 'omniauth-line'
gem 'omniauth-rails_csrf_protection'
```
バンドルインストールとビルドする。

.envファイルにLINEログイン用の環境変数を追加する。
```.env
# LineBotで使用したチャンネルシークレットとトークン
LINE_CHANNEL_SECRET="LINEBOT用のチャネルのsecret"
LINE_CHANNEL_TOKEN="LINEBOT用のチャネルのトークン"

#　今回設定するチャンネルIDとシークレット
LINE_LOGIN_KEY='LINEログイン用チャネルのChannel ID'
LINE_LOGIN_SECRET='LINEログイン用チャネルのChannel Secret'
```

config/initializers/devise.rbで接続するLINEチャネルのIDとシークレットをENVを利用して設定する。
```config/initializers/devise.rb
config.omniauth :line, ENV['LINE_LOGIN_KEY'], ENV['LINE_LOGIN_SECRET']
```
User.rbにomniauthの記述を追加する。
```user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :omniauthable, omniauth_providers: %i[line] # この1行を追加
end
```
omniauth_collbacks_controllerを作成する。
```
docker compose run web rails g controller omniauth_callbacks
```
```omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  
  def line
    basic_action
  end

  private
  
  def basic_action
	  # 認証情報を取得
    @omniauth = request.env["omniauth.auth"]
    # 認証情報が存在している場合
    if @omniauth.present?
	    # 認証情報のproviderとuidを元に、ユーザーを発見もしくは初期化する。
      @profile = User.find_or_initialize_by(provider: @omniauth["provider"], uid: @omniauth["uid"])
      # 保存したユーザー情報のemailが空白なら
      if @profile.email.blank?
	      # 三項演算子　認証情報の[info][email]が存在する場合その値を代入　そうでない場合認証情報の中身を使って、一意のダミーemailアドレスを作っている
        email = @omniauth["info"]["email"] ? @omniauth["info"]["email"] : "#{@omniauth["uid"]}-#{@omniauth["provider"]}@example.com"
        # current_userが存在するならそのまま代入、しないならその場で認証情報から新しいユーザーを作成している。
        @profile = current_user || User.create!(provider: @omniauth["provider"], uid: @omniauth["uid"], email: email, name: @omniauth["info"]["name"], password: Devise.friendly_token[0, 20])
      end
      # set_values(@omniauth)はuserモデルで定義されたメソッド
      @profile.set_values(@omniauth)
      # ログイン動作を実施する
      sign_in(:user, @profile)
    end
    #ログイン後のflash messageとリダイレクト先を設定
    flash[:notice] = "ログインしました"
    # ここのリダイレクト先は後で自分のルーティングに応じて変更する
    redirect_to expendable_items_path
  end

	# ダミーのemailアドレスを作成するメソッド
  def fake_email(uid, provider)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end
```
ルーティングを設定する
```
devise_for :users, controllers: {
  omniauth_callbacks: "omniauth_callbacks"
}
```
UserモデルにLINEログイン用のカラムを追加する。
```
docker compose run web rails g migration add_column_to_users
```
```
class ChangeColumnToUser < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
  end
end
```
Userモデルにメソッドを追加する。
```user.rb
# social_profilesの中身を順番に取り出して、指定したproviderと一致する最初のsocial_profileを返す
	def social_profile(provider)
    social_profiles.select { |sp| sp.provider == provider.to_s }.first
  end
	
	# omniauthから受け取った情報で、インスタンスの属性を設定する。
  def set_values(omniauth)
	  # omniauthのproviderとuidのどちらかが既存の値と異なる場合、この時点でreturnしてメソッドを終了する。
    return if provider.to_s != omniauth["provider"].to_s || uid != omniauth["uid"]
    # omniauthのcredentialsを取り出す
    credentials = omniauth["credentials"]
    # omniauthのinfoを取り出す
    info = omniauth["info"]
		
		# さっき取り出したcredentialsからrefresh_tokenを取り出す
    access_token = credentials["refresh_token"]
    # さっき取り出したcredentialsからsecretを取り出す
    access_secret = credentials["secret"]
    # 最後にcredentialsをjson化しておく
    credentials = credentials.to_json
    # さっき取り出したinfoからnameを取り出す
    name = info["name"]
  end
	
	# 引数で受け取ったraw_infoをjson化してインスタンスのraw_infoに格納する
  def set_values_by_raw_info(raw_info)
    self.raw_info = raw_info.to_json
    self.save!
  end
```

## LINE developersでコールバックのurlを設定する
ngrokを起動して外部からローカルサーバーにアクセスできるURLを確保

ngrokの導入と設定はこちらの記事で実施済み
https://qiita.com/tkhero555/items/4af4c7cd48a757974685

```
ngrok http 3000
```
ルーティングを確認して、ngrokのurl + omniauth_callbacks_controllerのlineアクションのルーティングをLINEログインのwebhookに設定する。

上手く行っていればdeviseのloginページにSign in with Lineのボタンができており、クリックするとLINE連携ログイン画面へ遷移するようになっている。
![スクリーンショット 2024-04-08 20.56.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3768250/dc82a428-ea3a-a394-85de-49ba51e4fa66.png)


## 参考記事等
https://qiita.com/prg_mt/items/b3238ebfae1a3df67cab
https://zenn.dev/yoiyoicho/articles/974c73ac75c100