This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/audience
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit a1c99bbf067f68074a66ccf387119091a1082ac3
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Sep 30 14:40:07 2026 +0100

    Adding a flag to require an audience in JwtAccessTokenValidator
---
 .../oauth2/filters/JwtAccessTokenValidator.java    | 17 +++-
 .../oauth2/filters/OAuthRequestFilter.java         | 17 +++-
 .../filters/JwtAccessTokenValidatorTest.java       | 33 ++++++++
 .../oauth2/filters/OAuthRequestFilterTest.java     | 95 ++++++++++++++++++++++
 4 files changed, 160 insertions(+), 2 deletions(-)

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 01f333fede1..5e8e1197c68 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
@@ -51,6 +51,7 @@ public class JwtAccessTokenValidator extends JoseJwtConsumer 
implements AccessTo
 
     private Map<String, String> jwtAccessTokenClaimMap;
     private boolean validateAudience = true;
+    private boolean requireAudience;
 
     public List<String> getSupportedAuthorizationSchemes() {
         return 
Collections.singletonList(OAuthConstants.BEARER_AUTHORIZATION_SCHEME);
@@ -78,7 +79,8 @@ public class JwtAccessTokenValidator extends JoseJwtConsumer 
implements AccessTo
             throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
         }
 
-        JwtUtils.validateTokenClaims(jwt.getClaims(), getTtl(), 
getClockOffset(), isValidateAudience());
+        JwtUtils.validateTokenClaims(jwt.getClaims(), getTtl(), 
getClockOffset(), isValidateAudience(),
+                                     isRequireAudience());
     }
 
     private void validateTokenType(JwtToken jwt) {
@@ -169,4 +171,17 @@ public class JwtAccessTokenValidator extends 
JoseJwtConsumer implements AccessTo
     public void setValidateAudience(boolean validateAudience) {
         this.validateAudience = validateAudience;
     }
+
+    public boolean isRequireAudience() {
+        return requireAudience;
+    }
+
+    /**
+     * Reject tokens which do not contain an "aud" claim. By default, a token 
without an "aud" claim
+     * passes the audience restriction check unless 
JwtConstants.EXPECTED_CLAIM_AUDIENCE is configured.
+     * This only applies if "validateAudience" is enabled.
+     */
+    public void setRequireAudience(boolean requireAudience) {
+        this.requireAudience = requireAudience;
+    }
 }
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
index f38f3ead660..c13844eeb2e 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
@@ -23,6 +23,7 @@ import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
 import jakarta.annotation.Priority;
@@ -47,6 +48,7 @@ import org.apache.cxf.jaxrs.utils.JAXRSUtils;
 import org.apache.cxf.message.Message;
 import org.apache.cxf.phase.PhaseInterceptorChain;
 import org.apache.cxf.rs.security.jose.common.JoseConstants;
+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.common.AuthenticationMethod;
 import org.apache.cxf.rs.security.oauth2.common.OAuthContext;
@@ -80,6 +82,7 @@ public class OAuthRequestFilter extends 
AbstractAccessTokenValidator
     private boolean allPermissionsMatch;
     private boolean blockPublicClients;
     private AuthenticationMethod am;
