Skip to main content
Version: v0.13

DPoP and PKCE

The IDK supports modern OAuth 2.0 security extensions that protect against token theft and interception attacks.

PKCE (Proof Key for Code Exchange)​

PKCE (RFC 7636) protects the authorization code flow from interception attacks. It's essential for public clients like mobile apps.

How PKCE Works​

  1. Client generates a random code_verifier
  2. Client creates a code_challenge by hashing the verifier
  3. Authorization request includes the challenge
  4. Token request includes the original verifier
  5. Server verifies that hash(verifier) == challenge

Using PKCE​

// PKCE is enabled by default
val authRequest = oauth2Client.buildAuthorizationRequest {
clientId = "my-client"
redirectUri = "myapp://callback"
scopes = setOf("openid", "profile")
usePkce = true // Default: true
// challengeMethod = "S256" // Default: S256
}

// The code verifier is generated automatically
val codeVerifier = authRequest.codeVerifier

// Code challenge is included in authorization URL
val authUrl = authRequest.authorizationUrl
// ...includes code_challenge and code_challenge_method parameters

// When exchanging the code, include the verifier
val tokens = oauth2Client.exchangeCode(
code = authorizationCode,
redirectUri = "myapp://callback",
codeVerifier = codeVerifier, // Must match the challenge
clientId = "my-client"
)

PKCE Challenge Methods​

MethodDescription
S256SHA-256 hash (recommended)
plainNo transformation (not recommended)

Always use S256 unless the server only supports plain.

DPoP (Demonstration of Proof of Possession)​

DPoP (RFC 9449) binds access tokens to a specific client by requiring proof of key possession for each request.

How DPoP Works​

  1. Client generates a DPoP key pair
  2. Token request includes a DPoP proof JWT
  3. Server issues a DPoP-bound access token
  4. Each API request includes a fresh DPoP proof
  5. Resource server verifies the token is bound to the proof

Enabling DPoP​

// Generate or retrieve a DPoP key
val dpopKey = keyManager.generateKeyAsync(
alg = SignatureAlgorithm.ES256,
keyId = "dpop-key"
)

// Build authorization request with DPoP
val authRequest = oauth2Client.buildAuthorizationRequest {
clientId = "my-client"
redirectUri = "myapp://callback"
scopes = setOf("openid", "api:read")
useDpop = true
dpopKey = dpopKey
}

// Exchange code with DPoP proof
val tokens = oauth2Client.exchangeCodeWithDpop(
code = authorizationCode,
redirectUri = "myapp://callback",
codeVerifier = codeVerifier,
clientId = "my-client",
dpopKey = dpopKey,
tokenEndpoint = "https://auth.example.com/token"
)

// The access token is now DPoP-bound
// tokens.tokenType == "DPoP"

Making DPoP-Protected Requests​

// Create a DPoP proof for each request
val dpopProof = oauth2Client.createDpopProof(
key = dpopKey,
httpMethod = "GET",
httpUri = "https://api.example.com/resource",
accessToken = tokens.accessToken // Optional: for token binding
)

// Include both the token and proof in the request
val request = HttpRequest.builder()
.url("https://api.example.com/resource")
.header("Authorization", "DPoP ${tokens.accessToken}")
.header("DPoP", dpopProof)
.build()

val response = httpClient.execute(request)

DPoP Proof Structure​

A DPoP proof is a JWT with these claims:

{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": { /* public key */ }
}
{
"jti": "unique-id",
"htm": "POST",
"htu": "https://auth.example.com/token",
"iat": 1234567890,
"ath": "base64url(sha256(access_token))" // If binding to token
}

DPoP Nonce Handling​

Some servers require a nonce in DPoP proofs:

try {
val tokens = oauth2Client.exchangeCodeWithDpop(...)
} catch (e: DpopNonceRequiredException) {
// Server provided a nonce, retry with it
val tokens = oauth2Client.exchangeCodeWithDpop(
code = authorizationCode,
redirectUri = "myapp://callback",
codeVerifier = codeVerifier,
clientId = "my-client",
dpopKey = dpopKey,
tokenEndpoint = "https://auth.example.com/token",
dpopNonce = e.nonce // Use the provided nonce
)
}

Combining PKCE and DPoP​

For maximum security, use both PKCE and DPoP:

// Generate DPoP key
val dpopKey = keyManager.generateKeyAsync(
alg = SignatureAlgorithm.ES256,
keyId = "dpop-key"
)

// Authorization request with both
val authRequest = oauth2Client.buildAuthorizationRequest {
clientId = "my-client"
redirectUri = "myapp://callback"
scopes = setOf("openid", "api:read")
usePkce = true // Protect authorization code
useDpop = true // Bind tokens to key
dpopKey = dpopKey
}

// After callback, exchange with both protections
val tokens = oauth2Client.exchangeCodeWithDpop(
code = authorizationCode,
redirectUri = "myapp://callback",
codeVerifier = authRequest.codeVerifier, // PKCE
clientId = "my-client",
dpopKey = dpopKey, // DPoP
tokenEndpoint = "https://auth.example.com/token"
)

Best Practices​

Always use PKCE for public clients. This protects against authorization code interception.

Use S256 challenge method. Plain challenge method provides no security benefit.

Consider DPoP for high-security scenarios. DPoP prevents token theft even if the token is leaked.

Protect DPoP keys appropriately. Use hardware-backed storage when available.

Handle nonce requirements gracefully. Some servers require nonces; be prepared to retry.

Generate fresh proofs for each request. Reusing DPoP proofs is a security risk.