From 07f9621b02da8fa5423f9decdd2691c7a3cc6d6e Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Tue, 13 May 2025 15:03:23 -0400 Subject: [PATCH] Fix DPoP jkt claim to be JWK SHA-256 thumbprint Closes gh-2007 --- .../DefaultOAuth2TokenCustomizers.java | 19 ++++--------------- .../OAuth2AuthorizationCodeGrantTests.java | 2 ++ .../OAuth2DeviceCodeGrantTests.java | 2 ++ .../OAuth2RefreshTokenGrantTests.java | 2 ++ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DefaultOAuth2TokenCustomizers.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DefaultOAuth2TokenCustomizers.java index b240fa8b..155f860a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DefaultOAuth2TokenCustomizers.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DefaultOAuth2TokenCustomizers.java @@ -16,7 +16,6 @@ package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers; import java.security.MessageDigest; -import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.Base64; import java.util.Collections; @@ -24,7 +23,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; -import com.nimbusds.jose.jwk.AsymmetricJWK; import com.nimbusds.jose.jwk.JWK; 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) Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY); if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) { - PublicKey publicKey = null; + JWK jwk = null; @SuppressWarnings("unchecked") Map jwkJson = (Map) dPoPProofJwt.getHeaders().get("jwk"); try { - JWK jwk = JWK.parse(jwkJson); - if (jwk instanceof AsymmetricJWK asymmetricJWK) { - publicKey = asymmetricJWK.toPublicKey(); - } + jwk = JWK.parse(jwkJson); } catch (Exception ignored) { } - if (publicKey == null) { + if (jwk == null) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jwk header is missing or invalid.", null); throw new OAuth2AuthenticationException(error); } try { - String sha256Thumbprint = computeSHA256Thumbprint(publicKey); + String sha256Thumbprint = jwk.computeThumbprint().toString(); if (cnfClaims == null) { cnfClaims = new HashMap<>(); } @@ -149,10 +144,4 @@ final class DefaultOAuth2TokenCustomizers { 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); - } - } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java index 4d8b1d67..55a2bdbe 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java @@ -1011,6 +1011,8 @@ public class OAuth2AuthorizationCodeGrantTests { @SuppressWarnings("unchecked") Map cnfClaims = (Map) authorization.getAccessToken().getClaims().get("cnf"); assertThat(cnfClaims).containsKey("jkt"); + String jwkThumbprintClaim = (String) cnfClaims.get("jkt"); + assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString()); } @Test diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java index 19df0f31..338438f0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java @@ -605,6 +605,8 @@ public class OAuth2DeviceCodeGrantTests { @SuppressWarnings("unchecked") Map cnfClaims = (Map) authorization.getAccessToken().getClaims().get("cnf"); 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) { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2RefreshTokenGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2RefreshTokenGrantTests.java index 6df1d73a..ce37fb15 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2RefreshTokenGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2RefreshTokenGrantTests.java @@ -319,6 +319,8 @@ public class OAuth2RefreshTokenGrantTests { @SuppressWarnings("unchecked") Map cnfClaims = (Map) authorization.getAccessToken().getClaims().get("cnf"); assertThat(cnfClaims).containsKey("jkt"); + String jwkThumbprintClaim = (String) cnfClaims.get("jkt"); + assertThat(jwkThumbprintClaim).isEqualTo(TestJwks.DEFAULT_EC_JWK.toPublicJWK().computeThumbprint().toString()); } @Test