Skip to content

Commit e52dd81

Browse files
committed
Customize mapping the OidcUser
Closes gh-14672
1 parent d6382b8 commit e52dd81

File tree

5 files changed

+252
-42
lines changed

5 files changed

+252
-42
lines changed

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

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,15 @@
1818

1919
import java.time.Instant;
2020
import java.util.HashMap;
21-
import java.util.HashSet;
2221
import java.util.Map;
23-
import java.util.Set;
22+
import java.util.function.BiFunction;
2423
import java.util.function.Function;
2524
import java.util.function.Predicate;
2625

2726
import reactor.core.publisher.Mono;
2827

2928
import org.springframework.core.convert.TypeDescriptor;
3029
import org.springframework.core.convert.converter.Converter;
31-
import org.springframework.security.core.GrantedAuthority;
3230
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3331
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3432
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
@@ -40,14 +38,14 @@
4038
import org.springframework.security.oauth2.core.OAuth2Error;
4139
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
4240
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
41+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
4342
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
4443
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
4544
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
4645
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
4746
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
4847
import org.springframework.security.oauth2.core.user.OAuth2User;
4948
import org.springframework.util.Assert;
50-
import org.springframework.util.StringUtils;
5149

5250
/**
5351
* An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect
@@ -75,6 +73,8 @@ public class OidcReactiveOAuth2UserService implements ReactiveOAuth2UserService<
7573

7674
private Predicate<OidcUserRequest> retrieveUserInfo = OidcUserRequestUtils::shouldRetrieveUserInfo;
7775

76+
private BiFunction<OidcUserRequest, OidcUserInfo, Mono<OidcUser>> oidcUserMapper = this::getUser;
77+
7878
/**
7979
* Returns the default {@link Converter}'s used for type conversion of claim values
8080
* for an {@link OidcUserInfo}.
@@ -103,29 +103,15 @@ public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2Authent
103103
Assert.notNull(userRequest, "userRequest cannot be null");
104104
// @formatter:off
105105
return getUserInfo(userRequest)
106-
.map((userInfo) ->
107-
new OidcUserAuthority(userRequest.getIdToken(), userInfo)
108-
)
109-
.defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), null))
110-
.map((authority) -> {
111-
OidcUserInfo userInfo = authority.getUserInfo();
112-
Set<GrantedAuthority> authorities = new HashSet<>();
113-
authorities.add(authority);
114-
OAuth2AccessToken token = userRequest.getAccessToken();
115-
for (String scope : token.getScopes()) {
116-
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
117-
}
118-
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
119-
.getUserInfoEndpoint().getUserNameAttributeName();
120-
if (StringUtils.hasText(userNameAttributeName)) {
121-
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo,
122-
userNameAttributeName);
123-
}
124-
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
125-
});
106+
.flatMap((userInfo) -> this.oidcUserMapper.apply(userRequest, userInfo))
107+
.switchIfEmpty(Mono.defer(() -> this.oidcUserMapper.apply(userRequest, null)));
126108
// @formatter:on
127109
}
128110

111+
private Mono<OidcUser> getUser(OidcUserRequest userRequest, OidcUserInfo userInfo) {
112+
return Mono.just(OidcUserRequestUtils.getUser(userRequest, userInfo));
113+
}
114+
129115
private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) {
130116
if (!this.retrieveUserInfo.test(userRequest)) {
131117
return Mono.empty();
@@ -193,4 +179,60 @@ public final void setRetrieveUserInfo(Predicate<OidcUserRequest> retrieveUserInf
193179
this.retrieveUserInfo = retrieveUserInfo;
194180
}
195181

182+
/**
183+
* Sets the {@code BiFunction} used to map the {@link OidcUser user} from the
184+
* {@link OidcUserRequest user request} and {@link OidcUserInfo user info}.
185+
* <p>
186+
* This is useful when you need to map the user or authorities from the access token
187+
* itself. For example, when the authorization server provides authorization
188+
* information in the access token payload you can do the following: <pre>
189+
* &#64;Bean
190+
* public OidcReactiveOAuth2UserService oidcUserService() {
191+
* var userService = new OidcReactiveOAuth2UserService();
192+
* userService.setOidcUserMapper(oidcUserMapper());
193+
* return userService;
194+
* }
195+
*
196+
* private static BiFunction&lt;OidcUserRequest, OidcUserInfo, Mono&lt;OidcUser&gt;&gt; oidcUserMapper() {
197+
* return (userRequest, userInfo) -> {
198+
* var accessToken = userRequest.getAccessToken();
199+
* var grantedAuthorities = new HashSet&lt;GrantedAuthority&gt;();
200+
* // TODO: Map authorities from the access token
201+
* var userNameAttributeName = "preferred_username";
202+
* return Mono.just(new DefaultOidcUser(
203+
* grantedAuthorities,
204+
* userRequest.getIdToken(),
205+
* userInfo,
206+
* userNameAttributeName
207+
* ));
208+
* };
209+
* }
210+
* </pre>
211+
* <p>
212+
* Note that you can access the {@code userNameAttributeName} via the
213+
* {@link ClientRegistration} as follows: <pre>
214+
* var userNameAttributeName = userRequest.getClientRegistration()
215+
* .getProviderDetails()
216+
* .getUserInfoEndpoint()
217+
* .getUserNameAttributeName();
218+
* </pre>
219+
* <p>
220+
* By default, a {@link DefaultOidcUser} is created with authorities mapped as
221+
* follows:
222+
* <ul>
223+
* <li>An {@link OidcUserAuthority} is created from the {@link OidcIdToken} and
224+
* {@link OidcUserInfo} with an authority of {@code OIDC_USER}</li>
225+
* <li>Additional {@link SimpleGrantedAuthority authorities} are mapped from the
226+
* {@link OAuth2AccessToken#getScopes() access token scopes} with a prefix of
227+
* {@code SCOPE_}</li>
228+
* </ul>
229+
* @param oidcUserMapper the function used to map the {@link OidcUser} from the
230+
* {@link OidcUserRequest} and {@link OidcUserInfo}
231+
* @since 6.3
232+
*/
233+
public final void setOidcUserMapper(BiFunction<OidcUserRequest, OidcUserInfo, Mono<OidcUser>> oidcUserMapper) {
234+
Assert.notNull(oidcUserMapper, "oidcUserMapper cannot be null");
235+
this.oidcUserMapper = oidcUserMapper;
236+
}
237+
196238
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,18 @@
1616

