DPoP検証
DPoPとは?
Section titled “DPoPとは?”DPoP(Demonstrating Proof of Possession、RFC 9449)は、アクセストークンをクライアントの暗号鍵ペアにバインドするメカニズムです。標準的なBearerトークンは、所持している人なら誰でも使用できます。DPoPバインドトークンは、対応する秘密鍵を保持しているクライアントのみが使用できます。
クライアントは各リクエストにDPoPプルーフ(署名されたJWT)を含めます。リソースサーバーは以下を検証します:
- プルーフがアクセストークンにバインドされた鍵で署名されていること
- プルーフが現在のHTTPメソッドとURLに一致すること
- プルーフがリプレイされていないこと
リソースサーバーの役割
Section titled “リソースサーバーの役割”@authrim/serverを使用するリソースサーバーとして、DPoPに関する責任は以下の通りです:
| 責任 | SDKが処理 | アプリケーションが処理 |
|---|---|---|
| DPoPプルーフJWTの解析 | はい | — |
| プルーフ署名の検証 | はい | — |
typ、alg、jwkヘッダーの検証 | はい | — |
htm、htu、iatクレームの検証 | はい | — |
ath(アクセストークンハッシュ)の検証 | はい | — |
JWK Thumbprintバインディング(cnf.jkt)の検証 | はい | — |
| JWK内の秘密鍵パラメータの拒否 | はい | — |
jtiリプレイ保護 | — | はい |
| DPoP nonce管理 | — | はい |
validateDPoP API
Section titled “validateDPoP API”const dpopResult = await authrim.validateDPoP(proof, options);| パラメータ | 型 | 説明 |
|---|---|---|
proof | string | DPoPリクエストヘッダーからの生のDPoPプルーフJWT |
options | DPoPValidationOptions | 検証パラメータ |
DPoPValidationOptions
Section titled “DPoPValidationOptions”interface DPoPValidationOptions { method: string; uri: string; accessToken?: string; expectedThumbprint?: string; expectedNonce?: string; maxAge?: number; clockTolerance?: number;}| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
method | string | — | 現在のリクエストのHTTPメソッド(例: 'GET'、'POST') |
uri | string | — | 現在のリクエストURI。SDKは htu 比較のためにスキーム + ホスト + パスへ正規化します |
accessToken | string | — | proofのathクレーム検証に使うアクセストークン |
expectedThumbprint | string | — | トークンのcnf.jktクレームからの期待されるJWK Thumbprint |
expectedNonce | string | — | プルーフに含まれている必要があるサーバー発行のnonce |
maxAge | number | 60 | プルーフの最大有効期間(秒)(iatに基づく) |
clockTolerance | number | 60 | クロックスキュー許容値(秒) |
現在のSDKが受け入れるDPoP proof署名アルゴリズムは ES256、PS256、EdDSA です。
DPoP検証フロー
Section titled “DPoP検証フロー”flowchart TD
A["DPoPプルーフを受信"] --> B["JWTヘッダーを解析<br>(typ: dpop+jwt)"]
B --> C["アルゴリズムチェック<br>(none拒否, 許可リスト確認)"]
C --> D["公開鍵を抽出<br>(秘密鍵パラメータを拒否)"]
D --> E["プルーフ署名を検証"]
E --> F["htmを検証<br>(HTTPメソッドと一致)"]
F --> G["htuを検証<br>(リクエストURLと一致)"]
G --> H["iatを検証<br>(maxAge以内)"]
H --> I{"athあり?"}
I -->|はい| J["アクセストークンハッシュを検証"]
I -->|いいえ| K["athチェックをスキップ"]
J --> L{"expectedThumbprint?"}
K --> L
L -->|はい| M["JWK Thumbprintを検証<br>(タイミングセーフ)"]
L -->|いいえ| N["バインディングチェックをスキップ"]
M --> O["DPoP結果を返却"]
N --> O
完全なDPoP検証の例
Section titled “完全なDPoP検証の例”DPoPバインドリクエストを検証する典型的なフローを示します:
import { createAuthrimServer } from '@authrim/server';
const authrim = createAuthrimServer({ issuer: 'https://auth.example.com', audience: 'https://api.example.com',});await authrim.init();
async function handleRequest(req) { // 1. Extract tokens from headers const authHeader = req.headers['authorization']; const dpopProof = req.headers['dpop'];
if (!authHeader?.startsWith('DPoP ')) { throw new Error('Missing DPoP authorization'); }
const accessToken = authHeader.slice(5); // Remove 'DPoP ' prefix
// 2. Validate the access token const tokenResult = await authrim.validateToken(accessToken); if (tokenResult.error) { throw new Error(tokenResult.error.message); }
// 3. If token is DPoP-bound, validate the proof if (tokenResult.data.tokenType === 'DPoP' && dpopProof) { const dpopResult = await authrim.validateDPoP(dpopProof, { method: req.method, uri: `${req.protocol}://${req.hostname}${req.path}`, accessToken, expectedThumbprint: tokenResult.data.claims.cnf?.jkt, }); if (!dpopResult.valid) { throw new Error(dpopResult.errorMessage ?? 'Invalid DPoP proof'); } }
return tokenResult.data;}JWK Thumbprint検証(RFC 7638)
Section titled “JWK Thumbprint検証(RFC 7638)”JWK Thumbprintは公開鍵の正規形式のSHA-256ハッシュです。アクセストークン(cnf.jktクレーム経由)をDPoPプルーフの署名鍵にバインドします。
SDKはThumbprint操作用のユーティリティ関数を提供しています:
import { calculateJwkThumbprint, verifyJwkThumbprint,} from '@authrim/server';
// Calculate a thumbprint from a JWKconst thumbprint = await calculateJwkThumbprint(publicJwk);
// Verify a thumbprint matches a JWK (timing-safe)const isValid = await verifyJwkThumbprint(publicJwk, expectedThumbprint);Thumbprintの計算はRFC 7638に従います:
- 鍵タイプに必要なメンバーを抽出(ECの場合は
kty、crv、x、y、RSAの場合はkty、e、n) - ソートされたキーと空白なしのJSONオブジェクトとしてシリアライズ
- SHA-256ハッシュを計算
- Base64urlとしてエンコード
jtiリプレイ保護
Section titled “jtiリプレイ保護”validateDPoP() は検証結果とproof鍵のthumbprintを返します。jtiは戻り値に含まれないため、replay cacheへ書き込む前にアプリケーション側でproof payloadから取り出してください。
// Example: DPoP jti replay protection with Redisconst DPOP_JTI_TTL = 60; // maxAge と揃える
async function checkDPoPReplay(jti: string): Promise<boolean> { const key = `dpop:jti:${jti}`; // SET NX returns 'OK' only if the key did not exist const result = await redis.set(key, '1', 'EX', DPOP_JTI_TTL, 'NX'); return result === 'OK'; // true = new jti, false = replay}
// Usage in your request handlerasync function handleProtectedRequest(req) { const tokenResult = await authrim.validateToken(accessToken); if (tokenResult.error) { throw new Error(tokenResult.error.message); }
const jti = extractJtiFromProof(dpopProof); const dpopResult = await authrim.validateDPoP(dpopProof, { /* ... */ }); if (!dpopResult.valid) { throw new Error(dpopResult.errorMessage ?? 'Invalid DPoP proof'); }
// Check for jti replay const isNew = await checkDPoPReplay(jti); if (!isNew) { throw new Error('DPoP proof replay detected'); }
// Proceed with the request}jti追跡に関する重要な考慮事項:
- TTLは
maxAgeに一致させる — キャッシュエントリのTTLをmaxAgeと同じ値(デフォルト: 60秒)に設定します。エントリは自動的に期限切れになります。 - 分散キャッシュが必要 — マルチサーバーデプロイメントでは、すべてのサーバーが同じ
jti値を参照できるように共有キャッシュ(Redis、Memcached)を使用します。 - 高カーディナリティ — 各DPoPプルーフは一意の
jtiを持ちます。高トラフィックのAPIでは、キャッシュがボリュームを処理できることを確認してください。
DPoP Nonceハンドリング
Section titled “DPoP Nonceハンドリング”認可サーバーは、事前生成されたプルーフを防止するためにDPoP-Nonceを発行する場合があります。リソースサーバーでnonceを適用する必要がある場合:
// 1. Generate and store a nonceconst nonce = generateSecureNonce();storeNonce(nonce);
// 2. Return the nonce to the client in the response headerres.setHeader('DPoP-Nonce', nonce);
// 3. On subsequent requests, verify the nonceconst dpopResult = await authrim.validateDPoP(dpopProof, { method: req.method, uri: requestUrl, expectedNonce: getStoredNonce(),});クライアントが期待されるnonceなしでリクエストを送信した場合、以下で応答します:
res.status(401).json({ error: 'use_dpop_nonce' });res.setHeader('DPoP-Nonce', newNonce);クライアントはnonceを含む新しいDPoPプルーフでリクエストを再試行する必要があります。
セキュリティ: 秘密鍵の拒否
Section titled “セキュリティ: 秘密鍵の拒否”DPoPエラーコード
Section titled “DPoPエラーコード”validateDPoP() は代表的なproof検証失敗を dpopResult.errorCode に以下のコードで返します。
| コード | 説明 |
|---|---|
dpop_proof_invalid | malformed proof、サポート外アルゴリズム、必須claim欠落、proof JWK内の秘密鍵パラメータなど、一般的なDPoP proof検証失敗 |
dpop_proof_signature_invalid | proof署名検証が失敗 |
dpop_binding_mismatch | proof鍵がトークンのcnf.jktと一致しない |
dpop_iat_expired | proofのiatがmaxAgeとclock toleranceを超過 |
dpop_method_mismatch | proofのhtmがリクエストメソッドと一致しない |
dpop_uri_mismatch | proofのhtuがリクエストURIと一致しない |
dpop_ath_mismatch | proofのathがアクセストークンと一致しない |
dpop_nonce_required | proof nonceが期待されるnonceと一致しない |
次のステップ
Section titled “次のステップ”- トークン検証 — JWT検証パイプラインとクレーム処理
- JWKS管理 — 鍵のディスカバリ、キャッシュ、ローテーション
- セキュリティに関する考慮事項 — 本番セキュリティチェックリストとベストプラクティス
- イントロスペクションと失効 — 認可サーバーでのトークンの問い合わせと失効