authorization code grantに沿ったDoorkeeperのコードリーディング

さまざまな都合により、OAuth 2のプロバイダになるためのDoorkeeperというgemのコードを読むことがしばしばある生活を送っている。

似た名前のモジュールやクラスが多く、読むたびに混乱しているので、authorization code grantでアクセストークンを取得するときの登場するクラス/モジュールと流れをあらためて自分なりに整理した。基本的に自分用であって、網羅的ではない。

前提

2020-11-28現在での最新版であるDoorkeeper 5.5.0.rc1を読む。authorization code grantが正常に通るときのパスだけを見る。

RailsのAPIモードは無効とし、Doorkeeperの設定resource_owner_authenticatorで渡すブロックでは特定のリソースオーナーの認証に常に成功しているとする。本来は認証を実際に実行し、失敗すれば再認証させるべき。

以降の文章では、Doorkeeperが提供する名前空間のうちDoorkeeperDと省略する。

Doorkeeper用エンドポイントの登録

DoorkeeperはRails Engineであり、ルーティングを拡張するためのuse_doorkeeperというメソッドが提供されている。このメソッドでルーティングを拡張するまでの流れは次のとおり。

主に登場するクラス/モジュール

名前 概要
D::Engine Rails EngineとしてRailtieのinitializerを設定する
D::Rails::AbstractRouter Doorkeeper用ルーティング拡張クラスのためのインタフェースを表す
D::Rails::Routes 親アプリのルーティングにDoorkeeper用のエンドポイントを追加するメソッドを持つ

ルーティングの設定フロー

  • D::Engine"doorkeeper.routes"としてinitializerを登録する
    • 親アプリの初期化時にD::Rails::Routes.install!を実行する
      • ActionDispatch::Routing::Mapperuse_doorkeeperをincludeすることでルーティングの設定でuse_doorkeeperできるようにする
  • Doorkeeper利用時に親アプリのconfig/routes.rbでuse_doorkeeperする
    • D::Rails::Routes#generate_routes!を実行する
    • Railsのscopeを呼び出して、その中で D::Rails::AbstractRouter#map_routeによってDoorkeeperのエンドポイントを定義する
      • D::Rails::Routesのprivateメソッドで個々のルーティングが定義されており、それらのメソッドをsendで呼び出している

authorization code grantの認可リクエスト

authorization code grantでは、あるクライアントとして認可リクエストを送り認可コードを得る必要がある。

主に登場するクラス/モジュール

D::RequestD::OAuthそれぞれの配下に似たような名前のモジュールやクラスがあって混乱する。

コントローラ関連

名前 概要
D::AuthorizationsController /oauth/authorizeへのリクエストがルーティングされるコントローラ
D::Helpers::Controller Doorkeeperの設定をもとにした値などを取得するためのメソッドが集められたモジュール

認可リクエスト

名前 概要
D::OAuth::PreAuthorization 認可リクエストのパラメータのラッパークラス。バリデーションを実行したりscope文字列をパースする
D::Validations D::OAuth::PreAuthorizationD::OAuth::BaseRequestでのバリデーションの仕組みを提供するモジュール
D::Models::AccessTokenMixin アクセストークンに関するロジックを提供するモジュール。ORマッパーへの依存を減らすために、アクセストークンのモデルからは切り離されている
D::OAuth::Hooks::Context 認可前後のフック関数に渡すコンテキストを表すクラス
D::Server 認可サーバとして必要なリクエスト、パラメータ、現在のリソースオーナーやクライアントへアクセスするためのメソッドを提供するクラス。コントローラをコンテキストとして渡して使う
D::Request response_typeを渡して、対応する認可/トークンリクエストを処理するストラテジクラスを返すためのメソッドを提供するクラス
D::Request::Strategy リクエストをもとに認可するストラテジクラスの基底クラス。#authorizeというメソッドを提供する
D::Request::Code D::Request::Strategyを継承するauthorization code grantのストラテジ。#requestではD::OAuth::CodeRequestはインスタンスを返す。D::Request::Strategy#authorizeを呼ぶと、そのインスタンスに#authorizeを委譲する
D::OAuth::CodeRequest 認可コードをD::OAuth::Authorization::Codeのインスタンスとして生成して、認可エンドポイントのレスポンスを作成する
D::OAuth::Authorization::Code 認可コードのラッパークラス。認可コードを発行しグラントを記録するテーブルへ保存する#issue_token!を提供する
D::OAuth::CodeResponse 認可エンドポイントのレスポンスのラッパークラス。コールバックまたはネイティブアプリ向けの方法で認可コードをクライアントに渡すために必要なデータを提供する
D::GrantFlow D::GrantFlow::RegistryにOAuthのグラントの種類とDoorkeeperのストラテジークラスの対応を登録するモジュール
D::GrantFlow::Flow D::GrantFlowで登録する対応を表すクラス

