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

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

commit d7df7294f86d898550ed5e68be61ad142650ff1f
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Tue Sep 29 11:25:50 2026 +0100

    Harden JWT issuer and audience validation
---
 .../jaxrs/AbstractJwtAuthenticationFilter.java     |  41 ++++++++
 .../jose/jaxrs/JwtAuthenticationFilterTest.java    | 116 +++++++++++++++++++++
 .../oauth2/grants/jwt/AbstractJwtHandler.java      |   5 +
 .../oauth2/grants/jwt/JwtBearerAuthHandler.java    |   5 +
 .../grants/jwt/JwtBearerGrantHandlerTest.java      |  32 +++++-
 5 files changed, 196 insertions(+), 3 deletions(-)

diff --git 
a/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/AbstractJwtAuthenticationFilter.java
 
b/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/AbstractJwtAuthenticationFilter.java
index 7c99b7fd530..ddc2778deb4 100644
--- 
a/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/AbstractJwtAuthenticationFilter.java
+++ 
b/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/AbstractJwtAuthenticationFilter.java
@@ -19,6 +19,8 @@
 package org.apache.cxf.rs.security.jose.jaxrs;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.Set;
 import java.util.logging.Logger;
 
 import jakarta.annotation.Priority;
@@ -33,6 +35,7 @@ import org.apache.cxf.message.MessageUtils;
 import org.apache.cxf.rs.security.jose.common.JoseConstants;
 import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
 import org.apache.cxf.rs.security.jose.jwt.JoseJwtConsumer;
+import org.apache.cxf.rs.security.jose.jwt.JwtException;
 import org.apache.cxf.rs.security.jose.jwt.JwtToken;
 import org.apache.cxf.rs.security.jose.jwt.JwtUtils;
 import org.apache.cxf.security.SecurityContext;
@@ -45,6 +48,8 @@ public abstract class AbstractJwtAuthenticationFilter extends 
JoseJwtConsumer im
 
     private String roleClaim;
     private boolean validateAudience = true;
