Skip to content

Commit 49b75a8

Browse files
committed
Allow injecting the principal name into DefaultOAuth2User
- Add username field to DefaultOAuth2User - Deprecate constructor that uses nameAttributeKey lookup - Add static factory method withUsername() for direct username injection - Update Jackson mixin to maintain backward compatibility - Preserve serialization format by excluding username field This change prepares for SpEL support in the next commit. Signed-off-by: yybmion <[email protected]>
1 parent 455a2ec commit 49b75a8

File tree

5 files changed

+158
-104
lines changed

5 files changed

+158
-104
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121

2222
import com.fasterxml.jackson.annotation.JsonAutoDetect;
2323
import com.fasterxml.jackson.annotation.JsonCreator;
24+
import com.fasterxml.jackson.annotation.JsonIgnore;
2425
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2526
import com.fasterxml.jackson.annotation.JsonProperty;
2627
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -31,7 +32,14 @@
3132
/**
3233
* This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}.
3334
*
35+
* <p>
36+
* Note: The {@code username} field is intentionally excluded from serialization as it's a
37+
* derived value that can be reconstructed from {@code nameAttributeKey} and
38+
* {@code attributes} during deserialization.
39+
* </p>
40+
*
3441
* @author Joe Grandja
42+
* @author YooBin Yoon
3543
* @since 5.3
3644
* @see DefaultOAuth2User
3745
* @see OAuth2ClientJackson2Module
@@ -42,6 +50,9 @@
4250
@JsonIgnoreProperties(ignoreUnknown = true)
4351
abstract class DefaultOAuth2UserMixin {
4452

53+
@JsonIgnore
54+
private String username;
55+
4556
@JsonCreator
4657
DefaultOAuth2UserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
4758
@JsonProperty("attributes") Map<String, Object> attributes,

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

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -119,51 +119,51 @@ public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2Aut
119119
// @formatter:off
120120
Mono<Map<String, Object>> userAttributes = requestHeadersSpec.retrieve()
121121
.onStatus(HttpStatusCode::isError, (response) ->
122-
parse(response)
123-
.map((userInfoErrorResponse) -> {
124-
String description = userInfoErrorResponse.getErrorObject().getDescription();
125-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, description,
126-
null);
127-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
128-
})
122+
parse(response)
123+
.map((userInfoErrorResponse) -> {
124+
String description = userInfoErrorResponse.getErrorObject().getDescription();
125+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, description,
126+
null);
127+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
128+
})
129129
)
130130
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP)
131131
.mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes));
132132
return userAttributes.map((attrs) -> {
133-
GrantedAuthority authority = new OAuth2UserAuthority(attrs, userNameAttributeName);
134-
Set<GrantedAuthority> authorities = new HashSet<>();
135-
authorities.add(authority);
136-
OAuth2AccessToken token = userRequest.getAccessToken();
137-
for (String scope : token.getScopes()) {
138-
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
139-
}
140-
141-
return new DefaultOAuth2User(authorities, attrs, userNameAttributeName);
142-
})
143-
.onErrorMap((ex) -> (ex instanceof UnsupportedMediaTypeException ||
144-
ex.getCause() instanceof UnsupportedMediaTypeException), (ex) -> {
145-
String contentType = (ex instanceof UnsupportedMediaTypeException) ?
146-
((UnsupportedMediaTypeException) ex).getContentType().toString() :
147-
((UnsupportedMediaTypeException) ex.getCause()).getContentType().toString();
148-
String errorMessage = "An error occurred while attempting to retrieve the UserInfo Resource from '"
149-
+ userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
133+
GrantedAuthority authority = new OAuth2UserAuthority(attrs, userNameAttributeName);
134+
Set<GrantedAuthority> authorities = new HashSet<>();
135+
authorities.add(authority);
136+
OAuth2AccessToken token = userRequest.getAccessToken();
137+
for (String scope : token.getScopes()) {
138+
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
139+
}
140+
141+
return new DefaultOAuth2User(authorities, attrs, userNameAttributeName);
142+
})
143+
.onErrorMap((ex) -> (ex instanceof UnsupportedMediaTypeException ||
144+
ex.getCause() instanceof UnsupportedMediaTypeException), (ex) -> {
145+
String contentType = (ex instanceof UnsupportedMediaTypeException) ?
146+
((UnsupportedMediaTypeException) ex).getContentType().toString() :
147+
((UnsupportedMediaTypeException) ex.getCause()).getContentType().toString();
148+
String errorMessage = "An error occurred while attempting to retrieve the UserInfo Resource from '"
149+
+ userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
150150
.getUri()
151-
+ "': response contains invalid content type '" + contentType + "'. "
152-
+ "The UserInfo Response should return a JSON object (content type 'application/json') "
153-
+ "that contains a collection of name and value pairs of the claims about the authenticated End-User. "
154-
+ "Please ensure the UserInfo Uri in UserInfoEndpoint for Client Registration '"
155-
+ userRequest.getClientRegistration().getRegistrationId()
156-
+ "' conforms to the UserInfo Endpoint, "
157-
+ "as defined in OpenID Connect 1.0: 'https://openid.net/specs/openid-connect-core-1_0.html#UserInfo'";
158-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorMessage,
159-
null);
160-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
161-
})
162-
.onErrorMap((ex) -> {
163-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
164-
"An error occurred reading the UserInfo response: " + ex.getMessage(), null);
165-
return new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
166-
});
151+
+ "': response contains invalid content type '" + contentType + "'. "
152+
+ "The UserInfo Response should return a JSON object (content type 'application/json') "
153+
+ "that contains a collection of name and value pairs of the claims about the authenticated End-User. "
154+
+ "Please ensure the UserInfo Uri in UserInfoEndpoint for Client Registration '"
155+
+ userRequest.getClientRegistration().getRegistrationId()
156+
+ "' conforms to the UserInfo Endpoint, "
157+
+ "as defined in OpenID Connect 1.0: 'https://openid.net/specs/openid-connect-core-1_0.html#UserInfo'";
158+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorMessage,
159+
null);
160+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
161+
})
162+
.onErrorMap((ex) -> {
163+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
164+
"An error occurred reading the UserInfo response: " + ex.getMessage(), null);
165+
return new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
166+
});
167167
});
168168
// @formatter:on
169169
}

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

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,13 @@ public void loadUserWhenUserNameAttributeNameIsNullThenThrowOAuth2Authentication
128128
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
129129
// @formatter:off
130130
String userInfoResponse = "{\n"
131-
+ " \"user-name\": \"user1\",\n"
132-
+ " \"first-name\": \"first\",\n"
133-
+ " \"last-name\": \"last\",\n"
134-
+ " \"middle-name\": \"middle\",\n"
135-
+ " \"address\": \"address\",\n"
136-
+ " \"email\": \"[email protected]\"\n"
137-
+ "}\n";
131+
+ " \"user-name\": \"user1\",\n"
132+
+ " \"first-name\": \"first\",\n"
133+
+ " \"last-name\": \"last\",\n"
134+
+ " \"middle-name\": \"middle\",\n"
135+
+ " \"address\": \"address\",\n"
136+
+ " \"email\": \"[email protected]\"\n"
137+
+ "}\n";
138138
// @formatter:on
139139
this.server.enqueue(jsonResponse(userInfoResponse));
140140
String userInfoUri = this.server.url("/user").toString();
@@ -204,12 +204,12 @@ public void loadUserWhenNestedUserInfoSuccessThenReturnUser() {
204204
public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() {
205205
// @formatter:off
206206
String userInfoResponse = "{\n"
207-
+ " \"user-name\": \"user1\",\n"
208-
+ " \"first-name\": \"first\",\n"
209-
+ " \"last-name\": \"last\",\n"
210-
+ " \"middle-name\": \"middle\",\n"
211-
+ " \"address\": \"address\",\n"
212-
+ " \"email\": \"[email protected]\"\n";
207+
+ " \"user-name\": \"user1\",\n"
208+
+ " \"first-name\": \"first\",\n"
209+
+ " \"last-name\": \"last\",\n"
210+
+ " \"middle-name\": \"middle\",\n"
211+
+ " \"address\": \"address\",\n"
212+
+ " \"email\": \"[email protected]\"\n";
213213
// "}\n"; // Make the JSON invalid/malformed
214214
// @formatter:on
215215
this.server.enqueue(jsonResponse(userInfoResponse));
@@ -295,13 +295,13 @@ public void loadUserWhenUserInfoUriInvalidThenThrowOAuth2AuthenticationException
295295
public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exception {
296296
// @formatter:off
297297
String userInfoResponse = "{\n"
298-
+ " \"user-name\": \"user1\",\n"
299-
+ " \"first-name\": \"first\",\n"
300-
+ " \"last-name\": \"last\",\n"
301-
+ " \"middle-name\": \"middle\",\n"
302-
+ " \"address\": \"address\",\n"
303-
+ " \"email\": \"[email protected]\"\n"
304-
+ "}\n";
298+
+ " \"user-name\": \"user1\",\n"
299+
+ " \"first-name\": \"first\",\n"
300+
+ " \"last-name\": \"last\",\n"
301+
+ " \"middle-name\": \"middle\",\n"
302+
+ " \"address\": \"address\",\n"
303+
+ " \"email\": \"[email protected]\"\n"
304+
+ "}\n";
305305
// @formatter:on
306306
this.server.enqueue(jsonResponse(userInfoResponse));
307307
String userInfoUri = this.server.url("/user").toString();
@@ -319,13 +319,13 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc
319319
public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception {
320320
// @formatter:off
321321
String userInfoResponse = "{\n"
322-
+ " \"user-name\": \"user1\",\n"
323-
+ " \"first-name\": \"first\",\n"
324-
+ " \"last-name\": \"last\",\n"
325-
+ " \"middle-name\": \"middle\",\n"
326-
+ " \"address\": \"address\",\n"
327-
+ " \"email\": \"[email protected]\"\n"
328-
+ "}\n";
322+
+ " \"user-name\": \"user1\",\n"
323+
+ " \"first-name\": \"first\",\n"
324+
+ " \"last-name\": \"last\",\n"
325+
+ " \"middle-name\": \"middle\",\n"
326+
+ " \"address\": \"address\",\n"
327+
+ " \"email\": \"[email protected]\"\n"
328+
+ "}\n";
329329
// @formatter:on
330330
this.server.enqueue(jsonResponse(userInfoResponse));
331331
String userInfoUri = this.server.url("/user").toString();
@@ -346,13 +346,13 @@ public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodG
346346
public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception {
347347
// @formatter:off
348348
String userInfoResponse = "{\n"
349-
+ " \"user-name\": \"user1\",\n"
350-
+ " \"first-name\": \"first\",\n"
351-
+ " \"last-name\": \"last\",\n"
352-
+ " \"middle-name\": \"middle\",\n"
353-
+ " \"address\": \"address\",\n"
354-
+ " \"email\": \"[email protected]\"\n"
355-
+ "}\n";
349+
+ " \"user-name\": \"user1\",\n"
350+
+ " \"first-name\": \"first\",\n"
351+
+ " \"last-name\": \"last\",\n"
352+
+ " \"middle-name\": \"middle\",\n"
353+
+ " \"address\": \"address\",\n"
354+
+ " \"email\": \"[email protected]\"\n"
355+
+ "}\n";
356356
// @formatter:on
357357
this.server.enqueue(jsonResponse(userInfoResponse));
358358
String userInfoUri = this.server.url("/user").toString();

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

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ public void loadUserWhenUserNameAttributeNameIsNullThenThrowOAuth2Authentication
121121
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
122122
// @formatter:off
123123
String userInfoResponse = "{\n"
124-
+ " \"id\": \"user1\",\n"
125-
+ " \"first-name\": \"first\",\n"
126-
+ " \"last-name\": \"last\",\n"
127-
+ " \"middle-name\": \"middle\",\n"
128-
+ " \"address\": \"address\",\n"
129-
+ " \"email\": \"[email protected]\"\n"
130-
+ "}\n";
124+
+ " \"id\": \"user1\",\n"
125+
+ " \"first-name\": \"first\",\n"
126+
+ " \"last-name\": \"last\",\n"
127+
+ " \"middle-name\": \"middle\",\n"
128+
+ " \"address\": \"address\",\n"
129+
+ " \"email\": \"[email protected]\"\n"
130+
+ "}\n";
131131
// @formatter:on
132132
enqueueApplicationJsonBody(userInfoResponse);
133133
OAuth2User user = this.userService.loadUser(oauth2UserRequest()).block();
@@ -213,13 +213,13 @@ public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodG
213213
this.clientRegistration.userInfoAuthenticationMethod(AuthenticationMethod.HEADER);
214214
// @formatter:off
215215
String userInfoResponse = "{\n"
216-
+ " \"id\": \"user1\",\n"
217-
+ " \"first-name\": \"first\",\n"
218-
+ " \"last-name\": \"last\",\n"
219-
+ " \"middle-name\": \"middle\",\n"
220-
+ " \"address\": \"address\",\n"
221-
+ " \"email\": \"[email protected]\"\n"
222-
+ "}\n";
216+
+ " \"id\": \"user1\",\n"
217+
+ " \"first-name\": \"first\",\n"
218+
+ " \"last-name\": \"last\",\n"
219+
+ " \"middle-name\": \"middle\",\n"
220+
+ " \"address\": \"address\",\n"
221+
+ " \"email\": \"[email protected]\"\n"
222+
+ "}\n";
223223
// @formatter:on
224224
enqueueApplicationJsonBody(userInfoResponse);
225225
this.userService.loadUser(oauth2UserRequest()).block();
@@ -236,13 +236,13 @@ public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPos
236236
this.clientRegistration.userInfoAuthenticationMethod(AuthenticationMethod.FORM);
237237
// @formatter:off
238238
String userInfoResponse = "{\n"
239-
+ " \"id\": \"user1\",\n"
240-
+ " \"first-name\": \"first\",\n"
241-
+ " \"last-name\": \"last\",\n"
242-
+ " \"middle-name\": \"middle\",\n"
243-
+ " \"address\": \"address\",\n"
244-
+ " \"email\": \"[email protected]\"\n"
245-
+ "}\n";
239+
+ " \"id\": \"user1\",\n"
240+
+ " \"first-name\": \"first\",\n"
241+
+ " \"last-name\": \"last\",\n"
242+
+ " \"middle-name\": \"middle\",\n"
243+
+ " \"address\": \"address\",\n"
244+
+ " \"email\": \"[email protected]\"\n"
245+
+ "}\n";
246246
// @formatter:on
247247
enqueueApplicationJsonBody(userInfoResponse);
248248
this.userService.loadUser(oauth2UserRequest()).block();
@@ -257,12 +257,12 @@ public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPos
257257
public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() {
258258
// @formatter:off
259259
String userInfoResponse = "{\n"
260-
+ " \"id\": \"user1\",\n"
261-
+ " \"first-name\": \"first\",\n"
262-
+ " \"last-name\": \"last\",\n"
263-
+ " \"middle-name\": \"middle\",\n"
264-
+ " \"address\": \"address\",\n"
265-
+ " \"email\": \"[email protected]\"\n";
260+
+ " \"id\": \"user1\",\n"
261+
+ " \"first-name\": \"first\",\n"
262+
+ " \"last-name\": \"last\",\n"
263+
+ " \"middle-name\": \"middle\",\n"
264+
+ " \"address\": \"address\",\n"
265+
+ " \"email\": \"[email protected]\"\n";
266266
// "}\n"; // Make the JSON invalid/malformed
267267
// @formatter:on
268268
enqueueApplicationJsonBody(userInfoResponse);

0 commit comments

Comments
 (0)