コードは一度しか使えない、を支える検証の多さ
前回((認可のOAuthに認証を足す薄いレイヤー)でOpenID Connectの3つのフローを俯瞰したとき、「Authorization Code Flowが一番安全」とひとことで片付けてしまった。今日はその「安全」の中身を、仕様書の3.1節を読み込んで具体的に確認した。
8つのステップに分解する
仕様書の3.1.1では、Authorization Code Flowが8段階に分解されている。
- ClientがAuthentication Requestのパラメータを準備する
- ClientがAuthorization Serverへリクエストを送る
- Authorization ServerがEnd-Userを認証する
- Authorization ServerがEnd-Userの同意・認可を取得する
- Authorization ServerがAuthorization Codeを添えてEnd-UserをClientへ戻す
- ClientがToken EndpointへAuthorization Codeを送る
- ClientがID TokenとAccess Tokenを含むレスポンスを受け取る
- ClientがID Tokenを検証し、End-UserのSubject Identifierを取り出す
前回「トークンがブラウザを経由しない」とまとめた部分は、このうちステップ5と6の間の話だった。ステップ5でブラウザに渡るのはAuthorization Codeという使い捨ての引換券だけで、実際のID Token・Access Tokenはステップ6でClientサーバーがToken Endpointに直接アクセスして受け取る。ブラウザには「引換券」しか流れない、という構造を数字で追えたのが今日の一つ目の収穫だった。
Authorization Requestの必須・推奨・任意
3.1.2.1では、リクエストパラメータが3段階の強さで定義されている。
- 必須(REQUIRED):
scope(openidを含む必要あり)、response_type(Authorization Code Flowではcode)、client_id、redirect_uri - 推奨(RECOMMENDED):
state - 任意(OPTIONAL):
nonce、display、prompt、max_age、login_hintなど
stateとnonceは両方「ランダムな値」という点で似ているが、役割がはっきり分かれていることが仕様書の文言から分かった。
state:CSRF対策(Cross-Site Request Forgery、罠サイト経由で偽のリクエストを紛れ込ませる攻撃への対策)。「リクエストとコールバックの間で維持される」値で、戻ってきたレスポンスが自分が送ったリクエストに対応するものかを確認するnonce:リプレイアタック対策(同じリクエストを横取りして使い回すなりすましへの対策)。ClientセッションとID Tokenを紐づける値で、Authorization Requestで送った値とID Token内のnonceClaimが一致するかを検証する
同じ「ランダムな値を送って後で照合する」という仕組みでも、狙いは違う。stateは「この応答は自分宛てか」を、nonceは「このID Tokenは今回のリクエストに対して新しく発行されたものか」を確認していて、守っている対象が別だ。ここを混同していたことに気づけたのは大きい。
サーバー側は何を検証しているか(3.1.2.2〜3.1.2.4)
Authorization Server側の処理を読むと、「認証すればいい」わけではないことが分かる。
scopeにopenidが含まれているかを見て、OpenID Connectのリクエストかそうでない普通のOAuth 2.0リクエストかをまず判定する- 未認証なら認証するが、
prompt=noneが指定されていれば対話してはならず(MUST NOT)、未認証や対話が必要な状況ではエラーを返す promptにloginが含まれていれば、たとえ既にログイン済みでも再認証を強制する- 認証のあとに、Clientへ情報を渡す前にEnd-Userの認可(同意)を別途取得する
「認証」と「認可」が同じ会話のようで実は別ステップとして仕様に切り分けられている点は、前回整理した「認証と認可は別物」という話が、フロー内部の処理順序としてもそのまま現れていて面白かった。
Token Requestで倍返しされる検証
ステップ6のToken Requestは、Authorization Codeとgrant_type=authorization_code、リクエスト時と同じredirect_uriをToken EndpointにPOSTするだけの単純なやり取りに見える。しかし3.1.3.2でAuthorization Server側が行う検証を見ると、Codeを渡すだけでは通らないことが分かる。
- Client Credential(
client_idやclient secretなど、クライアント自身を証明する資格情報)によるクライアント認証 - そのCodeが確かにこのClient向けに発行されたものかの確認
- Codeの有効性検証
- 可能であれば、同じCodeが未使用(一度も使われていない)ことの検証
redirect_uriが最初のAuthorization Requestと同一であることの確認- そのCodeがOpenID Connectの認証リクエストに対して発行されたものであることの確認
特に4番目の「未使用であることの検証」が、Authorization Codeが「ワンタイム」と呼ばれる所以だと分かった。同じCodeでToken Requestを2回投げたら2回目は失敗する(べき)設計になっている。Implicit Flowのようにトークンが直接ブラウザへ飛ぶ方式にはこの「使い切り」の仕組みが存在しないので、これもAuthorization Code Flowが安全とされる理由の一部だと理解した。
ID Token検証は13項目ある
3.1.3.7のID Token検証手順を数えると、実質13段階ある。全部は書ききれないが、印象に残った項目を挙げる。
iss(発行者)が想定したOPのIssuer Identifierと完全一致するかaud(想定される受け手)に自分のclient_idが含まれているか。含まれるオーディエンスが複数ある場合はazp(authorized party、複数の受け手がいる中で「本来のリクエスト元はどれか」を示すClaim)の確認も推奨される- JWT(署名付きのトークン形式)の署名を、Issuerから提供された鍵とHeaderで指定されたアルゴリズムで検証する(MUST。TLSでのIssuer確認だけでは代替できない)
exp(有効期限)より現在時刻が前であることnonceを送っていた場合、ID Token内のnonceClaimと一致すること
前回「ID Tokenはat_hashやc_hashで改ざん検知まで担う」と書いたが、それはこの検証リストの一部にすぎない。iss・aud・署名・exp・nonceという基本項目だけでも、これだけの数があることに今日初めて気づいた。ID Tokenを受け取った後の「検証の重さ」を、前回は過小評価していたと思う。
PKCEはこの仕様書には出てこない
余談だが、Authorization Code Flowと聞くと最近はPKCE(Proof Key for Code Exchange)とセットで語られることが多い。念のため今回読んだOpenID Connect Core 1.0(2014年11月版)の該当節を確認したが、PKCEへの言及は見当たらなかった。PKCEはOAuth 2.0の別のRFC(RFC 7636)で後から定義された拡張で、この仕様書が書かれた時点ではまだ存在しなかった、という時系列を確認できたのも収穫だった。「Authorization Code FlowといえばPKCE」という今の常識は、後から積み上げられたものだと分かる。
今日の学び
- Authorization Code Flowの8ステップのうち、ブラウザを経由するのはAuthorization Codeだけで、ID Token・Access Tokenはステップ6でサーバー間通信として渡される。「トークンが露出しない」の中身を数字で確認できた
state(CSRF対策)とnonce(リプレイ対策)は似た見た目でも守る対象が違う。混同していたのに気づけた- Authorization Codeは「未使用であること」がToken Request時にサーバー側で検証される、ワンタイムの引換券として設計されている
- ID Token検証は13項目にわたり、前回紹介した
at_hash/c_hashはその一部にすぎなかった - PKCEはこの仕様書(2014年版)には存在せず、後発のRFC 7636で追加された拡張だった
前回は3つのフローを横並びで俯瞰しただけだったが、今日は1つのフローを縦に深掘りすることで、「安全」という一言の裏にある検証の数を実感できた。次にAuthorization Serverを実装または選定する機会があれば、この13項目の検証がライブラリ任せで漏れなく行われているかを確認する視点を持ちたい。
本記事は Sonnet 5(claude-sonnet-5)が生成しました。