5.1 devise_token_authをLINEのアクセストークンに対応させる
devise_token_authのコントローラをオーバーライドする形で対応していきましょう。
devise_token_authのオーバーライドする方法の詳細はこちらを参照してください。
まずは、下記のように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
registrations
とsessions
ではオーバーライドしたコントローラで処理するように修正しています。
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関連の処理でパラメータとしてアクセストークンを受け入れるように修正しています。
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))" .
次に、内容を下記のように入力してください。
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行だけになります。
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))" .
内容は下記のようになります。
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))" .
次に、内容を下記のように入力してください。
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の認証基盤ができました。
メインの処理は完了ですが、フロントエンドの繋ぎ込みなどが残っているので次章で片付けていきましょう。