Skip to content

Commit 3f21089

Browse files
committed
Allow configurable accessible scopes for UserInfo resource
Fixes gh-6886
1 parent 6e76df8 commit 3f21089

File tree

2 files changed

+110
-14
lines changed

2 files changed

+110
-14
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
6262
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
6363
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
6464
new ClaimTypeConverter(createDefaultClaimTypeConverters());
65-
private final Set<String> userInfoScopes = new HashSet<>(
66-
Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE));
65+
private Set<String> accessibleScopes = new HashSet<>(Arrays.asList(
66+
OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE));
6767
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultOAuth2UserService();
6868
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
6969
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
@@ -160,8 +160,9 @@ private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
160160
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(
161161
userRequest.getClientRegistration().getAuthorizationGrantType())) {
162162

163-
// Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s)
164-
return CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.userInfoScopes);
163+
// Return true if there is at least one match between the authorized scope(s) and accessible scope(s)
164+
return this.accessibleScopes.isEmpty() ||
165+
CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.accessibleScopes);
165166
}
166167

167168
return false;
@@ -190,4 +191,19 @@ public final void setClaimTypeConverterFactory(Function<ClientRegistration, Conv
190191
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
191192
this.claimTypeConverterFactory = claimTypeConverterFactory;
192193
}
194+
195+
/**
196+
* Sets the scope(s) that allow access to the user info resource.
197+
* The default is {@link OidcScopes#PROFILE profile}, {@link OidcScopes#EMAIL email}, {@link OidcScopes#ADDRESS address} and {@link OidcScopes#PHONE phone}.
198+
* The scope(s) are checked against the "granted" scope(s) associated to the {@link OidcUserRequest#getAccessToken() access token}
199+
* to determine if the user info resource is accessible or not.
200+
* If there is at least one match, the user info resource will be requested, otherwise it will not.
201+
*
202+
* @since 5.2
203+
* @param accessibleScopes the scope(s) that allow access to the user info resource
204+
*/
205+
public final void setAccessibleScopes(Set<String> accessibleScopes) {
206+
Assert.notNull(accessibleScopes, "accessibleScopes cannot be null");
207+
this.accessibleScopes = accessibleScopes;
208+
}
193209
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@
4141
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
4242

4343
import java.time.Instant;
44-
import java.util.Arrays;
44+
import java.util.Collections;
4545
import java.util.HashMap;
46-
import java.util.LinkedHashSet;
4746
import java.util.Map;
48-
import java.util.Set;
4947
import java.util.concurrent.TimeUnit;
5048
import java.util.function.Function;
5149

@@ -116,6 +114,17 @@ public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentExceptio
116114
.isInstanceOf(IllegalArgumentException.class);
117115
}
118116

