Skip to content

Commit 6d2ecda

Browse files
nikitanagar08niqSolve
authored andcommitted
Add OAuth2AuthorizedScopesMapper for Client Credentials Grant
Add OAuth2AuthorizedScopesMapper functional interface and OAuth2AuthorizedScopesContext to allow filtering/transforming authorized scopes before the OAuth2Authorization is persisted. Integrate the mapper into OAuth2ClientCredentialsAuthenticationProvider with a setter following the existing setAuthenticationValidator pattern. Fixes gh-1504 Signed-off-by: Nikita Nagar <permanayan84@gmail.com>
1 parent 12768b5 commit 6d2ecda

4 files changed

Lines changed: 241 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2020-2025 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+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import java.util.Collections;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.Set;
22+
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
26+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* An {@link OAuth2AuthenticationContext} that holds information about the authorized
31+
* scopes and is used when mapping the OAuth 2.0 authorized scopes.
32+
*
33+
* @author Nikita Nagar
34+
* @since 1.5
35+
* @see OAuth2AuthenticationContext
36+
* @see OAuth2AuthorizedScopesMapper
37+
*/
38+
public final class OAuth2AuthorizedScopesContext implements OAuth2AuthenticationContext {
39+
40+
private static final String AUTHORIZED_SCOPES_KEY = OAuth2AuthorizedScopesContext.class.getName()
41+
.concat(".AUTHORIZED_SCOPES");
42+
43+
private final Map<Object, Object> context;
44+
45+
private OAuth2AuthorizedScopesContext(Map<Object, Object> context) {
46+
this.context = Collections.unmodifiableMap(new HashMap<>(context));
47+
}
48+
49+
@SuppressWarnings("unchecked")
50+
@Nullable
51+
@Override
52+
public <V> V get(Object key) {
53+
return hasKey(key) ? (V) this.context.get(key) : null;
54+
}
55+
56+
@Override
57+
public boolean hasKey(Object key) {
58+
Assert.notNull(key, "key cannot be null");
59+
return this.context.containsKey(key);
60+
}
61+
62+
/**
63+
* Returns the {@link RegisteredClient registered client}.
64+
* @return the {@link RegisteredClient}
65+
*/
66+
public RegisteredClient getRegisteredClient() {
67+
return get(RegisteredClient.class);
68+
}
69+
70+
/**
71+
* Returns the {@link AuthorizationGrantType authorization grant type}.
72+
* @return the {@link AuthorizationGrantType}
73+
*/
74+
public AuthorizationGrantType getAuthorizationGrantType() {
75+
return get(AuthorizationGrantType.class);
76+
}
77+
78+
/**
79+
* Returns the authorized scopes.
80+
* @return the authorized scopes
81+
*/
82+
public Set<String> getAuthorizedScopes() {
83+
return get(AUTHORIZED_SCOPES_KEY);
84+
}
85+
86+
/**
87+
* Constructs a new {@link Builder} with the provided {@link Authentication}.
88+
* @param authentication the {@link Authentication}
89+
* @return the {@link Builder}
90+
*/
91+
public static Builder with(Authentication authentication) {
92+
return new Builder(authentication);
93+
}
94+
95+
/**
96+
* A builder for {@link OAuth2AuthorizedScopesContext}.
97+
*/
98+
public static final class Builder extends AbstractBuilder<OAuth2AuthorizedScopesContext, Builder> {
99+
100+
private Builder(Authentication authentication) {
101+
super(authentication);
102+
}
103+
104+
/**
105+
* Sets the {@link RegisteredClient registered client}.
106+
* @param registeredClient the {@link RegisteredClient}
107+
* @return the {@link Builder} for further configuration
108+
*/
109+
public Builder registeredClient(RegisteredClient registeredClient) {
110+
return put(RegisteredClient.class, registeredClient);
111+
}
112+
113+
/**
114+
* Sets the {@link AuthorizationGrantType authorization grant type}.
115+
* @param authorizationGrantType the {@link AuthorizationGrantType}
116+
* @return the {@link Builder} for further configuration
117+
*/
118+
public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
119+
return put(AuthorizationGrantType.class, authorizationGrantType);
120+
}
121+
122+
/**
123+
* Sets the authorized scopes.
124+
* @param authorizedScopes the authorized scopes
125+
* @return the {@link Builder} for further configuration
126+
*/
127+
public Builder authorizedScopes(Set<String> authorizedScopes) {
128+
return put(AUTHORIZED_SCOPES_KEY, authorizedScopes);
129+
}
130+
131+
/**
132+
* Builds a new {@link OAuth2AuthorizedScopesContext}.
133+
* @return the {@link OAuth2AuthorizedScopesContext}
134+
*/
135+
@Override
136+
public OAuth2AuthorizedScopesContext build() {
137+
Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
138+
Assert.notNull(get(AuthorizationGrantType.class), "authorizationGrantType cannot be null");
139+
Assert.notNull(get(AUTHORIZED_SCOPES_KEY), "authorizedScopes cannot be null");
140+
return new OAuth2AuthorizedScopesContext(getContext());
141+
}
142+
143+
}
144+
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2020-2025 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+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import java.util.Set;
19+
20+
/**
21+
* A mapper that transforms the authorized scopes before the
22+
* {@link org.springframework.security.oauth2.server.authorization.OAuth2Authorization} is
23+
* persisted.
24+
*
25+
* @author Nikita Nagar
26+
* @since 1.5
27+
* @see OAuth2AuthorizedScopesContext
28+
* @see OAuth2ClientCredentialsAuthenticationProvider#setAuthorizedScopesMapper(OAuth2AuthorizedScopesMapper)
29+
*/
30+
@FunctionalInterface
31+
public interface OAuth2AuthorizedScopesMapper {
32+
33+
/**
34+
* Maps the authorized scopes from the provided
35+
* {@link OAuth2AuthorizedScopesContext}.
36+
* @param context the {@link OAuth2AuthorizedScopesContext} containing the authorized
37+
* scopes and additional information
38+
* @return the mapped authorized scopes
39+
*/
40+
Set<String> map(OAuth2AuthorizedScopesContext context);
41+
42+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
7373

7474
private Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator = new OAuth2ClientCredentialsAuthenticationValidator();
7575

76+
private OAuth2AuthorizedScopesMapper authorizedScopesMapper = (ctx) -> ctx.getAuthorizedScopes();
77+
7678
/**
7779
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the
7880
* provided parameters.
@@ -117,6 +119,16 @@ public Authentication authenticate(Authentication authentication) throws Authent
117119

118120
Set<String> authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
119121

122+
// @formatter:off
123+
OAuth2AuthorizedScopesContext authorizedScopesContext = OAuth2AuthorizedScopesContext
124+
.with(clientCredentialsAuthentication)
125+
.registeredClient(registeredClient)
126+
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
127+
.authorizedScopes(authorizedScopes)
128+
.build();
129+
// @formatter:on
130+
authorizedScopes = this.authorizedScopesMapper.map(authorizedScopesContext);
131+
120132
// Verify the DPoP Proof (if available)
121133
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(clientCredentialsAuthentication);
122134

@@ -199,4 +211,17 @@ public void setAuthenticationValidator(
199211
this.authenticationValidator = authenticationValidator;
200212
}
201213

214+
/**
215+
* Sets the {@link OAuth2AuthorizedScopesMapper} used to map the authorized scopes
216+
* before the {@link OAuth2Authorization} is persisted. The default authorized scopes
217+
* mapper is the identity function.
218+
* @param authorizedScopesMapper the {@link OAuth2AuthorizedScopesMapper} used to map
219+
* the authorized scopes
220+
* @since 1.5
221+
*/
222+
public void setAuthorizedScopesMapper(OAuth2AuthorizedScopesMapper authorizedScopesMapper) {
223+
Assert.notNull(authorizedScopesMapper, "authorizedScopesMapper cannot be null");
224+
this.authorizedScopesMapper = authorizedScopesMapper;
225+
}
226+
202227
}

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,35 @@ public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException(
165165
.hasMessage("authenticationValidator cannot be null");
166166
}
167167

