You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/09/30 16:46:53 UTC

(cxf) branch 3.6.x-fixes updated (d08197c53e8 -> 260bed80620)

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

coheigea pushed a change to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


    from d08197c53e8 Store the whole Signature value for SignatureConfirmation (#3529)
     new 18cfcd5b9d9 Adding a flag to require an audience in JwtAccessTokenValidator (#3530)
     new 260bed80620 Recording .gitmergeinfo Changes

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../oauth2/filters/JwtAccessTokenValidator.java    |  17 +++-
 .../oauth2/filters/OAuthRequestFilter.java         |  17 +++-
 .../filters/JwtAccessTokenValidatorTest.java       |  33 ++++++
 .../oauth2/filters/OAuthRequestFilterTest.java     | 112 +++++++++++++++++++++
 4 files changed, 177 insertions(+), 2 deletions(-)


(cxf) 01/02: Adding a flag to require an audience in JwtAccessTokenValidator (#3530)

Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 18cfcd5b9d9869559fc7c0727eaa5c6ccb42451c
Author: Colm O hEigeartaigh <co...@users.noreply.github.com>
AuthorDate: Wed Sep 30 17:34:54 2026 +0100

    Adding a flag to require an audience in JwtAccessTokenValidator (#3530)
    
    (cherry picked from commit 996e1191217cfb77daef7a4a7791bc1dfd13a343)
---
 .../oauth2/filters/JwtAccessTokenValidator.java    |  17 +++-
 .../oauth2/filters/OAuthRequestFilter.java         |  17 +++-
 .../filters/JwtAccessTokenValidatorTest.java       |  33 ++++++
 .../oauth2/filters/OAuthRequestFilterTest.java     | 111 +++++++++++++++++++++
 4 files changed, 176 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 8b2bedca82b..4cc203943fd 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
@@ -52,6 +52,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);
@@ -79,7 +80,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) {
@@ -170,4 +172,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 3bd4df45ce6..cd699f9ee9e 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 javax.annotation.Priority;
@@ -48,6 +49,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;
@@ -81,6 +83,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) {
@@ -102,6 +105,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()) {
@@ -265,11 +274,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 7258771007e..7e134e3a5af 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
@@ -180,6 +180,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 5a2af3ae7bc..d13824efa71 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
@@ -23,9 +23,18 @@ import java.util.Collections;
 
 import javax.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;
@@ -33,9 +42,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);
@@ -106,6 +118,105 @@ public class OAuthRequestFilterTest {
         assertEquals("/api/read", result);
     }
 
+    @Test
+    public void testValidateAudiencesRequiresEndpointCheckEnabledForAudienceMatch() throws Exception {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setCompleteAudienceMatch(false);
+
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "/api/read/item");
+        setThreadLocalMessage(message);
+
+        // Default is disabled, so endpoint-address audience matching is skipped.
+        assertNull(filter.validateAudiences(Collections.singletonList("/api/read")));
+
+        filter.setAudienceIsEndpointAddress(true);
+        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);


(cxf) 02/02: Recording .gitmergeinfo Changes

Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 260bed80620840631619bcb85fa8302576eca2a9
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Wed Sep 30 17:35:44 2026 +0100

    Recording .gitmergeinfo Changes
---
 .../apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

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 d13824efa71..cfa576c6339 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
@@ -127,7 +127,8 @@ public class OAuthRequestFilterTest {
         message.put(Message.REQUEST_URL, "/api/read/item");
         setThreadLocalMessage(message);
 
-        // Default is disabled, so endpoint-address audience matching is skipped.
+        // Enabled by default on 4.1.x, unlike main
+        filter.setAudienceIsEndpointAddress(false);
         assertNull(filter.validateAudiences(Collections.singletonList("/api/read")));
 
         filter.setAudienceIsEndpointAddress(true);