Skip to content

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

Why Use DPoP?

Security Benefits

  1. 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
  2. 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
  3. Edge-Native Security

    • Perfect for distributed edge architectures
    • No shared session state required
    • Stateless verification using JWT
  4. 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

  1. Client generates key pair: Asymmetric key pair (RSA, EC)
  2. Client creates DPoP proof: Signs JWT with private key, includes public key in jwk header
  3. Client sends request with DPoP header: Includes DPoP header with proof JWT
  4. Server validates DPoP proof: Verifies signature, claims, freshness, replay protection
  5. Server binds token to key: Includes cnf claim with JWK thumbprint in access token
  6. Client uses DPoP-bound token: Sends token with fresh DPoP proof for each request

API Reference

DPoP Proof JWT Structure

{
"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"
}
ClaimTypeDescription
jtistringUnique identifier (prevents replay)
htmstringHTTP method (uppercase)
htustringHTTP URL (without query/fragment)
iatnumberIssued at timestamp (within 60 seconds)
athstringAccess token hash (required when using access token)

Token Endpoint with DPoP

POST /token

POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
DPoP: <dpop_proof_jwt>
grant_type=authorization_code
&code=abc123...
&redirect_uri=https://myapp.example.com/callback

Response:

{
"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.1
Authorization: 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 iat validation
  • Each jti can 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)
  • none and 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

References