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.
Complete C Programming Guide + Compilers Collection
1. C srand() Function – Understanding Seed Initialization
https://macronepal.com/understanding-the-c-srand-function
Explains how srand() initializes the pseudo-random number generator in C by setting a seed value. Using the same seed produces the same sequence, while time(NULL) gives different results each run.
2. C rand() Function Mechanics and Limitations
https://macronepal.com/c-rand-function-mechanics-and-limitations
Explains how rand() generates pseudo-random numbers between 0 and RAND_MAX, its deterministic nature, and limitations for security use cases.
3. C log() Function
https://macronepal.com/c-log-function-2
Covers natural logarithm calculation using <math.h> and its applications.
4. Mastering Date and Time in C
https://macronepal.com/mastering-date-and-time-in-c
Explains <time.h> functions like time(), clock(), difftime(), and struct tm.
5. Mastering time_t Type in C
https://macronepal.com/mastering-the-c-time_t-type-for-time-management
Explains time representation as seconds since Unix epoch and conversion functions.
6. C exp() Function
https://macronepal.com/c-exp-function-mechanics-and-implementation
Explains exponential function exp(x) and its scientific applications.
7. C log() Function (Alternate Guide)
https://macronepal.com/c-log-function
Comparison of log() and log10() with usage examples.
8. C log10() Function
https://macronepal.com/mastering-the-log10-function-in-c
Explains base-10 logarithm for engineering and scientific applications.
9. C tan() Function
https://macronepal.com/understanding-the-c-tan-function
Explains tangent function and radian-based calculations.
10. Random Numbers in C (Secure vs Predictable)
https://macronepal.com/mastering-c-random-numbers-for-secure-and-predictable-applications
Explains difference between rand() and secure randomness methods.
11. Free Online C Compiler
https://macronepal.com/free-online-c-code-compiler-2
Browser-based compiler for testing C programs instantly.
C Functions, Arguments, Parameters & Flow
Mastering Functions in C – Complete Guide
https://macronepal.com/c/mastering-functions-in-c-a-complete-guide/
Covers function structure, modular programming, and real-world usage.
Function Arguments in C
https://macronepal.com/c-function-arguments/
Explains how arguments are passed and used in function calls.
Function Parameters in C
https://macronepal.com/c-function-parameters/
Explains defining inputs for functions and matching them with arguments.
Function Declarations in C
https://macronepal.com/c-function-declarations-syntax-rules-and-best-practices/
Covers prototypes, syntax rules, and best practices.
Function Calls in C
https://macronepal.com/understanding-function-calls-in-c-syntax-mechanics-and-best-practices/
Explains execution flow and parameter handling during function calls.
Void Functions in C
https://macronepal.com/understanding-void-functions-in-c-syntax-patterns-and-best-practices/
Explains functions that do not return values.
Return Values in C
https://macronepal.com/c-return-values-mechanics-types-and-best-practices/
Explains different return types and how functions return results.
Pass-by-Value in C
https://macronepal.com/aws/understanding-pass-by-value-in-c-mechanics-implications-and-best-practices/
Explains how copies of variables are passed into functions.
Pass-by-Reference in C
https://macronepal.com/c/understanding-pass-by-reference-in-c-pointers-semantics-and-safe-practices/
Explains using pointers to modify original variables.
C strstr() Function
https://macronepal.com/aws/c-strstr-function/
Explains substring search inside strings in C.
C Preprocessor & Macros
https://macronepal.com/mastering-c-variadic-macros-for-flexible-debugging/
https://macronepal.com/mastering-the-stdc-macro-in-c/
https://macronepal.com/c-time-macro-mechanics-and-usage/
https://macronepal.com/understanding-the-c-date-macro/
https://macronepal.com/c-file-type/
https://macronepal.com/mastering-c-line-macro-for-debugging-and-diagnostics/
https://macronepal.com/mastering-predefined-macros-in-c/
https://macronepal.com/c-error-directive-mechanics-and-usage/
https://macronepal.com/understanding-the-c-pragma-directive/
https://macronepal.com/c-include-directive/
C Structures, Memory, Scope & Linkage
https://macronepal.com/mastering-structures-in-c/
https://macronepal.com/c-structure-declaration-mechanics-and-usage/
https://macronepal.com/c-structure-initialization-mechanics-and-best-practices/
https://macronepal.com/mastering-c-structure-member-access-for-reliable-data-handling/
https://macronepal.com/c-nested-structures/
https://macronepal.com/mastering-arrays-of-structures-in-c/
https://macronepal.com/c-structure-pointers-mechanics-and-implementation/
https://macronepal.com/understanding-c-structure-parameter-passing-mechanics/
https://macronepal.com/mastering-c-returning-structures-for-efficient-data-flow/
https://macronepal.com/c-self-referential-structures/
https://macronepal.com/mastering-structure-alignment-in-c/
https://macronepal.com/c-structure-padding-mechanics-and-optimization/
https://macronepal.com/understanding-c-flexible-array-members-mechanics-and-usage/
https://macronepal.com/mastering-c-anonymous-structures-for-flattened-data-layouts/
https://macronepal.com/c-unions/
https://macronepal.com/mastering-c-name-mangling-and-symbol-decoration/
https://macronepal.com/c-no-linkage-mechanics-and-scope-isolation/
https://macronepal.com/understanding-c-internal-linkage-mechanics-and-architecture/
C Scope, Storage Classes & Typedef
https://macronepal.com/mastering-function-prototype-scope-in-c/
https://macronepal.com/c-function-scope-mechanics-and-visibility/
https://macronepal.com/understanding-c-file-scope-mechanics-and-architecture/
https://macronepal.com/mastering-c-scope-rules-for-predictable-name-resolution/
https://macronepal.com/c-scope-rules/
https://macronepal.com/mastering-c-register-storage-class-for-historical-context-and-modern-alternatives/
https://macronepal.com/mastering-_thread_local-in-c/
https://macronepal.com/c-extern-storage-class-mechanics-and-usage/
https://macronepal.com/understanding-the-c-static-storage-class-mechanics-and-usage/
https://macronepal.com/c-auto-storage-class/
https://macronepal.com/c-typedef-with-pointers/
Extra Articles
https://macronepal.com/13757-2/
https://macronepal.com/13748-2/
https://macronepal.com/13747-2/
https://macronepal.com/13746-2/
https://macronepal.com/13745-2/
https://macronepal.com/13708-2/
https://macronepal.com/13707-2/
https://macronepal.com/13702-2/
Online Compilers
https://macronepal.com/free-html-online-code-compiler/
https://macronepal.com/free-online-python-code-compiler/
https://macronepal.com/free-online-python2-code-compiler/
https://macronepal.com/free-online-java-code-compiler/
https://macronepal.com/free-online-javascript-code-compiler/
https://macronepal.com/free-online-node-js-code-compiler/
https://macronepal.com/free-online-c-code-compiler/
https://macronepal.com/free-online-c-code-compiler-2/
https://macronepal.com/free-online-c-code-compiler-3/
https://macronepal.com/free-online-php-code-compiler/
https://macronepal.com/free-online-ruby-code-compiler/
https://macronepal.com/free-online-perl-code-compiler/
https://macronepal.com/free-online-lua-code-compiler/
https://macronepal.com/free-online-tcl-code-compiler/
https://macronepal.com/free-online-groovy-code-compiler/
https://macronepal.com/free-online-j-shell-code-compiler/
https://macronepal.com/free-online-haskell-code-compiler/
https://macronepal.com/free-online-scala-code-compiler/
https://macronepal.com/free-online-common-lisp-code-compiler/
https://macronepal.com/free-online-d-code-compiler/
https://macronepal.com/free-online-ada-code-compiler/
https://macronepal.com/free-erlang-code-compiler/
https://macronepal.com/free-online-assembly-code-compiler/
