コンテンツにスキップ

DPoP検証

DPoPとは?

DPoP(Demonstrating Proof of Possession、RFC 9449)は、アクセストークンをクライアントの暗号鍵ペアにバインドするメカニズムです。標準的なBearerトークンは、所持している人なら誰でも使用できます。DPoPバインドトークンは、対応する秘密鍵を保持しているクライアントのみが使用できます。

クライアントは各リクエストにDPoPプルーフ(署名されたJWT)を含めます。リソースサーバーは以下を検証します:

  1. プルーフがアクセストークンにバインドされた鍵で署名されていること
  2. プルーフが現在のHTTPメソッドとURLに一致すること
  3. プルーフがリプレイされていないこと

リソースサーバーの役割

@authrim/serverを使用するリソースサーバーとして、DPoPに関する責任は以下の通りです:

責任SDKが処理アプリケーションが処理
DPoPプルーフJWTの解析はい
プルーフ署名の検証はい
typalgjwkヘッダーの検証はい
htmhtuiatクレームの検証はい
ath(アクセストークンハッシュ)の検証はい
JWK Thumbprintバインディング(cnf.jkt)の検証はい
JWK内の秘密鍵パラメータの拒否はい
jtiリプレイ保護はい
DPoP nonce管理はい

validateDPoP API

const dpopResult = await authrim.validateDPoP(proof, options);

パラメータ

パラメータ説明
proofstringDPoPリクエストヘッダーからの生のDPoPプルーフJWT
optionsDPoPValidationOptions検証パラメータ

DPoPValidationOptions

interface DPoPValidationOptions {
method: string;
url: string;
accessTokenHash?: string;
expectedThumbprint?: string;
allowedAlgorithms?: string[];
maxAgeSeconds?: number;
expectedNonce?: string;
}
オプションデフォルト説明
methodstring現在のリクエストのHTTPメソッド(例: 'GET''POST'
urlstring現在のリクエストの完全なURL(スキーム + ホスト + パス)
accessTokenHashstringath検証用のアクセストークンのBase64urlエンコードSHA-256ハッシュ
expectedThumbprintstringトークンのcnf.jktクレームからの期待されるJWK Thumbprint
allowedAlgorithmsstring[]すべてサポート受け入れるプルーフ署名アルゴリズムを制限
maxAgeSecondsnumber300プルーフの最大有効期間(秒)(iatに基づく)
expectedNoncestringプルーフに含まれている必要があるサーバー発行の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 JWK
const thumbprint = await calculateJwkThumbprint(publicJwk);
// Verify a thumbprint matches a JWK (timing-safe)
const isValid = await verifyJwkThumbprint(publicJwk, expectedThumbprint);

Thumbprintの計算はRFC 7638に従います:

  1. 鍵タイプに必要なメンバーを抽出(ECの場合はktycrvxy、RSAの場合はktyen
  2. ソートされたキーと空白なしのJSONオブジェクトとしてシリアライズ
  3. SHA-256ハッシュを計算
  4. Base64urlとしてエンコード

jtiリプレイ保護

// Example: DPoP jti replay protection with Redis
const 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 handler
async 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 nonce
const nonce = generateSecureNonce();
storeNonce(nonce);
// 2. Return the nonce to the client in the response header
res.setHeader('DPoP-Nonce', nonce);
// 3. On subsequent requests, verify the nonce
const 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プルーフのiatmaxAgeSecondsを超過
DPoPMethodMismatchErrorプルーフのhtmがリクエストメソッドと一致しない
DPoPUrlMismatchErrorプルーフのhtuがリクエストURLと一致しない
DPoPNonceMismatchErrorプルーフのnonceが期待されるnonceと一致しない
DPoPPrivateKeyErrorプルーフのJWKに秘密鍵パラメータが含まれている

次のステップ