OAuth / OIDC フローの防御
実際の OAuth / OIDC 認可・認証フローでは、悪意ある第三者による攻撃を防ぐためにさらなるパラメータや検証工程が求められます。
パラメータ:state -> CSRF
CSRF (Cross-Site Request Forgeries / クロスサイトリクエストフォージェリ)
一般的なウェブアプリケーションにおいて、CSRF とは「ユーザーが意図しない処理を強制される」脆弱性の事をいいます。
例えば、CSRF 対策が施されていない何らかのサービスにログイン中の被害者が、攻撃者が用意したページにアクセスするなどした結果、
- 商品の購入
- 登録情報の変更
などのリクエストが勝手にユーザーのブラウザから送信され、被害者の意図しない処理を強制される可能性があります。
OAuth における CSRF
一方で、OAuth における CSRF とは、被害者のアカウントに攻撃者のリソースが意図せず紐付けされる脆弱性です。
上の一般的な CSRF と紛らわしいですが、攻撃者のアカウントに被害者のリソースが紐付けられる脆弱性ではありません。
CSRF の攻撃
- 攻撃者はまず自分の手元で RP -> IdP への認可リクエストを送信し、その後のログイン・同意によって IdP が生成した認可レスポンス(認可コードを含む)を取得します。
- 本来であればこの認可コード情報を含んだ RP のリンクへとリダイレクトするところ、CSRF の攻撃においては攻撃者がこのリダイレクト処理を意図的に中断させ、リダイレクト先のリンクだけを入手します。
- 攻撃者はこのリンクを被害者に誘導し、RP のセッションを持つ被害者がこのリンクを踏むと、通常通りのトークン交換が行われて IdP から攻撃者のアカウント情報・トークンが返却されます。
- 被害者の RP セッションに攻撃者の情報・リソースが紐づいた状態になります。
CSRF 攻撃による被害
通常の CSRF と異なり OAuth の CSRF 攻撃では被害者のセッションに攻撃者の情報・リソースが紐づくので、一見すると攻撃者が自爆しているようにも見えますがそうではありません。
被害者が入力した個人情報やクレジットカード情報があとから攻撃者によって閲覧・使用される情報漏えいや決済手段の乗っ取りが生じる可能性があります。
state による防御
CSRF 脆弱性が発生する根本的な原因は、認可リクエストを送信(上の攻撃シナリオでは攻撃者が実行)したブラウザと、認可コードを送信してきたブラウザが同一であるかの検証がなされていないことです。
この検証に不備がある場合に、攻撃者による認可リクエストを被害者が受け継いで認可コードでトークン交換を行える状況が成立します。
OAuth 2.0 では CSRF 対策として、RP が作成する認可リクエストに state パラメータという、推測困難な値を含めます。
パラメータ:PKCE → 認可コード横取り
前節の state は、「認可レスポンスを受け取ったブラウザが、認可リクエストを開始したブラウザと同じか」を検証するための仕組みでした。
一方、Authorization Code Flow にはもう一つ、別の脅威があります。ブラウザ経由で受け取った認可コードそのものが攻撃者の手元に渡ってしまうケースです。
認可コード横取り(Authorization Code Interception)
Authorization Code Flow では、認可フェーズの最後にブラウザが認可コード付きの URL で RP へ戻されます。この認可コードは本来 RP がトークン交換に用いる使い捨ての値ですが、これが横取りされると攻撃者によるトークン交換(取得)を許すことになります。
例えば、次のような状況があり得ます。
- ネイティブアプリがカスタム URI スキーム(
myapp://callbackなど)で認可コードを受け取る場合に、別の悪意あるアプリが同じスキームでコードを横取りする - ログ・Referer・中間者など何らかの経路で、認可コード付き URL が漏洩する
こうした状況では、攻撃者は被害者の認可コードを手にして、自分でトークン交換を試みます。
Confidential Client と Public Client
3 章で触れた通り、クライアントには大きく 2 種類があります。
| 種類 | 特徴 | トークン交換時 |
|---|---|---|
| Confidential Client | サーバーサイドを持ち、client_secret を安全に保持できる | secret によるクライアント認証が使える |
| Public Client | ブラウザやモバイルアプリなど、秘密情報を隠せない | client_secret を前提にできない |
Confidential Client でも認可コードの漏洩自体は問題ですが、トークン交換には通常 client_secret が必要なため、コードだけでは直ちにトークンを取れません。
Public Client ではこの防波堤が弱い(または無い)ため、認可コードの横取りはより深刻です。そこで導入されるのが PKCE(Proof Key for Code Exchange / ピクシー)です。
PKCE による防御
PKCE では、認可リクエストを組み立てる時点で「あとでトークン交換を行う本人だけが知りうる値」を作成して利用します。これには一方向ハッシュの性質が使われています。
- RP は推測困難なランダム文字列
code_verifierを生成し、手元(セッション等)に保持する - そのハッシュ(通常 SHA-256)を取り、
code_challengeとして認可リクエストに載せる(認可リクエストにはcode_challenge_method=S256のようにハッシュ方法の情報も含める必要があります) - IdP は認可コードと紐づけて
code_challengeを記憶する - トークン交換時、RP は元の
code_verifierを提示する - IdP は verifier から challenge を再計算し、認可時に受け取った値と一致するか検証する
攻撃者が認可コードだけを盗んでも、code_verifier が無ければトークン交換に失敗します。
state と PKCE は代替にならない
state は「どのブラウザが認可を開始したか」を RP 側で紐づける CSRF 対策です。PKCE は「認可コードをトークンに換えようとしているのが、認可を開始したクライアント本人か」を IdP 側で証明する仕組みです。
向きも検証主体も違うため、どちらか一方で足りる、という関係ではありません。
RFC 技術仕様
PKCE は RFC 7636 で定義されています。現在のベストプラクティス(OAuth 2.1 を含む)では、Public Client に限らず Confidential Client でも Authorization Code Flow では PKCE の併用が推奨されています。
パラメータ:nonce → リプレイ攻撃
state と PKCE は、認可コードが届くまでの経路を守る仕組みでした。OIDC ではさらに、トークン取得後に手元へ来る ID Token そのものの悪用に備える必要があります。
リプレイ攻撃(Replay Attack)
リプレイ攻撃とは、かつて正当に発行・送信されたデータを、攻撃者がそのまま再利用して不正な結果を得る攻撃です。
OIDC の文脈では、過去に発行された ID Token を攻撃者が入手し、別のタイミングで RP に提示してログイン処理を成立させようとする、といった形を想像すると分かりやすいです。
ID Token は署名付き JWT なので、中身の改ざんは検知できます。しかし「改ざんされていない」ことと「今このリクエストのために発行されたものだ」ことは別問題です。正当な過去のトークンを再利用されるのがリプレイです。
nonce による防御
OIDC では、認可リクエストに RP が推測困難な nonce を含めます。IdP は認証結果として発行する ID Token の中に、同じ nonce をクレームとして埋め込みます。
RP は次のように検証します。
- 認可開始時に
nonceを生成し、自セッション等へ保存する - トークン取得後、ID Token を検証するときに
nonceクレームを取り出す - 手元に保存した値と一致しなければ拒否する(使い回し・すり替えを防ぐ)
こうすることで、「いま自分が開始した認可フローの結果として受け取った ID Token か」を確認できます。
state と nonce の違い
どちらも「推測困難なランダム文字列」ですが、目的が違います。
state | nonce | |
|---|---|---|
| 主な攻撃面 | CSRF(認可コード付きコールバックのすりつけ) | リプレイ(ID Token の再利用) |
| 載せる場所 | 認可リクエスト ↔ 認可レスポンス | 認可リクエスト ↔ ID Token クレーム |
| 検証の焦点 | コールバックが自セッションの認可応答か | 受け取った ID Token が今回の認可の結果か |
似ているからといって片方に寄せるのではなく、それぞれ別に扱う必要があります。
検証:redirect_uri → オープンリダイレクト
ここまでのパラメータは「リクエストやトークンの中身」を紐づけ・証明する仕組みでした。一方 redirect_uri は、認可コードを どこへ配送するか という配送先そのものの話です。
オープンリダイレクトとは
オープンリダイレクトとは、ある信頼できるサイトから、攻撃者が指定した任意の URL へユーザーを飛ばせてしまう脆弱性です。
OAuth / OIDC では認可コードが redirect_uri 付きのリダイレクトでブラウザに戻るため、もし配送先を攻撃者サイトに差し替えられると、認可コードが攻撃者の手元に渡ります。これは 5-2 で見た認可コードの横取りと直結する脅威です。
攻撃のイメージ
- クライアント登録時の
redirect_uri照合が甘い(部分一致、不要なワイルドカードなど) - 攻撃者は、認可レスポンスが自分の管理する URL に飛ぶように仕向ける
- 被害者が正規のログイン・同意を完了すると、認可コード付きで攻撃者の受け取り口へリダイレクトされる
- 攻撃者はその認可コードでトークン交換を試みる(PKCE 未使用の Public Client などでは特に危険)
防御:登録と照合を厳密にする
OAuth 2.0 では、クライアント登録時にあらかじめ許可する redirect_uri を登録し、実行時にはその登録済み値と 厳密に一致する ものだけを許可します。
- 認可リクエストの
redirect_uriが登録一覧と一致するか - トークン交換時に送る
redirect_uriが、認可時に使ったものと一致するか
この 2 点を IdP 側で検証することで、「認可コードの届け先」を信頼できる境界の内側に閉じ込めます。
役割の違い
state / PKCE / nonce は「やり取りの内容が正当か」を証明します。redirect_uri の厳密検証は「コードの届け先そのものを信頼境界に閉じる」ための防御です。配送先が破られると、他の対策の前提ごと揺らぎます。
Nogiku での注意
Nogiku Auth に RP を登録するとき、許可するリダイレクト URI は必要最小限・完全一致で登録してください。曖昧なパターンや不要な URI を追加すると、オープンリダイレクトのリスクにつながります。
検証:aud → トークン流用
フローの最後に RP が行うのは、受け取った ID Token を検証し、ユーザーを特定してセッションを確立することです。ここで重要になるのが aud(audience) クレームです。
aud とは
4 章でも触れた通り、aud は「このトークンの宛先は誰か」を示します。ID Token では通常、宛先は RP の client_id です。
つまり IdP は「この ID Token は、このクライアント向けに発行した」と明示しています。
トークン流用とは
トークン流用とは、別のクライアント向けに発行されたトークンを、自分のクライアントでそのまま使ってしまう(または使わせてしまう)ことです。
Nogiku SSO のように、複数の連携システムが同じ IdP を共有している環境では、ある RP 向けの ID Token が別経路で手元に残ったり漏洩したりする可能性を考える必要があります。RP が aud を確認せずに受け入れると、他クライアント宛の ID Token で自サービスのログインが成立してしまう恐れがあります。
本章と 6 章の境界
ここで扱うのは、ログイン完了までの ID Token 検証における aud です。取得後のアクセストークンをリソースサーバー側でどう検証するか、JWT と opaque の違い、introspection や JWKS の運用などは次章で扱います。
aud による防御
RP は ID Token を受け取ったら、署名や有効期限などと併せて次を必須にします。
audが自クライアントのclient_idと一致すること- (併せて)
issが想定している IdP であること、expが有効期限内であること、nonceが一致すること など
aud が一致しない ID Token は、たとえ署名が正しくても受け入れてはいけません。
ここまでの防御を役割で整理すると、次のようになります。
| 防御 | 守っているもの |
|---|---|
state | 認可レスポンスのすりつけ(CSRF) |
| PKCE | 認可コードをトークンに換える主体 |
nonce | ID Token のリプレイ |
redirect_uri | 認可コードの配送先 |
aud | ID Token の宛先(トークン流用) |
認可・認証フローを守り切った先では、発行された各種トークンを継続してどのように扱い、検証するかが次の課題になります。続く 6 章では、取得後のトークン管理と検証について見ていきます。