DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Jakarta EE 11 and the Road Ahead With Jakarta EE 12
  • Thumbnail Generator Microservice for PDF in Spring Boot
  • Is Spring AI Strong Enough for AI?

Trending

  • Why We Chose Iceberg Over Delta After Evaluating Both at Scale
  • 11 Agentic Testing Tools to Know in 2026
  • LLM Integration in Enterprise Applications: A Practical Guide
  • Self-Hosted Inference Doesn’t Have to Be a Nightmare: How to Use GPUStack
  1. DZone
  2. Data Engineering
  3. Data
  4. Securing Verifiable Credentials With DPoP: A Spring Boot Implementation

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.

By 
Kyriakos Mandalas user avatar
Kyriakos Mandalas
DZone Core CORE ·
Jan. 05, 26 · Analysis
Likes (4)
Comment
Save
Tweet
Share
3.7K Views

Join the DZone community and get the full member experience.

Join For Free

In 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:

  1. The client generates a key pair and includes the public key in a signed DPoP proof.
  2. The authorization server binds the issued token to that key (via the cnf.jkt claim).
  3. On each resource request, the client proves possession of the private key with a fresh DPoP proof.
  4. 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-Nonce headers 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:

Sequence Diagram


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:

Java
 
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.jkt claim in the access token

Resource Server (Issuer)

With Spring Security 6.5+, DPoP validation is enabled by default. The configuration is minimal:

Java
 
@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 ath claim 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:

Kotlin
 
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:

Kotlin
 
// 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:
Markdown
 
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.

Implementation Spring Security Spring Boot Data Types

Opinions expressed by DZone contributors are their own.

Related

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Jakarta EE 11 and the Road Ahead With Jakarta EE 12
  • Thumbnail Generator Microservice for PDF in Spring Boot
  • Is Spring AI Strong Enough for AI?

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook