From ffc9adfe2bdd2f01f321729e4a597538257f25db Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 22 Nov 2025 11:52:47 +0700 Subject: [PATCH] Add hasScope and hasAnyScope Signed-off-by: Tran Ngoc Nhan --- ...aultOAuth2AuthorizationManagerFactory.java | 79 +++++++++++++++++ .../OAuth2AuthorizationManagerFactory.java | 73 ++++++++++++++++ ...Auth2AuthorizationManagerFactoryTests.java | 84 +++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/DefaultOAuth2AuthorizationManagerFactory.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactory.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactoryTests.java diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/DefaultOAuth2AuthorizationManagerFactory.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/DefaultOAuth2AuthorizationManagerFactory.java new file mode 100644 index 00000000000..7d91953923b --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/DefaultOAuth2AuthorizationManagerFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.security.oauth2.core.authorization; + +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagerFactory; +import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; +import org.springframework.util.Assert; + +/** + * A factory for creating different kinds of {@link AuthorizationManager} instances. + * + * @param the type of object that the authorization check is being done on + * @author Ngoc Nhan + * @since 7.0 + */ +public final class DefaultOAuth2AuthorizationManagerFactory implements OAuth2AuthorizationManagerFactory { + + private String scopePrefix = "SCOPE_"; + + private final AuthorizationManagerFactory authorizationManagerFactory; + + public DefaultOAuth2AuthorizationManagerFactory() { + this(new DefaultAuthorizationManagerFactory<>()); + } + + public DefaultOAuth2AuthorizationManagerFactory(AuthorizationManagerFactory authorizationManagerFactory) { + Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory can not be null"); + this.authorizationManagerFactory = authorizationManagerFactory; + } + + /** + * Sets the prefix used to create an authority name from a scope name. Can be an empty + * string. + * @param scopePrefix the scope prefix to use + */ + public void setScopePrefix(String scopePrefix) { + Assert.notNull(scopePrefix, "scopePrefix can not be null"); + this.scopePrefix = scopePrefix; + } + + @Override + public AuthorizationManager hasScope(String scope) { + Assert.notNull(scope, "scope can not be null"); + return hasAnyScope(scope); + } + + @Override + public AuthorizationManager hasAnyScope(String... scopes) { + Assert.notNull(scopes, "scopes can not be null"); + String[] mappedScopes = new String[scopes.length]; + for (int i = 0; i < scopes.length; i++) { + assertScope(scopes[i]); + mappedScopes[i] = this.scopePrefix + scopes[i]; + } + return this.authorizationManagerFactory.hasAnyAuthority(mappedScopes); + } + + private void assertScope(String scope) { + Assert.isTrue(!scope.startsWith(this.scopePrefix), () -> scope + " should not start with '" + this.scopePrefix + + "' since '" + this.scopePrefix + + "' is automatically prepended when using hasScope and hasAnyScope. Consider using AuthorityAuthorizationManager#hasAuthority or #hasAnyAuthority instead."); + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactory.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactory.java new file mode 100644 index 00000000000..508522d0ab4 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.security.oauth2.core.authorization; + +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +/** + * A factory for creating different kinds of {@link AuthorizationManager} instances. + * + * @param the type of object that the authorization check is being done on + * @author Ngoc Nhan + * @since 7.0 + */ +public interface OAuth2AuthorizationManagerFactory { + + /** + * Create an {@link AuthorizationManager} that requires an {@link Authentication} to + * have a {@code SCOPE_scope} authority. + * + *

+ * For example, if you call {@code hasScope("read")}, then this will require that each + * authentication have a {@link org.springframework.security.core.GrantedAuthority} + * whose value is {@code SCOPE_read}. + * + *

+ * This would equivalent to calling + * {@code AuthorityAuthorizationManager#hasAuthority("SCOPE_read")}. + * @param scope the scope value to require + * @return an {@link AuthorizationManager} that requires a {@code "SCOPE_scope"} + * authority + */ + default AuthorizationManager hasScope(String scope) { + return OAuth2AuthorizationManagers.hasScope(scope); + } + + /** + * Create an {@link AuthorizationManager} that requires an {@link Authentication} to + * have at least one authority among {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... + * {@code SCOPE_scopeN}. + * + *

+ * For example, if you call {@code hasAnyScope("read", "write")}, then this will + * require that each authentication have at least a + * {@link org.springframework.security.core.GrantedAuthority} whose value is either + * {@code SCOPE_read} or {@code SCOPE_write}. + * + *

+ * This would equivalent to calling + * {@code AuthorityAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}. + * @param scopes the scope values to allow + * @return an {@link AuthorizationManager} that requires at least one authority among + * {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}. + */ + default AuthorizationManager hasAnyScope(String... scopes) { + return OAuth2AuthorizationManagers.hasAnyScope(scopes); + } + +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactoryTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactoryTests.java new file mode 100644 index 00000000000..7c92965fac4 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactoryTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.security.oauth2.core.authorization; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagerFactories; +import org.springframework.security.authorization.AuthorizationResult; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2AuthorizationManagerFactory}. + * + * @author Ngoc Nhan + */ +public class OAuth2AuthorizationManagerFactoryTests { + + @Test + public void hasScopeReturnsAuthorityAuthorizationManagerByDefault() { + OAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasScope("message:read"); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasAnyScopeReturnsAuthorityAuthorizationManagerByDefault() { + OAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasAnyScope("message:read", "message:write"); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasScopeWhenSetAuthorizationManagerFactories() { + DefaultOAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>( + AuthorizationManagerFactories.multiFactor().requireFactors("SCOPE_message:read").build()); + assertUserGranted(factory.hasScope("message:read")); + assertUserDenied(factory.hasScope("message:write")); + } + + @Test + public void hasAnyScopeWhenSetAuthorizationManagerFactories() { + DefaultOAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>( + AuthorizationManagerFactories.multiFactor().requireFactors("SCOPE_message:read").build()); + assertUserGranted(factory.hasAnyScope("message:read")); + assertUserDenied(factory.hasAnyScope("message:write")); + } + + private void assertUserGranted(AuthorizationManager manager) { + AuthorizationResult authorizationResult = createAuthorizationResult(manager); + assertThat(authorizationResult).isNotNull(); + assertThat(authorizationResult.isGranted()).isTrue(); + } + + private void assertUserDenied(AuthorizationManager manager) { + AuthorizationResult authorizationResult = createAuthorizationResult(manager); + assertThat(authorizationResult).isNotNull(); + assertThat(authorizationResult.isGranted()).isFalse(); + } + + private AuthorizationResult createAuthorizationResult(AuthorizationManager manager) { + TestingAuthenticationToken authenticatedUser = new TestingAuthenticationToken("user", "pass", + "SCOPE_message:read"); + return manager.authorize(() -> authenticatedUser, ""); + } + +}