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/10/20 12:43:37 UTC

(cxf) branch main updated: require every configured scope in OAuthRequestFilter (#3603)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new ba77b1da77a require every configured scope in OAuthRequestFilter (#3603)
ba77b1da77a is described below

commit ba77b1da77a4461d32f93269efebb6be2e3d5aec
Author: Javid Khan <dx...@gmail.com>
AuthorDate: Tue Oct 20 18:13:31 2026 +0530

    require every configured scope in OAuthRequestFilter (#3603)
    
    * require every configured scope in OAuthRequestFilter
    
    * make hasAllRequiredScopes protected for subclass override
    
    * add getRequiredScopes and javadoc for hasAllRequiredScopes
---
 .../oauth2/filters/OAuthRequestFilter.java         | 30 +++++++++++++++-
 .../oauth2/filters/OAuthRequestFilterTest.java     | 40 ++++++++++++++++++++++
 2 files changed, 69 insertions(+), 1 deletion(-)

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 c13844eeb2e..0b079fc657b 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
@@ -22,7 +22,9 @@ import java.security.Principal;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
@@ -139,7 +141,7 @@ public class OAuthRequestFilter extends AbstractAccessTokenValidator
 
         if (!permissions.isEmpty() && matchingPermissions.isEmpty()
             || allPermissionsMatch && (matchingPermissions.size() != permissions.size())
-            || !requiredScopes.isEmpty() && requiredScopes.size() != matchingPermissions.size()) {
+            || !requiredScopes.isEmpty() && !hasAllRequiredScopes(matchingPermissions)) {
             String message = "Client has no valid permissions";
             LOG.warning(message);
             throw ExceptionUtils.toForbiddenException(null, null);
@@ -236,6 +238,28 @@ public class OAuthRequestFilter extends AbstractAccessTokenValidator
         }
         return foundValidScope;
     }
+
+    /**
+     * Checks that the token permissions matching the current request cover every configured
+     * required scope. Only called when required scopes have been set.
+     * <p>
+     * Subclasses overriding {@link #checkScopeProperty(String)} to match scopes more loosely
+     * (e.g. by pattern) should override this method as well, using {@link #getRequiredScopes()}.
+     *
+     * @param matchingPermissions the token permissions which passed the request URI, HTTP method
+     *        and {@link #checkScopeProperty(String)} checks
+     * @return true if every required scope is present among the matching permissions
+     */
+    protected boolean hasAllRequiredScopes(List<OAuthPermission> matchingPermissions) {
+        // Token scopes are not de-duplicated anywhere on the issuance path, so compare the set
+        // of matching scope names against the required scopes instead of the number of matches
+        Set<String> matchingScopes = new HashSet<>();
+        for (OAuthPermission perm : matchingPermissions) {
+            matchingScopes.add(perm.getPermission());
+        }
+        return matchingScopes.containsAll(requiredScopes);
+    }
+
     protected boolean checkScopeProperty(String scope) {
         if (!requiredScopes.isEmpty()) {
             return requiredScopes.contains(scope);
@@ -371,6 +395,10 @@ public class OAuthRequestFilter extends AbstractAccessTokenValidator
         this.requiredScopes = requiredScopes;
     }
 
+    protected List<String> getRequiredScopes() {
+        return requiredScopes;
+    }
+
     public void setAllPermissionsMatch(boolean allPermissionsMatch) {
         this.allPermissionsMatch = allPermissionsMatch;
     }
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 69f82b6ad82..66567a7fdf3 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
@@ -19,8 +19,10 @@
 package org.apache.cxf.rs.security.oauth2.filters;
 
 import java.lang.reflect.Field;
+import java.util.Arrays;
 import java.util.Collections;
 
+import jakarta.ws.rs.ForbiddenException;
 import jakarta.ws.rs.NotAuthorizedException;
 import org.apache.cxf.jaxrs.ext.MessageContext;
 import org.apache.cxf.message.Message;
@@ -34,6 +36,7 @@ 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.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
 
 import org.junit.After;
 import org.junit.Test;
@@ -187,6 +190,36 @@ public class OAuthRequestFilterTest {
         assertEquals("api://preset", message.get(JwtConstants.EXPECTED_CLAIM_AUDIENCE));
     }
 
+    @Test
+    public void testRequiredScopesRejectDuplicatedScopeInsteadOfMissingOne() throws Exception {
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "https://rs.example/orders");
+        setThreadLocalMessage(message);
+
+        String jwt = createSignedToken("api://orders", "read read");
+        OAuthRequestFilter filter = createJwtFilter(jwt);
+        filter.setAudience("api://orders");
+        filter.setRequiredScopes(Arrays.asList("read", "admin"));
+
+        assertThrows(ForbiddenException.class, () -> filter.validateRequest(message));
+    }
+
+    @Test
+    public void testRequiredScopesAcceptTokenWithAllRequiredScopes() throws Exception {
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "https://rs.example/orders");
+        setThreadLocalMessage(message);
+
+        String jwt = createSignedToken("api://orders", "admin read read");
+        OAuthRequestFilter filter = createJwtFilter(jwt);
+        filter.setAudience("api://orders");
+        filter.setRequiredScopes(Arrays.asList("read", "admin"));
+
+        filter.validateRequest(message);
+
+        assertEquals(3, message.getContent(OAuthContext.class).getPermissions().size());
+    }
+
     private static OAuthRequestFilter createJwtFilter(String jwt) {
         JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
         validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
@@ -203,6 +236,10 @@ public class OAuthRequestFilterTest {
     }
 
     private static String createSignedToken(String audience) {
+        return createSignedToken(audience, null);
+    }
+
+    private static String createSignedToken(String audience, String scope) {
         long now = System.currentTimeMillis() / 1000;
         JwtClaims claims = new JwtClaims();
         claims.setIssuedAt(now);
@@ -210,6 +247,9 @@ public class OAuthRequestFilterTest {
         claims.setIssuer("https://as.example");
         claims.setClaim("client_id", "client");
         claims.setAudience(audience);
+        if (scope != null) {
+            claims.setClaim(OAuthConstants.SCOPE, scope);
+        }
 
         JwsJwtCompactProducer producer = new JwsJwtCompactProducer(new JwtToken(claims));
         return producer.signWith(new HmacJwsSignatureProvider(SIGNING_KEY, SignatureAlgorithm.HS256));