【無職に転生 ~ 就職するまで毎日ブログ出す⑤】【Rails】OauthsControllerの解説
はじめに
こんにちは、大ちゃんの駆け出し技術ブログです。
タイトルにあるとおり【無職に転生 ~ 就職するまで毎日ブログ出す】というチャレンジをしています!!!大人気アニメのタイトルをまるパクリした毎日投稿チャレンジです。
RailsやらRubyやらSQLやらその他Webの知識やらが色々と抜け落ちているのを感じており、知識の定着のためにもアウトプットする機会を増やすためです。加えて、退職して文字通り無職に転生しましてプロニートになり、毎日時間に余裕ができたので引き締めるためにも毎日投稿を思い至りました!
【投稿内容】
- SQLの難しい処理 (副問合せ、JOINとか複雑な処理が書けない)
- Rails全般 (純粋に必要な知識が多すぎる、網羅的な理解が足りない)
- Rubyのあまり使わないメソッドや記述方法 (あまり重要ではないけど特に)
- Web知識全般 (クッキーやら、セッションやらなんとなくで理解しているものの自分の言葉で説明できない)
- 書籍 (スタートアップ企業に勤めるので、自分が会社に与える影響やパフォーマンスを高めるためビジネス書を読んでいきます。)
本記事でやること
昨日と一昨日でsorceryでslackログインを実装できたことを確認しましたが、実際にどのような流れでログインができているのか難しいと思います。そこでOauthsControllerコントローラーをデバックして処理を解説していきます。
# app/controllers/oauths_controller.rb class OauthsController < ApplicationController skip_before_action :require_login def oauth login_at(auth_params[:provider]) end def callback provider = auth_params[:provider] if (@user = login_from(provider)) redirect_to root_path, notice: "#{provider.titleize}でログインしました" else begin @user = create_from(provider) reset_session auto_login(@user) redirect_to root_path, notice: "#{provider.titleize}でログインしました" rescue StandardError redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました" end end end private def auth_params params.permit(:code, :provider) end end
ただ、やはりgemの処理は複雑で長い処理で構成されており、全てを開設するととんでもない文章量になるので所々は割愛します
slack認証画面までの処理
まずは「Login with Slack」ボタンを押してからSlackの認証画面に遷移するまでの処理を解説します。
まずoauth
メソッドですがこれは「Login with Slack」を押した際に実行されます。
def oauth login_at(auth_params[:provider]) end
privateメソッドであるauth_params
メソッドを引数にしてlogin_at
メソッドを実行します。params
の値は下記のようになっています。
#<ActionController::Parameters {"controller"=>"oauths", "action"=>"oauth", "provider"=>"slack"} permitted: false>
auth_params[:provider]
とproviderの値を取得しているので、login_at
メソッドの引数は"slack"になります。
auth_params[:provider] => "slack"
login_at
メソッドはgemの中で下記のように定義されています。外部認証するためにproviderのサイトにアクセスするメソッドです。
# vendor/bundle/ruby/2.6.0/gems/sorcery-0.16.1/lib/sorcery/controller/submodules/external.rb # sends user to authenticate at the provider's website. # after authentication the user is redirected to the callback defined in the provider config def login_at(provider_name, args = {}) redirect_to sorcery_login_url(provider_name, args) end
sorcery_login_url
メソッドは外部認証サイトへのURLを発行します。これにlogin_at
メソッドでredirect_to
メソッドを使うことで外部認証サイトへ遷移しているんですね。
# get the login URL from the provider, if applicable. Returns nil if the provider # does not provide a login URL. (as of v0.8.1 all providers provide a login URL) def sorcery_login_url(provider_name, args = {}) @provider = sorcery_get_provider provider_name sorcery_fixup_callback_url @provider return nil unless @provider.respond_to?(:login_url) && @provider.has_callback? @provider.state = args[:state] @provider.login_url(params, session) end
Slack認証後の処理
そしてSlackの認証画面で「許可する」を押した後の処理はcallback
メソッドです。
def callback provider = auth_params[:provider] if (@user = login_from(provider)) redirect_to root_path, notice: "#{provider.titleize}でログインしました" else begin @user = create_from(provider) reset_session auto_login(@user) redirect_to root_path, notice: "#{provider.titleize}でログインしました" rescue StandardError redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました" end end end
先ほどと同様に"slack"の値を取得します。
provider = auth_params[:provider]
login_from(provider)
で既にユーザーがSlackログイン済みであれば@userに格納します。
if (@user = login_from(provider))
login_from
メソッドの中身は以下のようになっています。流れとしてはユーザーを探し、ユーザーがいればセッションなどをクリアしユーザーをログインさせる処理が実行されます。ユーザーがいなければreturn
でnil
を返却するようにしています。
# tries to login the user from provider's callback def login_from(provider_name, should_remember = false) sorcery_fetch_user_hash provider_name return unless (user = user_class.load_from_provider(provider_name, @user_hash[:uid].to_s)) # we found the user. # clear the session return_to_url = session[:return_to_url] reset_sorcery_session session[:return_to_url] = return_to_url # sign in the user auto_login(user, should_remember) after_login!(user) # return the user user end
sorcery_fetch_user_hash
メソッドではどのような処理が行われているのでしょうか?この処理の中ではユーザーの情報をハッシュ値で取得する処理が書かれています。
# get the user hash from a provider using information from the params and session. def sorcery_fetch_user_hash(provider_name) # the application should never ask for user hashes from two different providers # on the same request. But if they do, we should be ready: on the second request, # clear out the instance variables if the provider is different provider = sorcery_get_provider provider_name if @provider.nil? || @provider != provider @provider = provider @access_token = nil @user_hash = nil end # delegate to the provider for the access token and the user hash. # cache them in instance variables. @access_token ||= @provider.process_callback(params, session) # sends request to oauth agent to get the token @user_hash ||= @provider.get_user_hash(@access_token) # uses the token to send another request to the oauth agent requesting user info nil end
重要な処理は下記の二つの処理です。
@access_token ||= @provider.process_callback(params, session) # sends request to oauth agent to get the token @user_hash ||= @provider.get_user_hash(@access_token) # uses the token to send another request to the oauth agent requesting user info
@provider
は実は現在下記の値になっておりSorcery::Providers::Slack
クラスのインスタンスです。
@provider => #<Sorcery::Providers::Slack:0x00007f944da01c78 @auth_path="/oauth/authorize", @callback_url="https://94e9-111-239-159-144.ngrok.io/oauth/callback?provider=slack", @key="xxxxxxxxxx.xxxxxxxxxxx", @original_callback_url="https://94e9-111-239-159-144.ngrok.io/oauth/callback?provider=slack", @scope="identity.basic, identity.email", @secret="xxxxxxxxxxxxxxxxxxxxxxxx", @site="https://slack.com/", @state=nil, @token_url="/api/oauth.access", @user_info_mapping={:email=>"email"}, @user_info_path="https://slack.com/api/users.identity">
よって@provider.process_callback
はそのクラス内の処理となっているのでslack.rb
に定義されている下記のメソッドが実行されます。アクセストークンを取得しているわけですね。
# tries to login the user from access token def process_callback(params, _session) args = {}.tap do |a| a[:code] = params[:code] if params[:code] end get_access_token(args, token_url: token_url, token_method: :post) end
アクセストークンを取得したらいよいよユーザーの情報を取得するメソッドが実行されます。
@user_hash ||= @provider.get_user_hash(@access_token)
get_user_hash
メソッドは下記の処理です。
def get_user_hash(access_token) response = access_token.get(user_info_path) auth_hash(access_token).tap do |h| h[:user_info] = JSON.parse(response.body) h[:user_info]['email'] = h[:user_info]['user']['email'] h[:uid] = h[:user_info]['user']['id'] end end
user_info_path
の値は下記のURLです。
'https://slack.com/api/users.identity'
これはSlackのAPIメソッドのユーザー情報を取得する時のURLになります。
https://api.slack.com/methods/users.identity
これによりユーザー情報のハッシュ値が返却されます。この情報を使用してログインを試みます。
@user_hash => {:token=>"xxxxx-xxxxxxx-xxxxxxxxx-xxxxxxxxx", :refresh_token=>nil, :expires_at=>nil, :expires_in=>nil, :user_info=> {"ok"=>true, "user"=>{"name"=>"kurukuruskt28", "id"=>"U02GCQ5VARK", "email"=>"kurukuruskt28@gmail.com"}, "team"=>{"id"=>"T02GUCGA5CZ"}, "email"=>"kurukuruskt28@gmail.com"}, :uid=>"U02GCQ5VARK"}
login_from
メソッドでユーザー情報が見つからなければcreate_from
メソッドでユーザーを作成します。処理としてはlogin_from
メソッド同様にユーザーのハッシュ値を取得し、user_class.create_from_provider
でユーザーを作成します。
def create_from(provider_name, &block) sorcery_fetch_user_hash provider_name # config = user_class.sorcery_config # TODO: Unused, remove? attrs = user_attrs(@provider.user_info_mapping, @user_hash) @user = user_class.create_from_provider(provider_name, @user_hash[:uid], attrs, &block) end
そしてセッションをクリアしてログインします。
reset_session
auto_login(@user)
deviseかsorceryか
自分が作成したポートフォリオではdeviseとomniauthを使ってslackログインを実装しました。
gem 'devise', github: 'heartcombo/devise', branch: 'ca-omniauth-2' gem 'ginjo-omniauth-slack', require:'omniauth-slack'
実装したsorceryとはどう使いわければいいのでしょうか?
使い分けとしてはAPI処理を使った複雑な処理がしたいのであればdeviseを、slackログインだけ行いAPI処理を使う必要がない場合はsorceryでいいと思います。
今回sorceryを実装して気づいたことはアクセストークンがgemの処理の中でしか使えないことです。
@access_token ||= @provider.process_callback(params, session)
このアクセストークンを使ってAPIメソッドを実行するのですが、sorcery内部でアクセストークンが使われているため、gemをカスタマイズしない限り使用できるAPIメソッドはusers.identity
のみです。
ginjo-omniauth-slack
のgemではアクセストークンを独自で実装したコントローラー内で使うことができます。下記の実装では自分が作成したコントローラーですが、request.env['omniauth.strategy'].access_token
でアクセストークンを取得し、そこから様々なAPIメソッドを使ってSlackから情報を取得するわけです。
module Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :authenticate_user!, only: %i[slack failure] def slack @user = User.from_omniauth(auth, bot_token) if @user.persisted? sign_in_and_redirect @user, event: :authentication else redirect_to root_path end end def failure flash[:alert] = 'Slack認証に失敗しました。' redirect_to root_path end private def auth request.env['omniauth.auth'] end def bot_token request.env['omniauth.strategy'].access_token end end end
よって、以下のように使い分けることができます。
- Slackを介したログイン処理のみが必要な場合 ⇒ sorcery
- SlackのAPIメソッドを利用したい場合 ⇒ devise + omniauth
終わりに
Slackログインの日本語の実装記事は自分の記事ぐらいしかないかもしれませんので、誰かのお役に立てば幸いです。