プロンプトとスコープ
あなたは、モバイルおよびブラウザクライアントから使用される OAuth API を担当しています。現在、リソースサーバーは Bearer アクセストークンのみを検証しています。ログが1回漏洩しただけで、攻撃者はトークンが有効な間にリクエストをリプレイできてしまいます。認可サーバー、クライアント、リソースサーバー、および DPoP が利用できない場合の動作を含め、DPoP (Demonstrating Proof-of-Possession) スキームを設計してください。
これはバックエンドセキュリティアーキテクチャに関する質問であり、ベンダー設定のクイズではありません。RFC 9449 はアクセストークンをクライアントの公開鍵にバインドし、各呼び出しに HTTP メソッドと URI にバインドされた証明を含めることを要求します。RFC 9700 は、脆弱なフローの回避や PKCE の使用など、最新の OAuth セキュリティガイダンスを追加しています。
面接官がテストしていること
優れた回答は、Bearer トークンの障害モードから始まります。文字列を保持している人なら誰でもそれを使用できるため、盗まれたトークンと正当なリクエストを区別できません。次に、エンドツーエンドのフローを提示します。クライアントが鍵を生成し、トークンエンドポイントが DPoP 証明を検証して公開鍵をバインドし、リソースサーバーが署名、メソッド、URI、トークンハッシュ、および時間枠をチェックします。
面接官はまた、jti のリプレイ、サーバーノンス、鍵の紛失、プロキシによる URI の書き換え、リフレッシュトークン、古いクライアント、およびテレメトリに対する計画も期待しています。「JWT に署名する」と言うだけでは不十分です。通常の JWT 署名は発行者の完全性を証明するものであり、呼び出し元が発行時に使用された秘密鍵をまだ所持していることを証明するものではありません。
最初に明確にすべき質問
クライアントと脅威モデル
クライアントがブラウザ SPA、ネイティブモバイルアプリ、またはサーバーアプリケーションのいずれであるかを確認します。秘密鍵をハードウェア支援ストレージに保存できるかどうか、またインストールごとに独自の鍵を取得するかどうかによって、ライフサイクルが変わります。攻撃者がログ、プロキシ、ブラウザストレージ、またはデバイスファイルを読み取ることができるか、またリクエストをリアルタイムで傍受できるかどうかを尋ねます。
リソースサーバーの境界
ゲートウェイ、TLS 終端、および内部転送を確認します。DPoP の htu は、リソースサーバーによって検証される URI セマンティクスと一致する必要があります。ゲートウェイがパスを書き換える場合は、正規化と信頼できる転送ヘッダーを定義します。クライアントがあるアドレスに署名し、バックエンドが別のアドレスを検証するような事態を防ぎます。
可用性と移行の制約
レガシーな Bearer クライアントが動作し続ける必要があるか、短いデュアルスタック期間が許容されるか、ログインとリフレッシュが同じ認可サーバーを使用するかどうかを尋ねます。規制や価値の高い API で強力な送信者制約が必要な場合は、DPoP を必須にします。リスクの低いリソースについては、機能ネゴシエーションと測定されたロールアウトを通じて移行します。
30秒の回答フレームワーク
「私は DPoP を3者間プロトコルとして設計します。クライアントはインストールごとに鍵ペアを生成し、トークンリクエスト中に1回限りの DPoP 証明を送信します。認可サーバーはそれを検証し、アクセストークンの cnf.jkt を公開鍵にバインドします。すべての API 呼び出しには新しい DPoP JWT が付与され、リソースサーバーはその署名、htm、htu、iat、一意の jti、ath、およびトークンの cnf.jkt をチェックし、ノンスと短い時間枠を使用してリプレイを低減します。秘密鍵が失われた場合、バインドされたトークンは失効します。レガシーなクライアントには、明示的に低リスクなリソースに対してのみ期間限定の Bearer パスを提供し、高価値な操作は DPoP 必須へと移行します。」
ステップごとのソリューション
ステップ 1: 鍵を作成し、認可をバインドする
初回実行時に、クライアントは非対称鍵ペアを作成し、秘密鍵を安全なデバイスストレージに保持します。トークンをリクエストする際、DPoP 証明に含まれる JWK 内で公開鍵を送信します。認可サーバーは証明の署名、タイプ、および時刻を検証し、トークンの確認データを公開鍵の JWK サムプリントにバインドします。認可コードフローを使用する場合でも、RFC 9700 の PKCE ガイダンスを引き続き使用します。DPoP はトークンの所持を証明するものであり、コードの保護を代替するものではありません。
ステップ 2: リクエストごとに証明を生成する
リクエストごとに、クライアントは少なくとも jti、htm、htu、および iat を含む新しい JWT を作成し、バインドされた秘密鍵で署名します。アクセストークンのハッシュを ath に配置することで、リソースサーバーは証明が同じ鍵で署名された別のトークンではなく、このトークンを対象としていることを検証できます。証明は DPoP ヘッダーに入り、アクセストークンは Bearer の代わりに DPoP 認可スキームを使用します。
ステップ 3: 固定の順序で検証する
リソースサーバーはまずアルゴリズムと JWK タイプを制限し、次に JWT 署名、鍵フォーマット、時間枠、メソッド、および URI を検証します。アクセストークンをハッシュ化し、その結果を ath と比較します。トークンから cnf.jkt を読み取り、それを証明鍵のサムプリントと比較します。その後、通常のスコープ、オーディエンス、およびリソース認可を適用する前に、現在の時間枠内で jti が出現していないかを確認します。
ステップ 4: ノンスとリプレイを処理する
サーバーは高リスクなリソースに対してノンスを発行し、クライアントに次の証明にそれを含めるよう要求できます。受け入れ時間枠内での jti の重複排除が必要です。小規模なデプロイでは単一ノードのキャッシュで十分です。マルチインスタンスのリソースサーバーには、有効期限付きの共有キャッシュが必要であるか、または短い時間枠での重複リスクを明示的に受け入れ、セキュリティイベントとして記録する必要があります。拒否テレメトリは、鍵素材を公開することなく、期限切れ、不正な署名、サムプリントの不一致、およびノンスの欠落を区別する必要があります。
ステップ 5: ローテーション、失効、およびフォールバックを設計する
デバイスを紛失した場合、秘密鍵が漏洩した場合、またはサーバーが悪用を検出した場合は、そのサムプリントに関連付けられたアクセスおよびリフレッシュトークンを失効させ、再認可を要求します。ローテーションはクライアント側だけのサイレントな置き換えにはできません。新しい公開鍵には新しい認可バインディングが必要です。移行中、リソースサーバーは両方のスキームを受け入れることができますが、互換性がセキュリティのデフォルトにならないように、高価値な書き込みには個別の DPoP 必須ポリシーを設ける必要があります。
ステップ 6: mTLS とプレーン Bearer の比較
mTLS は TLS クライアント証明書レイヤーに証明を配置し、成熟した証明書運用を行うサーバー間システムに適しています。DPoP はアプリケーションレイヤーで動作し、モバイルおよびブラウザクライアントに適していますが、証明、ノンス、jti、および鍵ストレージの運用が追加されます。プレーンな Bearer はデプロイが最も容易ですが、トークン文字列がコピーされた後のリプレイに対抗できません。すべてのエンドポイントで盲目的に DPoP を有効にするのではなく、クライアントの機能、プロキシトポロジー、コンプライアンス、および運用コストに基づいて選択します。
高品質な回答例
私は脅威モデルから始めます。ログ、プロキシ、またはクライアントストレージの漏洩によってアクセストークンが公開される可能性があり、その後、攻撃者はその有効期間中に API を呼び出すことができます。有効期限を短くすることで期間を狭めることはできますが、呼び出し元が元のクライアントであることを証明することはできません。
クライアントはインストールごとに鍵ペアを作成し、秘密鍵を保護します。認可コード交換中、私は PKCE と DPoP 証明を要求します。認可サーバーは証明を検証し、トークンの cnf.jkt を公開鍵にバインドします。リソースサーバーでは、証明の署名、htm、htu、iat、jti、ath、およびノンスを固定の順序で検証し、次に証明鍵のサムプリントをトークンバインディングと比較します。不一致がある場合はすべて未認可とし、セキュリティメトリクスに分類します。
マルチインスタンスのデプロイ向けに、jti の値を TTL ベースの共有重複排除キャッシュに保存します。高リスクな書き込みにはノンスと DPoP が必要です。移行中、低リスクな読み取りはデュアルスタックにすることができますが、期限はクライアントのバージョンとリソースのリスクに紐付けられます。鍵が失われた場合はそのトークンを失効させて新しい認可を開始し、Bearer フォールバックが高価値な書き込みパスを共有することは決してありません。最後に、偽造された証明、古い jti のリプレイ、URI の変更、不正なトークンハッシュ、期限切れのノンス、プロキシの書き換えに対する統合テストを実行し、拒否率、ノンスの再試行、異常なサムプリントを監視してリプレイがブロックされていることを証明します。
よくある間違い
- 間違い: アクセストークンを JWT として署名するだけにする。→ 失敗する理由: 署名は発行者の完全性を証明しますが、クライアントの秘密鍵の所持は証明しません。→ 修正方法:
cnf.jktをバインドし、リクエストごとに新しい DPoP 証明を検証します。 - 間違い: 1つの DPoP 証明を再利用する。→ 失敗する理由:
jtiと時間チェックで区別できないため、攻撃者がリクエスト全体をリプレイできます。→ 修正方法: リクエストごとに新しいjtiを生成し、受け入れ時間枠内で重複排除を行い、必要に応じてノンスを追加します。 - 間違い: ゲートウェイでのみ DPoP を検証し、内部サービスはバインドされていないユーザーヘッダーを信頼する。→ 失敗する理由: 転送によってメソッド、URI、またはトークンバインディングが失われ、偽造された同一性が通過してしまう可能性があります。→ 修正方法: 信頼できる境界を越えて正規化された検証結果を渡し、それを設定できるエンティティを制限します。
- 間違い: すべての検証エラーで Bearer にフォールバックする。→ 失敗する理由: 攻撃者が意図的に DPoP エラーをトリガーして、最も脆弱なパスに到達させることができます。→ 修正方法: 登録済みのレガシークライアントおよび低リスクリソースに対してのみ、期間限定のフォールバックを許可します。
フォローアップの質問と回答
フォローアップ 1: リージョンをまたいで jti をどのように重複排除しますか?
リソースのリスクに応じて整合性を選択します。高価値な書き込みでは、リージョンをまたぐ共有ストアを使用して1回の追加ラウンドトリップを許容できます。低リスクな読み取りでは、リージョンごとのキャッシュと短い時間枠を使用し、可用性を損なうことなく重複を記録できます。どちらの設計でも、時間枠、障害時の動作、および重複率を SLO に明記します。
フォローアップ 2: XSS がブラウザの秘密鍵を読み取った場合はどうなりますか?
DPoP は、完全に制御されたクライアント実行環境を修復することはできません。エクスポート不可能な鍵、厳格なコンテンツセキュリティポリシー、短命なトークン、およびリフレッシュローテーションを採用し、サムプリントが異常な動作を示した場合はバインディングを失効させます。保護の境界を明確に述べてください。DPoP は秘密鍵がコピーされていない場合にコピーされたトークンを対象とするものであり、エンドポイント乗っ取りのあらゆる形態を対象とするわけではありません。
フォローアップ 3: プロキシが URI を変更します。htu をどのように検証しますか?
外部の正規 URI から内部ルートへのマッピングを定義し、認証済みの元のスキーム、ホスト、およびパスを提供できるのは信頼できるプロキシのみに制限します。リソースサーバーは比較前に同じ正規化を適用し、クライアント制御の認証されていない転送ヘッダーを拒否します。信頼できるマッピングが存在しない場合は、元の URI を推測するのではなく、境界で DPoP を終端し、短命な内部識別子を発行します。
フォローアップ 4: レガシークライアントは Bearer のみ送信できます。これらを永久にサポートできますか?
永続的な互換性はセキュリティ設計ではありません。クライアントのバージョン、ユーザーリスク、およびリソースタイプによって移行の期限を設定します。まず高価値な書き込みに DPoP を要求し、アクション可能なアップグレードエラーを返し、残存トラフィックを監視します。リスク評価によって正当化され、デバイスバインディングまたはレート制限が追加されている場合にのみ、制限付きの Bearer ルートを維持します。