1717
package org.springframework.security.oauth2.client.oidc.userinfo;
1818

19+
import java.util.LinkedHashSet;
20+
import java.util.Set;
21+
22+
import org.springframework.security.core.GrantedAuthority;
23+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
1924
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2025
import org.springframework.security.oauth2.core.AuthorizationGrantType;
26+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
27+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
28+
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
29+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
30+
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
2131
import org.springframework.util.CollectionUtils;
2232
import org.springframework.util.StringUtils;
2333

@@ -66,6 +76,21 @@ static boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
6676
return false;
6777
}
6878

79+
static OidcUser getUser(OidcUserRequest userRequest, OidcUserInfo userInfo) {
80+
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
81+
authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo));
82+
OAuth2AccessToken token = userRequest.getAccessToken();
83+
for (String scope : token.getScopes()) {
84+
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
85+
}
86+
ClientRegistration.ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
87+
String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName();
88+
if (StringUtils.hasText(userNameAttributeName)) {
89+
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
90+
}
91+
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
92+
}
93+
6994
private OidcUserRequestUtils() {
7095
}
7196

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

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
import java.util.Arrays;
2121
import java.util.HashMap;
2222
import java.util.HashSet;
23-
import java.util.LinkedHashSet;
2423
import java.util.Map;
2524
import java.util.Set;
25+
import java.util.function.BiFunction;
2626
import java.util.function.Function;
2727
import java.util.function.Predicate;
2828

2929
import org.springframework.core.convert.TypeDescriptor;
3030
import org.springframework.core.convert.converter.Converter;
31-
import org.springframework.security.core.GrantedAuthority;
3231
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3332
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3433
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
@@ -41,6 +40,7 @@
4140
import org.springframework.security.oauth2.core.OAuth2Error;
4241
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
4342
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
43+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
4444
import org.springframework.security.oauth2.core.oidc.OidcScopes;
4545
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
4646
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
@@ -57,6 +57,7 @@
5757
* Provider's.
5858
*
5959
* @author Joe Grandja
60+
* @author Steve Riesenberg
6061
* @since 5.0
6162
* @see OAuth2UserService
6263
* @see OidcUserRequest
@@ -81,6 +82,8 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
8182

8283
private Predicate<OidcUserRequest> retrieveUserInfo = this::shouldRetrieveUserInfo;
8384

