This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/self-issued in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 76566eefaf371aed3fdd48fe40938dc42fc1375c Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Wed Jul 22 10:48:36 2026 +0100 Perform validation for OIDC self-issued tokens --- .../rs/security/oidc/rp/OidcClaimsValidator.java | 112 ++++++++++++++------- 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClaimsValidator.java b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClaimsValidator.java index 2883c2fed46..b831fe00bd5 100644 --- a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClaimsValidator.java +++ b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcClaimsValidator.java @@ -57,60 +57,94 @@ public class OidcClaimsValidator extends OAuthJoseJwtConsumer { if (issuer == null && validateClaimsAlways) { throw new OAuthServiceException("Invalid issuer"); } - if (supportSelfIssuedProvider && issuerId == null - && issuer != null && SELF_ISSUED_ISSUER.equals(issuer)) { - validateSelfIssuedProvider(claims, clientId, validateClaimsAlways); + + boolean isSelfIssued = supportSelfIssuedProvider && issuerId == null + && issuer != null && SELF_ISSUED_ISSUER.equals(issuer); + + if (isSelfIssued) { + // Self-issued specific: validate sub_jwk binding (OIDC Core ยง7) + validateSelfIssuedSubJwkBinding(claims); } else { + // Normal issuer validation if (issuer != null && !issuer.equals(issuerId)) { throw new OAuthServiceException("Invalid issuer"); } - // validate subject - if (claims.getSubject() == null) { - throw new OAuthServiceException("Invalid subject"); - } - // validate authorized party + // validate authorized party (not applicable for self-issued) String authorizedParty = (String)claims.getClaim(IdToken.AZP_CLAIM); if (authorizedParty != null && !authorizedParty.equals(clientId)) { throw new OAuthServiceException("Invalid authorized party"); } - // validate audience - List<String> audiences = claims.getAudiences(); - if (StringUtils.isEmpty(audiences) && validateClaimsAlways - || !StringUtils.isEmpty(audiences) && !audiences.contains(clientId)) { - throw new OAuthServiceException("Invalid audience"); - } + } - // If strict time validation: if no issuedTime claim is set then an expiresAt claim must be set - // Otherwise: validate only if expiresAt claim is set - boolean expiredRequired = - validateClaimsAlways || strictTimeValidation && claims.getIssuedAt() == null; - try { - JwtUtils.validateJwtExpiry(claims, getClockOffset(), expiredRequired); - } catch (JwtException ex) { - throw new OAuthServiceException("ID Token has expired", ex); - } + // Common validations for both self-issued and normal issuers + validateSubject(claims); + validateAudience(claims, clientId, validateClaimsAlways); + validateTokenTime(claims, validateClaimsAlways); + } - // If strict time validation: If no expiresAt claim is set then an issuedAt claim must be set - // Otherwise: validate only if issuedAt claim is set - boolean issuedAtRequired = - validateClaimsAlways || strictTimeValidation && claims.getExpiryTime() == null; - try { - JwtUtils.validateJwtIssuedAt(claims, getTtl(), getClockOffset(), issuedAtRequired); - } catch (JwtException ex) { - throw new OAuthServiceException("Invalid issuedAt claim", ex); - } + private void validateSelfIssuedSubJwkBinding(JwtClaims claims) { + String subJwk = (String)claims.getClaim("sub_jwk"); + if (subJwk == null) { + throw new OAuthServiceException("Missing sub_jwk claim for self-issued token"); + } - // Validate nbf - but don't require it to be present - try { - JwtUtils.validateJwtNotBefore(claims, getClockOffset(), false); - } catch (JwtException ex) { - throw new OAuthServiceException("ID Token can not be used yet", ex); + try { + JsonWebKey publicKey = JwkUtils.readJwkKey(subJwk); + String thumbprint = JwkUtils.getThumbprint(publicKey); + String sub = claims.getSubject(); + if (!thumbprint.equals(sub)) { + throw new OAuthServiceException("sub_jwk thumbprint does not match sub claim"); } + } catch (RuntimeException ex) { + if (ex instanceof OAuthServiceException) { + throw ex; + } + throw new OAuthServiceException("Invalid sub_jwk claim: " + ex.getMessage(), ex); } } - private void validateSelfIssuedProvider(JwtClaims claims, String clientId, boolean validateClaimsAlways) { + private void validateSubject(JwtClaims claims) { + if (claims.getSubject() == null) { + throw new OAuthServiceException("Invalid subject"); + } + } + + private void validateAudience(JwtClaims claims, String clientId, boolean validateClaimsAlways) { + List<String> audiences = claims.getAudiences(); + if (StringUtils.isEmpty(audiences) && validateClaimsAlways + || !StringUtils.isEmpty(audiences) && !audiences.contains(clientId)) { + throw new OAuthServiceException("Invalid audience"); + } + } + + private void validateTokenTime(JwtClaims claims, boolean validateClaimsAlways) { + // If strict time validation: if no issuedTime claim is set then an expiresAt claim must be set + // Otherwise: validate only if expiresAt claim is set + boolean expiredRequired = + validateClaimsAlways || strictTimeValidation && claims.getIssuedAt() == null; + try { + JwtUtils.validateJwtExpiry(claims, getClockOffset(), expiredRequired); + } catch (JwtException ex) { + throw new OAuthServiceException("ID Token has expired", ex); + } + + // If strict time validation: If no expiresAt claim is set then an issuedAt claim must be set + // Otherwise: validate only if issuedAt claim is set + boolean issuedAtRequired = + validateClaimsAlways || strictTimeValidation && claims.getExpiryTime() == null; + try { + JwtUtils.validateJwtIssuedAt(claims, getTtl(), getClockOffset(), issuedAtRequired); + } catch (JwtException ex) { + throw new OAuthServiceException("Invalid issuedAt claim", ex); + } + + // Validate nbf - but don't require it to be present + try { + JwtUtils.validateJwtNotBefore(claims, getClockOffset(), false); + } catch (JwtException ex) { + throw new OAuthServiceException("ID Token can not be used yet", ex); + } } public void setIssuerId(String issuerId) { @@ -124,7 +158,7 @@ public class OidcClaimsValidator extends OAuthJoseJwtConsumer { @Override protected JwsSignatureVerifier getInitializedSignatureVerifier(JwtToken jwt) { JsonWebKey key = null; - if (supportSelfIssuedProvider && SELF_ISSUED_ISSUER.equals(jwt.getClaim("issuer"))) { + if (supportSelfIssuedProvider && SELF_ISSUED_ISSUER.equals(jwt.getClaims().getIssuer())) { String publicKeyJson = (String)jwt.getClaim("sub_jwk"); if (publicKeyJson != null) { JsonWebKey publicKey = JwkUtils.readJwkKey(publicKeyJson);
