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が提供する名前空間のうちDoorkeeperはDと省略する。
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::Mapperにuse_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::RequestとD::OAuthそれぞれの配下に似たような名前のモジュールやクラスがあって混乱する。
コントローラ関連
| 名前 | 概要 |
|---|---|
D::AuthorizationsController | /oauth/authorizeへのリクエストがルーティングされるコントローラ |
D::Helpers::Controller | Doorkeeperの設定をもとにした値などを取得するためのメソッドが集められたモジュール |
認可リクエスト
| 名前 | 概要 |
|---|---|
D::OAuth::PreAuthorization | 認可リクエストのパラメータのラッパークラス。バリデーションを実行したりscope文字列をパースする |
D::Validations | D::OAuth::PreAuthorizationや D::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のインスタンスを作る
- Doorkeeperの設定、認可リクエストのパラメータ、
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→#request→D::OAuth::CodeRequest#authorizeと委譲されるpre_authとresource_ownerを引数に取ってD::OAuth::CodeRequestのインスタンスを作るD::OAuth::CodeRequest#authorizeでD::OAuth::Authorization::Codeのインスタンスを作り#issue_token!を呼び出す- 認可コードを生成して、既定のActive Recordモデルを通じてテーブルに保存する
D::OAuth::CodeResponseのインスタンスをpre_authとD::OAuth::Authorization::Codeのインスタンスを渡して作り、returnする
認可コードをコールバックURIに付与するかネイティブアプリ用画面のURIのパラメータとして返す。
- 認可コードを返すために
redirect_or_render authorization_responseするD::OAuth::CodeResponseのインスタンスであるauthorization_responseがredirectable?なら、そのURIへリダイレクトする- 認可コードのときは常にtrueなので、oobであればoob用のURIに、それ以外は設定済みのURIに、認可コードをパラメータとして付けてリダイレクトする
- oobのとき
D::OAuth::Authorization::Code#oob_redirectをもとにリダイレクトし、app/views/doorkeeper/authorizations/show.html.erbをレンダリングする
- oobのとき
トークンエンドポイント
主に登場するクラス/モジュール
認可エンドポイントで登場したものは省略。
| 名前 | 概要 |
|---|---|
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→#request→D::OAuth::AuthorizationCodeRequest#authorize→D::OAuth::BaseRequest#authorizeと委譲されるD::Request::AuthorizationCode#requestでgrant呼び出し時に認可コードをもとにoauth_access_grantsのレコードを探しているD::Models::AccessGrantMixins.by_tokenで実行している
D::OAuth::BaseRequestはD::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サーバを作りたいという希望にかなうライブラリとなっている。そのぶん、やっていることが複雑であったりもするし、細かいカスタマイズを施したくなる場面もたびたびある。また、認可という場合によってはクリティカルな機能に関わるライブラリでもある。そういう点で、ただのブラックボックスとして扱うよりは、できるだけ内部を知っておいたほうがいいだろうと思う。どのライブラリにも言えることではあるが、アプリケーション開発の延長として、ライブラリの新バージョンリリース時などのタイミングでこまめにコードを読むことを継続していく。