85+
private BiFunction<OidcUserRequest, OidcUserInfo, OidcUser> oidcUserMapper = OidcUserRequestUtils::getUser;
86+
8487
/**
8588
* Returns the default {@link Converter}'s used for type conversion of claim values
8689
* for an {@link OidcUserInfo}.
@@ -130,13 +133,7 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
130133
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
131134
}
132135
}
133-
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
134-
authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo));
135-
OAuth2AccessToken token = userRequest.getAccessToken();
136-
for (String authority : token.getScopes()) {
137-
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
138-
}
139-
return getUser(userRequest, userInfo, authorities);
136+
return this.oidcUserMapper.apply(userRequest, userInfo);
140137
}
141138

142139
private Map<String, Object> getClaims(OidcUserRequest userRequest, OAuth2User oauth2User) {
@@ -148,15 +145,6 @@ private Map<String, Object> getClaims(OidcUserRequest userRequest, OAuth2User oa
148145
return DEFAULT_CLAIM_TYPE_CONVERTER.convert(oauth2User.getAttributes());
149146
}
150147

151-
private OidcUser getUser(OidcUserRequest userRequest, OidcUserInfo userInfo, Set<GrantedAuthority> authorities) {
152-
ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
153-
String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName();
154-
if (StringUtils.hasText(userNameAttributeName)) {
155-
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
156-
}
157-
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
158-
}
159-
160148
private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
161149
// Auto-disabled if UserInfo Endpoint URI is not provided
162150
ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
@@ -255,4 +243,60 @@ public final void setRetrieveUserInfo(Predicate<OidcUserRequest> retrieveUserInf
255243
this.retrieveUserInfo = retrieveUserInfo;
256244
}
257245

246+
/**
247+
* Sets the {@code BiFunction} used to map the {@link OidcUser user} from the
248+
* {@link OidcUserRequest user request} and {@link OidcUserInfo user info}.
249+
* <p>
250+
* This is useful when you need to map the user or authorities from the access token
251+
* itself. For example, when the authorization server provides authorization
252+
* information in the access token payload you can do the following: <pre>
253+
* &#64;Bean
254+
* public OidcUserService oidcUserService() {
255+
* var userService = new OidcUserService();
256+
* userService.setOidcUserMapper(oidcUserMapper());
257+
* return userService;
258+
* }
259+
*
260+
* private static BiFunction&lt;OidcUserRequest, OidcUserInfo, OidcUser&gt; oidcUserMapper() {
261+
* return (userRequest, userInfo) -> {
262+
* var accessToken = userRequest.getAccessToken();
263+
* var grantedAuthorities = new HashSet&lt;GrantedAuthority&gt;();
264+
* // TODO: Map authorities from the access token
265+
* var userNameAttributeName = "preferred_username";
266+
* return new DefaultOidcUser(
267+
* grantedAuthorities,
268+
* userRequest.getIdToken(),
269+
* userInfo,
270+
* userNameAttributeName
271+
* );
272+
* };
273+
* }
274+
* </pre>
275+
* <p>
276+
* Note that you can access the {@code userNameAttributeName} via the
277+
* {@link ClientRegistration} as follows: <pre>
278+
* var userNameAttributeName = userRequest.getClientRegistration()
279+
* .getProviderDetails()
280+
* .getUserInfoEndpoint()
281+
* .getUserNameAttributeName();
282+
* </pre>
283+
* <p>
284+
* By default, a {@link DefaultOidcUser} is created with authorities mapped as
285+
* follows:
286+
* <ul>
287+
* <li>An {@link OidcUserAuthority} is created from the {@link OidcIdToken} and
288+
* {@link OidcUserInfo} with an authority of {@code OIDC_USER}</li>
289+
* <li>Additional {@link SimpleGrantedAuthority authorities} are mapped from the
290+
* {@link OAuth2AccessToken#getScopes() access token scopes} with a prefix of
291+
* {@code SCOPE_}</li>
292+
* </ul>
293+
* @param oidcUserMapper the function used to map the {@link OidcUser} from the
294+
* {@link OidcUserRequest} and {@link OidcUserInfo}
295+
* @since 6.3
296+
*/
297+
public final void setOidcUserMapper(BiFunction<OidcUserRequest, OidcUserInfo, OidcUser> oidcUserMapper) {
298+
Assert.notNull(oidcUserMapper, "oidcUserMapper cannot be null");
299+
this.oidcUserMapper = oidcUserMapper;
300+
}
301+
258302
}

0 commit comments

Comments
 (0)