🎉

OIDCでのLINEログインをomniauthに乗っかる形で実装する[Rails]

2021/07/06に公開

業務にて、WebにLINEログイン機能を実装したので、その方法について書いておきます。

omniauthに乗っかる形でlib/以下にstrategyを自作することで対応しました。

理由としては、Gemとしてはこちらのomniauth-lineがありますが、
OpenID Connectを使用していないので、OpenID Connectに対応するためです。
https://github.com/kazasiki/omniauth-line

全体

https://developers.line.biz/ja/docs/line-login/verify-id-token/
LINE公式に全体の流れが書かれているのでこれに沿う形でstrategyを実装していきます。

自作のストラテジーをomniauthが使えるように設定。

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  require 'omniauth/strategies/line'
  provider :line, ENV['channel_id'], ENV['channel_secret']
end

今回はこのファイルを作成します。

# lib/omniauth/strategies/line.rb

require 'omniauth-oauth2'

module OmniAuth
  module Strategies
    class Line < OmniAuth::Strategies::OAuth2

      # IDトークンからemailを取得するために'email'が必要
      # IDトークンからプロフィール画像を取得するのに'profile'が必要
      option :scope, 'openid email profile'

      option :client_options, {
        site:          'https://api.line.me',
        authorize_url: 'https://access.line.me/oauth2/v2.1/authorize',
        token_url:     '/oauth2/v2.1/token'
      }

      uid do
        raw_info['userId']
      end

      info do
        {
          'user_id'     => raw_info['sub'],
          'email'       => raw_info['email'],
          'picture_url' => raw_info['picture'],
        }
      end

      def raw_info
        @raw_info ||= verify_id_token
      end

      private

      # nonceをリクエストパラメータに追加するためoverride
      def authorize_params
        super.tap do |params|
          params[:nonce] = SecureRandom.uuid
          session["omniauth.nonce"] = params[:nonce]
        end
      end

      # デフォルトだとクエリパラメータがついてエラーになるのでoverride
      def callback_url
        full_host + script_name + callback_path
      end

      def verify_id_token
        @id_token_payload ||= 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
        
        @id_token_payload
      end
    end
  end
end

処理の流れを追っていきます。

リクエストフェーズ

/auth/lineにPOSTしたあとに、リダイレクト先を決めているのはここです。
https://github.com/omniauth/omniauth-oauth2/blob/8438b89b712e03337932a0d95096258c892ea0f3/lib/omniauth/strategies/oauth2.rb#L58

# OmniAuth::Strategies::OAuth2

def request_phase
  redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end

ここでリクエストパラメーターにnonceをセットする必要があります。
そのためauthorize_paramsをoverrideします。

# lib/omniauth/strategies/line.rb

# nonceをリクエストパラメータに追加するためoverride
def authorize_params
  super.tap do |params|
    params[:nonce] = SecureRandom.uuid
    session["omniauth.nonce"] = params[:nonce]
  end
end

また、今回はline上でのuser_id, email, プロフィール画像を取得したいのでscopeパラメータは'openid email profile'とする必要があります。
https://developers.line.biz/ja/reference/line-login/#verify-id-token-response

# lib/omniauth/strategies/line.rb

# IDトークンからemailを取得するために'email'が必要
# IDトークンからプロフィール画像を取得するのに'profile'必要
option :scope, 'openid email profile'

コールバックフェーズ

LINE側で認証が完了して、
/auth/line/callbackに戻ってきたときの処理です。

def callback_urlをoverrideしないと以下のエラーが発生します。
OAuth2::Error (invalid_grant: redirect_uri does not match web_1 | {"error":"invalid_grant","error_description":"redirect_uri does not match"}):

/auth/line/callbackに戻ってきたときに、得られた認可コードからアクセストークンを取得しにいきます。
そのときにアクセストークン取得リクエストに付与するredirect_urlパラメーターはここで実装されていますが、
https://github.com/omniauth/omniauth-oauth2/blob/8438b89b712e03337932a0d95096258c892ea0f3/lib/omniauth/strategies/oauth2.rb#L124

# OmniAuth::Strategies::OAuth2
def build_access_token
  verifier = request.params["code"]
  client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
end
      
# Omniauth::Strategy
def callback_url
  full_host + callback_path + query_string
end

このままだとhttps://example.com/auth/line/callback?code=Fk19LszLSk7xFzAulJHn&state=af671fe52e40609f9127b8c691322f7c71ce180ee5624b02
となりcodeとstateパラメータが付いてしまい、LINE側に登録しているコールバックURLと異なる形になってしまうためにエラーが発生します。

以下のようにoverrideすることで

# lib/omniauth/strategies/line.rb

def callback_url
  full_host + script_name + callback_path
end

https://example.com/auth/line/callbackとなるのでLINE側に登録しているコールバックURLと一致し正常に処理をすすめることができます。

つづいて、IDトークン検証です。
def verify_id_tokenで、access_tokenからIDトークンを取得しIDトークンを検証してユーザー情報を取得します。

# lib/omniauth/strategies/line.rb

info do
  {
    'user_id'     => raw_info['userId'],
    'email'       => raw_info['email'],
    'picture_url' => raw_info['pictureUrl'],
  }
end

def raw_info
  @raw_info ||= verify_id_token
end

private

def verify_id_token
  @id_token_payload ||= 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
        
  @id_token_payload
end

コントローラー

# config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  get '/auth/line/callback', to: 'sessions#create'
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  # If you're using a strategy that POSTs during callback, you'll need to skip the authenticity token check for the callback action only. 
  skip_before_action :verify_authenticity_token, only: :create

  def create
    p "auth_hash", auth_hash
    p "info", auth_hash.info
  end

  private

  def auth_hash
    request.env['omniauth.auth']
  end
end

request.env['omniauth.auth']経由で、得られたユーザーID, email, プロフィール画像を取得できます。

ちなみに、認可の段階でエンドユーザーがemailの取得を許可しなかったり、そもそもLINEの登録はemailが無くても可能なのでemailが存在しないこともあります。
emailは必ずしも取得できるものではないということを念頭に置く必要があります。(アプリケーションでユーザーのemailを必須にしている設計にしている場合など)

Discussion