This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/access-token-validator in repository https://gitbox.apache.org/repos/asf/cxf.git
commit a0da1e9ed47c1570472339ebdc30b3c545fd0636 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Wed May 20 15:01:51 2026 +0100 Validate expiry, nbg + audience for the JwtAccessTokenValidator --- .../oauth2/filters/JwtAccessTokenValidator.java | 18 ++ .../filters/JwtAccessTokenValidatorTest.java | 200 +++++++++++++++++++++ .../jaxrs/security/oauth2/tls/serverTls.xml | 4 +- 3 files changed, 221 insertions(+), 1 deletion(-) diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidator.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidator.java index 80ce114da2f..3f27d739f80 100644 --- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidator.java +++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidator.java @@ -32,6 +32,7 @@ import org.apache.cxf.rs.security.jose.jwt.JoseJwtConsumer; import org.apache.cxf.rs.security.jose.jwt.JwtClaims; import org.apache.cxf.rs.security.jose.jwt.JwtConstants; import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.cxf.rs.security.jose.jwt.JwtUtils; import org.apache.cxf.rs.security.oauth2.common.AccessTokenValidation; import org.apache.cxf.rs.security.oauth2.common.OAuthPermission; import org.apache.cxf.rs.security.oauth2.common.UserSubject; @@ -46,6 +47,7 @@ public class JwtAccessTokenValidator extends JoseJwtConsumer implements AccessTo private static final String USERNAME_PROP = "username"; private Map<String, String> jwtAccessTokenClaimMap; + private boolean validateAudience = true; public List<String> getSupportedAuthorizationSchemes() { return Collections.singletonList(OAuthConstants.BEARER_AUTHORIZATION_SCHEME); @@ -64,6 +66,15 @@ public class JwtAccessTokenValidator extends JoseJwtConsumer implements AccessTo } } + @Override + protected void validateToken(JwtToken jwt) { + // We must have an issuer + if (jwt.getClaim(JwtConstants.CLAIM_ISSUER) == null) { + throw new OAuthServiceException(OAuthConstants.INVALID_GRANT); + } + + JwtUtils.validateTokenClaims(jwt.getClaims(), getTtl(), getClockOffset(), isValidateAudience()); + } private AccessTokenValidation convertClaimsToValidation(JwtClaims claims) { AccessTokenValidation atv = new AccessTokenValidation(); @@ -134,4 +145,11 @@ public class JwtAccessTokenValidator extends JoseJwtConsumer implements AccessTo this.jwtAccessTokenClaimMap = jwtAccessTokenClaimMap; } + public boolean isValidateAudience() { + return validateAudience; + } + + public void setValidateAudience(boolean validateAudience) { + this.validateAudience = validateAudience; + } } diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidatorTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidatorTest.java new file mode 100644 index 00000000000..e2f96ce609e --- /dev/null +++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidatorTest.java @@ -0,0 +1,200 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.rs.security.oauth2.filters; + +import java.lang.reflect.Field; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.message.Message; +import org.apache.cxf.message.MessageImpl; +import org.apache.cxf.phase.PhaseInterceptorChain; +import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; +import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureProvider; +import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureVerifier; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer; +import org.apache.cxf.rs.security.jose.jwt.JwtClaims; +import org.apache.cxf.rs.security.jose.jwt.JwtConstants; +import org.apache.cxf.rs.security.oauth2.common.AccessTokenValidation; +import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException; + +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +public class JwtAccessTokenValidatorTest { + + private static final String SIGNING_KEY = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoq2QjvA6P5f8"; + private static final String DIFFERENT_SIGNING_KEY = "hJtXIZ2uSN5kbQfbtTNWbg6X5U0ZSyxP6oJ6H3f3j1k"; + + @Test + public void testValidateAccessTokenSignedAndSignatureVerified() { + JwtAccessTokenValidator validator = new JwtAccessTokenValidator(); + validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256)); + validator.setValidateAudience(false); + + String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + AccessTokenValidation result = validator.validateAccessToken( + mock(MessageContext.class), "Bearer", jwt, params); + + assertNotNull(result); + assertTrue(result.isInitialValidationSuccessful()); + assertEquals("signed-client", result.getClientId()); + } + + @Test + public void testValidateAccessTokenSignedButSignatureValidationFails() { + JwtAccessTokenValidator validator = new JwtAccessTokenValidator(); + validator.setJwsVerifier(new HmacJwsSignatureVerifier(DIFFERENT_SIGNING_KEY, SignatureAlgorithm.HS256)); + validator.setValidateAudience(false); + + String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + OAuthServiceException ex = assertThrows(OAuthServiceException.class, () -> + validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params)); + + assertNotNull(ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("Invalid Signature")); + } + + @Test + public void testValidateAccessTokenExpired() { + JwtAccessTokenValidator validator = new JwtAccessTokenValidator(); + validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256)); + validator.setValidateAudience(false); + + String jwt = createSignedToken(SIGNING_KEY, "signed-client", -3600); // Expired 1 hour ago + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + OAuthServiceException ex = assertThrows(OAuthServiceException.class, () -> + validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params)); + + assertNotNull(ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("expired")); + } + + @Test + public void testValidateAccessTokenNotBefore() { + JwtAccessTokenValidator validator = new JwtAccessTokenValidator(); + validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256)); + validator.setValidateAudience(false); + + // Not valid before 1 hour from now + String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600, 3600, null); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + OAuthServiceException ex = assertThrows(OAuthServiceException.class, () -> + validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params)); + + assertNotNull(ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("cannot be accepted")); + } + + @After + public void clearCurrentMessage() throws Exception { + setThreadLocalMessage(null); + } + + @Test + public void testValidAudience() throws Exception { + JwtAccessTokenValidator validator = new JwtAccessTokenValidator(); + validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256)); + + String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600, 0, "valid-audience"); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + Message message = new MessageImpl(); + message.put(JwtConstants.EXPECTED_CLAIM_AUDIENCE, "valid-audience"); + setThreadLocalMessage(message); + + AccessTokenValidation result = validator.validateAccessToken( + mock(MessageContext.class), "Bearer", jwt, params); + + assertNotNull(result); + assertTrue(result.isInitialValidationSuccessful()); + assertEquals("signed-client", result.getClientId()); + } + + @Test + public void testInvalidAudience() throws Exception { + JwtAccessTokenValidator validator = new JwtAccessTokenValidator(); + validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256)); + + String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600, 0, "invalid-audience"); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + + Message message = new MessageImpl(); + message.put(JwtConstants.EXPECTED_CLAIM_AUDIENCE, "valid-audience"); + setThreadLocalMessage(message); + + OAuthServiceException ex = assertThrows(OAuthServiceException.class, () -> + validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params)); + + assertNotNull(ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("Invalid audience restriction")); + } + + private static String createSignedToken(String key, String clientId, long expiresInSeconds) { + return createSignedToken(key, clientId, expiresInSeconds, 0, null); + } + + private static String createSignedToken(String key, String clientId, long expiresInSeconds, + long notBeforeOffsetSeconds, String audience) { + long now = System.currentTimeMillis() / 1000; + JwtClaims claims = new JwtClaims(); + claims.setIssuedAt(now); + claims.setExpiryTime(now + expiresInSeconds); + if (clientId != null) { + claims.setClaim("client_id", clientId); + } + claims.setIssuer("SomeIssuer"); + claims.setSubject("SomeSubject"); + if (notBeforeOffsetSeconds != 0) { + claims.setNotBefore(now + notBeforeOffsetSeconds); + } + if (audience != null) { + claims.setAudience(audience); + } + + JwsJwtCompactProducer producer = new JwsJwtCompactProducer(claims); + return producer.signWith(new HmacJwsSignatureProvider(key, SignatureAlgorithm.HS256)); + } + + private static void setThreadLocalMessage(Message message) throws Exception { + Field f = PhaseInterceptorChain.class.getDeclaredField("CURRENT_MESSAGE"); + f.setAccessible(true); + @SuppressWarnings("unchecked") + ThreadLocal<Message> tl = (ThreadLocal<Message>) f.get(null); + if (message == null) { + tl.remove(); + } else { + tl.set(message); + } + } +} \ No newline at end of file diff --git a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/tls/serverTls.xml b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/tls/serverTls.xml index 6f2017a161a..748036ac1e7 100644 --- a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/tls/serverTls.xml +++ b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/tls/serverTls.xml @@ -69,7 +69,9 @@ under the License. <bean id="oauthJson" class="org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider"/> <bean id="dataProvider" class="org.apache.cxf.systest.jaxrs.security.oauth2.tls.OAuthDataProviderImpl"/> - <bean id="dataProviderJwt" class="org.apache.cxf.systest.jaxrs.security.oauth2.tls.OAuthDataProviderImplJwt"/> + <bean id="dataProviderJwt" class="org.apache.cxf.systest.jaxrs.security.oauth2.tls.OAuthDataProviderImplJwt"> + <property name="issuer" value="Some-Issuer"/> + </bean> <bean id="rsService" class="org.apache.cxf.systest.jaxrs.security.BookStore"/> <bean id="accessTokenService1" class="org.apache.cxf.rs.security.oauth2.services.AccessTokenService">
