Skip to content

Commit aa1c80c

Browse files
committed
Grant Individual Authorities From Claims
Fixes gh-7339
1 parent 409285f commit aa1c80c

File tree

5 files changed

+268
-24
lines changed

5 files changed

+268
-24
lines changed

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

+16-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@
1515
*/
1616
package org.springframework.security.oauth2.client.oidc.userinfo;
1717

18+
import java.time.Instant;
19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.HashSet;
24+
import java.util.LinkedHashSet;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.function.Function;
28+
1829
import org.springframework.core.convert.TypeDescriptor;
1930
import org.springframework.core.convert.converter.Converter;
2031
import org.springframework.security.core.GrantedAuthority;
@@ -38,15 +49,6 @@
3849
import org.springframework.util.CollectionUtils;
3950
import org.springframework.util.StringUtils;
4051

41-
import java.time.Instant;
42-
import java.util.Arrays;
43-
import java.util.Collections;
44-
import java.util.HashMap;
45-
import java.util.HashSet;
46-
import java.util.Map;
47-
import java.util.Set;
48-
import java.util.function.Function;
49-
5052
/**
5153
* An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0 Provider's.
5254
*
@@ -94,6 +96,7 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
9496
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
9597
Assert.notNull(userRequest, "userRequest cannot be null");
9698
OidcUserInfo userInfo = null;
99+
Collection<? extends GrantedAuthority> oauth2UserAuthorities = Collections.emptyList();
97100
if (this.shouldRetrieveUserInfo(userRequest)) {
98101
OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest);
99102

@@ -106,6 +109,7 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
106109
claims = DEFAULT_CLAIM_TYPE_CONVERTER.convert(oauth2User.getAttributes());
107110
}
108111
userInfo = new OidcUserInfo(claims);
112+
oauth2UserAuthorities = oauth2User.getAuthorities();
109113

110114
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
111115

@@ -127,8 +131,9 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
127131
}
128132
}
129133

130-
Set<GrantedAuthority> authorities = Collections.singleton(
131-
new OidcUserAuthority(userRequest.getIdToken(), userInfo));
134+
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
135+
authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo));
136+
authorities.addAll(oauth2UserAuthorities);
132137

133138
OidcUser user;
134139

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

+47-5
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,22 @@
1515
*/
1616
package org.springframework.security.oauth2.client.userinfo;
1717

18+
import java.util.Arrays;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.LinkedHashSet;
22+
import java.util.Map;
23+
import java.util.Set;
24+
1825
import org.springframework.core.ParameterizedTypeReference;
1926
import org.springframework.core.convert.converter.Converter;
2027
import org.springframework.http.RequestEntity;
2128
import org.springframework.http.ResponseEntity;
2229
import org.springframework.security.core.GrantedAuthority;
30+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2331
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
2432
import org.springframework.security.oauth2.client.registration.ClientRegistration;
33+
import org.springframework.security.oauth2.core.ClaimAccessor;
2534
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2635
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
2736
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -35,10 +44,6 @@
3544
import org.springframework.web.client.RestOperations;
3645
import org.springframework.web.client.RestTemplate;
3746

