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
The following commit(s) were added to refs/heads/3.6.x-fixes by this push:
new 7e64f155deb require every configured scope in OAuthRequestFilter
(#3603)
7e64f155deb is described below
commit 7e64f155deb62a3138d10b5066a66558fd0b3f0d
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 cd699f9ee9e..76917d434db 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;
@@ -140,7 +142,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);
@@ -229,6 +231,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);
@@ -364,6 +388,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 cfa576c6339..41156b1a57d 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 javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import org.apache.cxf.jaxrs.ext.MessageContext;
@@ -35,6 +37,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;
@@ -190,6 +193,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));
@@ -206,6 +239,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);
@@ -213,6 +250,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));