承諾画面の表示

GET /oauth/authorizeを呼び出すときの流れ。

まず、リソースオーナーのデータを取得する。

  • D::AuthorizationsController#newへルーティングする
  • before_action :authenticate_resource_owner!を実行する
  • D::Helpers::Controller#current_resource_ownerを実行する
  • 親アプリのconfig/initializers/doorkeeper.rbなどでD.configureで設定する authenticate_resource_ownerのブロックを呼び出し、返り値を@current_resource_ownerへ入れる

次に、認可エンドポイントへのリクエストを検証する。

  • D::AuthorizationsController#new#pre_authを呼び出す
    • Doorkeeperの設定、認可リクエストのパラメータ、@current_resource_ownerをもとにD::OAuth::PreAuthorizationのインスタンスを作る
  • pre_auth#authorizable?を実行する
    • D::Validations#validateを実行する
      • あらかじめD::OAuth::PreAuthorizationの序盤で定義しているvalidate :client_id, ...などはこのモジュールのメソッドであり、バリデーションを登録している
      • 登録されたバリデーションを順番に実行する
        • バリデーションメソッドはvalidate_#{属性名}sendする
        • これらもD::OAuth::PreAuthorizationにあらかじめ定義してある
        • それぞれのバリデーションはOAuth 2に基づいたもの

リクエストが妥当であれば、クライアントへ承諾画面を返す。

  • authorizable?であればrender_successを実行する
    • D::Helpers::Controller#skip_authorizationを実行する
      • 認可スコープの承諾画面を表示するか否かを決める
      • Doorkeeperの設定のskip_authorizationのブロックを実行する
    • #matching_tokenを実行する
      • D::Models::AccessTokenMixin#matching_token_forですでに対象のクライアントとリソースオーナーの組み合わせで作成済みのアクセストークンを探す
    • #skip_authorization#matching_tokenのどちらかがtrueなら、すぐにauthorize_responseで作成する認可済みのレスポンスを返す
    • そうでなければ:newをレンダリングする
      • app/views/doorkeeper/authorizations/new.html.erbをレンダリングする
      • リクエストしているスコープを表示し、承諾もしくは拒否を求める画面

認可コードの発行

承諾画面で承諾をサブミットし、POST /oauth/authorizeを呼び出すときの流れ。

認可コードを生成する。

  • D::AuthorizationsController#createにルーティングする
  • #authorization_responseを呼び出す
    • pre_authをもとにD::OAuth::Hooks::Contextのインスタンスを作る
    • フックbefore_successful_authorizationを実行する
    • #strategyを呼び出す
      • #serverを呼び出す
        • D::Helpers::Controller#serverを呼び出す
        • コントローラ自身をcontextとして渡してインスタンスを作る
      • Server#authorization_requestを呼び出す
        • 引数に pre_auth.response_typeを渡す。いまは"code"
        • D::Request.authorization_strategyから"code"に対応する認可strategyクラスを取得する
          • D::GrantFlowで各グラントのstrategyクラスなどは設定済み
          • Doorkeeperの設定にあるgrantから対応するものをD::GrantFlow::Flowとして取り出す
          • request_type_strategyを呼び出してD::Request::Codeを返す
          • D::Request::Codeにserverを渡してストラテジーオブジェクトを作る
      • strategyとしてD::Request::Codeのオブジェクトが得られた
    • strategyであるD::Request::Code#authorizeを実行する
      • D::Request::Strategy#authorize#requestD::OAuth::CodeRequest#authorizeと委譲される
      • pre_authresource_ownerを引数に取ってD::OAuth::CodeRequestのインスタンスを作る
      • D::OAuth::CodeRequest#authorizeD::OAuth::Authorization::Codeのインスタンスを作り#issue_token!を呼び出す
        • 認可コードを生成して、既定のActive Recordモデルを通じてテーブルに保存する
      • D::OAuth::CodeResponseのインスタンスをpre_authD::OAuth::Authorization::Codeのインスタンスを渡して作り、returnする

