Chapter 05

LIFFアプリの認証処理の実装

arahabica
arahabica
2022.01.14に更新

5.1 devise_token_authをLINEのアクセストークンに対応させる

devise_token_authのコントローラをオーバーライドする形で対応していきましょう。
devise_token_authのオーバーライドする方法の詳細はこちらを参照してください。

まずは、下記のようにconfig/routes.rb を編集します。

config/routes.rb
Rails.application.routes.draw do
  root to: 'static#root'
  scope '/api' do
    scope format: 'json' do
-     mount_devise_token_auth_for 'User', at: 'auth'
+     mount_devise_token_auth_for 'User', at: 'auth', controllers: {
+       registrations: 'line_token_auth/registrations',
+       sessions: 'line_token_auth/sessions'
+     }
      resources :stamps, only: [:index, :show]
      resources :imprints, only: [:create]
      delete 'imprints', to: 'imprints#clear'
      get '/hello', to: 'hello#index'
    end
  end
end

registrationssessionsではオーバーライドしたコントローラで処理するように修正しています。

5.1.1 Deviseの設定の修正

config/initializers/devise.rbを編集してDeviseの設定を修正します。
デフォルトでは認証キーがemailに設定されているので、これをuidに修正します。

-   #config.authentication_keys = [:email]
+   config.authentication_keys = [:uid]

5.1.2 ApplicationControllerの修正

application_controller.rbを下記のように書き換えましょう。
Devise関連の処理でパラメータとしてアクセストークンを受け入れるように修正しています。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  before_action :authenticate_user!, unless: :devise_controller?
  before_action :configure_permitted_parameters, if: :devise_controller?
  skip_before_action :verify_authenticity_token # skip CSRF check if API

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:uid, :access_token])
    devise_parameter_sanitizer.permit(:sign_in, keys: [:uid, :access_token])
  end
end

5.1.3 LineTokenAuth::RegistrationsControllerの作成

LineTokenAuth::RegistrationsController(認証用コントローラ)を作成していきます。

まずは下記コマンドでコントローラを作成してください。

$ docker-compose exec web rails g controller LineTokenAuth::registrations
$ docker-compose exec web chown -R "$(id -u $(whoami)):$(id -g $(whoami))" .

次に、内容を下記のように入力してください。

app/controllers/line_token_auth/registrations_controller.rb
module LineTokenAuth
  # DeviseTokenAuthのコントローラを継承している
  class RegistrationsController < DeviseTokenAuth::RegistrationsController
    include LineTokenAuth::Concerns::LineAuthenticator

    def create
      build_resource

      unless @resource.present?
        raise DeviseTokenAuth::Errors::NoResourceDefinedError,
              "#{self.class.name} #build_resource does not define @resource,"\
              ' execution stopped.'
      end

      # if whitelist is set, validate redirect_url against whitelist
      return render_create_error_redirect_url_not_allowed if blacklisted_redirect_url?(@redirect_url)

      auth_result = authenticate(@resource[:uid], sign_up_params[:access_token])
      if auth_result[:error]
        return render_error(auth_result[:error][:code], auth_result[:error][:message])
      end
      @resource.name = auth_result[:profile][:name]
      @resource.image = auth_result[:profile][:image]
      if @resource.save
        yield @resource if block_given?

        if active_for_authentication?
          # email auth has been bypassed, authenticate user
          @token = @resource.create_token
          @resource.save!
          update_auth_header
        end

          render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end
    end
    protected

    def build_resource
      @resource            = resource_class.new(uid: sign_up_params[:uid])
      @resource.provider   = provider
    end

    private

    def provider
      'line'
    end
  end
end

DeviseTokenAuthのコントローラを継承して、create, build, provider メソッドをオーバーライドしています。

それぞれのメソッドを簡単にみていきます。

LineTokenAuth::RegistrationsController#create

create メソッドが長いですが、DeviseTokenAuthのコントローラとの差分は下記となっており、追加したのは5行だけになります。

create
    def create
      build_resource

      unless @resource.present?
        raise DeviseTokenAuth::Errors::NoResourceDefinedError,
              "#{self.class.name} #build_resource does not define @resource,"\
              ' execution stopped.'
      end
-
-     # give redirect value from params priority
-     @redirect_url = params.fetch(
-       :confirm_success_url,
-       DeviseTokenAuth.default_confirm_success_url
-     )
-
-     # success redirect url is required
-     if confirmable_enabled? && !@redirect_url
-       return render_create_error_missing_confirm_success_url
-     end

       # if whitelist is set, validate redirect_url against whitelist
       return render_create_error_redirect_url_not_allowed if blacklisted_redirect_url?(@redirect_url)


