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));