認可コードをコールバックURIに付与するかネイティブアプリ用画面のURIのパラメータとして返す。

  • 認可コードを返すためにredirect_or_render authorization_responseする
    • D::OAuth::CodeResponseのインスタンスであるauthorization_responseredirectable?なら、そのURIへリダイレクトする
    • 認可コードのときは常にtrueなので、oobであればoob用のURIに、それ以外は設定済みのURIに、認可コードをパラメータとして付けてリダイレクトする
      • oobのときD::OAuth::Authorization::Code#oob_redirectをもとにリダイレクトし、app/views/doorkeeper/authorizations/show.html.erbをレンダリングする

トークンエンドポイント

主に登場するクラス/モジュール

認可エンドポイントで登場したものは省略。

名前 概要
D::TokensController /oauth/token へのリクエストがルーティングされるコントローラ
D::Request::AuthorizationCode D::Request::Strategyを継承するauthorization code grantのストラテジ。#authorizeを提供する。Strategyでの#authorize#requestへの委譲時にD::OAuth::AuthorizationCodeRequestを生成する。そのときに#grantの呼び出しを通じて認可コードの検証を実行する
D::OAuth::BaseRequest トークンエンドポイントへのリクエストの基底クラス。#authorizeでトークンレスポンスの生成と前後のフックの実行を提供する
D::OAuth::AuthorizationCodeRequest authorization code grantでのトークンエンドポイントへのリクエストを表すクラス。PKCEのchallengeの検証も担う。フックD::BaseRequest#before_successful_responseをオーバーライドしてアクセストークンを作成している
D::Models::AccessGrantMixin アクセスグラントに関するロジックを提供するモジュール。ORマッパーへの依存を減らすために、アクセスグラントのモデルからは切り離されている
D::OAuth::TokenResponse トークンエンドポイントのレスポンスのラッパークラス。ステータスコードやレスポンスのJSONを取得できる

アクセストークン取得の流れ

POST /oauth/tokenを呼び出して、アクセストークンを含むJSONをレスポンスとして得る。

  • D::TokensController#createにルーティングする
  • #authorize_responseを呼び出す
    • 認可エンドポイントと同じ流れでserverを取得しD::Server#token_requestを呼び出す
      • 引数としてparams[:grant_type]を渡すが、ここでは"authorization_code"
      • D::Request.token_strategyであらかじめ登録済みのstrategyからgrant_type_strategyとしてD::Request::AuthorizationCodeを取得してreturnする
      • そのクラスのインスタンスを得る
    • D::Request::AuthorizationCode#authorizeを呼び出す
      • D::Request::Strategy#authorize#requestD::OAuth::AuthorizationCodeRequest#authorizeD::OAuth::BaseRequest#authorizeと委譲される
      • D::Request::AuthorizationCode#requestgrant呼び出し時に認可コードをもとにoauth_access_grantsのレコードを探している
        • D::Models::AccessGrantMixins.by_tokenで実行している
      • D::OAuth::BaseRequestD::OAuth::ValidationsをincludeしているのでD::OAuth::PreAuthorizationと同じく宣言済みのバリデーションをvalid?で実行できる
      • valid?ならトークンレスポンスを返す
        • D::OAuth::AuthorizationCodeRequest#before_successful_responseで認可コードをrevokeしてアクセストークンを生成する
        • D::OAuth::TokenResponseにアクセストークンを渡してインスタンスを作る
      • トークンレスポンスのオブジェクトをreturnする
  • D::TokensController#createでトークンレスポンスから#body#statusでトークンレスポンスのJSONのステータスを取得してrenderする

所感

DoorkeeperはOAuthの各グラントに対応し、またORマッパー非依存になるような設計で作られていて、さまざまな要件のもとでOAuth 2サーバを作りたいという希望にかなうライブラリとなっている。そのぶん、やっていることが複雑であったりもするし、細かいカスタマイズを施したくなる場面もたびたびある。また、認可という場合によってはクリティカルな機能に関わるライブラリでもある。そういう点で、ただのブラックボックスとして扱うよりは、できるだけ内部を知っておいたほうがいいだろうと思う。どのライブラリにも言えることではあるが、アプリケーション開発の延長として、ライブラリの新バージョンリリース時などのタイミングでこまめにコードを読むことを継続していく。