問題と適用コンテキスト
あなたのプロダクトは、ユーザーのカレンダーのパスワードを直接扱うことなく、ユーザーに代わってサードパーティのカレンダーを読み取る必要があります。アプリケーションバックエンドと認可サーバーが何を検証しなければならないかを含め、パブリッククライアントにおける PKCE を用いた完全な OAuth 2.0 認可コードフローを説明してください。
クライアントはブラウザのシングルページアプリケーション(SPA)またはネイティブアプリケーションであると仮定します。デプロイされた全インスタンスにわたって1つの静的クレデンシャルを安全に秘匿し続けることはできないため、パブリッククライアントに該当します。認可サーバーはユーザーを認証し、同意を取得してトークンを発行します。リソースサーバーはカレンダー API を公開します。初期スコープはカレンダーへの読み取り専用アクセスであり、リフレッシュトークンの発行は認可サーバーのポリシーに依存します。
2026年3月に公開されたあるパブリックなバックエンド面接ガイドでは、認可コードグラント、PKCE、パブリッククライアントとコンフィデンシャルクライアント、そして OAuth と OpenID Connect の境界についての説明を候補者に求めています。現在のセキュリティ基準は OAuth 2.0 Security Best Current Practice です。パブリッククライアントは PKCE を使用しなければならず(MUST)、コンフィデンシャルクライアントにもその使用が推奨され(RECOMMENDED)、認可サーバーは PKCE のダウングレードを防がなければならず(MUST)、登録されたリダイレクト URI は完全一致(Exact Matching)を要求します。
このスキルの本質は、サービス間認可プロトコル、トークンの境界、セキュリティコントロールの設計と検証にあるため、これはバックエンドの設問です。ブラウザのリダイレクトはその伝送手段にすぎません。
面接官が評価しているポイント
第1に、候補者が4つのロールを正しく区別できているかです。リソースオーナーはユーザー、クライアントはカレンダー連携プロダクト、認可サーバーは認証・同意・トークン発行を処理し、リソースサーバーはカレンダーデータを保持します。クライアントとユーザーを混同すると、パーミッションやトークンのオーディエンスに関する以降の説明がすべて破綻します。
第2に、候補者が認可コードフローがなぜ2段階(two legs)に分かれているかを説明できるかです。ブラウザは短寿命で1回限りの認可コードのみを受け取ります。クライアントはそれをトークンエンドポイントで交換するため、アクセストークンがブラウザのリダイレクト URL 内を流れることはありません。さらに PKCE は、認可リクエストのために生成されたランダムな秘密情報をそのコード交換に紐付けます。コードを傍受した第三者であっても、code_verifier を持っていなければトークンに引き換えることはできません。
第3に、候補者が各コントロールの責務を正確に整理できているかです。
| コントロール | 主なバインディング(紐付け) | 主に対処する問題 |
|---|---|---|
state | 開始時のクライアントセッションからコールバックへ | リクエストの相関付けおよびログイン CSRF |
| PKCE | 認可リクエストからトークン交換へ | 認可コードの傍受およびコード注入 |
| クライアント認証 | コンフィデンシャルクライアントから認可サーバーへ | コードを引き換えるクライアントの身元 |
| 完全一致リダイレクト URI | 登録済みクライアントから許可されたコールバックへ | 攻撃者が制御するエンドポイントへのコード送信 |
OIDC nonce | ログインリクエストから ID トークンへ | ID トークンのリプレイおよびログインセッションの相関付け |
最後に、優れた回答は PKCE の防御境界を明示します。PKCE は、同一オリジンの XSS によるベリファイアやトークンの読み取りを防ぐものではありません。また、TLS、リダイレクト検証、最小権限の原則、安全なトークンストレージ、あるいは認証プロトコルを代替するものでもありません。
回答前の確認質問(Clarifying Questions)
- クライアントはパブリックですか、それともコンフィデンシャルですか? SPA やネイティブアプリは静的なクライアントシークレットで身元を証明できません。制御可能なバックエンドを持つ Web アプリケーションであればコンフィデンシャルになり得、PKCE の使用に加えてトークンエンドポイントでの認証が必要です。
- 目的は API の認可ですか、それともプロダクトへのログインですか? カレンダー API アクセスには OAuth を使用します。プロダクトがユーザーのアイデンティティを必要とする場合は、アクセストークンから身元を推測するのではなく、OIDC を使用して ID トークンを検証します。
- クライアントは単一の認可サーバーを使用しますか、それともテナントごとに設定されたアイデンティティプロバイダーを使用しますか? 複数の発行者(Issuer)が存在する場合は、Mix-up 攻撃を防ぐために Issuer の検証または発行者ごとの個別リダイレクト URI が必要です。
- オフラインアクセスは必要ですか? 不要な場合はリフレッシュトークンを要求しないでください。必要な場合は、ローテーション、リプレイ検知、失効、有効期限を定義します。
- コールバックのトランザクション状態はどこに保存されますか?
state → code_verifierのバインディングを維持してください。単一のグローバルなベリファイアを使用すると並行するタブが壊れます。また、URL やログに含めると機密性が失われます。 - カレンダーへのアクセス権限はどの程度必要ですか? 機能に必要な読み取り専用スコープから開始します。書き込み権限やアカウント全体の権限には具体的な正当性が求められます。
30秒の回答フレームワーク
「認可を試行するたびに、予測不可能な state と高エントロピーの code_verifier を生成し、S256 の code_challenge を導出します。ブラウザは response_type=code、クライアント ID、完全一致で登録されたリダイレクト URI、最小限のスコープ、state、チャレンジを持って認可エンドポイントに遷移します。ユーザーは認可サーバーでのみ認証を行います。コールバックは短寿命で1回限りのコードと同じ state を返します。クライアントは state を検証し、そのトランザクションのベリファイアを読み込み、コード、リダイレクト URI、ベリファイアをトークンエンドポイントに送信します。サーバーはコード、クライアント、リダイレクト URI、チャレンジのバインディングを検証した後にのみアクセストークンを発行します。パブリッククライアントは静的シークレットをクレデンシャルとして扱えませんが、コンフィデンシャルクライアントはクライアント認証も行います。PKCE はコード交換を保護し、state はリクエストを相関付け、ログインのアイデンティティを支えるのは OIDC の ID トークンです。」
ステップごとの詳細解説
ステップ1:1回限りの認可トランザクションを作成する。
「カレンダーを連携する」アクションごとに、クライアントは独立した2つのランダム値を生成します:
stateは、現在のユーザーセッション、想定される発行者、コールバック URI、および認可後の遷移先に関連付けられた推測不可能なトランザクション ID です。code_verifierは、暗号論的に安全な乱数生成器を用いて生成されます。RFC 7636 では 43〜128 文字の unreserved 文字と定義されており、少なくとも 256 ビットのエントロピーが推奨されています。
クライアントは以下のようにチャレンジを導出します:
code_challenge = base64url_without_padding(
SHA256(ASCII(code_verifier))
)
code_challenge_method = S256トランザクションごとに個別の state → code_verifier レコードを保存します。2つのブラウザタブが同時に2つのカレンダー連携を開始する可能性があるため、「最新のベリファイア」というデータモデルは不適切です。トランザクションには短い有効期限を設定し、成功時または修復不能な失敗時に削除します。ベリファイアをリダイレクト URL、アナリティクスイベント、またはアプリケーションログに含めてはなりません。
ステップ2:認可リクエストを構築する。
クライアントはユーザーエージェントを認可エンドポイントにリダイレクトします:
GET /authorize
?response_type=code
&client_id=calendar-client
&redirect_uri=registered-callback
&scope=calendar.read
&state=random-transaction-id
&code_challenge=derived-challenge
&code_challenge_method=S256認可サーバーは、クライアント ID、レスポンスタイプ、スコープ、およびリダイレクト URI を検証します。リダイレクト URI は、広範なドメイン、任意のサブパス、ユーザー制御の転送パラメータではなく、事前に登録された値と完全一致しなければなりません。ユーザーのクレデンシャルは認可サーバーにのみ送信されます。クライアントは認可結果を受け取るだけであり、ユーザーのパスワードを受け取ることは決してありません。
ステップ3:トークンエンドポイントをまだ呼び出さずにコールバックを処理する。
同意後、認可サーバーはユーザーエージェントを登録済みコールバックへリダイレクトし、code と元の state を渡します。クライアントはまずローカルトランザクションを検証します:
- state が存在し、期限切れでなく、まだ消費されていないこと。
- 現在のブラウザセッションおよび想定される認可サーバーに紐付いていること。
- コールバックがこのトランザクション用に登録されたエンドポイントに到達したこと。
- エラーレスポンスの場合でも、state を使用して正しいトランザクションを特定し、安全に終了できること。
「とりあえずトークンリクエストを送信して通るか試す」のではなく、state の不一致が発生した時点で処理を中断(アボート)します。認可コードは短寿命かつ1回限りでなければなりません。再度の引き換えは失敗しなければならず、可能であれば認可サーバーはそのコードからすでに発行されたトークンを失効させる必要があります。
ステップ4:ベリファイアを用いてコードを交換する。
クライアントは以下のトークンリクエストを送信します:
POST /token
grant_type=authorization_code
code=returned-authorization-code
redirect_uri=registered-callback
client_id=calendar-client
code_verifier=stored-verifier認可サーバーは元のトランザクションを再構築します。すなわち、コードがどのクライアントおよびリダイレクト URI に属しているか、期限切れまたは使用済みでないか、認可リクエストにチャレンジが含まれていたか、そしてこのベリファイアに S256 を適用した結果が保存されているチャレンジと一致するかを検証します。認可リクエストにチャレンジがなかったにもかかわらずトークンリクエストでベリファイアが提示された場合、サーバーはトランザクションを非 PKCE フローへと暗黙的にダウングレードしてはなりません。
パブリッククライアントは識別のためにクライアント ID を送信しますが、パブリックなコード内に埋め込まれた静的シークレットでクライアントを認証することはできません。コンフィデンシャルクライアントは、さらに登録済みのクライアント認証方式を使用します。PKCE はその認証を置き換えるものではなく、補完するものです。
ステップ5:攻撃経路を通じて各コントロールを説明する。
正当なクライアントがベリファイア V1 とチャレンジ C1 を作成したとします。攻撃者がコールバックのコードを傍受したものの、V1 を知らない場合、攻撃者が任意に選んだベリファイアからは C1 が生成されないため、トークン交換は失敗します。これが認可コード傍受に対する PKCE の中核的な保護機能です。
次に、攻撃者が別の認可トランザクションを開始し、そのコードを被害者のコールバックに注入したとします。被害者のトランザクションは異なる state とベリファイアを持っています。state の相関検証または PKCE のバインディングが失敗するため、クライアントは処理を中断しなければなりません。また、認可サーバーはコードに対してチャレンジが存在したかどうかを記憶しておき、攻撃者がチャレンジを削除してダウングレードを引き起こすのを防ぐ必要があります。
攻撃者がリダイレクト URI を自身が制御するエンドポイントに変更しようとした場合、完全一致での登録と完全一致の文字列比較により認可時にリクエストが拒否されます。クライアントが複数の認可サーバーをサポートしている場合は、レスポンスの Issuer とトランザクションに保存された Issuer を比較するか、Issuer ごとに固有のリダイレクト URI を使用する必要があります。認可エンドポイントの URL のみを記憶しておくだけでは、Mix-up 攻撃を防ぐには不十分です。
ステップ6:トークンを本来の目的に制限する。
アクセストークンは認可を表すものであり、クライアントに対するユーザーのアイデンティティ表明ではありません。リソースサーバーはトークンが自身宛てであることを検証し、必要なリソースとアクションのみに制限します。クライアントは calendar.read のみを要求し、短寿命のアクセストークンを使用し、無関係なスクリプトからアクセス可能な URL、ログ、永続ストレージにトークンを置かないようにします。
コードがトークンに変換された時点で、PKCE の役割は完了します。XSS によって SPA のメモリ、トランザクションストレージ、またはアクセストークンが読み取られた場合、PKCE で秘密情報を取り戻すことはできません。より高リスクなブラウザアプリケーションでは BFF(Backend for Frontend)を使用できます。トークンは制御されたバックエンド内に留まり、ブラウザは HttpOnly のセッション Cookie のみを保持します。これにより JavaScript へのトークン露出は減少しますが、Cookie セッション、CSRF、バックエンドのスケーリング、API プロキシのコストが増加します。
パブリッククライアントがリフレッシュトークンを受け取る場合、認可サーバーは送信者制約(Sender Constraint)またはリフレッシュトークンローテーションによってリプレイを検知しなければなりません。ローテーションでは、リフレッシュごとに新しいリフレッシュトークンが発行され、古いトークンは無効化されます。古い値が再利用された場合は侵害の可能性を示しているため、グラントファミリー全体が失効し、ユーザーは再認可を行う必要があります。
ステップ7:OAuth、OIDC、およびその他のグラントを区別する。
OAuth はクライアントがユーザーに代わってリソースにアクセスできるかどうかを扱います。OIDC は OAuth の上にアイデンティティレイヤーを追加し、クライアントが ID トークンを通じてログインを検証できるようにします。OIDC のコールバックでは、署名、発行者、オーディエンス、有効期限、および nonce の検証も必要です。アクセストークンはリソースサーバー向けであり、ID トークンはクライアント向けです。両者に互換性はありません。
ユーザーが関与せず、サービスが自身の権限でリソースにアクセスする場合は Client Credentials を使用します。入力が制限されたデバイスでは Device Authorization Flow を使用できます。Implicit Flow は認可レスポンスにアクセストークンを含めるため、URL の漏洩やリプレイのリスクが高まります。現在のベストプラクティスでは、コードを返すフローが推奨されています。Resource Owner Password Credentials グラントはユーザーのパスワードをクライアントに露出させるため、現在のセキュリティ基準では禁止されています。
ステップ8:ハッピーパスだけでなく異常系(失敗系)を検証する。
| テストケース | 期待される結果 |
|---|---|
| ベリファイアの文字を1文字変更する | トークン交換が失敗する |
| 同じコードを2回引き換える | 2回目の交換が失敗し、セキュリティハンドリングがトリガーされる |
| コールバックの state が欠落、期限切れ、または別セッションのもの | クライアントがトークン交換前に拒否する |
| 認可時にチャレンジを省略したがトークンリクエストでベリファイアを送信 | 認可サーバーがダウングレードを拒否する |
| リダイレクトの大文字小文字、パス、末尾のスラッシュのみが異なる | 完全一致検証で失敗する |
| 2つのタブでフローを開始しコールバックが順不同で返る | 各 state が自身のベリファイアを正しく取得する |
| 2つの認可サーバーを接続後に発行者(Issuer)を入れ替える | クライアントが Mix-up を検知して拒否する |
| ログおよびアナリティクスをスキャンする | コード、ベリファイア、アクセストークン、リフレッシュトークンが出力されていない |
| XSS がブラウザからアクセス可能なストレージを読み取る | PKCE では不十分であると明示的に判断し、BFF の採用を検討する |
本番環境のモニタリングでは、認可拒否、state の不一致、PKCE の失敗、コードの再利用、リダイレクトの不一致、リフレッシュトークンのリプレイを区別して検知できるようにすべきです。単一の「OAuth 失敗」カウンターにまとめてしまうと、攻撃の兆候とクライアント実装の不具合の両方が見えなくなってしまいます。
高品質な回答例
「今回はサードパーティのカレンダー API にアクセスするパブリッククライアントとしてスコープを定義します。パブリッククライアントは共有シークレットを保護できないため、SPA のバンドル内に埋め込まれたシークレットはクライアント認証にはなりません。
認可を開始するたびに、推測不可能な state と、少なくとも 256 ビットのエントロピーを持つ code verifier を生成し、S256 チャレンジを導出した上で、その state に紐付けてベリファイア、ブラウザセッション、Issuer、リダイレクト URI、有効期限を保存します。認可リクエストには、code レスポンスタイプ、クライアント ID、完全一致で登録されたリダイレクト URI、読み取り専用スコープ、state、チャレンジを含めます。ユーザーは認可サーバーでのみ認証と同意を行います。
コールバックでコードと state が返されたら、まず state が現在のセッションに属しており未使用であることを証明した上で、対応するベリファイアを読み込みます。トークンリクエストでは、コード、同じリダイレクト URI、クライアント ID、ベリファイアを送信します。認可サーバーはアクセストークンを発行する前に、コードが短寿命かつ未使用であること、このクライアントおよびリダイレクト URI に対して発行されたものであること、そしてベリファイアの S256 値がチャレンジと一致することを検証します。傍受されたコードであってもベリファイアがなければ無意味です。また、サーバーは PKCE を削除して交換をダウングレードしようとするリクエストも拒否します。
state はブラウザリクエストを相関付け、PKCE はコード交換を紐付けます。これらを単一の曖昧な CSRF パラメータに統合すべきではありません。コンフィデンシャルな Web アプリケーションはトークンエンドポイントでも認証を行い、PKCE も維持すべきです。アクセストークンはカレンダーのリソースサーバー専用です。プロダクトがこのフローをログインに利用する場合は OIDC が必要であり、ID トークンの署名、発行者、オーディエンス、有効期限、nonce を検証しなければなりません。
テストとしては、不正なベリファイア、コードの再利用、別セッションの state、リダイレクトの完全一致の不一致、タブの順不同処理、PKCE ダウングレード、Issuer の Mix-up を検証し、ログにコードやトークンが含まれていないことを確認します。PKCE は同一オリジン XSS によるベリファイアやトークンの窃取を防ぐことはできません。高リスクなブラウザクライアントに対しては、トークンをサーバーサイドに保存し、ブラウザには HttpOnly セッション Cookie のみを渡す BFF の導入を検討します。」
よくある間違い
- クライアント ID を秘密情報として扱う → クライアント ID は認可リクエストに現れ、クライアントを識別するだけです → パブリッククライアントには PKCE を使用し、コンフィデンシャルクライアントには真正なクライアント認証を使用してください。
- 「コードはトークンより安全」とだけ答える → これでは傍受されたコードがなぜ引き換えられないのかの説明になりません → チャレンジとベリファイアのバインディング、およびトークンエンドポイントでの比較を示してください。
plainを使用する、または PKCE の省略を許可する → ベリファイアが露出する可能性があり、攻撃者がダウングレードを引き起こせます → S256 を使用し、認可サーバー側で PKCE を記録・強制してください。- セッションに紐付けずに state を生成する → 有効な値が移植される可能性があり、並行するフロー同士で上書きが発生します → 1回限りの state、セッション、Issuer、ベリファイアのマッピングを保存してください。
- あいまいなリダイレクト一致(Fuzzy Match)を許可する → 攻撃者が制御するサブパスやリダイレクターにコードが届く可能性があります → リダイレクト URI を事前登録し、完全一致で比較してください。
- アクセストークンをデコードしてログインアイデンティティとして扱う → そのオーディエンスやセマンティクスはリソースサーバーのものである可能性があります → OIDC ログインを使用し、ID トークンを検証してください。
- PKCE が XSS を防ぐと主張する → XSS はベリファイア、コード、発行済みトークンを直接読み取ることができます → ブラウザへのトークン露出を減らし、XSS を修正し、リスクに応じて BFF を検討してください。
- コード、ベリファイア、またはトークンをログに出力する → 診断ログによって引き換え可能な秘密情報の露出範囲が広がります → トランザクション ID、エラークラス、引き換え不可能な相関値のみをログに記録してください。
フォローアップ質問と回答
フォローアップ 1:バックエンドを持つ従来の Web アプリケーションでも PKCE は必要ですか?
はい、必要です。トークンエンドポイントでのクライアント認証は、コンフィデンシャルクライアントが保護されたクレデンシャルを保持していることを証明します。PKCE は今回の認可リクエストを今回のコード交換に紐付け、コードの注入や不正利用をさらに防ぎます。これらのコントロールは異なる関係性をバインドしています。サーバーサイドセッションでベリファイアと state を保存し、ブラウザには推測不可能なセッション識別子のみを持たせることができます。
フォローアップ 2:「カレンダー連携」が「カレンダーアカウントでログイン」も兼ねる場合、何が変わりますか?
アクセストークンや任意の UserInfo レスポンスをログインの証明として扱うのではなく、OIDC を使用してください。openid スコープと nonce を要求します。ID トークンの署名、発行者、オーディエンス、有効期限、および nonce を検証し、Issuer と Subject のペアを外部アイデンティティキーとして使用します。state は引き続きブラウザトランザクションをバインドし、nonce は認証リクエストを ID トークンにバインドします。
フォローアップ 3:各 SaaS テナントが異なる発行者(Issuer)を設定できる場合、どのような新しいリスクが発生しますか?
クライアントが認可サーバー A からのレスポンスを認可サーバー B のトークンエンドポイントに送信してしまい、Mix-up 攻撃が発生する可能性があります。想定される Issuer をトランザクションに保存し、レスポンスの Issuer を検証してください。別の防御策として、Issuer ごとに個別のリダイレクト URI を使用し、レスポンスが正しいエンドポイントに到達したことを検証します。認可エンドポイントとトークンエンドポイントは、同じ信頼できる Issuer メタデータから取得しなければなりません。
フォローアップ 4:長期間実行されるカレンダー同期のためにリフレッシュトークンをどのように保護しますか?
必要なスコープのみを付与し、リフレッシュトークンローテーションまたは送信者制約(Sender Constraint)を使用します。ローテーションではトークンファミリーを追跡します。リフレッシュのたびに古い値は無効化され、古いリフレッシュトークンが再利用された場合はアクティブなファミリー全体を失効させて再認可を要求します。また、クライアント側での安全なストレージ、失効パス、最大有効期間、無操作時の有効期限も必要です。PKCE が保護するのは初期のコード交換であり、後から盗まれたリフレッシュトークンは保護できません。
フォローアップ 5:異なるカレンダーアカウントを接続する2つのタブで間欠的に失敗が発生するのはなぜですか?
よくある原因は、グローバルな state またはベリファイアを1つしか保持しておらず、2つ目のタブが1つ目のタブの値を上書きしてしまうことです。コールバックは state をキーにして独立したトランザクションを検索しなければなりません。そのレコードには、ベリファイア、想定されるアカウントまたは Issuer、リダイレクト URI、および作成時刻が含まれます。これをアトミックに消費します。最新のベリファイアにフォールバックするのではなく、未知の state、重複した state、期限切れの state は拒否してください。