DPoP
DPoP (RFC 9449) is a modern OAuth 2.0 security extension that binds access tokens to a specific client’s cryptographic key, preventing token theft and replay attacks even if tokens are intercepted.
Overview
Section titled “Overview”- RFC: RFC 9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- Status: Fully Implemented
- Supported Endpoints:
/token,/userinfo
Why Use DPoP?
Section titled “Why Use DPoP?”Security Benefits
Section titled “Security Benefits”-
Token Theft Protection
- Access tokens are cryptographically bound to client’s key pair
- Stolen tokens cannot be used by attackers (no private key)
- Eliminates bearer token vulnerabilities
-
Replay Attack Prevention
- Each request requires fresh DPoP proof (60-second window)
- Unique
jti(JWT ID) enforced per proof - Automatic replay detection via nonce tracking
-
Edge-Native Security
- Perfect for distributed edge architectures
- No shared session state required
- Stateless verification using JWT
-
Modern OAuth Security
- DPoP is standardized as RFC 9449
- Useful for OAuth 2.1-style and FAPI deployments
- Stronger posture than bearer-only access tokens
Use Cases
Section titled “Use Cases”- Financial Services (FAPI compliance)
- Healthcare (HIPAA-compliant token security)
- High-Security APIs (government, banking, enterprise)
- Mobile Apps (secure token storage and usage)
- Zero Trust Architecture
How DPoP Works
Section titled “How DPoP Works”Flow Overview
Section titled “Flow Overview”- Client generates key pair: Asymmetric key pair (RSA, EC)
- Client creates DPoP proof: Signs JWT with private key, includes public key in
jwkheader - Client sends request with DPoP header: Includes
DPoPheader with proof JWT - Server validates DPoP proof: Verifies signature, claims, freshness, replay protection
- Server binds token to key: Includes
cnfclaim with JWK thumbprint in access token - Client uses DPoP-bound token: Sends token with fresh DPoP proof for each request
API Reference
Section titled “API Reference”DPoP Proof JWT Structure
Section titled “DPoP Proof JWT Structure”Header
Section titled “Header”{ "typ": "dpop+jwt", "alg": "RS256", "jwk": { "kty": "RSA", "n": "...", "e": "AQAB" }}Payload (Claims)
Section titled “Payload (Claims)”{ "jti": "unique-identifier-123", "htm": "POST", "htu": "https://auth.example.com/token", "iat": 1699876543, "ath": "base64url-hash-of-access-token"}| Claim | Type | Description |
|---|---|---|
jti | string | Unique identifier (prevents replay) |
htm | string | HTTP method (uppercase) |
htu | string | HTTP URL (without query/fragment) |
iat | number | Issued at timestamp (within 60 seconds) |
ath | string | Access token hash (required when using access token) |
Token Endpoint with DPoP
Section titled “Token Endpoint with DPoP”POST /token
POST /token HTTP/1.1Content-Type: application/x-www-form-urlencodedAuthorization: Basic <base64(client_id:client_secret)>DPoP: <dpop_proof_jwt>
grant_type=authorization_code&code=abc123...&redirect_uri=https://myapp.example.com/callbackResponse:
{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "DPoP", "expires_in": 3600, "id_token": "eyJhbGciOiJSUzI1NiJ9...", "refresh_token": "eyJhbGciOiJSUzI1NiJ9..."}Note: token_type is DPoP (not Bearer)
UserInfo Endpoint with DPoP
Section titled “UserInfo Endpoint with DPoP”GET /userinfo HTTP/1.1Authorization: DPoP <access_token>DPoP: <dpop_proof_jwt>Note: Authorization scheme is DPoP (not Bearer)
Usage Example
Section titled “Usage Example”import { SignJWT } from 'jose';
class DPoPClient { private keyPair: CryptoKeyPair | null = null; private publicKeyJwk: any = null;
async initialize() { this.keyPair = await crypto.subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['sign', 'verify'] ); this.publicKeyJwk = await crypto.subtle.exportKey('jwk', this.keyPair.publicKey); }
async createProof(method: string, url: string, accessToken?: string): Promise<string> { const jti = crypto.randomUUID(); const iat = Math.floor(Date.now() / 1000); const parsedUrl = new URL(url); const htu = `${parsedUrl.protocol}//${parsedUrl.host}${parsedUrl.pathname}`;
const claims: any = { jti, htm: method.toUpperCase(), htu, iat };
if (accessToken) { const encoder = new TextEncoder(); const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(accessToken)); claims.ath = base64url(new Uint8Array(hashBuffer)); }
return await new SignJWT(claims) .setProtectedHeader({ typ: 'dpop+jwt', alg: 'RS256', jwk: this.publicKeyJwk }) .sign(this.keyPair!.privateKey); }
async requestToken(authCode: string, clientId: string, clientSecret: string, redirectUri: string) { const dpopProof = await this.createProof('POST', 'https://auth.example.com/token');
const response = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${btoa(clientId + ':' + clientSecret)}`, 'DPoP': dpopProof, }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri, }), });
return await response.json(); }
async getUserInfo(accessToken: string) { const dpopProof = await this.createProof('GET', 'https://auth.example.com/userinfo', accessToken);
return await fetch('https://auth.example.com/userinfo', { headers: { 'Authorization': `DPoP ${accessToken}`, 'DPoP': dpopProof, }, }).then(r => r.json()); }}Security Considerations
Section titled “Security Considerations”Key Pair Management
Section titled “Key Pair Management”- Generate strong keys (RSA 2048+ or EC P-256+)
- Store private key securely (secure enclave, encrypted storage)
- Never transmit private key
- Rotate keys periodically
DPoP Proof Freshness
Section titled “DPoP Proof Freshness”- 60-second window for
iatvalidation - Each
jtican only be used once - Generate new proof for each request
Supported Algorithms
Section titled “Supported Algorithms”- RS256, RS384, RS512 (RSA)
- ES256, ES384, ES512 (ECDSA)
- PS256, PS384, PS512 (RSA-PSS)
noneand symmetric algorithms are rejected
Comparison: Bearer vs DPoP
Section titled “Comparison: Bearer vs DPoP”Bearer Token (Traditional)
Section titled “Bearer Token (Traditional)”Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...- Token can be used by anyone who has it
- Stolen tokens are fully usable
- No cryptographic binding
DPoP Token (Secure)
Section titled “DPoP Token (Secure)”Authorization: DPoP eyJhbGciOiJSUzI1NiJ9...DPoP: <proof_jwt>- Token bound to client’s private key
- Stolen tokens unusable without private key
- Cryptographic proof of possession
Error Responses
Section titled “Error Responses”{ "error": "invalid_dpop_proof", "error_description": "DPoP proof signature verification failed"}{ "error": "use_dpop_nonce", "error_description": "DPoP proof jti already used (replay attack detected)"}Compliance
Section titled “Compliance”- FAPI 2.0: DPoP recommended for token binding
- OAuth 2.1-style deployments: DPoP complements stronger token protection profiles