+      auth_result = authenticate(@resource[:uid], sign_up_params[:access_token])
+      if auth_result[:error]
+        return render_error(auth_result[:error][:code], auth_result[:error][:message])
-      # override email confirmation, must be sent manually from ctrl
-      callback_name = defined?(ActiveRecord) && resource_class < ActiveRecord::Base ? :commit : :create
-      resource_class.set_callback(callback_name, :after, :send_on_create_confirmation_instructions)
-      resource_class.skip_callback(callback_name, :after, :send_on_create_confirmation_instructions)
-
-      if @resource.respond_to? :skip_confirmation_notification!
-        # Fix duplicate e-mails by disabling Devise confirmation e-mail
-        @resource.skip_confirmation_notification!
       end
+      @resource.name = auth_result[:profile][:name]
+      @resource.image = auth_result[:profile][:image]
-
-       if @resource.save
-         yield @resource if block_given?
- 
-         unless @resource.confirmed?
-         # user will require email authentication
-         @resource.send_confirmation_instructions({
-           client_config: params[:config_name],
-           redirect_url: @redirect_url
-         })
-       end

        if active_for_authentication?
          # ここでトークンを生成している
          @token = @resource.create_token
          update_auth_header
        end

        render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end
    end

authenticate メソッドでIDとトークンを渡し、認証の結果が返ってきています。
authenticate メソッドは冒頭のLineTokenAuth::Concerns::LineAuthenticatorの中で定義されています。

また、@resource.create_tokenの部分でトークンを生成していることが確認できます。

LineTokenAuth::RegistrationsController#build_resource

build_resourceはUserモデルを生成しています。
もともとはemailベースなので関連するものを削除し、uidのみを使用するように修正しています。

    def build_resource
+      @resource            = resource_class.new(uid: sign_up_params[:uid])
-      @resource            = resource_class.new(sign_up_params)
      @resource.provider   = provider
-
-      # honor devise configuration for case_insensitive_keys
-      if resource_class.case_insensitive_keys.include?(:email)
-        @resource.email = sign_up_params[:email].try(:downcase)
-      else
-        @resource.email = sign_up_params[:email]
-      end
    end

LineTokenAuth::RegistrationsController#provider

デフォルトはemailなのでproviderをlineに修正しています。

def provider
  'line'
end

5.1.4 LineTokenAuth::Concerns::LineAuthenticatorの作成

先程のcreateメソッドでも使用したLineTokenAuth::Concerns::LineAuthenticatorを作成していきます。
このConcernに具体的なLINEの認証処理を書いています。

まずは下記コマンドでConcernファイルを作成してください。

$ docker-compose exec web mkdir app/controllers/line_token_auth/concerns
$ docker-compose exec web touch app/controllers/line_token_auth/concerns/line_authenticator.rb
$ docker-compose exec web chown -R "$(id -u $(whoami)):$(id -g $(whoami))" .

内容は下記のようになります。

app/controllers/line_token_auth/concerns/line_authenticator.rb
require 'net/http'
require 'uri'

module LineTokenAuth::Concerns::LineAuthenticator
  extend ActiveSupport::Concern

  protected

  def authenticate(uid, access_token)
    verify_result = verify_line_token(access_token)
    if verify_result[:code] != 200
      return fail_authenticate(verify_result[:code], verify_result[:body]["error_description"])
    end
    if verify_result[:body]["client_id"] != line_channel_id
      return fail_authenticate(401, 'LINE Channel ID is not matched.')
    end
    if verify_result[:body]["expires_in"] <= 0
      return fail_authenticate(401, 'LINE access token is expired')
    end
    profile_result = get_profile_by_line_token(access_token)
    if profile_result[:code] != 200
      return fail_authenticate(profile_result[:code], profile_result[:body][:error_description])
    end
    if profile_result[:body]["userId"] != uid
      return fail_authenticate(401, 'uid is not matched.')
    end
    success_authenticate({
      uid: uid,
      name: profile_result[:body]["displayName"],
      image: profile_result[:body]["pictureUrl"]
    })
  end
  private
  def line_channel_id
    @line_channel_id ||=  ENV["LINE_LOGIN_CHANNEL_ID"]
  end
  def verify_line_token(access_token)
    uri = URI.parse("https://api.line.me/oauth2/v2.1/verify")
    uri.query = URI.encode_www_form(access_token: access_token)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req = Net::HTTP::Get.new uri.request_uri
    res = http.request req
    {
      code: res.code.to_i,
      body: JSON.parse(res.body)
    }
  end
  def get_profile_by_line_token(access_token)
    uri = URI.parse("https://api.line.me/v2/profile")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req = Net::HTTP::Get.new uri.request_uri
    req[:Authorization] = "Bearer #{access_token}"
    res = http.request req
    {
      code: res.code.to_i,
      body: JSON.parse(res.body)
    }
  end
  def fail_authenticate(code, message)
    { error: { code: code, message: message }, profile: nil }
  end
  def success_authenticate(profile)
    { error: nil, profile: profile }
  end
