This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 94d69c29630 Validate token hashes if they are available (#3385)
94d69c29630 is described below
commit 94d69c2963060fa7a58d69a62ebb4decbd8153af
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Aug 24 08:57:19 2026 +0100
Validate token hashes if they are available (#3385)
---
.../cxf/rs/security/oidc/rp/IdTokenReader.java | 16 ++++-
.../cxf/rs/security/oidc/rp/IdTokenReaderTest.java | 78 +++++++++++++++++++++-
2 files changed, 92 insertions(+), 2 deletions(-)
diff --git
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReader.java
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReader.java
index 38e0488e838..9ed1d18cf4e 100644
---
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReader.java
+++
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReader.java
@@ -21,6 +21,7 @@ package org.apache.cxf.rs.security.oidc.rp;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
import org.apache.cxf.rs.security.oauth2.client.Consumer;
import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken;
+import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
import org.apache.cxf.rs.security.oidc.common.IdToken;
import org.apache.cxf.rs.security.oidc.utils.OidcUtils;
@@ -43,7 +44,7 @@ public class IdTokenReader extends OidcClaimsValidator {
}
public JwtToken getIdJwtToken(ClientAccessToken at, String code, Consumer
client) {
String idJwtToken = at.getParameters().get(OidcUtils.ID_TOKEN);
- JwtToken jwt = getIdJwtToken(idJwtToken, client);
+ JwtToken jwt = parseAndValidateClaims(idJwtToken, client);
OidcUtils.validateAccessTokenHash(at, jwt, requireAtHash);
if (code != null) {
// The spec requires c_hash to be present in the id_token for
hybrid flows,
@@ -55,7 +56,20 @@ public class IdTokenReader extends OidcClaimsValidator {
public JwtToken getIdJwtToken(ClientAccessToken at, Consumer client) {
return getIdJwtToken(at, null, client);
}
+ // No access_token/code is available on this path, so at_hash/c_hash
binding cannot be
+ // verified here; reject tokens that assert such a binding rather than
silently accepting it.
public JwtToken getIdJwtToken(String idJwtToken, Consumer client) {
+ JwtToken jwt = parseAndValidateClaims(idJwtToken, client);
+ if (requireAtHash &&
jwt.getClaims().getClaim(IdToken.ACCESS_TOKEN_HASH_CLAIM) != null) {
+ throw new OAuthServiceException("at_hash claim cannot be validated
without an access token");
+ }
+ if (requireCodeHash &&
jwt.getClaims().getClaim(IdToken.AUTH_CODE_HASH_CLAIM) != null) {
+ throw new OAuthServiceException("c_hash claim cannot be validated
without an authorization code");
+ }
+ return jwt;
+ }
+
+ private JwtToken parseAndValidateClaims(String idJwtToken, Consumer
client) {
JwtToken jwt = getJwtToken(idJwtToken, client.getClientSecret());
validateJwtClaims(jwt.getClaims(), client.getClientId(), true);
return jwt;
diff --git
a/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReaderTest.java
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReaderTest.java
index 7f8bb97f2e8..d997697ed2b 100644
---
a/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReaderTest.java
+++
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/IdTokenReaderTest.java
@@ -24,6 +24,7 @@ import org.apache.cxf.rs.security.oauth2.client.Consumer;
import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken;
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+import org.apache.cxf.rs.security.oidc.common.IdToken;
import org.apache.cxf.rs.security.oidc.utils.OidcUtils;
import org.junit.Test;
@@ -55,6 +56,61 @@ public class IdTokenReaderTest {
idTokenReader.getIdJwtToken(accessToken, "auth-code", new
Consumer("client-id"));
}
+ // The String overload has no access_token/code to check at_hash/c_hash
against: if the
+ // id_token asserts such a claim while hash validation is required, it
must be rejected.
+ @Test(expected = OAuthServiceException.class)
+ public void testStringOverloadRejectsUnverifiableAtHashByDefault() {
+ JwtClaims claims = validClaims();
+ claims.setClaim(IdToken.ACCESS_TOKEN_HASH_CLAIM, "some-hash");
+ IdTokenReader idTokenReader = new StubJwtParsingIdTokenReader(new
JwtToken(claims));
+ idTokenReader.setIssuerId("https://idp.example.com");
+
+ idTokenReader.getIdJwtToken("id-token", new Consumer("client-id"));
+ }
+
+ @Test(expected = OAuthServiceException.class)
+ public void testStringOverloadRejectsUnverifiableCodeHashWhenRequired() {
+ JwtClaims claims = validClaims();
+ claims.setClaim(IdToken.AUTH_CODE_HASH_CLAIM, "some-hash");
+ IdTokenReader idTokenReader = new StubJwtParsingIdTokenReader(new
JwtToken(claims));
+ idTokenReader.setIssuerId("https://idp.example.com");
+ idTokenReader.setRequireAccessTokenHash(false);
+ idTokenReader.setRequireCodeHash(true);
+
+ idTokenReader.getIdJwtToken("id-token", new Consumer("client-id"));
+ }
+
+ @Test
+ public void testStringOverloadAcceptsTokenWithoutHashClaimsByDefault() {
+ JwtClaims claims = validClaims();
+ IdTokenReader idTokenReader = new StubJwtParsingIdTokenReader(new
JwtToken(claims));
+ idTokenReader.setIssuerId("https://idp.example.com");
+
+ assertNotNull(idTokenReader.getIdJwtToken("id-token", new
Consumer("client-id")));
+ }
+
+ @Test
+ public void testStringOverloadAcceptsAtHashWhenNotRequired() {
+ JwtClaims claims = validClaims();
+ claims.setClaim(IdToken.ACCESS_TOKEN_HASH_CLAIM, "some-hash");
+ IdTokenReader idTokenReader = new StubJwtParsingIdTokenReader(new
JwtToken(claims));
+ idTokenReader.setIssuerId("https://idp.example.com");
+ idTokenReader.setRequireAccessTokenHash(false);
+
+ assertNotNull(idTokenReader.getIdJwtToken("id-token", new
Consumer("client-id")));
+ }
+
+ private static JwtClaims validClaims() {
+ JwtClaims claims = new JwtClaims();
+ claims.setIssuer("https://idp.example.com");
+ claims.setSubject("subject");
+ claims.setAudience("client-id");
+ long now = System.currentTimeMillis() / 1000L;
+ claims.setIssuedAt(now);
+ claims.setExpiryTime(now + 300L);
+ return claims;
+ }
+
private static final class StubIdTokenReader extends IdTokenReader {
private final JwtToken jwt;
@@ -63,7 +119,27 @@ public class IdTokenReaderTest {
}
@Override
- public JwtToken getIdJwtToken(String idJwtToken, Consumer client) {
+ public JwtToken getJwtToken(String wrappedJwtToken, String
clientSecret) {
+ return jwt;
+ }
+
+ @Override
+ public void validateJwtClaims(JwtClaims claims, String clientId,
boolean validateClaimsAlways) {
+ // Claims validation is exercised separately in
OidcClaimsValidatorTest.
+ }
+ }
+
+ // Bypasses actual JWS parsing/signature verification so the real
getIdJwtToken(String, Consumer)
+ // logic (claims validation + hash-claim guard) under test still executes.
+ private static final class StubJwtParsingIdTokenReader extends
IdTokenReader {
+ private final JwtToken jwt;
+
+ private StubJwtParsingIdTokenReader(JwtToken jwt) {
+ this.jwt = jwt;
+ }
+
+ @Override
+ public JwtToken getJwtToken(String wrappedJwtToken, String
clientSecret) {
return jwt;
}
}