117+
@Test
118+
public void setAccessibleScopesWhenNullThenThrowIllegalArgumentException() {
119+
assertThatThrownBy(() -> this.userService.setAccessibleScopes(null))
120+
.isInstanceOf(IllegalArgumentException.class);
121+
}
122+
123+
@Test
124+
public void setAccessibleScopesWhenEmptyThenSet() {
125+
this.userService.setAccessibleScopes(Collections.emptySet());
126+
}
127+
119128
@Test
120129
public void loadUserWhenUserRequestIsNullThenThrowIllegalArgumentException() {
121130
this.exception.expect(IllegalArgumentException.class);
@@ -130,20 +139,91 @@ public void loadUserWhenUserInfoUriIsNullThenUserInfoEndpointNotRequested() {
130139
}
131140

132141
@Test
133-
public void loadUserWhenAuthorizedScopesDoesNotContainUserInfoScopesThenUserInfoEndpointNotRequested() {
142+
public void loadUserWhenNonStandardScopesAuthorizedThenUserInfoEndpointNotRequested() {
134143
ClientRegistration clientRegistration = this.clientRegistrationBuilder
135144
.userInfoUri("https://provider.com/user").build();
136-
137-
Set<String> authorizedScopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2"));
138-
OAuth2AccessToken accessToken = new OAuth2AccessToken(
139-
OAuth2AccessToken.TokenType.BEARER, "access-token",
140-
Instant.MIN, Instant.MAX, authorizedScopes);
145+
this.accessToken = scopes("scope1", "scope2");
141146

142147
OidcUser user = this.userService.loadUser(
143-
new OidcUserRequest(clientRegistration, accessToken, this.idToken));
148+
new OidcUserRequest(clientRegistration, this.accessToken, this.idToken));
144149
assertThat(user.getUserInfo()).isNull();
145150
}
146151

152+
// gh-6886
153+
@Test
154+
public void loadUserWhenNonStandardScopesAuthorizedAndAccessibleScopesMatchThenUserInfoEndpointRequested() {
155+
String userInfoResponse = "{\n" +
156+
" \"sub\": \"subject1\",\n" +
157+
" \"name\": \"first last\",\n" +
158+
" \"given_name\": \"first\",\n" +
159+
" \"family_name\": \"last\",\n" +
160+
" \"preferred_username\": \"user1\",\n" +
161+
" \"email\": \"[email protected]\"\n" +
162+
"}\n";
163+
this.server.enqueue(jsonResponse(userInfoResponse));
164+
165+
String userInfoUri = this.server.url("/user").toString();
166+
167+
ClientRegistration clientRegistration = this.clientRegistrationBuilder
168+
.userInfoUri(userInfoUri).build();
169+
170+
this.accessToken = scopes("scope1", "scope2");
171+
this.userService.setAccessibleScopes(Collections.singleton("scope2"));
172+
173+
OidcUser user = this.userService.loadUser(
174+
new OidcUserRequest(clientRegistration, this.accessToken, this.idToken));
175+
assertThat(user.getUserInfo()).isNotNull();
176+
}
177+
178+
// gh-6886
179+
@Test
180+
public void loadUserWhenNonStandardScopesAuthorizedAndAccessibleScopesEmptyThenUserInfoEndpointRequested() {
181+
String userInfoResponse = "{\n" +
182+
" \"sub\": \"subject1\",\n" +
183+
" \"name\": \"first last\",\n" +
184+
" \"given_name\": \"first\",\n" +
185+
" \"family_name\": \"last\",\n" +
186+
" \"preferred_username\": \"user1\",\n" +
187+
" \"email\": \"[email protected]\"\n" +
188+
"}\n";
189+
this.server.enqueue(jsonResponse(userInfoResponse));
190+
191+
String userInfoUri = this.server.url("/user").toString();
192+
193+
ClientRegistration clientRegistration = this.clientRegistrationBuilder
194+
.userInfoUri(userInfoUri).build();
195+
196+
this.accessToken = scopes("scope1", "scope2");
197+
this.userService.setAccessibleScopes(Collections.emptySet());
198+
199+
OidcUser user = this.userService.loadUser(
200+
new OidcUserRequest(clientRegistration, this.accessToken, this.idToken));
201+
assertThat(user.getUserInfo()).isNotNull();
202+
}
203+
204+
// gh-6886
205+
@Test
206+
public void loadUserWhenStandardScopesAuthorizedThenUserInfoEndpointRequested() {
207+
String userInfoResponse = "{\n" +
208+
" \"sub\": \"subject1\",\n" +
209+
" \"name\": \"first last\",\n" +
210+
" \"given_name\": \"first\",\n" +
211+
" \"family_name\": \"last\",\n" +
212+
" \"preferred_username\": \"user1\",\n" +
213+
" \"email\": \"[email protected]\"\n" +
214+
"}\n";
215+
this.server.enqueue(jsonResponse(userInfoResponse));
216+
217+
String userInfoUri = this.server.url("/user").toString();
218+
219+
ClientRegistration clientRegistration = this.clientRegistrationBuilder
220+
.userInfoUri(userInfoUri).build();
221+
222+
OidcUser user = this.userService.loadUser(
223+
new OidcUserRequest(clientRegistration, this.accessToken, this.idToken));
224+
assertThat(user.getUserInfo()).isNotNull();
225+
}
226+
147227
@Test
148228
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
149229
String userInfoResponse = "{\n" +

0 commit comments

Comments
 (0)