+    private final AtomicBoolean audienceWarningLogged = new AtomicBoolean();
 
     @Override
     public void filter(ContainerRequestContext context) {
@@ -101,6 +104,12 @@ public class OAuthRequestFilter extends 
AbstractAccessTokenValidator
         String authScheme = authParts[0];
         String authSchemeData = authParts[1];
 
+        // Make the configured audience available to JWT access token 
validators, unless an
+        // expected audience has already been configured
+        if (audience != null && 
m.getContextualProperty(JwtConstants.EXPECTED_CLAIM_AUDIENCE) == null) {
+            m.put(JwtConstants.EXPECTED_CLAIM_AUDIENCE, audience);
+        }
+
         // Get the access token
         AccessTokenValidation accessTokenV = 
getAccessTokenValidation(authScheme, authSchemeData, null);
         if (!accessTokenV.isInitialValidationSuccessful()) {
@@ -272,11 +281,17 @@ public class OAuthRequestFilter extends 
AbstractAccessTokenValidator
     }
 
     protected String validateAudiences(List<String> audiences) {
+        if (audience == null && !audienceIsEndpointAddress
+            && audienceWarningLogged.compareAndSet(false, true)) {
+            LOG.warning("No audience is configured and 
audienceIsEndpointAddress is disabled, so access "
+                + "token audiences will not be validated. Configure the 
\"audience\" property to restrict "
+                + "this resource server to tokens issued for it.");
+        }
         if (StringUtils.isEmpty(audiences) && audience == null) {
             return null;
         }
         if (audience != null) {
-            if (audiences.contains(audience)) {
+            if (audiences != null && audiences.contains(audience)) {
                 return audience;
             }
             AuthorizationUtils.throwAuthorizationFailure(supportedSchemes, 
realm);
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
index 327390d3739..38ecb6ac8be 100644
--- 
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
@@ -179,6 +179,39 @@ public class JwtAccessTokenValidatorTest {
         assertTrue(ex.getCause().getMessage().contains("Invalid audience 
restriction"));
     }
 
+    @Test
+    public void testMissingAudienceAcceptedByDefault() throws Exception {
+        JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
+        validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+
+        String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600);
+        MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
+        setThreadLocalMessage(new MessageImpl());
+
+        AccessTokenValidation result = validator.validateAccessToken(
+            mock(MessageContext.class), "Bearer", jwt, params);
+
+        assertNotNull(result);
+        assertTrue(result.isInitialValidationSuccessful());
+    }
+
+    @Test
+    public void testMissingAudienceRejectedWhenRequired() throws Exception {
+        JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
+        validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+        validator.setRequireAudience(true);
+
+        String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600);
+        MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
+        setThreadLocalMessage(new MessageImpl());
+
+        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, 
null, null);
     }
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
index d52a148f8a4..69f82b6ad82 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
@@ -22,9 +22,18 @@ import java.lang.reflect.Field;
 import java.util.Collections;
 
 import jakarta.ws.rs.NotAuthorizedException;
+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.jose.jwt.JwtToken;
+import org.apache.cxf.rs.security.oauth2.common.OAuthContext;
 
 import org.junit.After;
 import org.junit.Test;
@@ -32,9 +41,12 @@ import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
 
 public class OAuthRequestFilterTest {
 
+    private static final String SIGNING_KEY = 
"AyM1SysPpbyDfgZld3umj1qzKObwVMkoq2QjvA6P5f8";
+
     @After
     public void clearCurrentMessage() throws Exception {
         setThreadLocalMessage(null);
@@ -120,6 +132,89 @@ public class OAuthRequestFilterTest {
         assertEquals("/api/read", 
filter.validateAudiences(Collections.singletonList("/api/read")));
     }
 
+    @Test
+    public void 
testValidateAudiencesRejectsMissingAudiencesWhenAudienceConfigured() {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setAudience("https://rs.example/api";);
+
+        assertThrows(NotAuthorizedException.class, () -> 
filter.validateAudiences(null));
+        assertThrows(NotAuthorizedException.class, () -> 
filter.validateAudiences(Collections.emptyList()));
+        assertEquals("https://rs.example/api";,
+            
filter.validateAudiences(Collections.singletonList("https://rs.example/api";)));
+    }
+
+    @Test
+    public void testConfiguredAudienceIsUsedByJwtAccessTokenValidator() throws 
Exception {
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "https://rs.example/orders";);
+        setThreadLocalMessage(message);
+
+        String jwt = createSignedToken("api://orders");
+        OAuthRequestFilter filter = createJwtFilter(jwt);
+        filter.setAudience("api://orders");
+
+        filter.validateRequest(message);
+
+        assertEquals("api://orders", 
message.get(JwtConstants.EXPECTED_CLAIM_AUDIENCE));
+        assertEquals("api://orders", 
message.getContent(OAuthContext.class).getTokenAudience());
+    }
+
+    @Test
+    public void testConfiguredAudienceRejectsJwtForOtherAudience() throws 
Exception {
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "https://rs.example/orders";);
+        setThreadLocalMessage(message);
+
+        String jwt = createSignedToken("api://other");
+        OAuthRequestFilter filter = createJwtFilter(jwt);
+        filter.setAudience("api://orders");
+
+        assertThrows(NotAuthorizedException.class, () -> 
filter.validateRequest(message));
+    }
+
+    @Test
+    public void testConfiguredAudienceDoesNotOverrideExpectedAudience() throws 
Exception {
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "https://rs.example/orders";);
+        message.put(JwtConstants.EXPECTED_CLAIM_AUDIENCE, "api://preset");
+        setThreadLocalMessage(message);
+
+        String jwt = createSignedToken("api://orders");
+        OAuthRequestFilter filter = createJwtFilter(jwt);
+        filter.setAudience("api://orders");
+
+        assertThrows(NotAuthorizedException.class, () -> 
filter.validateRequest(message));
+        assertEquals("api://preset", 
message.get(JwtConstants.EXPECTED_CLAIM_AUDIENCE));
+    }
+
+    private static OAuthRequestFilter createJwtFilter(String jwt) {
+        JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
+        validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+
+        OAuthRequestFilter filter = new OAuthRequestFilter() {
+            @Override
+            protected String[] getAuthorizationParts(Message m) {
+                return new String[] {"Bearer", jwt};
+            }
+        };
+        filter.setTokenValidator(validator);
+        filter.setMessageContext(mock(MessageContext.class));
+        return filter;
+    }
+
+    private static String createSignedToken(String audience) {
+        long now = System.currentTimeMillis() / 1000;
+        JwtClaims claims = new JwtClaims();
+        claims.setIssuedAt(now);
+        claims.setExpiryTime(now + 3600);
+        claims.setIssuer("https://as.example";);
+        claims.setClaim("client_id", "client");
+        claims.setAudience(audience);
+
+        JwsJwtCompactProducer producer = new JwsJwtCompactProducer(new 
JwtToken(claims));
+        return producer.signWith(new HmacJwsSignatureProvider(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+    }
+
     private static void setThreadLocalMessage(Message message) throws 
Exception {
         Field f = 
PhaseInterceptorChain.class.getDeclaredField("CURRENT_MESSAGE");
         f.setAccessible(true);

Reply via email to