Securing Verifiable Credentials With DPoP: A Spring Boot Implementation
DPoP binds access tokens to a client's key so even if intercepted, they can't be misused. It's mandatory for EUDI/HAIP 1.0 and supported since Spring Boot 3.5.
Join the DZone community and get the full member experience.
Join For FreeIn my previous article, I demonstrated how to implement OIDC4VCI (credential issuance) and OIDC4VP (credential presentation) using Spring Boot and an Android wallet. This follow-up focuses on a critical security enhancement now mandated by EUDI standards: DPoP (Demonstrating Proof-of-Possession).
The Problem With Bearer Tokens
Traditional Bearer tokens have an inherent weakness: anyone who obtains the token can use it. If an attacker intercepts or steals a Bearer token, they can impersonate the legitimate client until the token expires (or is revoked).
Enter DPoP (RFC 9449)
DPoP (RFC 9449) solves this by binding access tokens to a client’s cryptographic key. Even if an attacker steals a DPoP-bound token, it is useless without the corresponding private key.
Here’s how it works in practice:
- The client generates a key pair and includes the public key in a signed DPoP proof.
- The authorization server binds the issued token to that key (via the
cnf.jktclaim). - On each resource request, the client proves possession of the private key with a fresh DPoP proof.
- The resource server validates that the proof matches the token’s key binding.
Why Now? HAIP 1.0 Mandates DPoP
The OpenID4VC High Assurance Interoperability Profile (HAIP) 1.0, published in December 2025, establishes mandatory requirements for EUDI wallet implementations:
- MUST support sender-constrained tokens via DPoP (RFC 9449)
- MUST support PKCE with S256 (RFC 7636)
- Wallets MUST handle
DPoP-Nonceheaders if servers provide them
DPoP Out of the Box: Since Spring Boot 3.5
Starting with Spring Boot 3.5 (May 2025), native DPoP support is available via:
- Spring Authorization Server 1.5.0 – automatically issues DPoP-bound tokens when clients send a DPoP header
- Spring Security 6.5.0 – auto-validates DPoP proofs on resource servers
The following sequence diagram demonstrates the flow:

Implementation Highlights
In the sections below, we present additions to the existing Authorization & Resource servers (backend) and the Android Wallet (mobile client). Spring Boot versions must be updated in the respective POM files.
Authorization Server
Spring Authorization Server 1.5+ handles DPoP automatically. The key configuration addition is supporting public clients (no client secret) for mobile wallets:
RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("wallet-client")
// Public client support for mobile wallets (PKCE + DPoP, no secret)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientSettings(ClientSettings.builder()
.requireProofKey(true) // PKCE required
.build())
.build();
When a client sends a DPoP header, the authorization server automatically:
- Validates the DPoP proof
- Extracts the public key and computes its thumbprint
- Includes the
cnf.jktclaim in the access token
Resource Server (Issuer)
With Spring Security 6.5+, DPoP validation is enabled by default. The configuration is minimal:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/.well-known/**").permitAll()
.anyRequest().authenticated()
)
// Spring Security 6.5+ auto-enables DPoP validation
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
Spring Security automatically:
- Accepts
Authorization: DPoP <token>scheme - Validates the DPoP proof JWT (signature,
htm,htu,iat,jti) - Verifies the
athclaim matches the access token hash - Confirms the proof's public key matches the token’s
cnf.jkt
Android Wallet
The wallet creates DPoP proofs for each request. In our simplified PoC codebase, we can reuse the wallet key previously used for JWT proofs (rather than managing a separate key pair) and introduce a DPoPManager class:
class DPoPManager(private val walletKeyManager: WalletKeyManager) {
fun createDPoPProof(
httpMethod: String,
httpUri: String,
accessTokenHash: String? = null
): String {
val walletKey = walletKeyManager.getWalletKey()
val header = JWSHeader.Builder(JWSAlgorithm.ES256)
.type(JOSEObjectType("dpop+jwt"))
.jwk(walletKey.toPublicJWK())
.build()
val claimsBuilder = JWTClaimsSet.Builder()
.jwtID(UUID.randomUUID().toString())
.claim("htm", httpMethod)
.claim("htu", httpUri)
.issueTime(Date())
// Include access token hash for resource requests
accessTokenHash?.let { claimsBuilder.claim("ath", it) }
val signedJWT = SignedJWT(header, claimsBuilder.build())
signedJWT.sign(AndroidKeystoreSigner(KEY_ALIAS))
return signedJWT.serialize()
}
fun computeAccessTokenHash(accessToken: String): String {
val hash = MessageDigest.getInstance("SHA-256")
.digest(accessToken.toByteArray(Charsets.US_ASCII))
return Base64.encodeToString(hash,
Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
}
}
And then the usage in the issuance flow can be modified like:
// Token request
val dpopProof = dpopManager.createDPoPProof("POST", tokenUrl)
client.submitForm(tokenUrl, parameters) {
header("DPoP", dpopProof)
}
// Resource request (include ath claim)
val dpopProof = dpopManager.createDPoPProof(
"POST",
credentialUrl,
dpopManager.computeAccessTokenHash(accessToken)
)
client.post(credentialUrl) {
header("Authorization", "DPoP $accessToken")
header("DPoP", dpopProof)
}
Verifying It Works
- Positive test: The full flow completes successfully with DPoP headers
- Negative test: Remove the DPoP header from a request, and Logcat should show:
HTTP 401 Unauthorized
WWW-Authenticate: DPoP error="invalid_request",
error_description="DPoP proof is missing or invalid."
- Debugging: Set a breakpoint in
org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider.authenticate()to step through the validation.
Conclusion
DPoP is no longer optional for EUDI-compliant implementations. The good news: Spring Boot 3.5+ makes adoption straightforward with built-in support in both the Authorization Server and Resource Server. The main implementation effort is on the wallet side, creating fresh DPoP proofs for each request.
Beyond EUDI and mobile wallets, DPoP is a valuable security hardening measure for any OAuth 2.0 implementation where token misuse is a concern.
Opinions expressed by DZone contributors are their own.
Comments