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..deb175e221e --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/DefaultOAuth2AuthorizationManagerFactory.java @@ -0,0 +1,89 @@ +/* + * 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.1 + */ +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"); + assertScope(scope); + return this.authorizationManagerFactory.hasAuthority(this.scopePrefix + scope); + } + + @Override + public AuthorizationManager hasAnyScope(String... scopes) { + return this.authorizationManagerFactory.hasAnyAuthority(this.mappedScopes(scopes)); + } + + @Override + public AuthorizationManager hasAllScopes(String... scopes) { + return this.authorizationManagerFactory.hasAllAuthorities(this.mappedScopes(scopes)); + } + + private String[] mappedScopes(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 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 AuthorizationManagerFactory#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..9dfd5419ae8 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactory.java @@ -0,0 +1,95 @@ +/* + * 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.1 + */ +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); + } + + /** + * Create an {@link AuthorizationManager} that requires an {@link Authentication} to + * have all authorities {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... + * {@code SCOPE_scopeN}. + * + *

+ * For example, if you call {@code hasAllScopes("read", "write")}, then each + * {@link org.springframework.security.core.Authentication} must have all + * {@link org.springframework.security.core.GrantedAuthority} values of + * {@code SCOPE_read} and {@code SCOPE_write}. + * + *

+ * This would be equivalent to calling + * {@code AllAuthoritiesAuthorizationManager#hasAllAuthorities("SCOPE_read", "SCOPE_write")}. + * @param scopes the scope values to require + * @return an {@link AuthorizationManager} that requires all authorities + * {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}. + */ + default AuthorizationManager hasAllScopes(String... scopes) { + return OAuth2AuthorizationManagers.hasAllScopes(scopes); + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java index 50e7bfb6457..36d3da05ae9 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.core.authorization; +import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; @@ -28,6 +29,7 @@ * @author Josh Cummings * @since 6.2 * @see AuthorityAuthorizationManager + * @see AllAuthoritiesAuthorizationManager */ public final class OAuth2AuthorizationManagers { @@ -85,6 +87,34 @@ public static AuthorizationManager hasAnyScope(String... scopes) { return AuthorityAuthorizationManager.hasAnyAuthority(mappedScopes); } + /** + * Create an {@link AuthorizationManager} that requires an {@link Authentication} to + * have all authorities {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... + * {@code SCOPE_scopeN}. + * + *

+ * For example, if you call {@code hasAllScopes("read", "write")}, then each + * {@link org.springframework.security.core.Authentication} must have all + * {@link org.springframework.security.core.GrantedAuthority} values of + * {@code SCOPE_read} and {@code SCOPE_write}. + * + *

+ * This would be equivalent to calling + * {@code AllAuthoritiesAuthorizationManager#hasAllAuthorities("SCOPE_read", "SCOPE_write")}. + * @param scopes the scope values to require + * @return an {@link AuthorizationManager} that requires all authorities + * {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}. + * @since 7.1 + */ + public static AuthorizationManager hasAllScopes(String... scopes) { + String[] mappedScopes = new String[scopes.length]; + for (int i = 0; i < scopes.length; i++) { + assertScope(scopes[i]); + mappedScopes[i] = "SCOPE_" + scopes[i]; + } + return AllAuthoritiesAuthorizationManager.hasAllAuthorities(mappedScopes); + } + private static void assertScope(String scope) { Assert.isTrue(!scope.startsWith("SCOPE_"), () -> scope + " should not start with SCOPE_ since SCOPE_" 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..b9f48127c13 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagerFactoryTests.java @@ -0,0 +1,108 @@ +/* + * 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 { + + private static final String MSG_READ = "message:read"; + + private static final String MSG_WRITE = "message:write"; + + private static final String SCOPE_MSG_READ = "SCOPE_message:read"; + + private static final String SCOPE_MSG_WRITE = "SCOPE_message:write"; + + @Test + public void hasScopeReturnsAuthorityAuthorizationManagerByDefault() { + OAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasScope(MSG_READ); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasAnyScopeReturnsAuthorityAuthorizationManagerByDefault() { + OAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasAnyScope(MSG_READ, MSG_WRITE); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasAllScopesReturnsAuthorityAuthorizationManagerByDefault() { + OAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>(); + AuthorizationManager authorizationManager = factory.hasAnyScope(MSG_READ, MSG_WRITE); + assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class); + } + + @Test + public void hasScopeWhenSetAuthorizationManagerFactories() { + DefaultOAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>( + AuthorizationManagerFactories.multiFactor().requireFactors(SCOPE_MSG_READ).build()); + assertUserGranted(factory.hasScope(MSG_READ), SCOPE_MSG_READ); + assertUserDenied(factory.hasScope(MSG_WRITE), SCOPE_MSG_READ); + } + + @Test + public void hasAnyScopeWhenSetAuthorizationManagerFactories() { + DefaultOAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>( + AuthorizationManagerFactories.multiFactor().requireFactors(SCOPE_MSG_READ).build()); + assertUserGranted(factory.hasAnyScope(MSG_READ), SCOPE_MSG_READ); + assertUserDenied(factory.hasAnyScope(MSG_WRITE), SCOPE_MSG_READ); + } + + @Test + public void hasAllScopesWhenSetAuthorizationManagerFactories() { + DefaultOAuth2AuthorizationManagerFactory factory = new DefaultOAuth2AuthorizationManagerFactory<>( + AuthorizationManagerFactories.multiFactor() + .requireFactors(SCOPE_MSG_READ, SCOPE_MSG_WRITE) + .build()); + assertUserGranted(factory.hasAllScopes(MSG_READ, MSG_WRITE), SCOPE_MSG_READ, SCOPE_MSG_WRITE); + assertUserDenied(factory.hasAllScopes(MSG_READ, MSG_WRITE), SCOPE_MSG_READ); + } + + private void assertUserGranted(AuthorizationManager manager, String... authorities) { + AuthorizationResult authorizationResult = createAuthorizationResult(manager, authorities); + assertThat(authorizationResult).isNotNull(); + assertThat(authorizationResult.isGranted()).isTrue(); + } + + private void assertUserDenied(AuthorizationManager manager, String... authorities) { + AuthorizationResult authorizationResult = createAuthorizationResult(manager, authorities); + assertThat(authorizationResult).isNotNull(); + assertThat(authorizationResult.isGranted()).isFalse(); + } + + private AuthorizationResult createAuthorizationResult(AuthorizationManager manager, String... authorities) { + TestingAuthenticationToken authenticatedUser = new TestingAuthenticationToken("user", "pass", authorities); + return manager.authorize(() -> authenticatedUser, ""); + } + +}