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
- Client generates a random
code_verifier - Client creates a
code_challengeby hashing the verifier - Authorization request includes the challenge
- Token request includes the original verifier
- Server verifies that
hash(verifier) == challenge
Using PKCE
- Android/kotlin
- iOS/Swift
// 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 is enabled by default
let authRequest = try await oauth2Client.buildAuthorizationRequest { builder in
builder.clientId = "my-client"
builder.redirectUri = "myapp://callback"
builder.scopes = Set(["openid", "profile"])
builder.usePkce = true // Default: true
// builder.challengeMethod = "S256" // Default: S256
}
// The code verifier is generated automatically
let codeVerifier = authRequest.codeVerifier
// Code challenge is included in authorization URL
let authUrl = authRequest.authorizationUrl
// ...includes code_challenge and code_challenge_method parameters
// When exchanging the code, include the verifier
let tokens = try await oauth2Client.exchangeCode(
code: authorizationCode,
redirectUri: "myapp://callback",
codeVerifier: codeVerifier, // Must match the challenge
clientId: "my-client"
)
PKCE Challenge Methods
| Method | Description |
|---|---|
| S256 | SHA-256 hash (recommended) |
| plain | No 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
- Client generates a DPoP key pair
- Token request includes a DPoP proof JWT
- Server issues a DPoP-bound access token
- Each API request includes a fresh DPoP proof
- Resource server verifies the token is bound to the proof
Enabling DPoP
- Android/kotlin
- iOS/Swift
// 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"
// Generate or retrieve a DPoP key
let dpopKey = try await keyManager.generateKeyAsync(
alg: .es256,
keyId: "dpop-key"
)
// Build authorization request with DPoP
let authRequest = try await oauth2Client.buildAuthorizationRequest { builder in
builder.clientId = "my-client"
builder.redirectUri = "myapp://callback"
builder.scopes = Set(["openid", "api:read"])
builder.useDpop = true
builder.dpopKey = dpopKey
}
// Exchange code with DPoP proof
let tokens = try await 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
- Android/kotlin
- iOS/Swift
// 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)
// Create a DPoP proof for each request
let dpopProof = try await 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
var request = URLRequest(url: URL(string: "https://api.example.com/resource")!)
request.setValue("DPoP \(tokens.accessToken)", forHTTPHeaderField: "Authorization")
request.setValue(dpopProof, forHTTPHeaderField: "DPoP")
let response = try await urlSession.data(for: 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:
- Android/kotlin
- iOS/Swift
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
)
}
do {
let tokens = try await oauth2Client.exchangeCodeWithDpop(...)
} catch let error as DpopNonceRequiredException {
// Server provided a nonce, retry with it
let tokens = try await oauth2Client.exchangeCodeWithDpop(
code: authorizationCode,
redirectUri: "myapp://callback",
codeVerifier: codeVerifier,
clientId: "my-client",
dpopKey: dpopKey,
tokenEndpoint: "https://auth.example.com/token",
dpopNonce: error.nonce // Use the provided nonce
)
}
Combining PKCE and DPoP
For maximum security, use both PKCE and DPoP:
- Android/kotlin
- iOS/Swift
// 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"
)
// Generate DPoP key
let dpopKey = try await keyManager.generateKeyAsync(
alg: .es256,
keyId: "dpop-key"
)
// Authorization request with both
let authRequest = try await oauth2Client.buildAuthorizationRequest { builder in
builder.clientId = "my-client"
builder.redirectUri = "myapp://callback"
builder.scopes = Set(["openid", "api:read"])
builder.usePkce = true // Protect authorization code
builder.useDpop = true // Bind tokens to key
builder.dpopKey = dpopKey
}
// After callback, exchange with both protections
let tokens = try await 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.