DPoP検証
DPoPとは?
DPoP(Demonstrating Proof of Possession、RFC 9449)は、アクセストークンをクライアントの暗号鍵ペアにバインドするメカニズムです。標準的なBearerトークンは、所持している人なら誰でも使用できます。DPoPバインドトークンは、対応する秘密鍵を保持しているクライアントのみが使用できます。
クライアントは各リクエストにDPoPプルーフ(署名されたJWT)を含めます。リソースサーバーは以下を検証します:
- プルーフがアクセストークンにバインドされた鍵で署名されていること
- プルーフが現在のHTTPメソッドとURLに一致すること
- プルーフがリプレイされていないこと
リソースサーバーの役割
@authrim/serverを使用するリソースサーバーとして、DPoPに関する責任は以下の通りです:
| 責任 | SDKが処理 | アプリケーションが処理 |
|---|---|---|
| DPoPプルーフJWTの解析 | はい | — |
| プルーフ署名の検証 | はい | — |
typ、alg、jwkヘッダーの検証 | はい | — |
htm、htu、iatクレームの検証 | はい | — |
ath(アクセストークンハッシュ)の検証 | はい | — |
JWK Thumbprintバインディング(cnf.jkt)の検証 | はい | — |
| JWK内の秘密鍵パラメータの拒否 | はい | — |
jtiリプレイ保護 | — | はい |
| DPoP nonce管理 | — | はい |
validateDPoP API
const dpopResult = await authrim.validateDPoP(proof, options);パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
proof | string | DPoPリクエストヘッダーからの生のDPoPプルーフJWT |
options | DPoPValidationOptions | 検証パラメータ |
DPoPValidationOptions
interface DPoPValidationOptions { method: string; url: string; accessTokenHash?: string; expectedThumbprint?: string; allowedAlgorithms?: string[]; maxAgeSeconds?: number; expectedNonce?: string;}| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
method | string | — | 現在のリクエストのHTTPメソッド(例: 'GET'、'POST') |
url | string | — | 現在のリクエストの完全なURL(スキーム + ホスト + パス) |
accessTokenHash | string | — | ath検証用のアクセストークンのBase64urlエンコードSHA-256ハッシュ |
expectedThumbprint | string | — | トークンのcnf.jktクレームからの期待されるJWK Thumbprint |
allowedAlgorithms | string[] | すべてサポート | 受け入れるプルーフ署名アルゴリズムを制限 |
maxAgeSeconds | number | 300 | プルーフの最大有効期間(秒)(iatに基づく) |
expectedNonce | string | — | プルーフに含まれている必要があるサーバー発行のnonce |
DPoP検証フロー
flowchart TD
A["DPoPプルーフを受信"] --> B["JWTヘッダーを解析
(typ: dpop+jwt)"]
B --> C["アルゴリズムチェック
(none拒否, 許可リスト確認)"]
C --> D["公開鍵を抽出
(秘密鍵パラメータを拒否)"]
D --> E["プルーフ署名を検証"]
E --> F["htmを検証
(HTTPメソッドと一致)"]
F --> G["htuを検証
(リクエストURLと一致)"]
G --> H["iatを検証
(maxAgeSeconds以内)"]
H --> I{"athあり?"}
I -->|はい| J["アクセストークンハッシュを検証"]
I -->|いいえ| K["athチェックをスキップ"]
J --> L{"expectedThumbprint?"}
K --> L
L -->|はい| M["JWK Thumbprintを検証
(タイミングセーフ)"]
L -->|いいえ| N["バインディングチェックをスキップ"]
M --> O["DPoP結果を返却"]
N --> O
完全なDPoP検証の例
DPoPバインドリクエストを検証する典型的なフローを示します:
import { AuthrimServer } from '@authrim/server';
const authrim = new AuthrimServer({ 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);
// 3. If token is DPoP-bound, validate the proof if (tokenResult.tokenType === 'DPoP' && dpopProof) { await authrim.validateDPoP(dpopProof, { method: req.method, url: `${req.protocol}://${req.hostname}${req.path}`, accessTokenHash: await computeAccessTokenHash(accessToken), expectedThumbprint: tokenResult.claims.cnf?.jkt, }); }
return tokenResult;}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リプレイ保護
// Example: DPoP jti replay protection with Redisconst DPOP_JTI_TTL = 300; // 5 minutes (matches maxAgeSeconds)
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); const dpopResult = await authrim.validateDPoP(dpopProof, { /* ... */ });
// Check for jti replay const isNew = await checkDPoPReplay(dpopResult.jti); if (!isNew) { throw new Error('DPoP proof replay detected'); }
// Proceed with the request}jti追跡に関する重要な考慮事項:
- TTLは
maxAgeSecondsに一致させる — キャッシュエントリのTTLをmaxAgeSecondsと同じ値(デフォルト: 300秒)に設定します。エントリは自動的に期限切れになります。 - 分散キャッシュが必要 — マルチサーバーデプロイメントでは、すべてのサーバーが同じ
jti値を参照できるように共有キャッシュ(Redis、Memcached)を使用します。 - 高カーディナリティ — 各DPoPプルーフは一意の
jtiを持ちます。高トラフィックのAPIでは、キャッシュがボリュームを処理できることを確認してください。
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, url: requestUrl, expectedNonce: getStoredNonce(),});クライアントが期待されるnonceなしでリクエストを送信した場合、以下で応答します:
res.status(401).json({ error: 'use_dpop_nonce' });res.setHeader('DPoP-Nonce', newNonce);クライアントはnonceを含む新しいDPoPプルーフでリクエストを再試行する必要があります。
セキュリティ: 秘密鍵の拒否
DPoPエラータイプ
| エラー | 説明 |
|---|---|
DPoPProofError | 一般的なDPoPプルーフ検証の失敗 |
DPoPAlgorithmError | プルーフがalg: noneまたはサポートされていないアルゴリズムを使用 |
DPoPSignatureError | プルーフの署名検証が失敗 |
DPoPThumbprintMismatchError | プルーフの鍵がトークンのcnf.jktと一致しない |
DPoPExpiredError | プルーフのiatがmaxAgeSecondsを超過 |
DPoPMethodMismatchError | プルーフのhtmがリクエストメソッドと一致しない |
DPoPUrlMismatchError | プルーフのhtuがリクエストURLと一致しない |
DPoPNonceMismatchError | プルーフのnonceが期待されるnonceと一致しない |
DPoPPrivateKeyError | プルーフのJWKに秘密鍵パラメータが含まれている |
次のステップ
- トークン検証 — JWT検証パイプラインとクレーム処理
- JWKS管理 — 鍵のディスカバリ、キャッシュ、ローテーション
- セキュリティに関する考慮事項 — 本番セキュリティチェックリストとベストプラクティス
- イントロスペクションと失効 — 認可サーバーでのトークンの問い合わせと失効