大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【無職に転生 ~ 就職するまで毎日ブログ出す⑤】【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の認証画面に遷移するまでの処理を解説します。

https://i.gyazo.com/ae4bafe3683bd7f43c0a8e0e730603f8.gif

まず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メソッドの中身は以下のようになっています。流れとしてはユーザーを探し、ユーザーがいればセッションなどをクリアしユーザーをログインさせる処理が実行されます。ユーザーがいなければreturnnilを返却するようにしています。

# 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ログインの日本語の実装記事は自分の記事ぐらいしかないかもしれませんので、誰かのお役に立てば幸いです。