Skip to content

Commit f2b7cb2

Browse files
ngocnhan-tran1996jzheaux
authored andcommitted
Support hasScope in Method Security
Closes gh-18013 Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
1 parent 8652950 commit f2b7cb2

4 files changed

Lines changed: 322 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.core.authorization;
18+
19+
import org.springframework.security.authorization.AuthorizationManager;
20+
import org.springframework.security.authorization.AuthorizationManagerFactory;
21+
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* A factory for creating different kinds of {@link AuthorizationManager} instances.
26+
*
27+
* @param <T> the type of object that the authorization check is being done on
28+
* @author Ngoc Nhan
29+
* @since 7.1
30+
*/
31+
public final class DefaultOAuth2AuthorizationManagerFactory<T> implements OAuth2AuthorizationManagerFactory<T> {
32+
33+
private String scopePrefix = "SCOPE_";
34+
35+
private final AuthorizationManagerFactory<T> authorizationManagerFactory;
36+
37+
public DefaultOAuth2AuthorizationManagerFactory() {
38+
this(new DefaultAuthorizationManagerFactory<>());
39+
}
40+
41+
public DefaultOAuth2AuthorizationManagerFactory(AuthorizationManagerFactory<T> authorizationManagerFactory) {
42+
Assert.notNull(authorizationManagerFactory, "authorizationManagerFactory can not be null");
43+
this.authorizationManagerFactory = authorizationManagerFactory;
44+
}
45+
46+
/**
47+
* Sets the prefix used to create an authority name from a scope name. Can be an empty
48+
* string.
49+
* @param scopePrefix the scope prefix to use
50+
*/
51+
public void setScopePrefix(String scopePrefix) {
52+
Assert.notNull(scopePrefix, "scopePrefix can not be null");
53+
this.scopePrefix = scopePrefix;
54+
}
55+
56+
@Override
57+
public AuthorizationManager<T> hasScope(String scope) {
58+
Assert.notNull(scope, "scope can not be null");
59+
assertScope(scope);
60+
return this.authorizationManagerFactory.hasAuthority(this.scopePrefix + scope);
61+
}
62+
63+
@Override
64+
public AuthorizationManager<T> hasAnyScope(String... scopes) {
65+
return this.authorizationManagerFactory.hasAnyAuthority(this.mappedScopes(scopes));
66+
}
67+
68+
@Override
69+
public AuthorizationManager<T> hasAllScopes(String... scopes) {
70+
return this.authorizationManagerFactory.hasAllAuthorities(this.mappedScopes(scopes));
71+
}
72+
73+
private String[] mappedScopes(String... scopes) {
74+
Assert.notNull(scopes, "scopes can not be null");
75+
String[] mappedScopes = new String[scopes.length];
76+
for (int i = 0; i < scopes.length; i++) {
77+
assertScope(scopes[i]);
78+
mappedScopes[i] = this.scopePrefix + scopes[i];
79+
}
80+
return mappedScopes;
81+
}
82+
83+
private void assertScope(String scope) {
84+
Assert.isTrue(!scope.startsWith(this.scopePrefix), () -> scope + " should not start with '" + this.scopePrefix
85+
+ "' since '" + this.scopePrefix
86+
+ "' is automatically prepended when using hasScope and hasAnyScope. Consider using AuthorizationManagerFactory#hasAuthority or #hasAnyAuthority instead.");
87+
}
88+
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.core.authorization;
18+
19+
import org.springframework.security.authorization.AuthorizationManager;
20+
import org.springframework.security.core.Authentication;
21+
22+
/**
23+
* A factory for creating different kinds of {@link AuthorizationManager} instances.
24+
*
25+
* @param <T> the type of object that the authorization check is being done on
26+
* @author Ngoc Nhan
27+
* @since 7.1
28+
*/
29+
public interface OAuth2AuthorizationManagerFactory<T> {
30+
31+
/**
32+
* Create an {@link AuthorizationManager} that requires an {@link Authentication} to
33+
* have a {@code SCOPE_scope} authority.
34+
*
35+
* <p>
36+
* For example, if you call {@code hasScope("read")}, then this will require that each
37+
* authentication have a {@link org.springframework.security.core.GrantedAuthority}
38+
* whose value is {@code SCOPE_read}.
39+
*
40+
* <p>
41+
* This would equivalent to calling
42+
* {@code AuthorityAuthorizationManager#hasAuthority("SCOPE_read")}.
43+
* @param scope the scope value to require
44+
* @return an {@link AuthorizationManager} that requires a {@code "SCOPE_scope"}
45+
* authority
46+
*/
47+
default AuthorizationManager<T> hasScope(String scope) {
48+
return OAuth2AuthorizationManagers.hasScope(scope);
49+
}
50+
51+
/**
52+
* Create an {@link AuthorizationManager} that requires an {@link Authentication} to
53+
* have at least one authority among {@code SCOPE_scope1}, {@code SCOPE_scope2}, ...
54+
* {@code SCOPE_scopeN}.
55+
*
56+
* <p>
57+
* For example, if you call {@code hasAnyScope("read", "write")}, then this will
58+
* require that each authentication have at least a
59+
* {@link org.springframework.security.core.GrantedAuthority} whose value is either
60+
* {@code SCOPE_read} or {@code SCOPE_write}.
61+
*
62+
* <p>
63+
* This would equivalent to calling
64+
* {@code AuthorityAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}.
65+
* @param scopes the scope values to allow
66+
* @return an {@link AuthorizationManager} that requires at least one authority among
67+
* {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}.
68+
*/
69+
default AuthorizationManager<T> hasAnyScope(String... scopes) {
70+
return OAuth2AuthorizationManagers.hasAnyScope(scopes);
71+
}
72+
73+
/**
74+
* Create an {@link AuthorizationManager} that requires an {@link Authentication} to
75+
* have all authorities {@code SCOPE_scope1}, {@code SCOPE_scope2}, ...
76+
* {@code SCOPE_scopeN}.
77+
*
78+
* <p>
79+
* For example, if you call {@code hasAllScopes("read", "write")}, then each
80+
* {@link org.springframework.security.core.Authentication} must have all
81+
* {@link org.springframework.security.core.GrantedAuthority} values of
82+
* {@code SCOPE_read} and {@code SCOPE_write}.
83+
*
84+
* <p>
85+
* This would be equivalent to calling
86+
* {@code AllAuthoritiesAuthorizationManager#hasAllAuthorities("SCOPE_read", "SCOPE_write")}.
87+
* @param scopes the scope values to require
88+
* @return an {@link AuthorizationManager} that requires all authorities
89+
* {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}.
90+
*/
91+
default AuthorizationManager<T> hasAllScopes(String... scopes) {
92+
return OAuth2AuthorizationManagers.hasAllScopes(scopes);
93+
}
94+
95+
}

oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.oauth2.core.authorization;
1818

19+
import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager;
1920
import org.springframework.security.authorization.AuthorityAuthorizationManager;
2021
import org.springframework.security.authorization.AuthorizationManager;
2122
import org.springframework.security.core.Authentication;
@@ -28,6 +29,7 @@
2829
* @author Josh Cummings
2930
* @since 6.2
3031
* @see AuthorityAuthorizationManager
32+
* @see AllAuthoritiesAuthorizationManager
3133
*/
3234
public final class OAuth2AuthorizationManagers {
3335

@@ -85,6 +87,34 @@ public static <T> AuthorizationManager<T> hasAnyScope(String... scopes) {
8587
return AuthorityAuthorizationManager.hasAnyAuthority(mappedScopes);
8688
}
8789

90+
/**
91+
* Create an {@link AuthorizationManager} that requires an {@link Authentication} to
92+
* have all authorities {@code SCOPE_scope1}, {@code SCOPE_scope2}, ...
93+
* {@code SCOPE_scopeN}.
94+
*
95+
* <p>
96+
* For example, if you call {@code hasAllScopes("read", "write")}, then each
97+
* {@link org.springframework.security.core.Authentication} must have all
98+
* {@link org.springframework.security.core.GrantedAuthority} values of
99+
* {@code SCOPE_read} and {@code SCOPE_write}.
100+
*
101+
* <p>
102+
* This would be equivalent to calling
103+
* {@code AllAuthoritiesAuthorizationManager#hasAllAuthorities("SCOPE_read", "SCOPE_write")}.
104+
* @param scopes the scope values to require
105+
* @return an {@link AuthorizationManager} that requires all authorities
106+
* {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}.
107+
* @since 7.1
108+
*/
109+
public static <T> AuthorizationManager<T> hasAllScopes(String... scopes) {
110+
String[] mappedScopes = new String[scopes.length];
111+
for (int i = 0; i < scopes.length; i++) {
112+
assertScope(scopes[i]);
113+
mappedScopes[i] = "SCOPE_" + scopes[i];
114+
}
115+
return AllAuthoritiesAuthorizationManager.hasAllAuthorities(mappedScopes);
116+
}
117+
88118
private static void assertScope(String scope) {
89119
Assert.isTrue(!scope.startsWith("SCOPE_"),
90120
() -> scope + " should not start with SCOPE_ since SCOPE_"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.core.authorization;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.security.authentication.TestingAuthenticationToken;
22+
import org.springframework.security.authorization.AuthorityAuthorizationManager;
23+
import org.springframework.security.authorization.AuthorizationManager;
24+
import org.springframework.security.authorization.AuthorizationManagerFactories;
25+
import org.springframework.security.authorization.AuthorizationResult;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link OAuth2AuthorizationManagerFactory}.
31+
*
32+
* @author Ngoc Nhan
33+
*/
34+
public class OAuth2AuthorizationManagerFactoryTests {
35+
36+
private static final String MSG_READ = "message:read";
37+
38+
private static final String MSG_WRITE = "message:write";
39+
40+
private static final String SCOPE_MSG_READ = "SCOPE_message:read";
41+
42+
private static final String SCOPE_MSG_WRITE = "SCOPE_message:write";
43+
44+
@Test
45+
public void hasScopeReturnsAuthorityAuthorizationManagerByDefault() {
46+
OAuth2AuthorizationManagerFactory<String> factory = new DefaultOAuth2AuthorizationManagerFactory<>();
47+
AuthorizationManager<String> authorizationManager = factory.hasScope(MSG_READ);
48+
assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
49+
}
50+
51+
@Test
52+
public void hasAnyScopeReturnsAuthorityAuthorizationManagerByDefault() {
53+
OAuth2AuthorizationManagerFactory<String> factory = new DefaultOAuth2AuthorizationManagerFactory<>();
54+
AuthorizationManager<String> authorizationManager = factory.hasAnyScope(MSG_READ, MSG_WRITE);
55+
assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
56+
}
57+
58+
@Test
59+
public void hasAllScopesReturnsAuthorityAuthorizationManagerByDefault() {
60+
OAuth2AuthorizationManagerFactory<String> factory = new DefaultOAuth2AuthorizationManagerFactory<>();
61+
AuthorizationManager<String> authorizationManager = factory.hasAnyScope(MSG_READ, MSG_WRITE);
62+
assertThat(authorizationManager).isInstanceOf(AuthorityAuthorizationManager.class);
63+
}
64+
65+
@Test
66+
public void hasScopeWhenSetAuthorizationManagerFactories() {
67+
DefaultOAuth2AuthorizationManagerFactory<String> factory = new DefaultOAuth2AuthorizationManagerFactory<>(
68+
AuthorizationManagerFactories.<String>multiFactor().requireFactors(SCOPE_MSG_READ).build());
69+
assertUserGranted(factory.hasScope(MSG_READ), SCOPE_MSG_READ);
70+
assertUserDenied(factory.hasScope(MSG_WRITE), SCOPE_MSG_READ);
71+
}
72+
73+
@Test
74+
public void hasAnyScopeWhenSetAuthorizationManagerFactories() {
75+
DefaultOAuth2AuthorizationManagerFactory<String> factory = new DefaultOAuth2AuthorizationManagerFactory<>(
76+
AuthorizationManagerFactories.<String>multiFactor().requireFactors(SCOPE_MSG_READ).build());
77+
assertUserGranted(factory.hasAnyScope(MSG_READ), SCOPE_MSG_READ);
78+
assertUserDenied(factory.hasAnyScope(MSG_WRITE), SCOPE_MSG_READ);
79+
}
80+
81+
@Test
82+
public void hasAllScopesWhenSetAuthorizationManagerFactories() {
83+
DefaultOAuth2AuthorizationManagerFactory<String> factory = new DefaultOAuth2AuthorizationManagerFactory<>(
84+
AuthorizationManagerFactories.<String>multiFactor()
85+
.requireFactors(SCOPE_MSG_READ, SCOPE_MSG_WRITE)
86+
.build());
87+
assertUserGranted(factory.hasAllScopes(MSG_READ, MSG_WRITE), SCOPE_MSG_READ, SCOPE_MSG_WRITE);
88+
assertUserDenied(factory.hasAllScopes(MSG_READ, MSG_WRITE), SCOPE_MSG_READ);
89+
}
90+
91+
private void assertUserGranted(AuthorizationManager<String> manager, String... authorities) {
92+
AuthorizationResult authorizationResult = createAuthorizationResult(manager, authorities);
93+
assertThat(authorizationResult).isNotNull();
94+
assertThat(authorizationResult.isGranted()).isTrue();
95+
}
96+
97+
private void assertUserDenied(AuthorizationManager<String> manager, String... authorities) {
98+
AuthorizationResult authorizationResult = createAuthorizationResult(manager, authorities);
99+
assertThat(authorizationResult).isNotNull();
100+
assertThat(authorizationResult.isGranted()).isFalse();
101+
}
102+
103+
private AuthorizationResult createAuthorizationResult(AuthorizationManager<String> manager, String... authorities) {
104+
TestingAuthenticationToken authenticatedUser = new TestingAuthenticationToken("user", "pass", authorities);
105+
return manager.authorize(() -> authenticatedUser, "");
106+
}
107+
108+
}

0 commit comments

Comments
 (0)