Fix DPoP jkt claim to be JWK SHA-256 thumbprint
Closes gh-2007
This commit is contained in:
@@ -16,7 +16,6 @@
|
|||||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -24,7 +23,6 @@ import java.util.HashMap;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.nimbusds.jose.jwk.AsymmetricJWK;
|
|
||||||
import com.nimbusds.jose.jwk.JWK;
|
import com.nimbusds.jose.jwk.JWK;
|
||||||
|
|
||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
@@ -91,25 +89,22 @@ final class DefaultOAuth2TokenCustomizers {
|
|||||||
// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
|
// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
|
||||||
Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);
|
Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);
|
||||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {
|
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {
|
||||||
PublicKey publicKey = null;
|
JWK jwk = null;
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get("jwk");
|
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get("jwk");
|
||||||
try {
|
try {
|
||||||
JWK jwk = JWK.parse(jwkJson);
|
jwk = JWK.parse(jwkJson);
|
||||||
if (jwk instanceof AsymmetricJWK asymmetricJWK) {
|
|
||||||
publicKey = asymmetricJWK.toPublicKey();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ignored) {
|
catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
if (publicKey == null) {
|
if (jwk == null) {
|
||||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
|
||||||
"jwk header is missing or invalid.", null);
|
"jwk header is missing or invalid.", null);
|
||||||
throw new OAuth2AuthenticationException(error);
|
throw new OAuth2AuthenticationException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String sha256Thumbprint = computeSHA256Thumbprint(publicKey);
|
String sha256Thumbprint = jwk.computeThumbprint().toString();
|
||||||
if (cnfClaims == null) {
|
if (cnfClaims == null) {
|
||||||
cnfClaims = new HashMap<>();
|
cnfClaims = new HashMap<>();
|
||||||
}
|
}
|
||||||
@@ -149,10 +144,4 @@ final class DefaultOAuth2TokenCustomizers {
|
|||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String computeSHA256Thumbprint(PublicKey publicKey) throws Exception {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] digest = md.digest(publicKey.getEncoded());
|
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1011,6 +1011,8 @@ public class OAuth2AuthorizationCodeGrantTests {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
||||||
assertThat(cnfClaims).containsKey("jkt");
|
assertThat(cnfClaims).containsKey("jkt");
|
||||||
|
String jwkThumbprintClaim = (String) cnfClaims.get("jkt");
|
||||||
|
assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -605,6 +605,8 @@ public class OAuth2DeviceCodeGrantTests {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
||||||
assertThat(cnfClaims).containsKey("jkt");
|
assertThat(cnfClaims).containsKey("jkt");
|
||||||
|
String jwkThumbprintClaim = (String) cnfClaims.get("jkt");
|
||||||
|
assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||||
|
|||||||
@@ -319,6 +319,8 @@ public class OAuth2RefreshTokenGrantTests {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
||||||
assertThat(cnfClaims).containsKey("jkt");
|
assertThat(cnfClaims).containsKey("jkt");
|
||||||
|
String jwkThumbprintClaim = (String) cnfClaims.get("jkt");
|
||||||
|
assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user