38-
import java.util.Collections;
39-
import java.util.Map;
40-
import java.util.Set;
41-
4247
/**
4348
* An implementation of an {@link OAuth2UserService} that supports standard OAuth 2.0 Provider's.
4449
* <p>
@@ -66,6 +71,9 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
6671
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
6772
new ParameterizedTypeReference<Map<String, Object>>() {};
6873

74+
private static final Collection<String> WELL_KNOWN_AUTHORITIES_CLAIM_NAMES =
75+
Arrays.asList("scope", "scp");
76+
6977
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
7078

7179
private RestOperations restOperations;
@@ -127,7 +135,11 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
127135
}
128136

129137
Map<String, Object> userAttributes = response.getBody();
130-
Set<GrantedAuthority> authorities = Collections.singleton(new OAuth2UserAuthority(userAttributes));
138+
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
139+
authorities.add(new OAuth2UserAuthority(userAttributes));
140+
for (String authority : getAuthorities(() -> userAttributes)) {
141+
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
142+
}
131143

132144
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
133145
}
@@ -160,4 +172,34 @@ public final void setRestOperations(RestOperations restOperations) {
160172
Assert.notNull(restOperations, "restOperations cannot be null");
161173
this.restOperations = restOperations;
162174
}
175+
176+
private String getAuthoritiesClaimName(ClaimAccessor claims) {
177+
for (String claimName : WELL_KNOWN_AUTHORITIES_CLAIM_NAMES) {
178+
if (claims.containsClaim(claimName)) {
179+
return claimName;
180+
}
181+
}
182+
return null;
183+
}
184+
185+
private Collection<String> getAuthorities(ClaimAccessor claims) {
186+
String claimName = getAuthoritiesClaimName(claims);
187+
188+
if (claimName == null) {
189+
return Collections.emptyList();
190+
}
191+
192+
Object authorities = claims.getClaim(claimName);
193+
if (authorities instanceof String) {
194+
if (StringUtils.hasText((String) authorities)) {
195+
return Arrays.asList(((String) authorities).split(" "));
196+
} else {
197+
return Collections.emptyList();
198+
}
199+
} else if (authorities instanceof Collection) {
200+
return (Collection<String>) authorities;
201+
}
202+
203+
return Collections.emptyList();
204+
}
163205
}

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

+92-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
*/
1616
package org.springframework.security.oauth2.client.oidc.userinfo;
1717

18+
import java.time.Instant;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.Iterator;
23+
import java.util.Map;
24+
import java.util.concurrent.TimeUnit;
25+
import java.util.function.Function;
26+
1827
import okhttp3.mockwebserver.MockResponse;
1928
import okhttp3.mockwebserver.MockWebServer;
2029
import okhttp3.mockwebserver.RecordedRequest;
@@ -23,12 +32,20 @@
2332
import org.junit.Rule;
2433
import org.junit.Test;
2534
import org.junit.rules.ExpectedException;
35+
36+
import org.springframework.core.ParameterizedTypeReference;
2637
import org.springframework.core.convert.converter.Converter;
2738
import org.springframework.http.HttpHeaders;
2839
import org.springframework.http.HttpMethod;
40+
import org.springframework.http.HttpStatus;
2941
import org.springframework.http.MediaType;
42+
import org.springframework.http.RequestEntity;
43+
import org.springframework.http.ResponseEntity;
44+
import org.springframework.security.core.GrantedAuthority;
45+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3046
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3147
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
48+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
3249
import org.springframework.security.oauth2.core.AuthenticationMethod;
3350
import org.springframework.security.oauth2.core.OAuth2AccessToken;
3451
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -39,20 +56,20 @@
3956
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
4057
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
4158
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
42-
43-
import java.time.Instant;
44-
import java.util.Collections;
45-
import java.util.HashMap;
46-
import java.util.Map;
47-
import java.util.concurrent.TimeUnit;
48-
import java.util.function.Function;
59+
import org.springframework.web.client.RestOperations;
4960

5061
import static org.assertj.core.api.Assertions.assertThat;
5162
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5263
import static org.hamcrest.CoreMatchers.containsString;
53-
import static org.mockito.Mockito.*;
64+
import static org.mockito.Mockito.any;
65+
import static org.mockito.Mockito.mock;
66+
import static org.mockito.Mockito.nullable;
67+
import static org.mockito.Mockito.same;
68+
import static org.mockito.Mockito.verify;
69+
import static org.mockito.Mockito.when;
5470
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration;
5571
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.scopes;
72+
import static org.springframework.security.oauth2.core.oidc.TestOidcIdTokens.idToken;
5673

