Ruby on RailsにLINE ログイン/配信 機能を実装する手順をまとめました。
LINEログインをRailsアプリに組み込む方法として、少し前の記事だとomniauth-linegemを使っているものが多いですが、GitHubを見る限りしばらくメンテナンスされていないgemなので、カスタムでOmniAuth strategyを作る方法を取りました。
Ruby, gemのバージョン:
- Ruby: 3.4.3
- rails gem: 8.0.2
- devise gem: 4.9.4
- omniauth gem: 2.1.3 (deviseでrequire)
- omniauth-oauth2 gem: 1.8.0
実装手順(LINEログイン機能 ↓, LINE配信機能 ↓)の説明をする前に、Webアプリケーションから外部Webサービス(今回だとLINE)での認可・認証について、整理します。
認証・認可の基本情報
(OAuth, OIDC, access token, ID token, request phase, callback phase, OmniAuth, strategy, provider)
OAuth
- Webサービスにおいて、リソースへのアクセス許可を安全に委譲する認可の仕組み。
- OAuth 2.0ではクライアントアプリ(今回だとRailsアプリ)が、認可サーバ(今回だとLINE)に対してaccess tokenを要求し、認可サーバはユーザーの許可を得てクライアントアプリにaccess tokenを発行する。
- ちなみに... OAuth 1.0は認証フローが複雑&対応アプリに制限あり&すべてのAPIリクエストで署名必須だった。OAuth 2.0は、OAuth 1.0の問題点を解決し、より柔軟で使いやすい認証・認可フレームワークを提供している。
- 参考:
OIDC (OpenID Connect)
- 異なるWebサービス間における認証の仕組み。OAuth 2.0の拡張仕様。
- クライアントアプリがOpenIDプロバイダー(今回だとLINE)にID tokenを要求し、ユーザーの認証&許可取得後に、OpenIDプロバイダーがクライアントアプリにID tokenを発行する。
- access token v.s. ID token
-
access token:OAuth 2.0で定義された、リソースへのアクセスを認可するためのトークン
- LINEの場合:「ユーザーの許可もらってるから、このWebアプリに対してこのLINEユーザのIDとメールアドレスを渡せるよ」
-
ID token:OIDCで定義された、ユーザが認証されたことを証明するトークン
- LINEの場合:「このユーザーはLINEログイン成功済み。このユーザーのIDとメールアドレス情報をtokenに練りこんであるよ」
- LINEの場合の参考:LINEログイン v2.1 APIリファレンス
-
access token:OAuth 2.0で定義された、リソースへのアクセスを認可するためのトークン
LINEログインのフロー(OAuth 2.0 + OIDC) 🔗 フロー図
- ユーザーが「LINEログイン」ボタン押下
(= GETusers/auth/line→<OmniAuth strategy>#request_phase) - WebアプリがLINE認可サーバにアクセスし、LINEログイン画面にリダイレクト
- ユーザーがログイン画面で同意(=認証&認可)すると、LINE認可サーバはWebアプリに認可コードを発行
(= redirect tousers/auth/line/callback) - WebアプリはLINE認可サーバに
access tokenを発行要求(受け取った認可コードを添付) - LINE認可サーバは認可コードを検証 →
access tokenを発行(ID token,scopeを添付。scopeは2でWebアプリから認可サーバに送ったパラメータのひとつでアクセス権限を定義) - WebアプリはLINE認可サーバにユーザー情報を要求(
ID tokenを添付) - LINE認可サーバは
ID tokenを検証 → LINE ID, LINEユーザー名などをWebアプリに返す(=scopeで定義した情報) - Webアプリにて、返されたユーザー情報を元にユーザー新規作成やログイン処理を行う
(1~2: request phase, 3~7: callback phase)
OmniAuthにおけるstrategy, provider
- OmniAuth: "multi-provider Authentication"。多様な認証フローを標準化したライブラリ。
- provider: どの認証フロー(= strategy)を使うかの設定 ← by Rackミドルウェアにstrategy登録
-
strategy: Railsアプリ本体(routes, controllers, views, models etc.)と認可サーバとの間の窓口係として、request / callback phaseにて二者の間に立って処理をする。
- (request phase 例) 上記フローの1~2にて、LINEログインボタン押下時に、line strategyがリクエストのパラメータなどを取りまとめてLINE認可サーバへ投げる。
- (callback phase 例) 上記フローの3にて、LINE認可サーバから発行された認可コードは、まずline strategyが受け取り、access token & ID token のやりとりを経て取得したLINEユーザ情報をセットし、controller(
omniauth_callbacks#line)へ処理が引き継がれる
- 参考:OmniAuth GitHub
Railsアプリと他Webサービスの間での認証・認可に使われる仕組みや必要な機構を把握できたところで、LINEログイン機能の実装をしていきます!
LINEログイン機能実装の手順
-
LINEログインチャネルの作成 🔗 公式doc
- チャネル(=WebアプリとLINEプラットフォームを接続する通信路)をLINE Developersコンソールにて作成
- チャネルにて、メールアドレスの取得権限を申請
(LINEのメールアドレス情報を使いたい場合のみ)
-
gemのインストール
Gemfile# devise gemはインストール済み gem 'omniauth-oauth2' gem 'omniauth-rails_csrf_protection' gem 'dotenv-rails' # 環境変数の管理 (LINE_CHANNEL_ID, SECRET etc.)
OmniAuth関連gemの役割-
omniauth: request phase, callback phaseなど認証・認可の骨組み。deviseがomniauthをロードしているので今回インストール不要 -
oauth2: OAuth 2.0の基本処理(リクエスト生成・アクセストークン取得・認可フロー etc.)の実装をサポート。omniauth-oauth2で読み込まれている。 -
omniauth-oauth2: OmniAuthのproviderとして使えるOAuth 2.0 Strategyのベースを提供 -
omniauth-rails_csrf_protection: OmniAuthのセキュリティ強化
-
-
line strategyを作成
line strategyのコード
lib/strategies/line.rbrequire 'omniauth-oauth2' module Strategies class Line < OmniAuth::Strategies::OAuth2 # request phase ----------------------------------------------------- # IDトークン, プロフィール情報, メールアドレスの取得権限を含める option :scope, 'openid profile email' # optionを渡す先 option :client_options, { site: 'https://api.line.me', authorize_url: 'https://access.line.me/oauth2/v2.1/authorize', token_url: 'https://api.line.me/oauth2/v2.1/token' } # callback phase --------------------------------------------------- # 取得したデータ(LINEユーザーID)からuid(=unique to the provider)をセット uid { raw_info['sub'] } # 取得したデータからinfo(= a hash of information about the user)をセット info do { name: raw_info['name'], email: raw_info['email'] } end def raw_info @raw_info ||= verify_id_token end private # ID Tokenに必須のnonceをパラメータに追加 def authorize_params super.tap do |params| params[:nonce] = SecureRandom.uuid session['omniauth.nonce'] = params[:nonce] end end # omniauthのcallback_urlはquery stringがついてしまい、LINE側に登録したcallback URLとの不一致エラーになるそうなのでoverride # 参考: https://zenn.dev/hid3/articles/40ab3d1060f013#%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA # callback_url: https://github.com/omniauth/omniauth/blob/0bcfd5b25bf946422cd4d9c40c4f514121ac04d6/lib/omniauth/strategy.rb#L498 def callback_url full_host + callback_path end # ID token 検証 & ユーザ情報取得のAPIリクエスト def verify_id_token @id_token_payload ||= begin client.request(:post, 'https://api.line.me/oauth2/v2.1/verify', { body: { id_token: access_token['id_token'], client_id: options.client_id, nonce: session.delete('omniauth.nonce') } } ).parsed rescue => e Rails.error.report(e, context: { action: '[LINE login] ID token verification & get user info', client_id: options.client_id, has_id_token: access_token['id_token'].present? }) raise end @id_token_payload end end end
-
line strategyをdeviseのRackミドルウェアとして組み込む
- 環境変数の設定
dev環境では.envにLINE_CHANNEL_ID,LINE_CHANNEL_SECRETを追加 - DBにOmniAuthで必要なカラムを追加
usersテーブルにproviderカラム(string),uidカラム(string)を追加 - deviseのinitializer & user model にline strategyを登録
# devise.rb (OmniAuthミドルウェアとしてline strategyを登録) require 'strategies/line' ... config.omniauth :line, ENV['LINE_CHANNEL_ID'], ENV['LINE_CHANNEL_SECRET'] # user.rb (DeviseにLINEログインを組み込む宣言) devise :omniauthable, omniauth_providers: [:line] # -> user_line_omniauth_authorized_path, user_line_omniauth_callback_pathが自動生成 validates :uid, uniqueness: { scope: :provider}, if: -> { provider.present? }
- 環境変数の設定
-
ルーティング & callbacks controller の作成
routes.rbdevise_for :users, controllers: { # /users/auth/line/callback -> users/omniauth_callbacks#line omniauth_callbacks: 'users/omniauth_callbacks' }callbacks controllerのコード(LINEサーバからユーザ情報取得後の挙動)
controllers/users/omniauth_callbacks_controller.rbmodule Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :verify_authenticity_token, only: :line def line @user = User.from_omniauth(request.env['omniauth.auth'], current_user) notify_line_already_linked and return if current_user && @user.nil? if @user.persisted? complete_line_login else fail_line_login end end private def notify_line_already_linked redirect_to user_setting_path set_flash_message(:alert, :failure, kind: 'LINE', reason: '他アカウントでLINE連携済みです') end def complete_line_login sign_in_and_redirect @user, event: :authentication set_flash_message(:notice, :success, kind: 'LINE') end def fail_line_login session['devise.line_data'] = request.env['omniauth.auth'].except(:extra) redirect_to new_user_registration_url set_flash_message(:alert, :failure, kind: 'LINE', reason: 'LINE連携に失敗しました') end end endUser.from_omniauthのコード(LINEユーザ情報からLINE連携/ログイン/ユーザー作成)
models/user.rbdef self.from_omniauth(auth, current_user = nil) return link_line_account(auth, current_user) if current_user&.line_connected? == false sign_in_or_create_user_from_line(auth) end def self.link_line_account(auth, current_user) success = current_user.update( provider: auth.provider, uid: auth.uid, email: auth.info.email, line_notify: true ) success ? current_user : nil end def line_connected? uid.present? && provider.present? end def self.sign_in_or_create_user_from_line(auth) # LINE連携済みのuserのusername, passwordは更新されない find_or_create_by( provider: auth.provider, uid: auth.uid, email: auth.info.email ) do |user| user.username = auth.info.name user.password = Devise.friendly_token[0, 20] user.line_notify = true end endLINEコンソール > LINEログインチャネルにて、callback URLの設定も必要
(callback URL = 上記のLINEログインのフローの3にて、ユーザの認証&認可後に認可コードを受け取るWebアプリのURL<domain name>/users/auth/line/callback)
dev環境ではngrokを使って開発中アプリを公開しているので、ngrokから発行されたドメイン名を含んだcallback URLを登録する。
-
LINEログイン機能のfeature specを作成
LINEログイン feature specのコード
spec/feature/line_login_spec.rbrequire 'rails_helper' RSpec.describe 'LINEログイン機能', type: :feature do let(:line_uid) { '1234567890' } let(:line_email) { 'line_user@example.com' } let(:line_name) { 'line_user' } before do # /auth/line -> /auth/line/callback への即時リダイレクト設定 OmniAuth.config.test_mode = true # /auth/line/callback へのリダイレクト時に渡されるデータ OmniAuth.config.mock_auth[:line] = OmniAuth::AuthHash.new({ provider: 'line', uid: line_uid, info: { name: line_name, email: line_email }, credentials: { token: '1234qwerty' } }) Rails.application.env_config['devise.mapping'] = Devise.mappings[:user] Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:line] end after do OmniAuth.config.mock_auth[:line] = nil end context '既存ユーザーがLINE未連携でログイン中の場合' do let!(:user) { create(:user, provider: nil, uid: nil) } before do login_as user visit user_setting_path click_button 'LINEと連携する' end it 'LINE連携時に、LINEに登録されたemailに更新され、LINE配信も許可に設定される' do user.reload expect(user.provider).to eq('line') expect(user.uid).to eq(line_uid) expect(user.email).to eq(line_email) expect(user.line_notify).to be(true) end end context '未サインアップでLINEログインにてアカウント作成する場合' do before do visit signup_path click_button 'LINEでログイン' end let(:created_user) { User.last } it 'LINE情報でアカウントが作成され、LINE配信が許可される' do expect(created_user.uid).to eq(line_uid) expect(created_user.provider).to eq('line') expect(created_user.email).to eq(line_email) expect(created_user.username).to eq(line_name) expect(created_user.line_notify).to be(true) end end context 'LINE連携済みのユーザーがLINEログインする場合' do let!(:user) do create(:user, provider: 'line', uid: line_uid, email: line_email, username: 'test_user', password: 'password') end before do visit login_path click_button 'LINEでログイン' end it 'LINEログイン時にusername, passwordはLINEのユーザ情報で上書きされない' do user.reload expect(user.username).to eq('test_user') expect(user.valid_password?('password')).to be(true) end end context 'すでに他ユーザーでLINE連携済みのLINEアカウントに対してLINE連携を試みた場合' do let(:user) { create(:user, provider: nil, uid: nil) } before do create(:user, provider: 'line', uid: line_uid, email: line_email) login_as user visit user_setting_path click_button 'LINEと連携する' end it 'LINE連携に失敗する' do expect(current_path).to eq(user_setting_path) expect(page).to have_content('他アカウントでLINE連携済みです') end end end
⇨ LINEログイン機能の実装完了
LINE配信機能実装の手順
-
MessagingAPIチャネルを作成
LINEコンソールにてLINEログイン用チャネルと同じプロバイダ内に、MessaginAPI用のチャネルを作成
-
gemのインストール
Gemfilegem 'line-bot-api'
-
LINE配信のジョブを作成
-
環境変数を設定
MessaginAPI用チャネルのアクセストークンLINE_BOT_CHANNEL_ACCESS_TOKENとWebアプリのホスト名であるAPP_HOSTを.envに保存
※ Webアプリのホスト名は、画像を配信する場合のURL生成に必要なため追加(Rails.application.routes.default_url_options[:host]) -
usersテーブルに
line_notifyカラムを追加(LINE配信許可の設定値)
(加えて、ユーザー設定画面にLINE配信許可の設定欄を追加し、コントローラでもparamsにline_notify追加) -
LINE配信のジョブを作成
push_line_jobのコード
app/jobs/push_line_job.rbrequire 'line/bot' class PushLineJob < ApplicationJob queue_as :default def perform(*_args) users = User.where.not(uid: nil).where(line_notify: true).includes(:objectives) users.each do |user| # LINE配信許可がONのユーザーに対して、登録されたコンテンツの中からランダムに選択してメッセージ配信 objective = user.objectives.sample next if objective.blank? message = build_message(objective) request = Line::Bot::V2::MessagingApi::PushMessageRequest.new(to: user.uid, messages: [message]) begin client.push_message(push_message_request: request) rescue StandardError => e # エラー通知処理 end end end private def build_message(objective) # メッセージオブジェクトを生成 # 画像メッセージ -> Line::Bot::V2::MessagingApi::ImageMessage.new(...) # テキストメッセージ -> Line::Bot::V2::MessagingApi::TextMessage.new(...) end def client Line::Bot::V2::MessagingApi::ApiClient.new( channel_access_token: ENV.fetch('LINE_BOT_CHANNEL_ACCESS_TOKEN', nil) ) end end
-
-
SolidQueueを導入
-
SolidQueue関連テーブルを作成
bin/rails solid_queue:installで生成されるqueue_schema.rbを元にDB更新 -
SolidQueueの設定
-
config/environments/*.rbにてconfig.active_job.queue_adapter = :solid_queue - アプリのデータとSolidQueueのデータを同一のDBに相乗りさせたいので、
config/environments/*.rbのconfig.solid_queue.connects_to削除
-
-
SolidQueueの起動を設定
開発環境ではpumaで起動する設定
config/puma.rbplugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] || Rails.env.development?- 本番環境(AWS)ではコンテナ化して常時起動
- Task Definitionにて、Railsアプリのコンテナと同一のTaskにSolid Queue用のコンテナを追加
- 本番環境(AWS)ではコンテナ化して常時起動
-
LINE配信ジョブの定期実行を設定
定期実行設定 YAMLのコード
config/recurring.yml# 毎日19:45にLINE配信ジョブ実行(開発環境) development: push_line_job: class: PushLineJob args: [] schedule: 45 19 * * * Asia/Tokyo # 毎日08:00にLINE配信ジョブ実行(本番環境) production: push_line_job: class: PushLineJob args: [] schedule: 0 8 * * * Asia/Tokyo
-
-
LINE配信機能のfeature specを作成
LINE配信 feature specのコード
spec/feature/push_line_job_spec.rbrequire 'rails_helper' RSpec.describe PushLineJob, type: :job do let(:user_without_line) { create(:user, provider: nil, uid: nil, line_notify: false) } let(:user_with_line_notify_on) { create(:user, provider: 'line', uid: '1234567890', line_notify: true) } let(:user_with_line_notify_off) { create(:user, provider: 'line', uid: '1234567891', line_notify: false) } let(:mock_client) { instance_double(Line::Bot::V2::MessagingApi::ApiClient) } before do allow(Line::Bot::V2::MessagingApi::ApiClient).to receive(:new).and_return(mock_client) allow(mock_client).to receive(:push_message).and_return(true) end context 'LINE未連携のユーザの場合' do let!(:user) { user_without_line } before do create(:objective, :image, user:) create(:objective, :verbal, user:) end it 'ビジョンボードの内容は配信されない' do described_class.perform_now expect(mock_client).not_to have_received(:push_message) end end context 'LINE連携済みだが通知許可がOFFの場合' do let!(:user) { user_with_line_notify_off } before do create(:objective, :image, user:) create(:objective, :verbal, user:) end it 'ビジョンボードの内容は配信されない' do described_class.perform_now expect(mock_client).not_to have_received(:push_message) end end context 'LINE連携済みで通知許可がONの場合' do let!(:user) { user_with_line_notify_on } before do create(:objective, :image, user:) create(:objective, :verbal, user:) end it 'ビジョンボードの内容が1件だけ配信される' do described_class.perform_now expect(mock_client).to have_received(:push_message).with( push_message_request: have_attributes( to: user.uid, messages: satisfy do |messages| messages.all? do |m| m.is_a?(Line::Bot::V2::MessagingApi::TextMessage) || m.is_a?(Line::Bot::V2::MessagingApi::ImageMessage) end end ) ) end end end
⇨ LINE配信機能の実装完了