This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 4.1.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 3e8512654107ee5c7c5122d3340e549dc35bc396 Author: Javid Khan <[email protected]> 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 (cherry picked from commit ba77b1da77a4461d32f93269efebb6be2e3d5aec) --- .../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 22f8f8a3a67..19692aae6a5 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 1da366774d3..e44d4864db9 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; @@ -189,6 +192,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)); @@ -205,6 +238,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); @@ -212,6 +249,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));
