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
- RFC: RFC 9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- Status: Fully Implemented
- Supported Endpoints:
/token,/userinfo
Why Use DPoP?
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
-
OAuth 2.1 Ready
- DPoP is part of OAuth 2.1 draft
- Recommended by OAuth working group
- Industry-leading security posture
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
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
DPoP Proof JWT Structure
Header
{ "typ": "dpop+jwt", "alg": "RS256", "jwk": { "kty": "RSA", "n": "...", "e": "AQAB" }}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
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
GET /userinfo HTTP/1.1Authorization: DPoP <access_token>DPoP: <dpop_proof_jwt>Note: Authorization scheme is DPoP (not Bearer)
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
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
- 60-second window for
iatvalidation - Each
jtican only be used once - Generate new proof for each request
Supported Algorithms
- RS256, RS384, RS512 (RSA)
- ES256, ES384, ES512 (ECDSA)
- PS256, PS384, PS512 (RSA-PSS)
noneand symmetric algorithms are rejected
Comparison: Bearer vs DPoP
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)
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
{ "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
- FAPI 2.0: DPoP recommended for token binding
- OAuth 2.1: DPoP is part of the draft specification