+    private boolean requireAudience;
+    private Set<String> supportedIssuers;
 
     protected AbstractJwtAuthenticationFilter() {
         setTtl(DEFAULT_TTL_SECS);
@@ -91,6 +96,22 @@ public abstract class AbstractJwtAuthenticationFilter 
extends JoseJwtConsumer im
     @Override
     protected void validateToken(JwtToken jwt) {
         JwtUtils.validateTokenClaims(jwt.getClaims(), getTtl(), 
getClockOffset(), isValidateAudience());
+        validateAudiencePresent(jwt.getClaims().getAudiences());
+        validateIssuer(jwt.getClaims().getIssuer());
+    }
+
+    protected void validateAudiencePresent(List<String> audiences) {
+        // If the audience is required, an "aud" claim must be present
+        if (requireAudience && audiences.isEmpty()) {
+            throw new JwtException("Invalid audience restriction");
+        }
+    }
+
+    protected void validateIssuer(String issuer) {
+        // If supported issuers are configured, a matching "iss" must be 
present
+        if (supportedIssuers != null && (issuer == null || 
!supportedIssuers.contains(issuer))) {
+            throw new JwtException("Invalid issuer");
+        }
     }
 
     public String getRoleClaim() {
@@ -109,4 +130,24 @@ public abstract class AbstractJwtAuthenticationFilter 
extends JoseJwtConsumer im
         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.
+     */
+    public void setRequireAudience(boolean requireAudience) {
+        this.requireAudience = requireAudience;
+    }
+
+    public Set<String> getSupportedIssuers() {
+        return supportedIssuers;
+    }
+
+    public void setSupportedIssuers(Set<String> supportedIssuers) {
+        this.supportedIssuers = supportedIssuers;
+    }
+
 }
diff --git 
a/rt/rs/security/jose-parent/jose-jaxrs/src/test/java/org/apache/cxf/rs/security/jose/jaxrs/JwtAuthenticationFilterTest.java
 
b/rt/rs/security/jose-parent/jose-jaxrs/src/test/java/org/apache/cxf/rs/security/jose/jaxrs/JwtAuthenticationFilterTest.java
new file mode 100644
index 00000000000..24695e9fc04
--- /dev/null
+++ 
b/rt/rs/security/jose-parent/jose-jaxrs/src/test/java/org/apache/cxf/rs/security/jose/jaxrs/JwtAuthenticationFilterTest.java
@@ -0,0 +1,116 @@
+/**
+ * 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.jose.jaxrs;
+
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Collections;
+
+import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
+import org.apache.cxf.rs.security.jose.jwt.JwtException;
+import org.apache.cxf.rs.security.jose.jwt.JwtToken;
+
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class JwtAuthenticationFilterTest {
+
+    @Test
+    public void testNoIssuerAcceptedWithoutSupportedIssuers() {
+        JwtAuthenticationFilter filter = createFilter();
+        filter.validateToken(new JwtToken(createClaims(null)));
+    }
+
+    @Test
+    public void testSupportedIssuerAccepted() {
+        JwtAuthenticationFilter filter = createFilter();
+        
filter.setSupportedIssuers(Collections.singleton("https://issuer.example.com";));
+        filter.validateToken(new 
JwtToken(createClaims("https://issuer.example.com";)));
+    }
+
+    @Test
+    public void testUnsupportedIssuerRejected() {
+        JwtAuthenticationFilter filter = createFilter();
+        
filter.setSupportedIssuers(Collections.singleton("https://issuer.example.com";));
+        try {
+            filter.validateToken(new 
JwtToken(createClaims("https://other.example.com";)));
+            fail("Failure expected on an unsupported issuer");
+        } catch (JwtException ex) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testMissingIssuerRejectedWithSupportedIssuers() {
+        JwtAuthenticationFilter filter = createFilter();
+        
filter.setSupportedIssuers(Collections.singleton("https://issuer.example.com";));
+        try {
+            filter.validateToken(new JwtToken(createClaims(null)));
+            fail("Failure expected on a missing issuer");
+        } catch (JwtException ex) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testNoAudienceAcceptedByDefault() {
+        JwtAuthenticationFilter filter = createFilter();
+        filter.validateToken(new JwtToken(createClaims(null)));
+    }
+
+    @Test
+    public void testAudienceAcceptedWithRequireAudience() {
+        JwtAuthenticationFilter filter = createFilter();
+        filter.setRequireAudience(true);
+        JwtClaims claims = createClaims(null);
+        claims.setAudience("https://service.example.com";);
+        filter.validateToken(new JwtToken(claims));
+    }
+
+    @Test
+    public void testNoAudienceRejectedWithRequireAudience() {
+        JwtAuthenticationFilter filter = createFilter();
+        filter.setRequireAudience(true);
+        try {
+            filter.validateToken(new JwtToken(createClaims(null)));
+            fail("Failure expected on a missing audience");
+        } catch (JwtException ex) {
+            // expected
+        }
+    }
+
+    private static JwtAuthenticationFilter createFilter() {
+        JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
+        filter.setValidateAudience(false);
+        return filter;
+    }
+
+    private static JwtClaims createClaims(String issuer) {
+        JwtClaims claims = new JwtClaims();
+        claims.setSubject("alice");
+        if (issuer != null) {
+            claims.setIssuer(issuer);
+        }
+        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
+        claims.setIssuedAt(now.toEpochSecond());
+        claims.setExpiryTime(now.plusMinutes(5L).toEpochSecond());
+        return claims;
+    }
+}
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/AbstractJwtHandler.java
 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/AbstractJwtHandler.java
index 8f962334223..a18bebe5844 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/AbstractJwtHandler.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/AbstractJwtHandler.java
@@ -69,6 +69,11 @@ public abstract class AbstractJwtHandler extends 
AbstractGrantHandler {
         if (claims.getClaim(JwtConstants.CLAIM_EXPIRY) == null) {
             throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
         }
+
+        // We must have an Audience (RFC 7523, section 3)
+        if (claims.getAudiences().isEmpty()) {
+            throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
+        }
     }
 
     protected void validateIssuer(String issuer) {
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerAuthHandler.java
 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerAuthHandler.java
index 8519a094e9d..aaeb3a80a8d 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerAuthHandler.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerAuthHandler.java
@@ -130,6 +130,11 @@ public class JwtBearerAuthHandler extends 
OAuthServerJoseJwtConsumer implements
             throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
         }
 
+        // We must have an Audience (RFC 7523, section 3)
+        if (isValidateAudience() && jwt.getClaims().getAudiences().isEmpty()) {
+            throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
+        }
+
         JwtUtils.validateTokenClaims(jwt.getClaims(), getTtl(), 
getClockOffset(), isValidateAudience());
     }
 
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerGrantHandlerTest.java
 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerGrantHandlerTest.java
index 2e0b41c43c5..243f39037a1 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerGrantHandlerTest.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/jwt/JwtBearerGrantHandlerTest.java
@@ -48,6 +48,7 @@ import static org.junit.Assert.fail;
 public class JwtBearerGrantHandlerTest {
 
     private static final String SIGNING_KEY = "jwt-grant-test-signing-key";
+    private static final String AUDIENCE = 
"https://localhost:8080/oauth2/token";;
 
     @Before
     public void setUp() throws Exception {
@@ -64,9 +65,10 @@ public class JwtBearerGrantHandlerTest {
         JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
         handler.setDataProvider(new SubjectAwareDataProvider());
         handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+        handler.setAudience(AUDIENCE);
 
         Client client = new Client("fuzz-client", "secret", true);
-        String assertion = createSignedAssertion("trusted-issuer", 
"victim-user");
+        String assertion = createSignedAssertion("trusted-issuer", 
"victim-user", AUDIENCE);
 
         MultivaluedMap<String, String> params = new MetadataMap<>();
         params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);
@@ -84,9 +86,10 @@ public class JwtBearerGrantHandlerTest {
         JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
         handler.setDataProvider(new SubjectAwareDataProvider());
         handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+        handler.setAudience(AUDIENCE);
 
         Client client = new Client("fuzz-client", "secret", true);
-        String assertion = createSignedAssertion("trusted-issuer", 
client.getClientId());
+        String assertion = createSignedAssertion("trusted-issuer", 
client.getClientId(), AUDIENCE);
 
         MultivaluedMap<String, String> params = new MetadataMap<>();
         params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);
@@ -98,13 +101,36 @@ public class JwtBearerGrantHandlerTest {
         assertEquals(client.getClientId(), token.getSubject().getLogin());
     }
 
-    private static String createSignedAssertion(String issuer, String subject) 
{
+    @Test
+    public void testMissingAudienceRejected() {
+        JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
+        handler.setDataProvider(new SubjectAwareDataProvider());
+        handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, 
SignatureAlgorithm.HS256));
+
+        Client client = new Client("fuzz-client", "secret", true);
+        String assertion = createSignedAssertion("trusted-issuer", 
client.getClientId(), null);
+
+        MultivaluedMap<String, String> params = new MetadataMap<>();
+        params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);
+
+        try {
+            handler.createAccessToken(client, params);
+            fail("OAuthServiceException expected");
+        } catch (OAuthServiceException expected) {
+            assertEquals("invalid_grant", expected.getMessage());
+        }
+    }
+
+    private static String createSignedAssertion(String issuer, String subject, 
String audience) {
         long now = System.currentTimeMillis() / 1000;
         JwtClaims claims = new JwtClaims();
         claims.setIssuer(issuer);
         claims.setSubject(subject);
         claims.setIssuedAt(now);
         claims.setExpiryTime(now + 300);
+        if (audience != null) {
+            claims.setAudience(audience);
+        }
 
         JwsJwtCompactProducer producer = new JwsJwtCompactProducer(claims);
         return producer.signWith(new HmacJwsSignatureProvider(SIGNING_KEY, 
SignatureAlgorithm.HS256));

Reply via email to