In the OAuth 2.0 and OpenID Connect ecosystems, bearer tokens have long been the standard for API access. However, bearer tokens have a fundamental security weakness: any party in possession of the token can use it. If a token is intercepted in transit, leaked in logs, or stolen from a database, an attacker can impersonate the legitimate client indefinitely. Demonstrating Proof-of-Possession (DPoP) addresses this vulnerability by cryptographically binding tokens to a specific client, ensuring that only the client holding the corresponding private key can use the token.
What is DPoP?
DPoP is an OAuth 2.0 extension (RFC 9449) that enables a client to prove possession of a private key when using an access token. The mechanism works as follows:
- The client generates a public/private key pair and creates a DPoP proof JWT signed with the private key
- The DPoP proof is sent with token requests and API calls
- The authorization server binds issued access tokens to the client's public key
- Resource servers verify that API calls include a valid DPoP proof signed by the same key
Key Concepts:
- DPoP Proof: A JWT signed by the client proving possession of the private key
- DPoP Access Token: A token cryptographically bound to the client's public key
- Public Key Confirmation: The token includes a hash of the client's public key (
cnfclaim) - Replay Prevention: Each DPoP proof includes a unique
jtiand timestamp
Why DPoP is Essential for Java Applications
- Token Theft Mitigation: Stolen tokens cannot be used without the private key
- Binding to Client Instance: Tokens are bound to a specific client instance
- Replay Protection: Each request must have a unique, single-use proof
- Defense in Depth: Adds cryptographic proof beyond simple bearer tokens
- Standardized Approach: Works with existing OAuth 2.0 infrastructure
- Complementary to mTLS: Provides proof-of-possession without client certificates
DPoP Flow
Client Authorization Server | | |-- 1. Generate key pair | |-- 2. POST /token + DPoP proof ----------------->| | | |<-- 3. Access token (bound to public key) -------| | | |-- 4. GET /resource + DPoP proof + token ------->| | | |<-- 5. Protected resource -----------------------|
Implementing DPoP in Java
1. Maven Dependencies
<dependencies> <!-- Nimbus JOSE + JWT for JWT handling --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37.3</version> </dependency> <!-- Spring Security OAuth2 Client --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>6.2.0</version> </dependency> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Bouncy Castle for additional algorithms --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78</version> </dependency> <!-- Apache HttpClient --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.3.1</version> </dependency> </dependencies>
2. Core DPoP Implementation
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;
import com.nimbusds.jwt.*;
import com.nimbusds.jwt.util.DateUtils;
import java.security.*;
import java.time.Instant;
import java.util.*;
/**
* DPoP Client implementation for generating and managing DPoP proofs
*/
public class DPoPClient {
private final JWK jwk;
private final JWSSigner signer;
private final String algorithm;
private final Set<String> usedNonces;
public DPoPClient() throws JOSEException {
// Generate EC key pair (recommended for DPoP)
this.jwk = new ECKeyGenerator(Curve.P_256)
.keyID(UUID.randomUUID().toString())
.generate();
this.signer = new ECDSASigner(jwk.toECKey().toECPrivateKey());
this.algorithm = "ES256";
this.usedNonces = Collections.synchronizedSet(new HashSet<>());
}
public DPoPClient(JWK jwk, JWSSigner signer, String algorithm) {
this.jwk = jwk;
this.signer = signer;
this.algorithm = algorithm;
this.usedNonces = Collections.synchronizedSet(new HashSet<>());
}
/**
* Generate a DPoP proof for a specific HTTP request
*/
public String generateDPoPProof(String method, String uri,
String accessToken, String serverNonce)
throws JOSEException {
Instant now = Instant.now();
String jti = UUID.randomUUID().toString();
// Create JWT claims
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.claim("htm", method) // HTTP Method
.claim("htu", uri) // HTTP URI (without query params)
.jwtID(jti) // Unique identifier
.issueTime(Date.from(now))
.expirationTime(Date.from(now.plusSeconds(60))); // Short-lived
// Add access token hash if provided
if (accessToken != null) {
String ath = hashAccessToken(accessToken);
claimsBuilder.claim("ath", ath);
}
// Add server nonce if provided
if (serverNonce != null && !serverNonce.isEmpty()) {
claimsBuilder.claim("nonce", serverNonce);
}
JWTClaimsSet claims = claimsBuilder.build();
// Create JWS header with JWK thumbprint
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(algorithm))
.type(new JOSEObjectType("dpop+jwt"))
.jwk(jwk.toPublicJWK())
.build();
// Create and sign JWT
SignedJWT signedJWT = new SignedJWT(header, claims);
signedJWT.sign(signer);
// Track used nonce to prevent replay
usedNonces.add(jti);
// Schedule cleanup of used nonces
scheduleNonceCleanup(jti, claims.getExpirationTime());
return signedJWT.serialize();
}
/**
* Generate DPoP proof for token request (no access token)
*/
public String generateTokenRequestProof(String method, String uri,
String serverNonce)
throws JOSEException {
return generateDPoPProof(method, uri, null, serverNonce);
}
/**
* Generate DPoP proof for API request (with access token)
*/
public String generateAPIRequestProof(String method, String uri,
String accessToken, String serverNonce)
throws JOSEException {
return generateDPoPProof(method, uri, accessToken, serverNonce);
}
/**
* Calculate access token hash (ath claim)
*/
private String hashAccessToken(String accessToken) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(accessToken.getBytes());
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(Arrays.copyOf(hash, 16)); // First 128 bits
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
/**
* Get JWK thumbprint for the public key
*/
public String getJWKThumbprint() throws JOSEException {
return jwk.computeThumbprint().toString();
}
/**
* Get public key as JWK
*/
public JWK getPublicJWK() {
return jwk.toPublicJWK();
}
/**
* Clean up used nonces after expiration
*/
private void scheduleNonceCleanup(String jti, Date expiration) {
Timer timer = new Timer(true);
timer.schedule(new TimerTask() {
@Override
public void run() {
usedNonces.remove(jti);
}
}, expiration);
}
/**
* Check if a nonce has been used (for server-side validation)
*/
public boolean isNonceUsed(String jti) {
return usedNonces.contains(jti);
}
public static void main(String[] args) throws Exception {
System.out.println("=== DPoP Client Example ===\n");
// Initialize DPoP client
DPoPClient dpopClient = new DPoPClient();
System.out.println("JWK Thumbprint: " + dpopClient.getJWKThumbprint());
System.out.println("Public JWK: " + dpopClient.getPublicJWK().toJSONString());
// 1. Token request proof
String tokenProof = dpopClient.generateTokenRequestProof(
"POST",
"https://auth.example.com/token",
null
);
System.out.println("\nToken Request DPoP Proof:");
System.out.println(" Proof: " + tokenProof);
// Parse and display claims
SignedJWT parsedProof = SignedJWT.parse(tokenProof);
JWTClaimsSet claims = parsedProof.getJWTClaimsSet();
System.out.println(" Claims:");
System.out.println(" HTM: " + claims.getClaim("htm"));
System.out.println(" HTU: " + claims.getClaim("htu"));
System.out.println(" JTI: " + claims.getJWTID());
System.out.println(" IAT: " + claims.getIssueTime());
System.out.println(" EXP: " + claims.getExpirationTime());
// 2. API request proof with access token
String accessToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
String apiProof = dpopClient.generateAPIRequestProof(
"GET",
"https://api.example.com/resource",
accessToken,
null
);
System.out.println("\nAPI Request DPoP Proof:");
System.out.println(" Proof: " + apiProof);
parsedProof = SignedJWT.parse(apiProof);
claims = parsedProof.getJWTClaimsSet();
System.out.println(" Claims:");
System.out.println(" HTM: " + claims.getClaim("htm"));
System.out.println(" HTU: " + claims.getClaim("htu"));
System.out.println(" ATH: " + claims.getClaim("ath"));
System.out.println(" JTI: " + claims.getJWTID());
}
}
3. DPoP-Enabled HTTP Client
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.net.URI;
/**
* HTTP Client with DPoP support
*/
public class DPoPHttpClient {
private final CloseableHttpClient httpClient;
private final DPoPClient dpopClient;
private String accessToken;
private String serverNonce;
public DPoPHttpClient(DPoPClient dpopClient) {
this.httpClient = HttpClients.createDefault();
this.dpopClient = dpopClient;
}
/**
* Obtain access token using DPoP
*/
public TokenResponse obtainToken(String tokenEndpoint,
String clientId,
String clientSecret) throws Exception {
// Generate DPoP proof
String dpopProof = dpopClient.generateTokenRequestProof(
"POST",
tokenEndpoint,
serverNonce
);
// Create token request
HttpUriRequestBase request = new HttpUriRequestBase("POST",
URI.create(tokenEndpoint));
request.setHeader("Content-Type", "application/x-www-form-urlencoded");
request.setHeader("DPoP", dpopProof);
String body = String.format(
"grant_type=client_credentials&client_id=%s&client_secret=%s",
clientId, clientSecret
);
request.setEntity(new StringEntity(body));
// Execute request
return httpClient.execute(request, response -> {
if (response.getCode() == 200) {
// Parse response
TokenResponse tokenResponse = parseTokenResponse(response);
// Store access token
this.accessToken = tokenResponse.accessToken;
// Check for DPoP-Nonce header
if (response.containsHeader("DPoP-Nonce")) {
this.serverNonce = response.getHeader("DPoP-Nonce").getValue();
}
return tokenResponse;
} else {
throw new RuntimeException("Token request failed: " + response.getCode());
}
});
}
/**
* Make authenticated API call with DPoP proof
*/
public String callApi(String method, String uri, String body) throws Exception {
if (accessToken == null) {
throw new IllegalStateException("No access token available");
}
// Generate DPoP proof
String dpopProof = dpopClient.generateAPIRequestProof(
method,
uri,
accessToken,
serverNonce
);
// Create API request
HttpUriRequestBase request = new HttpUriRequestBase(method, URI.create(uri));
request.setHeader("Authorization", "DPoP " + accessToken);
request.setHeader("DPoP", dpopProof);
if (body != null) {
request.setEntity(new StringEntity(body));
}
// Execute request
return httpClient.execute(request, response -> {
// Update nonce if provided
if (response.containsHeader("DPoP-Nonce")) {
this.serverNonce = response.getHeader("DPoP-Nonce").getValue();
}
if (response.getCode() == 200) {
return new String(response.getEntity().getContent().readAllBytes());
} else if (response.getCode() == 401) {
// Token might be invalid or expired
throw new SecurityException("Authentication failed");
} else {
throw new RuntimeException("API call failed: " + response.getCode());
}
});
}
private TokenResponse parseTokenResponse(ClassicHttpResponse response) throws Exception {
String json = new String(response.getEntity().getContent().readAllBytes());
// Simple JSON parsing (use Jackson in production)
return new TokenResponse();
}
public static class TokenResponse {
public String accessToken;
public String tokenType;
public int expiresIn;
}
public static void main(String[] args) throws Exception {
System.out.println("=== DPoP HTTP Client Example ===\n");
// Initialize DPoP client
DPoPClient dpopClient = new DPoPClient();
DPoPHttpClient client = new DPoPHttpClient(dpopClient);
// Obtain token
TokenResponse token = client.obtainToken(
"https://auth.example.com/token",
"client-id",
"client-secret"
);
System.out.println("Token obtained: " + token.accessToken);
// Call API
String response = client.callApi(
"GET",
"https://api.example.com/data",
null
);
System.out.println("API Response: " + response);
}
}
4. Spring Boot DPoP Client Integration
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.time.Duration;
/**
* Spring Boot RestTemplate interceptor for DPoP
*/
@Component
public class DPoPRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final DPoPClient dpopClient;
private String accessToken;
private String serverNonce;
public DPoPRestTemplateInterceptor(DPoPClient dpopClient) {
this.dpopClient = dpopClient;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution)
throws IOException {
try {
// Generate DPoP proof
String method = request.getMethod().toString();
String uri = request.getURI().toString();
String proof = dpopClient.generateAPIRequestProof(
method, uri, accessToken, serverNonce
);
// Add DPoP headers
request.getHeaders().add("DPoP", proof);
request.getHeaders().add("Authorization", "DPoP " + accessToken);
// Execute request
ClientHttpResponse response = execution.execute(request, body);
// Check for DPoP-Nonce header
if (response.getHeaders().containsKey("DPoP-Nonce")) {
this.serverNonce = response.getHeaders()
.getFirst("DPoP-Nonce");
}
return response;
} catch (Exception e) {
throw new IOException("DPoP processing failed", e);
}
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
@Bean
public RestTemplate dpopRestTemplate(DPoPRestTemplateInterceptor interceptor) {
return new RestTemplateBuilder()
.interceptors(interceptor)
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
5. DPoP Token Service
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class DPoPTokenService {
private final DPoPRestTemplateInterceptor interceptor;
private final RestTemplate restTemplate;
public DPoPTokenService(DPoPRestTemplateInterceptor interceptor,
RestTemplate restTemplate) {
this.interceptor = interceptor;
this.restTemplate = restTemplate;
}
public TokenResponse obtainToken(String tokenEndpoint,
String clientId,
String clientSecret) {
// Create token request with DPoP proof
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// DPoP proof will be added by interceptor
String body = String.format(
"grant_type=client_credentials&client_id=%s&client_secret=%s",
clientId, clientSecret
);
HttpEntity<String> request = new HttpEntity<>(body, headers);
ResponseEntity<TokenResponse> response = restTemplate.exchange(
tokenEndpoint,
HttpMethod.POST,
request,
TokenResponse.class
);
TokenResponse tokenResponse = response.getBody();
interceptor.setAccessToken(tokenResponse.getAccessToken());
return tokenResponse;
}
@Service
public static class ApiClient {
private final RestTemplate restTemplate;
public ApiClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public <T> T getResource(String url, Class<T> responseType) {
return restTemplate.getForObject(url, responseType);
}
public <T> T postResource(String url, Object request, Class<T> responseType) {
return restTemplate.postForObject(url, request, responseType);
}
}
public static class TokenResponse {
private String accessToken;
private String tokenType;
private int expiresIn;
// Getters and setters
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getTokenType() { return tokenType; }
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
public int getExpiresIn() { return expiresIn; }
public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; }
}
}
Server-Side DPoP Validation
1. DPoP Validator
import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.ThumbprintUtils;
import com.nimbusds.jwt.*;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
/**
* Server-side validator for DPoP proofs
*/
public class DPoPValidator {
private final JWKSet jwkSet; // Client public keys
private final Set<String> usedNonces;
private final long maxAgeSeconds;
public DPoPValidator() {
this.jwkSet = new JWKSet();
this.usedNonces = Collections.synchronizedSet(new HashSet<>());
this.maxAgeSeconds = 60; // Proofs older than 60 seconds are rejected
}
/**
* Validate a DPoP proof
*/
public ValidationResult validateDPoPProof(String dpopProof,
String method,
String uri,
String accessToken,
String expectedNonce) {
try {
// Parse the JWT
SignedJWT signedJWT = SignedJWT.parse(dpopProof);
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
// 1. Check JWT type
JOSEObjectType typ = signedJWT.getHeader().getType();
if (typ == null || !"dpop+jwt".equals(typ.toString())) {
return ValidationResult.invalid("Invalid typ header - expected 'dpop+jwt'");
}
// 2. Validate signature
JWK jwk = JWK.parse(signedJWT.getHeader().getJWK().toJSONObject());
JWSVerifier verifier = createVerifier(jwk);
if (!signedJWT.verify(verifier)) {
return ValidationResult.invalid("Invalid signature");
}
// 3. Check HTTP method
String htm = claims.getStringClaim("htm");
if (!method.equals(htm)) {
return ValidationResult.invalid("HTTP method mismatch");
}
// 4. Check HTTP URI (without query parameters)
String htu = claims.getStringClaim("htu");
String normalizedUri = normalizeUri(uri);
if (!normalizedUri.equals(htu)) {
return ValidationResult.invalid("HTTP URI mismatch");
}
// 5. Check timestamp
Date iat = claims.getIssueTime();
Date now = new Date();
if (iat == null) {
return ValidationResult.invalid("Missing iat claim");
}
long ageSeconds = (now.getTime() - iat.getTime()) / 1000;
if (ageSeconds > maxAgeSeconds) {
return ValidationResult.invalid("Proof too old");
}
if (ageSeconds < 0) {
return ValidationResult.invalid("Proof from future");
}
// 6. Check nonce for replay
String jti = claims.getJWTID();
if (jti == null) {
return ValidationResult.invalid("Missing jti claim");
}
if (usedNonces.contains(jti)) {
return ValidationResult.invalid("Replay detected");
}
// 7. Validate expiration
Date exp = claims.getExpirationTime();
if (exp != null && exp.before(now)) {
return ValidationResult.invalid("Proof expired");
}
// 8. Check access token hash (if provided)
if (accessToken != null) {
String ath = claims.getStringClaim("ath");
if (ath == null) {
return ValidationResult.invalid("Missing ath claim");
}
String calculatedAth = calculateAccessTokenHash(accessToken);
if (!calculatedAth.equals(ath)) {
return ValidationResult.invalid("Invalid access token hash");
}
}
// 9. Check server nonce (if expected)
if (expectedNonce != null) {
String nonce = claims.getStringClaim("nonce");
if (!expectedNonce.equals(nonce)) {
return ValidationResult.invalid("Invalid nonce");
}
}
// 10. Store used nonce
usedNonces.add(jti);
// Clean up later
scheduleNonceCleanup(jti, exp != null ? exp :
Date.from(Instant.now().plusSeconds(maxAgeSeconds * 2)));
// Store client public key for future validation
storeClientPublicKey(jwk, claims);
return ValidationResult.valid(
"jwk", jwk,
"thumbprint", ThumbprintUtils.compute(jwk),
"client_id", claims.getStringClaim("client_id")
);
} catch (Exception e) {
return ValidationResult.invalid("Validation error: " + e.getMessage());
}
}
/**
* Validate a DPoP proof for token request
*/
public ValidationResult validateTokenRequest(String dpopProof,
String method,
String uri,
String expectedNonce) {
return validateDPoPProof(dpopProof, method, uri, null, expectedNonce);
}
/**
* Validate a DPoP proof for API request
*/
public ValidationResult validateAPIRequest(String dpopProof,
String method,
String uri,
String accessToken,
String expectedNonce) {
return validateDPoPProof(dpopProof, method, uri, accessToken, expectedNonce);
}
private JWSVerifier createVerifier(JWK jwk) throws Exception {
if (jwk.getKeyType() == KeyType.RSA) {
return new RSASSAVerifier(jwk.toRSAKey().toRSAPublicKey());
} else if (jwk.getKeyType() == KeyType.EC) {
return new ECDSAVerifier(jwk.toECKey().toECPublicKey());
} else if (jwk.getKeyType() == KeyType.OCT) {
return new MACVerifier(jwk.toOctetSequenceKey().toSecretKey());
} else {
throw new JOSEException("Unsupported key type");
}
}
private String normalizeUri(String uri) {
// Remove query parameters and fragment
int queryIndex = uri.indexOf('?');
if (queryIndex > 0) {
return uri.substring(0, queryIndex);
}
int fragmentIndex = uri.indexOf('#');
if (fragmentIndex > 0) {
return uri.substring(0, fragmentIndex);
}
return uri;
}
private String calculateAccessTokenHash(String accessToken) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(accessToken.getBytes());
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(Arrays.copyOf(hash, 16));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
private void storeClientPublicKey(JWK jwk, JWTClaimsSet claims) {
// Store in JWKSet or database for future validation
// The client might use the same key for multiple requests
jwkSet.getKeys().removeIf(key ->
key.getKeyID() != null && key.getKeyID().equals(jwk.getKeyID()));
jwkSet.getKeys().add(jwk);
}
private void scheduleNonceCleanup(String jti, Date expiration) {
Timer timer = new Timer(true);
timer.schedule(new TimerTask() {
@Override
public void run() {
usedNonces.remove(jti);
}
}, expiration);
}
public static class ValidationResult {
private final boolean valid;
private final String errorMessage;
private final Map<String, Object> attributes;
private ValidationResult(boolean valid, String errorMessage,
Map<String, Object> attributes) {
this.valid = valid;
this.errorMessage = errorMessage;
this.attributes = attributes;
}
public static ValidationResult valid(Object... attributes) {
Map<String, Object> attrMap = new HashMap<>();
for (int i = 0; i < attributes.length; i += 2) {
attrMap.put(attributes[i].toString(), attributes[i + 1]);
}
return new ValidationResult(true, null, attrMap);
}
public static ValidationResult invalid(String errorMessage) {
return new ValidationResult(false, errorMessage, new HashMap<>());
}
public boolean isValid() { return valid; }
public String getErrorMessage() { return errorMessage; }
public Map<String, Object> getAttributes() { return attributes; }
}
}
2. Spring Boot Resource Server with DPoP
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@EnableWebSecurity
public class DPoPResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(new DPoPValidationFilter(),
BearerTokenAuthenticationFilter.class);
return http.build();
}
public static class DPoPValidationFilter extends OncePerRequestFilter {
private final DPoPValidator dpopValidator;
public DPoPValidationFilter() {
this.dpopValidator = new DPoPValidator();
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// Extract DPoP proof
String dpopProof = request.getHeader("DPoP");
String authHeader = request.getHeader("Authorization");
if (dpopProof == null || authHeader == null) {
response.sendError(401, "Missing DPoP proof or Authorization header");
return;
}
// Extract access token
String accessToken = authHeader.replace("DPoP ", "");
// Validate DPoP proof
DPoPValidator.ValidationResult result = dpopValidator.validateAPIRequest(
dpopProof,
request.getMethod(),
request.getRequestURL().toString(),
accessToken,
null // Server nonce
);
if (!result.isValid()) {
response.sendError(401, "Invalid DPoP proof: " + result.getErrorMessage());
return;
}
// Add validation result to request attributes
request.setAttribute("dpop.validation", result);
// Continue with filter chain
filterChain.doFilter(request, response);
}
}
}
Advanced DPoP Features
1. Key Rotation
public class DPoPKeyRotation {
private final DPoPClient activeClient;
private final DPoPClient nextClient;
private final ScheduledExecutorService scheduler;
public DPoPKeyRotation() throws JOSEException {
this.activeClient = new DPoPClient();
this.nextClient = new DPoPClient();
this.scheduler = Executors.newScheduledThreadPool(1);
scheduleKeyRotation();
}
private void scheduleKeyRotation() {
scheduler.scheduleAtFixedRate(() -> {
try {
rotateKeys();
} catch (JOSEException e) {
e.printStackTrace();
}
}, 24, 24, TimeUnit.HOURS);
}
private void rotateKeys() throws JOSEException {
// Generate new key
DPoPClient newClient = new DPoPClient();
// Notify authorization server about new public key
notifyAuthServer(newClient.getPublicJWK());
// Update active client (with overlap period)
DPoPClient oldClient = this.activeClient;
this.activeClient = newClient;
// Schedule old key removal
scheduler.schedule(() -> {
// Remove old key from auth server
removeKey(oldClient.getPublicJWK());
}, 1, TimeUnit.HOURS);
}
private void notifyAuthServer(JWK publicJWK) {
// Send public key to authorization server
// The server will associate it with the client
}
}
2. DPoP with Multiple Keys (Per-Resource Server)
public class MultiResourceDPoPClient {
private final Map<String, DPoPClient> resourceClients;
private final DPoPClient defaultClient;
public MultiResourceDPoPClient() throws JOSEException {
this.resourceClients = new ConcurrentHashMap<>();
this.defaultClient = new DPoPClient();
}
public DPoPClient getClientForResource(String resourceServer) {
return resourceClients.computeIfAbsent(resourceServer, k -> {
try {
return new DPoPClient();
} catch (JOSEException e) {
throw new RuntimeException(e);
}
});
}
public String generateProofForResource(String resourceServer,
String method,
String uri,
String accessToken,
String serverNonce) throws JOSEException {
DPoPClient client = getClientForResource(resourceServer);
return client.generateAPIRequestProof(method, uri, accessToken, serverNonce);
}
}
Security Best Practices for DPoP
- Use Strong Key Types: Prefer EC P-256 (ES256) over RSA for smaller proofs
- Short-Lived Proofs: DPoP proofs should expire quickly (e.g., 60 seconds)
- Replay Prevention: Track and reject reused
jtivalues - Include Access Token Hash: Always include
athclaim when using access tokens - Validate HTTP Bindings: Ensure
htmandhtumatch the actual request - Server Nonces: Use
DPoP-Nonceheader to prevent replay across sessions - Key Rotation: Regularly rotate client keys
- Monitor Anomalies: Track failed DPoP validations as security events
Conclusion
Demonstrating Proof-of-Possession (DPoP) represents a significant advancement in OAuth 2.0 security, addressing the fundamental weakness of bearer tokens by cryptographically binding tokens to specific clients. For Java applications handling sensitive data or operating in high-security environments, DPoP provides an additional layer of defense against token theft and replay attacks.
The Java ecosystem, with libraries like Nimbus JOSE + JWT and Spring Security, provides excellent support for implementing DPoP clients and servers. By following the patterns and best practices outlined in this article, developers can build applications that leverage DPoP to enhance security without sacrificing developer experience or performance.
As the industry moves toward more robust authentication mechanisms, DPoP is likely to become a standard component of OAuth 2.0 deployments, providing the cryptographic proof-of-possession that modern applications require.
Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)
Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/
Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/
Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/
Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/
Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/
Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/
Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/
Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/
Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/