end

authenticateメソッドでLINEログインAPIのアクセストークンの有効性を検証するAPIとユーザープロフィールを取得するAPIを使っていることが確認できると思います。

また、環境変数LINE_LOGIN_CHANNEL_IDを使用しているので自分のLINEチャネルのチャネルIDを設定するする必要があります。

5.1.5 LineTokenAuth::SessionsControllerの作成

LineTokenAuth::SessionsController(セッションコントローラ)を作成していきます。

まずは下記コマンドでコントローラを作成してください。

$ docker-compose exec web rails g controller LineTokenAuth::sessions
$ docker-compose exec web chown -R "$(id -u $(whoami)):$(id -g $(whoami))" .

次に、内容を下記のように入力してください。

app/controllers/line_token_auth/sessions_controller.rb
module LineTokenAuth
  class SessionsController < DeviseTokenAuth::SessionsController
    include LineTokenAuth::Concerns::LineAuthenticator

    def create
      # Check
      field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
      @resource = nil
      if field
        q_value = get_case_insensitive_field_from_resource_params(field)
        @resource = find_resource(field, q_value)
      end
      
      if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        auth_result = authenticate(@resource[field], resource_params[:access_token])
        if auth_result[:error]
          return render_error(auth_result[:error][:code], auth_result[:error][:message])
        end
        
        @token = @resource.create_token
        @resource.save
      
        sign_in(:user, @resource, store: false, bypass: false)
      
        yield @resource if block_given?
      
        render_create_success
      elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        if @resource.respond_to?(:locked_at) && @resource.locked_at
          render_create_error_account_locked
        else
          render_create_error_not_confirmed
        end
      else
        render_create_error_bad_credentials
      end
    end
    def valid_params?(key, val)
      resource_params[:access_token] && key && val
    end
    def provider
      'line'
    end
  end
end

LineTokenAuth::SessionsController#create

createメソッドの差分は下記です。

    def create
      # Check
      field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
      @resource = nil
      if field
        q_value = get_case_insensitive_field_from_resource_params(field)
        @resource = find_resource(field, q_value)
      end

      if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
+       auth_result = authenticate(@resource[field], + resource_params[:access_token])
+       if auth_result[:error]
+         return render_error(auth_result[:error][:code], auth_result[:error][:message])
-       valid_password = @resource.valid_password?(resource_params[:password])
-       if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
-         return render_create_error_bad_credentials
        end
-
-       create_and_assign_token
+       @token = @resource.create_token
+       @resource.save

        sign_in(:user, @resource, store: false, bypass: false)

        yield @resource if block_given?

        render_create_success
      elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        if @resource.respond_to?(:locked_at) && @resource.locked_at
          render_create_error_account_locked
        else
          render_create_error_not_confirmed
        end
      else
        render_create_error_bad_credentials
      end
    end

RegistrationsControllerと同様、Email認証の部分をconcernsで定義したLINE認証の処理に差し替えていることがわかると思います。

LineTokenAuth::SessionsController#valid_params?

差分は下記です。

    def valid_params?(key, val)
+      resource_params[:access_token] && key && val
-      resource_params[:password] && key && val
    end

パラメータチェックでアクセストークンを必須にしています。

LineTokenAuth::SessionsController#provider

RegistrationsControllerと同様です。
デフォルトはemailなのでproviderをlineに修正しています。

def provider
  'line'
end

5.2 Next Step

お疲れ様でした🎉
ちょっとややこしかったですが、これでLIFFの認証基盤ができました。

メインの処理は完了ですが、フロントエンドの繋ぎ込みなどが残っているので次章で片付けていきましょう。