5774
/**
5875
* Tests for {@link OidcUserService}.
@@ -481,6 +498,73 @@ public void loadUserWhenCustomClaimTypeConverterFactorySetThenApplied() {
481498
verify(customClaimTypeConverterFactory).apply(same(clientRegistration));
482499
}
483500

501+
@Test
502+
public void loadUserWhenAttributesContainScopeThenIndividualScopeAuthorities() {
503+
Map<String, Object> body = new HashMap<>();
504+
body.put("id", "id");
505+
body.put("sub", "test-subject");
506+
body.put("scope", "message:read message:write");
507+
OidcUserService userService = new OidcUserService();
508+
userService.setOauth2UserService(withMockResponse(body));
509+
OidcUserRequest request = new OidcUserRequest(clientRegistration().
510+
userInfoUri("uri").build(), scopes("profile"), idToken(body));
511+
OidcUser user = userService.loadUser(request);
512+
513+
assertThat(user.getAuthorities()).hasSize(3);
514+
Iterator<? extends GrantedAuthority> authorities = user.getAuthorities().iterator();
515+
assertThat(authorities.next()).isInstanceOf(OidcUserAuthority.class);
516+
assertThat(authorities.next()).isEqualTo(new SimpleGrantedAuthority("SCOPE_message:read"));
517+
assertThat(authorities.next()).isEqualTo(new SimpleGrantedAuthority("SCOPE_message:write"));
518+
}
519+
520+
@Test
521+
public void loadUserWhenAttributesContainScpThenIndividualScopeAuthorities() {
522+
Map<String, Object> body = new HashMap<>();
523+
body.put("id", "id");
524+
body.put("sub", "test-subject");
525+
body.put("scp", Arrays.asList("message:read", "message:write"));
526+
OidcUserService userService = new OidcUserService();
527+
userService.setOauth2UserService(withMockResponse(body));
528+
OidcUserRequest request = new OidcUserRequest(clientRegistration().
529+
userInfoUri("uri").build(), scopes("profile"), idToken(body));
530+
OidcUser user = userService.loadUser(request);
531+
532+
assertThat(user.getAuthorities()).hasSize(3);
533+
Iterator<? extends GrantedAuthority> authorities = user.getAuthorities().iterator();
534+
assertThat(authorities.next()).isInstanceOf(OidcUserAuthority.class);
535+
assertThat(authorities.next()).isEqualTo(new SimpleGrantedAuthority("SCOPE_message:read"));
536+
assertThat(authorities.next()).isEqualTo(new SimpleGrantedAuthority("SCOPE_message:write"));
537+
}
538+
539+
@Test
540+
public void loadUserWhenAttributesDoesNotContainScopesThenNoScopeAuthorities() {
541+
Map<String, Object> body = new HashMap<>();
542+
body.put("id", "id");
543+
body.put("sub", "test-subject");
544+
body.put("authorities", Arrays.asList("message:read", "message:write"));
545+
OidcUserService userService = new OidcUserService();
546+
userService.setOauth2UserService(withMockResponse(body));
547+
OidcUserRequest request = new OidcUserRequest(clientRegistration().
548+
userInfoUri("uri").build(), scopes("profile"), idToken(body));
549+
OidcUser user = userService.loadUser(request);
550+
551+
assertThat(user.getAuthorities()).hasSize(1);
552+
Iterator<? extends GrantedAuthority> authorities = user.getAuthorities().iterator();
553+
assertThat(authorities.next()).isInstanceOf(OidcUserAuthority.class);
554+
}
555+
556+
private DefaultOAuth2UserService withMockResponse(Map<String, Object> response) {
557+
ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(response, HttpStatus.OK);
558+
Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
559+
RestOperations rest = mock(RestOperations.class);
560+
when(rest.exchange(nullable(RequestEntity.class), any(ParameterizedTypeReference.class)))
561+
.thenReturn(responseEntity);
562+
DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
563+
userService.setRequestEntityConverter(requestEntityConverter);
564+
userService.setRestOperations(rest);
565+
return userService;
566+
}
567+
484568
private MockResponse jsonResponse(String json) {
485569
return new MockResponse()
486570
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)

0 commit comments

Comments
 (0)