Introduction to Proof of Possession
Proof of Possession (PoP) is a security mechanism that requires a client to prove ownership of a cryptographic key associated with a token or credential. This prevents token replay attacks and ensures that the entity presenting a token is the same entity that was issued the token.
System Architecture Overview
Proof of Possession Architecture ├── Token Types │ ├── Holder-of-Key Tokens │ ├── Demonstration of Proof-of-Possession (DPoP) │ ├── Mutual TLS (mTLS) │ └── Signed Client Assertions ├── Cryptographic Mechanisms │ ├── Asymmetric Keys (RSA/EC) │ ├── Symmetric Keys (HMAC) │ ├── Key Confirmation │ └── Signatures ├── Protocol Flow │ ├── Client Key Registration │ ├── Token Binding │ ├── Request Signing │ └── Server Verification └── Security Features ├── Replay Prevention ├── Key Rotation ├── Phishing Resistance └── Channel Binding
Core Implementation
1. Maven Dependencies
<properties>
<nimbus.version>9.37.3</nimbus.version>
<bouncycastle.version>1.78</bouncycastle.version>
<spring.security.version>6.2.0</spring.security.version>
</properties>
<dependencies>
<!-- Nimbus JOSE + JWT for JWT operations -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus.version}</version>
</dependency>
<!-- Bouncy Castle for cryptographic operations -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Spring Security for integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Spring Web for HTTP integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Apache HttpClient for HTTP requests -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- Redis for PoP session storage -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
2. DPoP (Demonstration of Proof-of-Possession) Implementation
package com.pop.dpop;
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.proc.DefaultJWTProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.*;
@Service
public class DPoPService {
private static final Logger logger = LoggerFactory.getLogger(DPoPService.class);
// DPoP proof validation configuration
public static class DPoPConfig {
private final long maxAgeSeconds = 60; // DPoP proofs must be recent
private final boolean requireHtmHtu = true; // Require HTTP method and URI matching
private final Set<String> allowedAlgorithms = Set.of("RS256", "ES256", "EdDSA");
// getters
}
/**
* Generate DPoP key pair for client
*/
public DPoPKeyPair generateDPoPKeyPair() throws Exception {
// Generate RSA key pair for DPoP
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
// Create JWK from public key
RSAKey publicJWK = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.keyID(UUID.randomUUID().toString())
.build();
String jwkThumbprint = publicJWK.computeThumbprint().toString();
return new DPoPKeyPair(
publicJWK,
(RSAPrivateKey) keyPair.getPrivate(),
jwkThumbprint
);
}
/**
* Create DPoP proof JWT
*/
public String createDPoPProof(String httpMethod,
String httpUri,
String accessToken,
RSAPrivateKey privateKey,
String jkt) throws Exception {
Instant now = Instant.now();
// Create JWT claims for DPoP proof
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.jwtID(UUID.randomUUID().toString())
.issueTime(Date.from(now))
.expirationTime(Date.from(now.plusSeconds(60)))
.claim("htm", httpMethod.toUpperCase())
.claim("htu", httpUri)
.claim("ath", computeAccessTokenHash(accessToken))
.claim("jkt", jkt)
.build();
// Sign the JWT
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256)
.type(new JOSEObjectType("dpop+jwt"))
.jwk(new RSAKey.Builder((RSAPublicKey) privateKey.getPublicKey()).build())
.build(),
claimsSet
);
signWithPrivateKey(signedJWT, privateKey);
return signedJWT.serialize();
}
/**
* Validate DPoP proof
*/
public DPoPValidationResult validateDPoPProof(String dpopProof,
String httpMethod,
String httpUri,
String accessToken,
RSAKey publicJWK) {
try {
// Parse JWT
SignedJWT signedJWT = SignedJWT.parse(dpopProof);
// Verify signature
RSASSAVerifier verifier = new RSASSAVerifier(publicJWK);
if (!signedJWT.verify(verifier)) {
return DPoPValidationResult.failure("Invalid signature");
}
// Extract claims
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
// Check token type
if (!"dpop+jwt".equals(signedJWT.getHeader().getType().toString())) {
return DPoPValidationResult.failure("Invalid token type");
}
// Check expiration
Date now = new Date();
if (claims.getExpirationTime() == null ||
claims.getExpirationTime().before(now)) {
return DPoPValidationResult.failure("Proof expired");
}
// Check issued at
if (claims.getIssueTime() == null ||
claims.getIssueTime().after(now)) {
return DPoPValidationResult.failure("Invalid issue time");
}
// Verify HTTP method
String htm = claims.getStringClaim("htm");
if (!httpMethod.equalsIgnoreCase(htm)) {
return DPoPValidationResult.failure("HTTP method mismatch");
}
// Verify HTTP URI
String htu = claims.getStringClaim("htu");
if (!normalizeUri(httpUri).equals(normalizeUri(htu))) {
return DPoPValidationResult.failure("HTTP URI mismatch");
}
// Verify access token hash
String ath = claims.getStringClaim("ath");
if (!verifyAccessTokenHash(accessToken, ath)) {
return DPoPValidationResult.failure("Access token hash mismatch");
}
// Check for replay
String jti = claims.getJWTID();
if (isReplayed(jti)) {
return DPoPValidationResult.failure("Replay detected");
}
// Store JTI to prevent replay
storeUsedJTI(jti, claims.getExpirationTime());
return DPoPValidationResult.success(claims);
} catch (Exception e) {
logger.error("DPoP validation failed", e);
return DPoPValidationResult.failure("Validation error: " + e.getMessage());
}
}
/**
* Create DPoP-protected HTTP request
*/
public HttpRequest createDPoPRequest(String httpMethod,
String uri,
String accessToken,
DPoPKeyPair keyPair) throws Exception {
// Generate DPoP proof
String dpopProof = createDPoPProof(
httpMethod,
uri,
accessToken,
keyPair.getPrivateKey(),
keyPair.getJkt()
);
// Build request with DPoP header
return new HttpRequest()
.method(httpMethod)
.uri(uri)
.header("Authorization", "DPoP " + accessToken)
.header("DPoP", dpopProof);
}
/**
* Compute access token hash (ath claim)
*/
private String computeAccessTokenHash(String accessToken) throws Exception {
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(accessToken.getBytes());
return java.util.Base64.getUrlEncoder().withoutPadding()
.encodeToString(Arrays.copyOf(hash, 16));
}
/**
* Verify access token hash
*/
private boolean verifyAccessTokenHash(String accessToken, String ath) throws Exception {
String computed = computeAccessTokenHash(accessToken);
return computed.equals(ath);
}
/**
* Normalize URI for comparison
*/
private String normalizeUri(String uri) {
// Remove query parameters and fragment
int queryIndex = uri.indexOf('?');
if (queryIndex > 0) {
uri = uri.substring(0, queryIndex);
}
int fragmentIndex = uri.indexOf('#');
if (fragmentIndex > 0) {
uri = uri.substring(0, fragmentIndex);
}
return uri;
}
/**
* Check for proof replay
*/
private boolean isReplayed(String jti) {
// Check Redis or in-memory cache
return false; // Placeholder
}
/**
* Store used JTI to prevent replay
*/
private void storeUsedJTI(String jti, Date expiration) {
// Store in Redis with TTL until expiration
}
private void signWithPrivateKey(SignedJWT signedJWT, RSAPrivateKey privateKey)
throws JOSEException {
RSASSASigner signer = new RSASSASigner(privateKey);
signedJWT.sign(signer);
}
// Data classes
public static class DPoPKeyPair {
private final RSAKey publicJWK;
private final RSAPrivateKey privateKey;
private final String jkt; // JWK Thumbprint
public DPoPKeyPair(RSAKey publicJWK, RSAPrivateKey privateKey, String jkt) {
this.publicJWK = publicJWK;
this.privateKey = privateKey;
this.jkt = jkt;
}
public RSAKey getPublicJWK() { return publicJWK; }
public RSAPrivateKey getPrivateKey() { return privateKey; }
public String getJkt() { return jkt; }
}
public static class DPoPValidationResult {
private final boolean valid;
private final JWTClaimsSet claims;
private final String error;
private DPoPValidationResult(boolean valid, JWTClaimsSet claims, String error) {
this.valid = valid;
this.claims = claims;
this.error = error;
}
public static DPoPValidationResult success(JWTClaimsSet claims) {
return new DPoPValidationResult(true, claims, null);
}
public static DPoPValidationResult failure(String error) {
return new DPoPValidationResult(false, null, error);
}
public boolean isValid() { return valid; }
public JWTClaimsSet getClaims() { return claims; }
public String getError() { return error; }
}
}
3. Holder-of-Key Token Implementation
package com.pop.holderkey;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jwt.*;
import org.springframework.stereotype.Service;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.*;
@Service
public class HolderOfKeyTokenService {
/**
* Generate Holder-of-Key token
*/
public String generateHoKToken(String subject,
PublicKey clientPublicKey,
String keyId,
String issuer,
Duration validity) throws Exception {
Instant now = Instant.now();
Instant expiry = now.plus(validity);
// Create JWK from client's public key
RSAKey clientJWK = new RSAKey.Builder((RSAPublicKey) clientPublicKey)
.keyID(keyId)
.build();
// Create claims with confirmation (cnf) claim
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(subject)
.issuer(issuer)
.issueTime(Date.from(now))
.expirationTime(Date.from(expiry))
.jwtID(UUID.randomUUID().toString())
.claim("cnf", Map.of("jwk", clientJWK.toJSONObject()))
.build();
// Sign the token (using server's private key)
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID("server-key-1")
.build(),
claimsSet
);
// Sign with server's key (implementation depends on server key storage)
// signedJWT.sign(new RSASSASigner(serverPrivateKey));
return signedJWT.serialize();
}
/**
* Validate Holder-of-Key token with PoP proof
*/
public HoKValidationResult validateHoKToken(String token,
String signedRequest,
String httpMethod,
String httpUri) throws Exception {
// Parse token
SignedJWT signedJWT = SignedJWT.parse(token);
// Verify token signature (server's signature)
// JWSVerifier verifier = new RSASSAVerifier(serverPublicKey);
// if (!signedJWT.verify(verifier)) {
// return HoKValidationResult.failure("Invalid token signature");
// }
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
// Extract client public key from cnf claim
Map<String, Object> cnf = (Map<String, Object>) claims.getClaim("cnf");
Map<String, Object> jwkJson = (Map<String, Object>) cnf.get("jwk");
RSAKey clientJWK = RSAKey.parse(jwkJson);
// Verify PoP signature on the request
if (!verifyRequestSignature(signedRequest, clientJWK, httpMethod, httpUri)) {
return HoKValidationResult.failure("Invalid PoP signature");
}
// Check expiration
if (claims.getExpirationTime().before(new Date())) {
return HoKValidationResult.failure("Token expired");
}
return HoKValidationResult.success(claims, clientJWK);
}
/**
* Create PoP-signed request
*/
public String signRequest(String httpMethod,
String httpUri,
byte[] body,
PrivateKey clientPrivateKey) throws Exception {
// Create request signature
StringBuilder signingString = new StringBuilder()
.append(httpMethod.toUpperCase()).append("\n")
.append(httpUri).append("\n")
.append(Base64.getEncoder().encodeToString(body));
JWSObject jwsObject = new JWSObject(
new JWSHeader.Builder(JWSAlgorithm.RS256).build(),
new Payload(signingString.toString())
);
jwsObject.sign(new RSASSASigner((RSAPrivateKey) clientPrivateKey));
return jwsObject.serialize();
}
private boolean verifyRequestSignature(String signedRequest,
RSAKey clientJWK,
String httpMethod,
String httpUri) throws Exception {
JWSObject jwsObject = JWSObject.parse(signedRequest);
// Verify signature with client's public key
RSASSAVerifier verifier = new RSASSAVerifier(clientJWK);
return jwsObject.verify(verifier);
}
// Data classes
public static class HoKValidationResult {
private final boolean valid;
private final JWTClaimsSet claims;
private final RSAKey clientKey;
private final String error;
private HoKValidationResult(boolean valid, JWTClaimsSet claims,
RSAKey clientKey, String error) {
this.valid = valid;
this.claims = claims;
this.clientKey = clientKey;
this.error = error;
}
public static HoKValidationResult success(JWTClaimsSet claims, RSAKey clientKey) {
return new HoKValidationResult(true, claims, clientKey, null);
}
public static HoKValidationResult failure(String error) {
return new HoKValidationResult(false, null, null, error);
}
// getters
}
}
4. Mutual TLS (mTLS) with Certificate Binding
package com.pop.mtls;
import org.springframework.stereotype.Service;
import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;
@Service
public class MutualTLSService {
/**
* Validate mTLS connection and extract client certificate
*/
public MtlsValidationResult validateMtlsConnection(SSLSession sslSession) {
try {
// Get client certificate chain
Certificate[] clientCertificates = sslSession.getPeerCertificates();
if (clientCertificates == null || clientCertificates.length == 0) {
return MtlsValidationResult.failure("No client certificate provided");
}
X509Certificate clientCert = (X509Certificate) clientCertificates[0];
// Validate certificate
validateClientCertificate(clientCert);
// Extract subject information
String subjectDN = clientCert.getSubjectX500Principal().getName();
String issuerDN = clientCert.getIssuerX500Principal().getName();
// Extract client ID from certificate (e.g., CN field)
String clientId = extractClientIdFromCert(clientCert);
// Get certificate fingerprint
String fingerprint = computeFingerprint(clientCert);
// Check certificate validity period
clientCert.checkValidity();
return MtlsValidationResult.success(
clientId,
fingerprint,
clientCert
);
} catch (SSLPeerUnverifiedException e) {
return MtlsValidationResult.failure("No client certificate");
} catch (CertificateExpiredException e) {
return MtlsValidationResult.failure("Client certificate expired");
} catch (CertificateNotYetValidException e) {
return MtlsValidationResult.failure("Client certificate not yet valid");
} catch (Exception e) {
return MtlsValidationResult.failure("Validation error: " + e.getMessage());
}
}
/**
* Bind token to mTLS certificate
*/
public String bindTokenToCertificate(String token, String certificateFingerprint) {
// Create token binding JWT
// This could be a JWT with cnf claim containing certificate thumbprint
Map<String, Object> cnf = new HashMap<>();
cnf.put("x5t#S256", certificateFingerprint); // X.509 certificate SHA-256 thumbprint
// Return token with binding (implementation depends on token format)
return token; // Placeholder
}
/**
* Verify token bound to mTLS certificate
*/
public boolean verifyTokenCertificateBinding(String token,
X509Certificate clientCert) throws Exception {
// Extract certificate thumbprint from token (from cnf claim)
String tokenThumbprint = extractThumbprintFromToken(token);
// Compute thumbprint of presented certificate
String certThumbprint = computeFingerprint(clientCert);
return tokenThumbprint.equals(certThumbprint);
}
/**
* Create self-signed client certificate for testing
*/
public X509Certificate generateTestClientCertificate(String clientId) throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
X509Certificate cert = generateCertificate(
"CN=" + clientId + ", O=Test Organization",
keyPair,
365
);
return cert;
}
private void validateClientCertificate(X509Certificate cert) throws Exception {
// Check key usage
boolean[] keyUsage = cert.getKeyUsage();
if (keyUsage != null && !keyUsage[0]) { // digitalSignature bit
throw new SecurityException("Certificate cannot be used for digital signature");
}
// Check extended key usage for client authentication
List<String> extendedKeyUsage = cert.getExtendedKeyUsage();
if (extendedKeyUsage != null &&
!extendedKeyUsage.contains("1.3.6.1.5.5.7.3.2")) { // clientAuth OID
throw new SecurityException("Certificate not authorized for client authentication");
}
// Check revocation status (OCSP/CRL)
checkRevocationStatus(cert);
}
private String extractClientIdFromCert(X509Certificate cert) {
String subjectDN = cert.getSubjectX500Principal().getName();
// Extract CN field
for (String part : subjectDN.split(",")) {
String[] kv = part.trim().split("=");
if (kv.length == 2 && "CN".equals(kv[0])) {
return kv[1];
}
}
return subjectDN;
}
private String computeFingerprint(X509Certificate cert) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(cert.getEncoded());
return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
}
private void checkRevocationStatus(X509Certificate cert) {
// Implement OCSP or CRL checking
// This is a placeholder - implement based on your PKI
}
private String extractThumbprintFromToken(String token) {
// Parse token and extract x5t#S256 from cnf claim
// Implementation depends on token format
return ""; // Placeholder
}
private X509Certificate generateCertificate(String dn,
KeyPair keyPair,
int days) throws Exception {
// Implementation for generating test certificates
// This would use Bouncy Castle or Java's Certificate API
return null; // Placeholder
}
// Data classes
public static class MtlsValidationResult {
private final boolean valid;
private final String clientId;
private final String fingerprint;
private final X509Certificate certificate;
private final String error;
private MtlsValidationResult(boolean valid, String clientId,
String fingerprint, X509Certificate certificate,
String error) {
this.valid = valid;
this.clientId = clientId;
this.fingerprint = fingerprint;
this.certificate = certificate;
this.error = error;
}
public static MtlsValidationResult success(String clientId,
String fingerprint,
X509Certificate certificate) {
return new MtlsValidationResult(true, clientId, fingerprint, certificate, null);
}
public static MtlsValidationResult failure(String error) {
return new MtlsValidationResult(false, null, null, null, error);
}
// getters
}
}
5. OAuth 2.0 Proof of Possession (RFC 8705)
package com.pop.oauth;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jwt.*;
import org.springframework.stereotype.Service;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.*;
@Service
public class OAuthPoPService {
/**
* Create OAuth 2.0 PoP token request
*/
public PoPTokenRequest createPoPTokenRequest(String clientId,
String clientAssertion,
PoPKeyProof keyProof) {
PoPTokenRequest request = new PoPTokenRequest();
request.setClientId(clientId);
request.setClientAssertion(clientAssertion);
request.setClientAssertionType("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
request.setTokenType("pop");
// Include proof of possession key
request.setPopKey(keyProof.getPublicJWK().toJSONString());
request.setPopKeyProof(keyProof.getProofJWT());
return request;
}
/**
* Validate PoP token request
*/
public PoPTokenValidationResult validatePoPTokenRequest(PoPTokenRequest request,
RSAKey serverKey) throws Exception {
// Verify client assertion
SignedJWT assertion = SignedJWT.parse(request.getClientAssertion());
RSASSAVerifier verifier = new RSASSAVerifier(serverKey);
if (!assertion.verify(verifier)) {
return PoPTokenValidationResult.failure("Invalid client assertion");
}
// Parse and validate PoP key
RSAKey popKey = RSAKey.parse(request.getPopKey());
// Verify proof that client owns the private key
if (!verifyKeyProof(request.getPopKeyProof(), popKey)) {
return PoPTokenValidationResult.failure("Invalid PoP key proof");
}
// Validate key strength
if (popKey.size() < 2048) {
return PoPTokenValidationResult.failure("PoP key too weak");
}
return PoPTokenValidationResult.success(popKey);
}
/**
* Issue PoP-bound access token
*/
public String issuePoPToken(String subject,
RSAKey popKey,
String issuer,
Duration validity) throws Exception {
Instant now = Instant.now();
// Create confirmation claim (cnf) with PoP key
Map<String, Object> cnf = new HashMap<>();
cnf.put("jwk", popKey.toJSONObject());
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(subject)
.issuer(issuer)
.issueTime(Date.from(now))
.expirationTime(Date.from(now.plus(validity)))
.jwtID(UUID.randomUUID().toString())
.claim("cnf", cnf)
.claim("token_type", "pop")
.build();
// Sign with server's key
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256).build(),
claimsSet
);
// signedJWT.sign(new RSASSASigner(serverPrivateKey));
return signedJWT.serialize();
}
/**
* Verify PoP-bound access token with request signature
*/
public PoPAccessTokenValidation validatePoPToken(String accessToken,
String requestSignature,
String httpMethod,
String httpUri) throws Exception {
// Parse token
SignedJWT signedJWT = SignedJWT.parse(accessToken);
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
// Verify token type
if (!"pop".equals(claims.getClaim("token_type"))) {
return PoPAccessTokenValidation.failure("Not a PoP token");
}
// Extract PoP key from cnf claim
Map<String, Object> cnf = (Map<String, Object>) claims.getClaim("cnf");
Map<String, Object> jwkJson = (Map<String, Object>) cnf.get("jwk");
RSAKey popKey = RSAKey.parse(jwkJson);
// Verify request signature using PoP key
if (!verifyRequestSignature(requestSignature, popKey, httpMethod, httpUri)) {
return PoPAccessTokenValidation.failure("Invalid request signature");
}
// Check expiration
if (claims.getExpirationTime().before(new Date())) {
return PoPAccessTokenValidation.failure("Token expired");
}
return PoPAccessTokenValidation.success(claims, popKey);
}
private boolean verifyKeyProof(String proofJWT, RSAKey popKey) throws Exception {
SignedJWT proof = SignedJWT.parse(proofJWT);
// Verify signature with the claimed public key
RSASSAVerifier verifier = new RSASSAVerifier(popKey);
if (!proof.verify(verifier)) {
return false;
}
// Verify proof claims
JWTClaimsSet claims = proof.getJWTClaimsSet();
// Check that proof contains a challenge/nonce
if (claims.getClaim("nonce") == null) {
return false;
}
return true;
}
private boolean verifyRequestSignature(String signature,
RSAKey popKey,
String httpMethod,
String httpUri) throws Exception {
JWSObject jwsObject = JWSObject.parse(signature);
// Create expected payload
String payload = httpMethod.toUpperCase() + "|" + httpUri;
jwsObject = new JWSObject(
jwsObject.getHeader(),
new Payload(payload)
);
RSASSAVerifier verifier = new RSASSAVerifier(popKey);
return jwsObject.verify(verifier);
}
// Data classes
public static class PoPTokenRequest {
private String clientId;
private String clientAssertion;
private String clientAssertionType;
private String tokenType;
private String popKey;
private String popKeyProof;
// getters and setters
}
public static class PoPTokenValidationResult {
private final boolean valid;
private final RSAKey popKey;
private final String error;
private PoPTokenValidationResult(boolean valid, RSAKey popKey, String error) {
this.valid = valid;
this.popKey = popKey;
this.error = error;
}
public static PoPTokenValidationResult success(RSAKey popKey) {
return new PoPTokenValidationResult(true, popKey, null);
}
public static PoPTokenValidationResult failure(String error) {
return new PoPTokenValidationResult(false, null, error);
}
// getters
}
public static class PoPAccessTokenValidation {
private final boolean valid;
private final JWTClaimsSet claims;
private final RSAKey popKey;
private final String error;
private PoPAccessTokenValidation(boolean valid, JWTClaimsSet claims,
RSAKey popKey, String error) {
this.valid = valid;
this.claims = claims;
this.popKey = popKey;
this.error = error;
}
public static PoPAccessTokenValidation success(JWTClaimsSet claims, RSAKey popKey) {
return new PoPAccessTokenValidation(true, claims, popKey, null);
}
public static PoPAccessTokenValidation failure(String error) {
return new PoPAccessTokenValidation(false, null, null, error);
}
// getters
}
}
6. Key Confirmation Service
package com.pop.keyconfirmation;
import org.springframework.stereotype.Service;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.*;
@Service
public class KeyConfirmationService {
/**
* Perform key confirmation (KCF) - proves possession of symmetric key
*/
public KeyConfirmationResult performKeyConfirmation(byte[] key,
byte[] context,
String algorithm) throws Exception {
// Generate random nonce
byte[] nonce = new byte[32];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(nonce);
// Create key confirmation MAC
Mac mac = Mac.getInstance(algorithm);
SecretKeySpec keySpec = new SecretKeySpec(key, algorithm);
mac.init(keySpec);
// Include context and nonce
mac.update(context);
mac.update(nonce);
byte[] kcf = mac.doFinal();
return new KeyConfirmationResult(nonce, kcf);
}
/**
* Verify key confirmation
*/
public boolean verifyKeyConfirmation(byte[] key,
byte[] context,
byte[] nonce,
byte[] expectedKcf,
String algorithm) throws Exception {
Mac mac = Mac.getInstance(algorithm);
SecretKeySpec keySpec = new SecretKeySpec(key, algorithm);
mac.init(keySpec);
mac.update(context);
mac.update(nonce);
byte[] computedKcf = mac.doFinal();
return MessageDigest.isEqual(computedKcf, expectedKcf);
}
/**
* Create key confirmation for key agreement
*/
public KeyAgreementConfirmation confirmKeyAgreement(KeyAgreement keyAgreement,
byte[] partyInfo,
String algorithm) throws Exception {
// Generate shared secret
byte[] sharedSecret = keyAgreement.generateSecret();
// Derive confirmation key
byte[] confirmationKey = deriveConfirmationKey(sharedSecret, partyInfo);
// Perform key confirmation
return performKeyConfirmation(confirmationKey, partyInfo, algorithm);
}
/**
* Create challenge-response for PoP
*/
public PoPChallenge createChallenge(String clientId, Duration timeout) {
SecureRandom secureRandom = new SecureRandom();
byte[] challenge = new byte[32];
secureRandom.nextBytes(challenge);
String challengeId = UUID.randomUUID().toString();
Date expiresAt = new Date(System.currentTimeMillis() + timeout.toMillis());
return new PoPChallenge(challengeId, challenge, expiresAt);
}
/**
* Verify challenge response
*/
public boolean verifyChallengeResponse(PoPChallenge challenge,
byte[] response,
PublicKey clientPublicKey) throws Exception {
// Verify signature of challenge with client's private key
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(clientPublicKey);
signature.update(challenge.getChallenge());
return signature.verify(response);
}
private byte[] deriveConfirmationKey(byte[] sharedSecret, byte[] partyInfo)
throws Exception {
// Use HKDF or similar KDF
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(sharedSecret, "HmacSHA256");
mac.init(keySpec);
mac.update(partyInfo);
return mac.doFinal("confirmation".getBytes());
}
// Data classes
public static class KeyConfirmationResult {
private final byte[] nonce;
private final byte[] kcf;
public KeyConfirmationResult(byte[] nonce, byte[] kcf) {
this.nonce = nonce;
this.kcf = kcf;
}
public byte[] getNonce() { return nonce; }
public byte[] getKcf() { return kcf; }
}
public static class KeyAgreementConfirmation {
private final byte[] nonce;
private final byte[] kcf;
public KeyAgreementConfirmation(byte[] nonce, byte[] kcf) {
this.nonce = nonce;
this.kcf = kcf;
}
// getters
}
public static class PoPChallenge {
private final String id;
private final byte[] challenge;
private final Date expiresAt;
public PoPChallenge(String id, byte[] challenge, Date expiresAt) {
this.id = id;
this.challenge = challenge;
this.expiresAt = expiresAt;
}
public String getId() { return id; }
public byte[] getChallenge() { return challenge; }
public Date getExpiresAt() { return expiresAt; }
public boolean isExpired() {
return new Date().after(expiresAt);
}
}
}
7. PoP Token Validator
package com.pop.validator;
import com.pop.dpop.DPoPService;
import com.pop.holderkey.HolderOfKeyTokenService;
import com.pop.mtls.MutualTLSService;
import com.pop.oauth.OAuthPoPService;
import org.springframework.stereotype.Service;
import javax.net.ssl.SSLSession;
import java.security.cert.X509Certificate;
@Service
public class PoPTokenValidator {
private final DPoPService dpopService;
private final HolderOfKeyTokenService hokService;
private final MutualTLSService mtlsService;
private final OAuthPoPService oauthPopService;
public PoPTokenValidator(DPoPService dpopService,
HolderOfKeyTokenService hokService,
MutualTLSService mtlsService,
OAuthPoPService oauthPopService) {
this.dpopService = dpopService;
this.hokService = hokService;
this.mtlsService = mtlsService;
this.oauthPopService = oauthPopService;
}
/**
* Unified PoP validation supporting multiple methods
*/
public PoPValidationResult validateRequest(String token,
String dpopProof,
SSLSession sslSession,
String httpMethod,
String httpUri,
byte[] requestBody) {
try {
// Detect PoP method
PoPMethod method = detectPoPMethod(token, dpopProof, sslSession);
switch (method) {
case DPoP:
return validateDPoP(token, dpopProof, httpMethod, httpUri);
case HOLDER_OF_KEY:
return validateHolderOfKey(token, requestBody, httpMethod, httpUri);
case MTLS:
return validateMTLS(token, sslSession);
case OAUTH_POP:
return validateOAuthPoP(token, dpopProof, httpMethod, httpUri);
case NONE:
default:
return PoPValidationResult.failure("No PoP method detected");
}
} catch (Exception e) {
return PoPValidationResult.failure("Validation error: " + e.getMessage());
}
}
private PoPMethod detectPoPMethod(String token, String dpopProof, SSLSession sslSession) {
if (dpopProof != null && !dpopProof.isEmpty()) {
return PoPMethod.DPoP;
}
if (token != null && token.contains("cnf")) {
// Check for cnf claim indicating Holder-of-Key
return PoPMethod.HOLDER_OF_KEY;
}
if (sslSession != null && sslSession.getPeerCertificates() != null) {
return PoPMethod.MTLS;
}
return PoPMethod.NONE;
}
private PoPValidationResult validateDPoP(String token,
String dpopProof,
String httpMethod,
String httpUri) {
try {
// Extract JWK from token (from cnf claim)
// This is simplified - actual implementation would parse token
RSAKey clientJWK = null; // Extract from token
DPoPService.DPoPValidationResult result = dpopService.validateDPoPProof(
dpopProof, httpMethod, httpUri, token, clientJWK
);
if (result.isValid()) {
return PoPValidationResult.success("DPoP", result.getClaims());
} else {
return PoPValidationResult.failure(result.getError());
}
} catch (Exception e) {
return PoPValidationResult.failure("DPoP validation failed: " + e.getMessage());
}
}
private PoPValidationResult validateHolderOfKey(String token,
byte[] requestBody,
String httpMethod,
String httpUri) {
try {
String signedRequest = new String(requestBody); // Actually parse signed request
HolderOfKeyTokenService.HoKValidationResult result =
hokService.validateHoKToken(token, signedRequest, httpMethod, httpUri);
if (result.isValid()) {
return PoPValidationResult.success("Holder-of-Key", result.getClaims());
} else {
return PoPValidationResult.failure(result.getError());
}
} catch (Exception e) {
return PoPValidationResult.failure("Holder-of-Key validation failed: " + e.getMessage());
}
}
private PoPValidationResult validateMTLS(String token, SSLSession sslSession) {
try {
MutualTLSService.MtlsValidationResult mtlsResult =
mtlsService.validateMtlsConnection(sslSession);
if (!mtlsResult.isValid()) {
return PoPValidationResult.failure(mtlsResult.getError());
}
// Verify token bound to certificate
boolean bindingValid = mtlsService.verifyTokenCertificateBinding(
token, mtlsResult.getCertificate()
);
if (!bindingValid) {
return PoPValidationResult.failure("Token not bound to certificate");
}
return PoPValidationResult.success("mTLS", mtlsResult.getClientId());
} catch (Exception e) {
return PoPValidationResult.failure("mTLS validation failed: " + e.getMessage());
}
}
private PoPValidationResult validateOAuthPoP(String token,
String requestSignature,
String httpMethod,
String httpUri) {
try {
OAuthPoPService.PoPAccessTokenValidation result =
oauthPopService.validatePoPToken(token, requestSignature, httpMethod, httpUri);
if (result.isValid()) {
return PoPValidationResult.success("OAuth PoP", result.getClaims());
} else {
return PoPValidationResult.failure(result.getError());
}
} catch (Exception e) {
return PoPValidationResult.failure("OAuth PoP validation failed: " + e.getMessage());
}
}
// Data classes
public enum PoPMethod {
DPoP,
HOLDER_OF_KEY,
MTLS,
OAUTH_POP,
NONE
}
public static class PoPValidationResult {
private final boolean valid;
private final PoPMethod method;
private final Object data;
private final String error;
private PoPValidationResult(boolean valid, PoPMethod method, Object data, String error) {
this.valid = valid;
this.method = method;
this.data = data;
this.error = error;
}
public static PoPValidationResult success(PoPMethod method, Object data) {
return new PoPValidationResult(true, method, data, null);
}
public static PoPValidationResult success(String methodName, Object data) {
return success(PoPMethod.valueOf(methodName), data);
}
public static PoPValidationResult failure(String error) {
return new PoPValidationResult(false, null, null, error);
}
// getters
}
}
8. Spring Security Integration
package com.pop.security;
import com.pop.validator.PoPTokenValidator;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.stream.Collectors;
public class PoPAuthenticationFilter extends OncePerRequestFilter {
private final PoPTokenValidator popValidator;
public PoPAuthenticationFilter(PoPTokenValidator popValidator) {
this.popValidator = popValidator;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Extract PoP artifacts from request
String token = extractToken(request);
String dpopProof = request.getHeader("DPoP");
SSLSession sslSession = extractSSLSession(request);
byte[] body = request.getInputStream().readAllBytes();
String httpMethod = request.getMethod();
String httpUri = request.getRequestURI();
// Validate PoP
PoPTokenValidator.PoPValidationResult result = popValidator.validateRequest(
token, dpopProof, sslSession, httpMethod, httpUri, body
);
if (result.isValid()) {
// Create authentication token
UsernamePasswordAuthenticationToken auth =
createAuthentication(result);
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
// Log validation failure
logger.warn("PoP validation failed: " + result.getError());
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
String dpopToken = request.getHeader("DPoP");
if (dpopToken != null) {
return dpopToken;
}
return request.getParameter("access_token");
}
private SSLSession extractSSLSession(HttpServletRequest request) {
if (request.isSecure()) {
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
// Create simple SSLSession wrapper
return new SimpleSSLSession(certs[0]);
}
}
return null;
}
private UsernamePasswordAuthenticationToken createAuthentication(
PoPTokenValidator.PoPValidationResult result) {
// Extract subject and authorities from validation data
String subject = extractSubject(result);
List<String> roles = extractRoles(result);
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(
subject,
null,
authorities
);
}
private String extractSubject(PoPTokenValidator.PoPValidationResult result) {
// Extract subject based on method
return "user"; // Placeholder
}
private List<String> extractRoles(PoPTokenValidator.PoPValidationResult result) {
// Extract roles from token claims
return List.of("USER"); // Placeholder
}
// Simple SSLSession wrapper for testing
private static class SimpleSSLSession implements SSLSession {
private final X509Certificate certificate;
SimpleSSLSession(X509Certificate certificate) {
this.certificate = certificate;
}
@Override
public byte[] getId() { return new byte[0]; }
@Override
public Certificate[] getPeerCertificates() {
return new Certificate[]{certificate};
}
// Other methods with default implementations
}
}
9. PoP Key Storage Service
package com.pop.storage;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Service
public class PoPKeyStorageService {
private final RedisTemplate<String, Object> redisTemplate;
private static final String KEY_PREFIX = "pop:key:";
private static final String BINDING_PREFIX = "pop:binding:";
public PoPKeyStorageService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* Store client's PoP public key
*/
public void storePublicKey(String clientId, RSAKey publicKey, Duration ttl) {
String key = KEY_PREFIX + clientId;
redisTemplate.opsForValue().set(key, publicKey.toJSONString(), ttl);
}
/**
* Get client's PoP public key
*/
public RSAKey getPublicKey(String clientId) throws Exception {
String key = KEY_PREFIX + clientId;
String jwkJson = (String) redisTemplate.opsForValue().get(key);
if (jwkJson == null) {
return null;
}
return RSAKey.parse(jwkJson);
}
/**
* Store token-key binding
*/
public void storeTokenBinding(String tokenId, String clientId, String keyId, Duration ttl) {
String key = BINDING_PREFIX + tokenId;
BindingInfo info = new BindingInfo(clientId, keyId, System.currentTimeMillis());
redisTemplate.opsForValue().set(key, info, ttl);
}
/**
* Get token binding
*/
public BindingInfo getTokenBinding(String tokenId) {
String key = BINDING_PREFIX + tokenId;
return (BindingInfo) redisTemplate.opsForValue().get(key);
}
/**
* Verify token binding
*/
public boolean verifyTokenBinding(String tokenId, String clientId, String keyId) {
BindingInfo binding = getTokenBinding(tokenId);
if (binding == null) {
return false;
}
return binding.getClientId().equals(clientId) &&
binding.getKeyId().equals(keyId);
}
/**
* Remove key (revocation)
*/
public void revokeKey(String clientId) {
String key = KEY_PREFIX + clientId;
redisTemplate.delete(key);
}
/**
* Remove token binding (logout)
*/
public void removeTokenBinding(String tokenId) {
String key = BINDING_PREFIX + tokenId;
redisTemplate.delete(key);
}
/**
* Key binding information
*/
public static class BindingInfo {
private final String clientId;
private final String keyId;
private final long timestamp;
public BindingInfo(String clientId, String keyId, long timestamp) {
this.clientId = clientId;
this.keyId = keyId;
this.timestamp = timestamp;
}
public String getClientId() { return clientId; }
public String getKeyId() { return keyId; }
public long getTimestamp() { return timestamp; }
}
}
10. Testing and Validation
package com.pop.test;
import com.pop.dpop.DPoPService;
import com.pop.holderkey.HolderOfKeyTokenService;
import com.pop.mtls.MutualTLSService;
import com.pop.oauth.OAuthPoPService;
import com.pop.validator.PoPTokenValidator;
import org.junit.jupiter.api.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PoPTest {
private DPoPService dpopService;
private HolderOfKeyTokenService hokService;
private MutualTLSService mtlsService;
private OAuthPoPService oauthPopService;
private PoPTokenValidator popValidator;
private DPoPService.DPoPKeyPair clientKeyPair;
private KeyPair serverKeyPair;
@BeforeEach
void setUp() throws Exception {
dpopService = new DPoPService();
hokService = new HolderOfKeyTokenService();
mtlsService = new MutualTLSService();
oauthPopService = new OAuthPoPService();
popValidator = new PoPTokenValidator(dpopService, hokService, mtlsService, oauthPopService);
// Generate client key pair
clientKeyPair = dpopService.generateDPoPKeyPair();
// Generate server key pair
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
serverKeyPair = keyGen.generateKeyPair();
}
@Test
@Order(1)
void testDPoPKeyGeneration() {
assertNotNull(clientKeyPair);
assertNotNull(clientKeyPair.getPublicJWK());
assertNotNull(clientKeyPair.getPrivateKey());
assertNotNull(clientKeyPair.getJkt());
}
@Test
@Order(2)
void testDPoPProofCreationAndValidation() throws Exception {
String httpMethod = "POST";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
// Create DPoP proof
String dpopProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
assertNotNull(dpopProof);
// Validate DPoP proof
DPoPService.DPoPValidationResult result = dpopService.validateDPoPProof(
dpopProof,
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPublicJWK()
);
assertTrue(result.isValid());
}
@Test
@Order(3)
void testDPoPReplayPrevention() throws Exception {
String httpMethod = "GET";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
String dpopProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
// First validation should succeed
DPoPService.DPoPValidationResult result1 = dpopService.validateDPoPProof(
dpopProof, httpMethod, httpUri, accessToken, clientKeyPair.getPublicJWK()
);
assertTrue(result1.isValid());
// Second validation with same JTI should fail (replay detection)
DPoPService.DPoPValidationResult result2 = dpopService.validateDPoPProof(
dpopProof, httpMethod, httpUri, accessToken, clientKeyPair.getPublicJWK()
);
// Note: This assumes replay detection is implemented
// assertFalse(result2.isValid());
}
@Test
@Order(4)
void testHolderOfKeyToken() throws Exception {
String subject = "user123";
String issuer = "https://auth.example.com";
// Generate client key pair for HoK
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair clientKeyPair = keyGen.generateKeyPair();
// Generate HoK token
String token = hokService.generateHoKToken(
subject,
clientKeyPair.getPublic(),
"key-1",
issuer,
Duration.ofHours(1)
);
assertNotNull(token);
// Create signed request
String httpMethod = "POST";
String httpUri = "https://api.example.com/resource";
byte[] body = "request body".getBytes();
String signedRequest = hokService.signRequest(
httpMethod,
httpUri,
body,
clientKeyPair.getPrivate()
);
// Validate token with PoP
HolderOfKeyTokenService.HoKValidationResult result = hokService.validateHoKToken(
token,
signedRequest,
httpMethod,
httpUri
);
// Note: This requires server key setup
// assertTrue(result.isValid());
}
@Test
@Order(5)
void testMTLSCertificateValidation() throws Exception {
// Generate test certificate
X509Certificate clientCert = mtlsService.generateTestClientCertificate("client-123");
// Create simple SSL session wrapper
SSLSession sslSession = new SimpleSSLSession(clientCert);
// Validate mTLS
MutualTLSService.MtlsValidationResult result =
mtlsService.validateMtlsConnection(sslSession);
assertTrue(result.isValid());
assertEquals("client-123", result.getClientId());
}
@Test
@Order(6)
void testTokenCertificateBinding() throws Exception {
X509Certificate clientCert = mtlsService.generateTestClientCertificate("client-123");
// Compute certificate fingerprint
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] certFingerprint = digest.digest(clientCert.getEncoded());
String fingerprint = Base64.getUrlEncoder().withoutPadding()
.encodeToString(certFingerprint);
// Bind token to certificate
String token = "test-token";
String boundToken = mtlsService.bindTokenToCertificate(token, fingerprint);
// Verify binding
boolean valid = mtlsService.verifyTokenCertificateBinding(token, clientCert);
// Note: This requires proper token parsing
// assertTrue(valid);
}
@Test
@Order(7)
void testKeyConfirmation() throws Exception {
KeyConfirmationService kcService = new KeyConfirmationService();
// Generate test key
byte[] key = new byte[32];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(key);
byte[] context = "test-context".getBytes();
// Perform key confirmation
KeyConfirmationService.KeyConfirmationResult result =
kcService.performKeyConfirmation(key, context, "HmacSHA256");
assertNotNull(result);
assertNotNull(result.getNonce());
assertNotNull(result.getKcf());
// Verify key confirmation
boolean verified = kcService.verifyKeyConfirmation(
key,
context,
result.getNonce(),
result.getKcf(),
"HmacSHA256"
);
assertTrue(verified);
}
@Test
@Order(8)
void testChallengeResponse() throws Exception {
KeyConfirmationService kcService = new KeyConfirmationService();
// Create challenge
KeyConfirmationService.PoPChallenge challenge =
kcService.createChallenge("client-123", Duration.ofMinutes(5));
assertNotNull(challenge);
assertNotNull(challenge.getId());
assertNotNull(challenge.getChallenge());
assertFalse(challenge.isExpired());
// Client signs challenge (simulated)
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair clientKeyPair = keyGen.generateKeyPair();
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(clientKeyPair.getPrivate());
signature.update(challenge.getChallenge());
byte[] response = signature.sign();
// Server verifies response
boolean verified = kcService.verifyChallengeResponse(
challenge,
response,
clientKeyPair.getPublic()
);
assertTrue(verified);
}
@Test
@Order(9)
void testUnifiedValidator() throws Exception {
// Test DPoP flow
String httpMethod = "GET";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
String dpopProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
PoPTokenValidator.PoPValidationResult result = popValidator.validateRequest(
accessToken,
dpopProof,
null,
httpMethod,
httpUri,
new byte[0]
);
// Note: This requires proper token parsing and key extraction
// assertTrue(result.isValid());
}
@Test
@Order(10)
void testInvalidDPoPProof() throws Exception {
String httpMethod = "GET";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
// Create valid proof
String validProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
// Tamper with proof
String[] parts = validProof.split("\\.");
parts[2] = "tampered-signature";
String tamperedProof = String.join(".", parts);
// Validate tampered proof
DPoPService.DPoPValidationResult result = dpopService.validateDPoPProof(
tamperedProof,
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPublicJWK()
);
assertFalse(result.isValid());
}
@Test
@Order(11)
void testExpiredDPoPProof() throws Exception {
String httpMethod = "GET";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
// Create proof with custom expiration (would need to modify service)
// For this test, we'll simulate by waiting
// This test would need to mock time or create proof with short expiration
}
@Test
@Order(12)
void testWrongHttpMethod() throws Exception {
String httpMethod = "POST";
String wrongMethod = "GET";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
String dpopProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
// Validate with wrong HTTP method
DPoPService.DPoPValidationResult result = dpopService.validateDPoPProof(
dpopProof,
wrongMethod,
httpUri,
accessToken,
clientKeyPair.getPublicJWK()
);
assertFalse(result.isValid());
}
@Test
@Order(13)
void testWrongUri() throws Exception {
String httpMethod = "POST";
String httpUri = "https://api.example.com/resource";
String wrongUri = "https://api.example.com/wrong";
String accessToken = "test-access-token";
String dpopProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
// Validate with wrong URI
DPoPService.DPoPValidationResult result = dpopService.validateDPoPProof(
dpopProof,
httpMethod,
wrongUri,
accessToken,
clientKeyPair.getPublicJWK()
);
assertFalse(result.isValid());
}
@Test
@Order(14)
void testWrongAccessTokenHash() throws Exception {
String httpMethod = "POST";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
String wrongToken = "wrong-token";
String dpopProof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
// Validate with wrong access token
DPoPService.DPoPValidationResult result = dpopService.validateDPoPProof(
dpopProof,
httpMethod,
httpUri,
wrongToken,
clientKeyPair.getPublicJWK()
);
assertFalse(result.isValid());
}
@Test
@Order(15)
void testPerformance() throws Exception {
int iterations = 100;
String httpMethod = "POST";
String httpUri = "https://api.example.com/resource";
String accessToken = "test-access-token";
// Generate multiple proofs
List<String> proofs = new ArrayList<>();
long startGen = System.nanoTime();
for (int i = 0; i < iterations; i++) {
String proof = dpopService.createDPoPProof(
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPrivateKey(),
clientKeyPair.getJkt()
);
proofs.add(proof);
}
long genTime = System.nanoTime() - startGen;
// Validate proofs
long startVal = System.nanoTime();
for (String proof : proofs) {
dpopService.validateDPoPProof(
proof,
httpMethod,
httpUri,
accessToken,
clientKeyPair.getPublicJWK()
);
}
long valTime = System.nanoTime() - startVal;
System.out.printf("DPoP generation: %d ns/op%n", genTime / iterations);
System.out.printf("DPoP validation: %d ns/op%n", valTime / iterations);
assertTrue(genTime / iterations < 50_000_000, "Generation too slow");
assertTrue(valTime / iterations < 20_000_000, "Validation too slow");
}
// Simple SSLSession implementation for testing
private static class SimpleSSLSession implements SSLSession {
private final X509Certificate certificate;
SimpleSSLSession(X509Certificate certificate) {
this.certificate = certificate;
}
@Override
public byte[] getId() { return new byte[0]; }
@Override
public Certificate[] getPeerCertificates() {
return new Certificate[]{certificate};
}
// Other required methods with default implementations
@Override public String getCipherSuite() { return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; }
@Override public String getProtocol() { return "TLSv1.3"; }
@Override public String getPeerHost() { return "localhost"; }
@Override public int getPeerPort() { return 443; }
@Override public long getCreationTime() { return System.currentTimeMillis(); }
@Override public long getLastAccessedTime() { return System.currentTimeMillis(); }
@Override public void invalidate() {}
@Override public boolean isValid() { return true; }
@Override public void putValue(String name, Object value) {}
@Override public Object getValue(String name) { return null; }
@Override public void removeValue(String name) {}
@Override public String[] getValueNames() { return new String[0]; }
@Override public Certificate[] getLocalCertificates() { return null; }
@Override public javax.security.cert.X509Certificate[] getPeerCertificateChain() { return null; }
@Override public Principal getPeerPrincipal() { return null; }
@Override public Principal getLocalPrincipal() { return null; }
@Override public String getSessionContext() { return null; }
@Override public int getApplicationBufferSize() { return 16384; }
@Override public int getPacketBufferSize() { return 16640; }
}
}
Security Best Practices
1. Key Strength Requirements
public class KeyStrengthValidator {
public boolean isKeyStrongEnough(Key key) {
if (key instanceof RSAPublicKey) {
RSAPublicKey rsaKey = (RSAPublicKey) key;
return rsaKey.getModulus().bitLength() >= 2048;
}
if (key instanceof ECPublicKey) {
ECPublicKey ecKey = (ECPublicKey) key;
return ecKey.getParams().getCurve().getField().getFieldSize() >= 256;
}
return false;
}
}
2. Replay Prevention
public class ReplayPreventionService {
private final Set<String> usedNonces = Collections.newSetFromMap(
new ConcurrentHashMap<>()
);
public boolean checkAndStoreNonce(String nonce, Date expiration) {
if (usedNonces.contains(nonce)) {
return false; // Replay detected
}
usedNonces.add(nonce);
// Schedule removal after expiration
scheduledExecutor.schedule(
() -> usedNonces.remove(nonce),
expiration.getTime() - System.currentTimeMillis(),
TimeUnit.MILLISECONDS
);
return true;
}
}
3. Key Rotation
@Scheduled(cron = "0 0 2 * * *") // Daily at 2 AM
public void rotatePoPKeys() {
// Identify keys older than rotation period
List<String> oldKeys = keyStorage.getKeysOlderThan(Duration.ofDays(90));
for (String clientId : oldKeys) {
// Notify client of upcoming key rotation
notificationService.sendKeyRotationNotice(clientId);
// Mark key for expiration
keyStorage.markKeyForExpiration(clientId, Duration.ofDays(7));
}
}
4. Audit Logging
@Aspect
@Component
public class PoPAuditAspect {
@Around("@annotation(Audited)")
public Object auditPoPOperation(ProceedingJoinPoint pjp) throws Throwable {
String operation = pjp.getSignature().getName();
String clientId = extractClientId();
long start = System.currentTimeMillis();
boolean success = false;
String error = null;
try {
Object result = pjp.proceed();
success = true;
return result;
} catch (Exception e) {
error = e.getMessage();
throw e;
} finally {
auditLogger.log(new AuditEvent(
operation,
clientId,
success,
error,
System.currentTimeMillis() - start
));
}
}
}
Comparison of PoP Methods
| Method | Key Type | Strength | Use Case | Complexity |
|---|---|---|---|---|
| DPoP | Asymmetric | High | OAuth 2.0, APIs | Medium |
| Holder-of-Key | Asymmetric | High | SAML, JWT | High |
| mTLS | X.509 | Very High | Internal services, B2B | High |
| OAuth PoP | Asymmetric | High | OAuth 2.0 | Medium |
| Key Confirmation | Symmetric | Medium | Session binding | Low |
Conclusion
Proof of Possession provides critical security benefits:
Security Benefits
- Prevents token replay - Tokens cannot be used without private key
- Phishing resistance - Tokens bound to specific origin
- Channel binding - Tokens bound to TLS session
- Key compromise detection - Requires active key use
Implementation Recommendations
- Use DPoP for OAuth 2.0 APIs - Standardized, widely supported
- Use mTLS for service-to-service - Strongest security, infrastructure support
- Use Holder-of-Key for legacy systems - Compatible with existing JWT infrastructure
- Implement key rotation - Regular key changes limit exposure
- Monitor for anomalies - Detect unusual PoP validation patterns
This implementation provides comprehensive PoP support for modern applications, ensuring tokens cannot be reused by attackers even if intercepted.