u1f419

コード読んだめも

omniauth(と,少しomniauth-oauth2) を読む

前にも一回読んだけどあんまり理解できなかった.最近RackとかOAuthについてわかりを得てきたのでもう一度読んでみようと思った.気づいたら rails server command を読んでいたりして(rails s を読む)少し遠回り気味だったけど,OAuth2.0の認証フローに合わせてomniauthの挙動をたどってみた.

Rackに関してはRackとは何か - Qiitaが,OAuthについては色々な OAuth のフローと doorkeeper gem での実装 - Qiitaの認可コードの項がとても参考になった.

Client Application で /app/:provider にアクセスがあると,omniauth が Rack middleware としてこのリクエストを拾い,認可フローが開始される.

middleware の使用は config/initializers/omniauth.rb で宣言する.

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
end

Rails.application.config.middleware.use の引数に OmniAuth::Builder を指定することで,Blockの中身を Rack::Builder ではなく OmniAuth::BuilderDSL として処理してる(多分).

このブロック中の provider は, Rack::Builder をラップしている OmniAuth::Builder で提供されてる.

#provider

def provider(klass, *args, &block)
  # ...
  begin
    middleware = OmniAuth::Strategies.const_get(OmniAuth::Utils.camelize(klass.to_s).to_s)
  rescue NameError
    raise(LoadError.new("Could not find matching strategy for #{klass.inspect}. You may need to install an additional gem (such as omniauth-#{klass})."))
  end
  args.last.is_a?(Hash) ? args.push(options.merge(args.pop)) : args.push(options)
   use middleware, *args, &block
end

例えば

provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']

みたいに書くと, twitter strategy を探しにいって,あれば use してmiddleware の stack につんでくれる.

omniauth では, omniauth-twitter など,omniauth にのせる OAuth 用の middleware を strategy とよぶ.strategyを作る時は, OmniAuth::Strategies namespace 以下に定義することになっていて,これによって OmniAuth::Strategies.const_get で持ってこれるようになってる.

omniauth には 開発時の placeholder としてシンプルな developer strategy が用意されている.

strategy の本体 OminAuth::Strategyomniauth/omniauth.rbで autoload されてる.Rack middleware なのでおもむろに call を見に行く.Omniauth::Strategy#call!

def call!(env) # rubocop:disable CyclomaticComplexity, PerceivedComplexity
  # ...

  @env = env
  @env['omniauth.strategy'] = self if on_auth_path?

  return mock_call!(env) if OmniAuth.config.test_mode
  return options_call if on_auth_path? && options_request?
  return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)
  return callback_call if on_callback_path?
  return other_phase if respond_to?(:other_phase)
  @app.call(env)
end

色々やってるけどOAuthの本筋部分をたどると,

return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?

細かい条件は省くけど /auth/twitter みたいなrequestが来た時, request_call が呼ばれて Rack::Response Object が返される.これが認可サーバの 「 がアクセスを求めています,許可しますか?」みたいなビュー.ここでリソースオーナーが認証(ログイン)して認可すると,認可グラントとともにリソースオーナーを Client Application に返して,この認可グラントと Client Application の client id と client secret を使って認可サーバーに access token を要求するはずだけど,その部分が見られない.おもむろに omniauth/omniauth-oauth2を見てみると,callback_phase (後述)がオーバーライドされてた.omniauth-oauth2/oauth2.rb

def callback_phase # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity
  error = request.params["error_reason"] || request.params["error"]
  if error
    #... 
  else
    self.access_token = build_access_token
    self.access_token = access_token.refresh! if access_token.expired?
    super
  end
  # ...
end

build_access_token で認可サーバにアクセストークンを要求してる

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

callback_phaseは, callback path に返って来た時に callback_call がよばれて,その中で呼ばれてる.

def callback_call
  # ...
  callback_phase
end

super で呼ばれてる OmniAuth::Strategy#callback_paseここ

def callback_phase
  env['omniauth.auth'] = auth_hash
  call_app!
end

env['omniauth.auth'] に認可サーバからの情報をいれて Client Application を call して auth/twitter/callback みたいなところに戻ってくる.

Client Applocation では, SessionController とかで

class SessionsController < ApplicationController
  def create
    @user = User.find_or_create_from_auth_hash(auth_hash)
    self.current_user = @user
    redirect_to '/'
  end

  protected

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

みたいにして,access_token とか access_token_secret をもった User record を find_or_create する.

これで, 取得した tokenを使ってリソースサーバにリクエストが送れるようになる 🎉