168+
@Test
169+
public void setAuthorizedScopesMapperWhenNullThenThrowIllegalArgumentException() {
170+
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizedScopesMapper(null))
171+
.isInstanceOf(IllegalArgumentException.class)
172+
.hasMessage("authorizedScopesMapper cannot be null");
173+
}
174+
175+
@Test
176+
public void authenticateWhenCustomAuthorizedScopesMapperThenUsed() {
177+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
178+
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
179+
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
180+
Set<String> requestedScopes = registeredClient.getScopes();
181+
OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(
182+
clientPrincipal, requestedScopes, null);
183+
184+
Set<String> mappedScopes = Collections.singleton("scope1");
185+
this.authenticationProvider.setAuthorizedScopesMapper((context) -> mappedScopes);
186+
187+
given(this.jwtEncoder.encode(any())).willReturn(createJwt(mappedScopes));
188+
189+
this.authenticationProvider.authenticate(authentication);
190+
191+
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
192+
verify(this.authorizationService).save(authorizationCaptor.capture());
193+
OAuth2Authorization authorization = authorizationCaptor.getValue();
194+
assertThat(authorization.getAuthorizedScopes()).isEqualTo(mappedScopes);
195+
}
196+
168197
@Test
169198
public void authenticateWhenClientPrincipalNotOAuth2ClientAuthenticationTokenThenThrowOAuth2AuthenticationException() {
170199
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();

0 commit comments

Comments
 (0)