Add SpEL support for nested username extraction in OAuth2
- Add usernameExpression property with SpEL evaluation support - Auto-convert userNameAttributeName to SpEL for backward compatibility - Use SimpleEvaluationContext for secure expression evaluation - Pass evaluated username to OAuth2UserAuthority for spring-projectsgh-15012 compatibility - Add Builder pattern to DefaultOAuth2User - Add Builder pattern to OAuth2UserAuthority - Add Builder pattern to OidcUserAuthority with inherance support - Add Builder pattern to DefaultOidcUser with inherance support - Support nested property access (e.g., "data.username") - Add usernameExpression property to ClientRegistration documentation - Update What's New section Fixes gh-16390 Signed-off-by: yybmion <yunyubin54@gmail.com>
This commit is contained in:
parent
0dc9709018
commit
9c60779b29
|
@ -36,12 +36,13 @@ public final class ClientRegistration {
|
|||
private String uri; <14>
|
||||
private AuthenticationMethod authenticationMethod; <15>
|
||||
private String userNameAttributeName; <16>
|
||||
private String usernameExpression; <17>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ClientSettings {
|
||||
private boolean requireProofKey; // <17>
|
||||
private boolean requireProofKey; // <18>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
@ -67,8 +68,9 @@ The name may be used in certain scenarios, such as when displaying the name of t
|
|||
<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user.
|
||||
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
||||
The supported values are *header*, *form* and *query*.
|
||||
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
|
||||
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
|
||||
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. *Deprecated* - use `usernameExpression` instead.
|
||||
<17> `usernameExpression`: A SpEL expression used to extract the username from the UserInfo Response. Supports accessing nested attributes (e.g., `"data.username"`) and complex expressions (e.g., `"preferred_username ?: email"`).
|
||||
<18> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
|
||||
|
||||
A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint].
|
||||
|
||||
|
|
|
@ -31,18 +31,19 @@ public final class ClientRegistration {
|
|||
private UserInfoEndpoint userInfoEndpoint;
|
||||
private String jwkSetUri; <11>
|
||||
private String issuerUri; <12>
|
||||
private Map<String, Object> configurationMetadata; <13>
|
||||
private Map<String, Object> configurationMetadata; <13>
|
||||
|
||||
public class UserInfoEndpoint {
|
||||
private String uri; <14>
|
||||
private AuthenticationMethod authenticationMethod; <15>
|
||||
private AuthenticationMethod authenticationMethod; <15>
|
||||
private String userNameAttributeName; <16>
|
||||
private String usernameExpression; <17>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ClientSettings {
|
||||
private boolean requireProofKey; // <17>
|
||||
private boolean requireProofKey; // <18>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
@ -68,8 +69,9 @@ This information is available only if the Spring Boot property `spring.security.
|
|||
<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims and attributes of the authenticated end-user.
|
||||
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
||||
The supported values are *header*, *form*, and *query*.
|
||||
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
|
||||
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
|
||||
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. Deprecated - use usernameExpression instead.
|
||||
<17> `usernameExpression`: A SpEL expression used to extract the username from the UserInfo Response. Supports accessing nested attributes (e.g., "data.username") and complex expressions (e.g., "preferred_username ?: email").
|
||||
<18> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
|
||||
|
||||
You can initially configure a `ClientRegistration` by using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint].
|
||||
|
||||
|
|
|
@ -1,30 +1,17 @@
|
|||
[[new]]
|
||||
= What's New in Spring Security 6.5
|
||||
= What's New in Spring Security 7.0
|
||||
|
||||
Spring Security 6.5 provides a number of new features.
|
||||
Spring Security 7.0 provides a number of new features.
|
||||
Below are the highlights of the release, or you can view https://github.com/spring-projects/spring-security/releases[the release notes] for a detailed listing of each feature and bug fix.
|
||||
|
||||
== New Features
|
||||
== Web
|
||||
|
||||
* Support for automatic context-propagation with Micrometer (https://github.com/spring-projects/spring-security/issues/16665[gh-16665])
|
||||
* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
|
||||
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
|
||||
|
||||
== Breaking Changes
|
||||
== OAuth 2.0
|
||||
|
||||
=== Observability
|
||||
=== Username Expression Support for Nested Attributes - https://github.com/spring-projects/spring-security/pull/16390[gh-16390]
|
||||
|
||||
The `security.security.reached.filter.section` key name was corrected to `spring.security.reached.filter.section`.
|
||||
Note that this may affect reports that operate on this key name.
|
||||
OAuth2 Client now supports SpEL expressions for extracting usernames from nested UserInfo responses, eliminating the need for custom `OAuth2UserService` implementations in many cases. This is particularly useful for APIs like Twitter API v2 that return nested user data.
|
||||
|
||||
== OAuth
|
||||
|
||||
* https://github.com/spring-projects/spring-security/pull/16386[gh-16386] - Enable PKCE for confidential clients using `ClientRegistration.clientSettings.requireProofKey=true` for xref:servlet/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[servlet] and xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[reactive] applications
|
||||
|
||||
== WebAuthn
|
||||
|
||||
* https://github.com/spring-projects/spring-security/pull/16282[gh-16282] - xref:servlet/authentication/passkeys.adoc#passkeys-configuration-persistence[JDBC Persistence] for WebAuthn/Passkeys
|
||||
* https://github.com/spring-projects/spring-security/pull/16397[gh-16397] - Added the ability to configure a custom `HttpMessageConverter` for Passkeys using the optional xref:servlet/authentication/passkeys.adoc#passkeys-configuration[`messageConverter` property] on the `webAuthn` DSL.
|
||||
* https://github.com/spring-projects/spring-security/pull/16396[gh-16396] - Added the ability to configure a custom xref:servlet/authentication/passkeys.adoc#passkeys-configuration-pkccor[`PublicKeyCredentialCreationOptionsRepository`]
|
||||
|
||||
== One-Time Token Login
|
||||
|
||||
* https://github.com/spring-projects/spring-security/issues/16291[gh-16291] - `oneTimeTokenLogin()` now supports customizing GenerateOneTimeTokenRequest xref:servlet/authentication/onetimetoken.adoc#customize-generate-token-request[via GenerateOneTimeTokenRequestResolver]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.springframework.util.StringUtils;
|
|||
*
|
||||
* @author Joe Grandja
|
||||
* @author Michael Sosa
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2">Section 2
|
||||
* Client Registration</a>
|
||||
|
@ -299,8 +300,11 @@ public final class ClientRegistration implements Serializable {
|
|||
|
||||
private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;
|
||||
|
||||
@Deprecated
|
||||
private String userNameAttributeName;
|
||||
|
||||
private String usernameExpression;
|
||||
|
||||
UserInfoEndpoint() {
|
||||
}
|
||||
|
||||
|
@ -322,15 +326,23 @@ public final class ClientRegistration implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the attribute name used to access the user's name from the user
|
||||
* info response.
|
||||
* @return the attribute name used to access the user's name from the user
|
||||
* info response
|
||||
* @deprecated Use {@link #getUsernameExpression()} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public String getUserNameAttributeName() {
|
||||
return this.userNameAttributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SpEL expression used to extract the username from user info
|
||||
* response.
|
||||
* @return the SpEL expression for username extraction
|
||||
* @since 7.0
|
||||
*/
|
||||
public String getUsernameExpression() {
|
||||
return this.usernameExpression;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -370,8 +382,11 @@ public final class ClientRegistration implements Serializable {
|
|||
|
||||
private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
|
||||
|
||||
@Deprecated
|
||||
private String userNameAttributeName;
|
||||
|
||||
private String usernameExpression;
|
||||
|
||||
private String jwkSetUri;
|
||||
|
||||
private String issuerUri;
|
||||
|
@ -399,6 +414,7 @@ public final class ClientRegistration implements Serializable {
|
|||
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
|
||||
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
|
||||
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
|
||||
this.usernameExpression = clientRegistration.providerDetails.userInfoEndpoint.usernameExpression;
|
||||
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
|
||||
this.issuerUri = clientRegistration.providerDetails.issuerUri;
|
||||
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
|
||||
|
@ -552,14 +568,43 @@ public final class ClientRegistration implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the attribute name used to access the user's name from the user info
|
||||
* response.
|
||||
* @param userNameAttributeName the attribute name used to access the user's name
|
||||
* from the user info response
|
||||
* Sets the username attribute name. This method automatically converts the
|
||||
* attribute name to a SpEL expression for backward compatibility.
|
||||
*
|
||||
* <p>
|
||||
* This is a convenience method that internally calls
|
||||
* {@link #usernameExpression(String)} with the attribute name wrapped in bracket
|
||||
* notation.
|
||||
* @param userNameAttributeName the username attribute name
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder userNameAttributeName(String userNameAttributeName) {
|
||||
this.userNameAttributeName = userNameAttributeName;
|
||||
if (userNameAttributeName != null) {
|
||||
this.usernameExpression = "['" + userNameAttributeName + "']";
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SpEL expression used to extract the username from user info response.
|
||||
*
|
||||
* <p>
|
||||
* Examples:
|
||||
* <ul>
|
||||
* <li>Simple attribute: {@code "['username']"} or {@code "username"}</li>
|
||||
* <li>Nested attribute: {@code "data.username"}</li>
|
||||
* <li>Complex expression: {@code "user_info?.name ?: 'anonymous'"}</li>
|
||||
* <li>Array access: {@code "users[0].name"}</li>
|
||||
* <li>Conditional:
|
||||
* {@code "preferred_username != null ? preferred_username : email"}</li>
|
||||
* </ul>
|
||||
* @param usernameExpression the SpEL expression for username extraction
|
||||
* @return the {@link Builder}
|
||||
* @since 7.0
|
||||
*/
|
||||
public Builder usernameExpression(String usernameExpression) {
|
||||
this.usernameExpression = usernameExpression;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -672,7 +717,10 @@ public final class ClientRegistration implements Serializable {
|
|||
providerDetails.tokenUri = this.tokenUri;
|
||||
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
|
||||
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
|
||||
|
||||
providerDetails.userInfoEndpoint.usernameExpression = this.usernameExpression;
|
||||
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
|
||||
|
||||
providerDetails.jwkSetUri = this.jwkSetUri;
|
||||
providerDetails.issuerUri = this.issuerUri;
|
||||
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -20,8 +20,12 @@ import java.util.Collection;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.expression.MapAccessor;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
@ -47,16 +51,17 @@ import org.springframework.web.client.UnknownContentTypeException;
|
|||
* An implementation of an {@link OAuth2UserService} that supports standard OAuth 2.0
|
||||
* Provider's.
|
||||
* <p>
|
||||
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
|
||||
* from the UserInfo response is required and therefore must be available via
|
||||
* {@link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName()
|
||||
* UserInfoEndpoint.getUserNameAttributeName()}.
|
||||
* For standard OAuth 2.0 Provider's, the username expression used to extract the user's
|
||||
* name from the UserInfo response is required and therefore must be available via
|
||||
* {@link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUsernameExpression()
|
||||
* UserInfoEndpoint.getUsernameExpression()}.
|
||||
* <p>
|
||||
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and
|
||||
* therefore will vary. Please consult the provider's API documentation for the set of
|
||||
* supported user attribute names.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.0
|
||||
* @see OAuth2UserService
|
||||
* @see OAuth2UserRequest
|
||||
|
@ -71,6 +76,10 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
|
|||
|
||||
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
|
||||
|
||||
private static final String INVALID_USERNAME_EXPRESSION_ERROR_CODE = "invalid_username_expression";
|
||||
|
||||
private static final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<>() {
|
||||
};
|
||||
|
||||
|
@ -90,13 +99,67 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
|
|||
@Override
|
||||
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
Assert.notNull(userRequest, "userRequest cannot be null");
|
||||
String userNameAttributeName = getUserNameAttributeName(userRequest);
|
||||
String usernameExpression = getUsernameExpression(userRequest);
|
||||
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
|
||||
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
|
||||
OAuth2AccessToken token = userRequest.getAccessToken();
|
||||
Map<String, Object> attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
|
||||
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, userNameAttributeName);
|
||||
return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
|
||||
|
||||
String evaluatedUsername = evaluateUsername(attributes, usernameExpression);
|
||||
|
||||
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, evaluatedUsername);
|
||||
|
||||
return DefaultOAuth2User.withUsername(evaluatedUsername)
|
||||
.authorities(authorities)
|
||||
.attributes(attributes)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getUsernameExpression(OAuth2UserRequest userRequest) {
|
||||
if (!StringUtils
|
||||
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
|
||||
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
|
||||
+ userRequest.getClientRegistration().getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
String usernameExpression = userRequest.getClientRegistration()
|
||||
.getProviderDetails()
|
||||
.getUserInfoEndpoint()
|
||||
.getUsernameExpression();
|
||||
if (!StringUtils.hasText(usernameExpression)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
|
||||
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
|
||||
+ userRequest.getClientRegistration().getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
return usernameExpression;
|
||||
}
|
||||
|
||||
private String evaluateUsername(Map<String, Object> attributes, String usernameExpression) {
|
||||
Object value = null;
|
||||
|
||||
try {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
|
||||
.withRootObject(attributes)
|
||||
.build();
|
||||
value = expressionParser.parseExpression(usernameExpression).getValue(context);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USERNAME_EXPRESSION_ERROR_CODE,
|
||||
"Invalid username expression or SPEL expression: " + usernameExpression, null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
|
||||
"An error occurred while attempting to retrieve the UserInfo Resource: username cannot be null",
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,33 +227,11 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
|
|||
}
|
||||
}
|
||||
|
||||
private String getUserNameAttributeName(OAuth2UserRequest userRequest) {
|
||||
if (!StringUtils
|
||||
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
|
||||
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
|
||||
+ userRequest.getClientRegistration().getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
String userNameAttributeName = userRequest.getClientRegistration()
|
||||
.getProviderDetails()
|
||||
.getUserInfoEndpoint()
|
||||
.getUserNameAttributeName();
|
||||
if (!StringUtils.hasText(userNameAttributeName)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
|
||||
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
|
||||
+ userRequest.getClientRegistration().getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
return userNameAttributeName;
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> getAuthorities(OAuth2AccessToken token, Map<String, Object> attributes,
|
||||
String userNameAttributeName) {
|
||||
String username) {
|
||||
Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
|
||||
authorities.add(new OAuth2UserAuthority(attributes, userNameAttributeName));
|
||||
authorities.add(OAuth2UserAuthority.withUsername(username).attributes(attributes).build());
|
||||
|
||||
for (String authority : token.getScopes()) {
|
||||
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -25,8 +25,12 @@ import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
|||
import net.minidev.json.JSONObject;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.expression.MapAccessor;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -49,16 +53,17 @@ import org.springframework.web.reactive.function.client.WebClient;
|
|||
* An implementation of an {@link ReactiveOAuth2UserService} that supports standard OAuth
|
||||
* 2.0 Provider's.
|
||||
* <p>
|
||||
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
|
||||
* from the UserInfo response is required and therefore must be available via
|
||||
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName()
|
||||
* UserInfoEndpoint.getUserNameAttributeName()}.
|
||||
* For standard OAuth 2.0 Provider's, the username expression used to extract the user's
|
||||
* name from the UserInfo response is required and therefore must be available via
|
||||
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint#getUsernameExpression()
|
||||
* UserInfoEndpoint.getUsernameExpression()}.
|
||||
* <p>
|
||||
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and
|
||||
* therefore will vary. Please consult the provider's API documentation for the set of
|
||||
* supported user attribute names.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.1
|
||||
* @see ReactiveOAuth2UserService
|
||||
* @see OAuth2UserRequest
|
||||
|
@ -73,6 +78,10 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
|||
|
||||
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
|
||||
|
||||
private static final String INVALID_USERNAME_EXPRESSION_ERROR_CODE = "invalid_username_expression";
|
||||
|
||||
private static final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
|
||||
};
|
||||
|
||||
|
@ -99,17 +108,7 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
|||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
String userNameAttributeName = userRequest.getClientRegistration()
|
||||
.getProviderDetails()
|
||||
.getUserInfoEndpoint()
|
||||
.getUserNameAttributeName();
|
||||
if (!StringUtils.hasText(userNameAttributeName)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
|
||||
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
|
||||
+ userRequest.getClientRegistration().getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
String usernameExpression = getUsernameExpression(userRequest);
|
||||
AuthenticationMethod authenticationMethod = userRequest.getClientRegistration()
|
||||
.getProviderDetails()
|
||||
.getUserInfoEndpoint()
|
||||
|
@ -130,16 +129,21 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
|||
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP)
|
||||
.mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes));
|
||||
return userAttributes.map((attrs) -> {
|
||||
GrantedAuthority authority = new OAuth2UserAuthority(attrs, userNameAttributeName);
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorities.add(authority);
|
||||
OAuth2AccessToken token = userRequest.getAccessToken();
|
||||
for (String scope : token.getScopes()) {
|
||||
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
|
||||
}
|
||||
String username = evaluateUsername(attrs, usernameExpression);
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorities.add(OAuth2UserAuthority.withUsername(username)
|
||||
.attributes(attrs)
|
||||
.build());
|
||||
OAuth2AccessToken token = userRequest.getAccessToken();
|
||||
for (String scope : token.getScopes()) {
|
||||
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
|
||||
}
|
||||
|
||||
return new DefaultOAuth2User(authorities, attrs, userNameAttributeName);
|
||||
})
|
||||
return DefaultOAuth2User.withUsername(username)
|
||||
.authorities(authorities)
|
||||
.attributes(attrs)
|
||||
.build();
|
||||
})
|
||||
.onErrorMap((ex) -> (ex instanceof UnsupportedMediaTypeException ||
|
||||
ex.getCause() instanceof UnsupportedMediaTypeException), (ex) -> {
|
||||
String contentType = (ex instanceof UnsupportedMediaTypeException) ?
|
||||
|
@ -168,6 +172,47 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
|||
// @formatter:on
|
||||
}
|
||||
|
||||
private String getUsernameExpression(OAuth2UserRequest userRequest) {
|
||||
String usernameExpression = userRequest.getClientRegistration()
|
||||
.getProviderDetails()
|
||||
.getUserInfoEndpoint()
|
||||
.getUsernameExpression();
|
||||
if (!StringUtils.hasText(usernameExpression)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
|
||||
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
|
||||
+ userRequest.getClientRegistration().getRegistrationId(),
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
return usernameExpression;
|
||||
}
|
||||
|
||||
private String evaluateUsername(Map<String, Object> attributes, String usernameExpression) {
|
||||
Object value = null;
|
||||
|
||||
try {
|
||||
SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
|
||||
.withRootObject(attributes)
|
||||
.build();
|
||||
value = expressionParser.parseExpression(usernameExpression).getValue(context);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USERNAME_EXPRESSION_ERROR_CODE,
|
||||
"Invalid username expression or SPEL expression: " + usernameExpression, null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
|
||||
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
|
||||
"An error occurred while attempting to retrieve the UserInfo Resource: username cannot be null",
|
||||
null);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
private WebClient.RequestHeadersSpec<?> getRequestHeaderSpec(OAuth2UserRequest userRequest, String userInfoUri,
|
||||
AuthenticationMethod authenticationMethod) {
|
||||
if (AuthenticationMethod.FORM.equals(authenticationMethod)) {
|
||||
|
|
|
@ -248,11 +248,12 @@ public class OAuth2AuthenticationTokenMixinTests {
|
|||
return "{\n" +
|
||||
" \"@class\": \"org.springframework.security.oauth2.core.user.OAuth2UserAuthority\",\n" +
|
||||
" \"authority\": \"" + oauth2UserAuthority.getAuthority() + "\",\n" +
|
||||
" \"userNameAttributeName\": \"username\",\n" +
|
||||
" \"attributes\": {\n" +
|
||||
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
|
||||
" \"username\": \"user\"\n" +
|
||||
" }\n" +
|
||||
" },\n" +
|
||||
" \"userNameAttributeName\": \"username\",\n" +
|
||||
" \"username\": \"user\"\n" +
|
||||
" }";
|
||||
// @formatter:on
|
||||
}
|
||||
|
@ -262,9 +263,10 @@ public class OAuth2AuthenticationTokenMixinTests {
|
|||
return "{\n" +
|
||||
" \"@class\": \"org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority\",\n" +
|
||||
" \"authority\": \"" + oidcUserAuthority.getAuthority() + "\",\n" +
|
||||
" \"userNameAttributeName\": \"" + oidcUserAuthority.getUserNameAttributeName() + "\",\n" +
|
||||
" \"idToken\": " + asJson(oidcUserAuthority.getIdToken()) + ",\n" +
|
||||
" \"userInfo\": " + asJson(oidcUserAuthority.getUserInfo()) + "\n" +
|
||||
" \"userInfo\": " + asJson(oidcUserAuthority.getUserInfo()) + ",\n" +
|
||||
" \"userNameAttributeName\": \"" + oidcUserAuthority.getUserNameAttributeName() + "\",\n" +
|
||||
" \"username\": \"subject\"\n" +
|
||||
" }";
|
||||
// @formatter:on
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -145,6 +145,8 @@ public class OAuth2AuthorizedClientMixinTests {
|
|||
.isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod());
|
||||
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo(
|
||||
expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
|
||||
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression())
|
||||
.isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression());
|
||||
assertThat(clientRegistration.getProviderDetails().getJwkSetUri())
|
||||
.isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri());
|
||||
assertThat(clientRegistration.getProviderDetails().getIssuerUri())
|
||||
|
@ -306,6 +308,8 @@ public class OAuth2AuthorizedClientMixinTests {
|
|||
.map((key) -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"")
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
String usernameExpression = (userInfoEndpoint.getUsernameExpression() != null)
|
||||
? "\"" + userInfoEndpoint.getUsernameExpression() + "\"" : null;
|
||||
// @formatter:off
|
||||
return "{\n" +
|
||||
" \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" +
|
||||
|
@ -333,7 +337,8 @@ public class OAuth2AuthorizedClientMixinTests {
|
|||
" \"authenticationMethod\": {\n" +
|
||||
" \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" +
|
||||
" },\n" +
|
||||
" \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" +
|
||||
" \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + ",\n" +
|
||||
" \"usernameExpression\": " + usernameExpression + "\n" +
|
||||
" },\n" +
|
||||
" \"jwkSetUri\": " + ((providerDetails.getJwkSetUri() != null) ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" +
|
||||
" \"issuerUri\": " + ((providerDetails.getIssuerUri() != null) ? "\"" + providerDetails.getIssuerUri() + "\"" : null) + ",\n" +
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -43,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
|||
* Tests for {@link ClientRegistration}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
*/
|
||||
public class ClientRegistrationTests {
|
||||
|
||||
|
@ -716,6 +717,7 @@ public class ClientRegistrationTests {
|
|||
.isEqualTo(updatedUserInfoEndpoint.getAuthenticationMethod());
|
||||
assertThat(userInfoEndpoint.getUserNameAttributeName())
|
||||
.isEqualTo(updatedUserInfoEndpoint.getUserNameAttributeName());
|
||||
assertThat(userInfoEndpoint.getUsernameExpression()).isEqualTo(updatedUserInfoEndpoint.getUsernameExpression());
|
||||
assertThat(providerDetails.getJwkSetUri()).isEqualTo(updatedProviderDetails.getJwkSetUri());
|
||||
assertThat(providerDetails.getIssuerUri()).isEqualTo(updatedProviderDetails.getIssuerUri());
|
||||
assertThat(providerDetails.getConfigurationMetadata())
|
||||
|
@ -802,6 +804,84 @@ public class ClientRegistrationTests {
|
|||
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenUsernameExpressionProvidedThenSet() {
|
||||
String usernameExpression = "data.username";
|
||||
// @formatter:off
|
||||
ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.clientId(CLIENT_ID)
|
||||
.clientSecret(CLIENT_SECRET)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.usernameExpression(usernameExpression)
|
||||
.build();
|
||||
// @formatter:on
|
||||
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression())
|
||||
.isEqualTo(usernameExpression);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenBothUserNameAttributeNameAndUsernameExpressionProvidedThenUsernameExpressionTakesPrecedence() {
|
||||
String userNameAttributeName = "username";
|
||||
String usernameExpression = "data.username";
|
||||
// @formatter:off
|
||||
ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.clientId(CLIENT_ID)
|
||||
.clientSecret(CLIENT_SECRET)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.userNameAttributeName(userNameAttributeName)
|
||||
.usernameExpression(usernameExpression)
|
||||
.build();
|
||||
// @formatter:on
|
||||
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression())
|
||||
.isEqualTo(usernameExpression);
|
||||
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
|
||||
.isEqualTo(userNameAttributeName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenOnlyUserNameAttributeNameProvidedThenAutoConvertToSpelExpression() {
|
||||
String userNameAttributeName = "username";
|
||||
// @formatter:off
|
||||
ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.clientId(CLIENT_ID)
|
||||
.clientSecret(CLIENT_SECRET)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.userNameAttributeName(userNameAttributeName)
|
||||
.build();
|
||||
// @formatter:on
|
||||
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression())
|
||||
.isEqualTo("['" + userNameAttributeName + "']");
|
||||
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
|
||||
.isEqualTo(userNameAttributeName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenCopyingClientRegistrationWithUsernameExpressionThenPreserved() {
|
||||
String usernameExpression = "profile.name";
|
||||
// @formatter:off
|
||||
ClientRegistration original = ClientRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.clientId(CLIENT_ID)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.usernameExpression(usernameExpression)
|
||||
.build();
|
||||
// @formatter:on
|
||||
ClientRegistration copy = ClientRegistration.withClientRegistration(original).build();
|
||||
assertThat(copy.getProviderDetails().getUserInfoEndpoint().getUsernameExpression())
|
||||
.isEqualTo(usernameExpression);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidPkceGrantTypes")
|
||||
void buildWhenInvalidGrantTypeForPkceThenException(AuthorizationGrantType invalidGrantType) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -61,6 +61,7 @@ import static org.mockito.Mockito.mock;
|
|||
*
|
||||
* @author Joe Grandja
|
||||
* @author Eddú Meléndez
|
||||
* @author Yoobin Yoon
|
||||
*/
|
||||
public class DefaultOAuth2UserServiceTests {
|
||||
|
||||
|
@ -121,7 +122,7 @@ public class DefaultOAuth2UserServiceTests {
|
|||
// @formatter:on
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
||||
.withMessageContaining("missing_user_name_attribute");
|
||||
.withMessageContaining("invalid_user_info_response");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -153,23 +154,26 @@ public class DefaultOAuth2UserServiceTests {
|
|||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
||||
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
|
||||
OAuth2UserAuthority userAuthority = OAuth2UserAuthority.withUsername("user1")
|
||||
.attributes(user.getAttributes())
|
||||
.authority("OAUTH2_USER")
|
||||
.build();
|
||||
assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name");
|
||||
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"user\": {\"user-name\": \"user1\"},\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
+ " \"user\": {\"user-name\": \"user1\"},\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
|
@ -194,10 +198,13 @@ public class DefaultOAuth2UserServiceTests {
|
|||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
||||
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
|
||||
OAuth2UserAuthority userAuthority = OAuth2UserAuthority.withUsername("user1")
|
||||
.attributes(user.getAttributes())
|
||||
.authority("OAUTH2_USER")
|
||||
.build();
|
||||
assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name");
|
||||
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -247,8 +254,8 @@ public class DefaultOAuth2UserServiceTests {
|
|||
public void loadUserWhenUserInfoErrorResponseThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
String userInfoErrorResponse = "{\n"
|
||||
+ " \"error\": \"invalid_token\"\n"
|
||||
+ "}\n";
|
||||
+ " \"error\": \"invalid_token\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoErrorResponse).setResponseCode(400));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
|
@ -421,6 +428,134 @@ public class DefaultOAuth2UserServiceTests {
|
|||
.isThrownBy(() -> this.userService.setAttributesConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenBackwardCompatibilityWithUserNameAttributeNameThenWorks() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"user-name\": \"backwardCompatUser\",\n"
|
||||
+ " \"email\": \"backward@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
|
||||
.userNameAttributeName("user-name")
|
||||
.build();
|
||||
OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken));
|
||||
assertThat(user.getName()).isEqualTo("backwardCompatUser");
|
||||
assertThat(user.getAttributes()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionIsSimpleAttributeThenUseDirectly() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"username\": \"simpleUser\",\n"
|
||||
+ " \"id\": \"54321\",\n"
|
||||
+ " \"email\": \"simple@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
|
||||
.usernameExpression("username")
|
||||
.build();
|
||||
OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken));
|
||||
assertThat(user.getName()).isEqualTo("simpleUser");
|
||||
assertThat(user.getAttributes()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionIsSpelThenEvaluateCorrectly() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"data\": {\n"
|
||||
+ " \"user\": {\n"
|
||||
+ " \"username\": \"spelUser\"\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"id\": \"12345\",\n"
|
||||
+ " \"email\": \"spel@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
|
||||
.usernameExpression("data.user.username")
|
||||
.build();
|
||||
OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken));
|
||||
assertThat(user.getName()).isEqualTo("spelUser");
|
||||
assertThat(user.getAttributes()).hasSize(3);
|
||||
assertThat((String) user.getAttribute("id")).isEqualTo("12345");
|
||||
assertThat((String) user.getAttribute("email")).isEqualTo("spel@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionInvalidSpelThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"username\": \"testUser\",\n"
|
||||
+ " \"id\": \"12345\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
|
||||
.usernameExpression("nonexistent.invalid.path") // invalid SpEL
|
||||
.build();
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
||||
.withMessageContaining("invalid_username_expression")
|
||||
.withMessageContaining("Invalid username expression or SPEL expression");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionResultsInNullThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"username\": \"testUser\",\n"
|
||||
+ " \"data\": {\n"
|
||||
+ " \"username\": null\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
|
||||
.usernameExpression("data.username")
|
||||
.build();
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||
.isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
||||
.withMessageContaining("invalid_user_info_response")
|
||||
.withMessageContaining("username cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionWithArrayAccessThenEvaluateCorrectly() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"accounts\": [\n"
|
||||
+ " {\"username\": \"primary_user\", \"type\": \"primary\"},\n"
|
||||
+ " {\"username\": \"secondary_user\", \"type\": \"secondary\"}\n"
|
||||
+ " ],\n"
|
||||
+ " \"id\": \"12345\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
|
||||
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
|
||||
.usernameExpression("accounts[0].username")
|
||||
.build();
|
||||
OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken));
|
||||
assertThat(user.getName()).isEqualTo("primary_user");
|
||||
}
|
||||
|
||||
private DefaultOAuth2UserService withMockResponse(Map<String, Object> response) {
|
||||
ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(response, HttpStatus.OK);
|
||||
Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -60,6 +60,7 @@ import static org.mockito.Mockito.spy;
|
|||
/**
|
||||
* @author Rob Winch
|
||||
* @author Eddú Meléndez
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.1
|
||||
*/
|
||||
public class DefaultReactiveOAuth2UserServiceTests {
|
||||
|
@ -104,19 +105,6 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUserNameAttributeNameIsNullThenThrowOAuth2AuthenticationException() {
|
||||
this.clientRegistration.userNameAttributeName(null);
|
||||
// @formatter:off
|
||||
StepVerifier.create(this.userService.loadUser(oauth2UserRequest()))
|
||||
.expectErrorSatisfies((ex) -> assertThat(ex)
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("missing_user_name_attribute")
|
||||
)
|
||||
.verify();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
|
||||
// @formatter:off
|
||||
|
@ -141,10 +129,13 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
||||
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
|
||||
OAuth2UserAuthority userAuthority = OAuth2UserAuthority.withUsername("user1")
|
||||
.attributes(user.getAttributes())
|
||||
.authority("OAUTH2_USER")
|
||||
.build();
|
||||
assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("id");
|
||||
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||
}
|
||||
|
||||
// gh-9336
|
||||
|
@ -152,13 +143,13 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
public void loadUserWhenUserInfo201CreatedResponseThenReturnUser() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"id\": \"user1\",\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
+ " \"id\": \"user1\",\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
this.server.enqueue(new MockResponse().setResponseCode(201)
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
|
@ -170,13 +161,13 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"user\": {\"user-name\": \"user1\"},\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
+ " \"user\": {\"user-name\": \"user1\"},\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
+ " \"address\": \"address\",\n"
|
||||
+ " \"email\": \"user1@example.com\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
enqueueApplicationJsonBody(userInfoResponse);
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
|
@ -201,10 +192,13 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
||||
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
|
||||
OAuth2UserAuthority userAuthority = OAuth2UserAuthority.withUsername("user1")
|
||||
.attributes(user.getAttributes())
|
||||
.authority("OAUTH2_USER")
|
||||
.build();
|
||||
assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name");
|
||||
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||
}
|
||||
|
||||
// gh-5500
|
||||
|
@ -257,7 +251,7 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"id\": \"user1\",\n"
|
||||
+ " \"id\": \"user1\",\n"
|
||||
+ " \"first-name\": \"first\",\n"
|
||||
+ " \"last-name\": \"last\",\n"
|
||||
+ " \"middle-name\": \"middle\",\n"
|
||||
|
@ -338,6 +332,66 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
|||
.isThrownBy(() -> this.userService.setAttributesConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionIsSpelThenEvaluateCorrectly() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"data\": {\n"
|
||||
+ " \"profile\": {\n"
|
||||
+ " \"name\": \"reactiveSpelUser\"\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"id\": \"reactive123\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
enqueueApplicationJsonBody(userInfoResponse);
|
||||
ClientRegistration clientRegistration = this.clientRegistration.usernameExpression("data.profile.name").build();
|
||||
OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))
|
||||
.block();
|
||||
assertThat(user.getName()).isEqualTo("reactiveSpelUser");
|
||||
assertThat(user.getAttributes()).hasSize(2);
|
||||
assertThat((String) user.getAttribute("id")).isEqualTo("reactive123");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionInvalidSpelThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"id\": \"reactive123\",\n"
|
||||
+ " \"username\": \"reactiveUser\"\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
enqueueApplicationJsonBody(userInfoResponse);
|
||||
ClientRegistration clientRegistration = this.clientRegistration.usernameExpression("invalid.spel.expression")
|
||||
.build();
|
||||
StepVerifier.create(this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
||||
.expectErrorSatisfies((ex) -> {
|
||||
assertThat(ex).isInstanceOf(OAuth2AuthenticationException.class);
|
||||
assertThat(ex.getMessage()).contains("Invalid username expression or SPEL expression");
|
||||
})
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUsernameExpressionResultsInNullThenThrowException() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n"
|
||||
+ " \"username\": \"testUser\",\n"
|
||||
+ " \"data\": {\n"
|
||||
+ " \"username\": null\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
enqueueApplicationJsonBody(userInfoResponse);
|
||||
ClientRegistration clientRegistration = this.clientRegistration.usernameExpression("data.username").build();
|
||||
StepVerifier.create(this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
||||
.expectErrorSatisfies((ex) -> {
|
||||
assertThat(ex).isInstanceOf(OAuth2AuthenticationException.class);
|
||||
assertThat(ex.getMessage()).contains("username cannot be null");
|
||||
})
|
||||
.verify();
|
||||
}
|
||||
|
||||
private DefaultReactiveOAuth2UserService withMockResponse(Map<String, Object> body) {
|
||||
WebClient real = WebClient.builder().build();
|
||||
WebClient.RequestHeadersUriSpec spec = spy(real.post());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -25,6 +25,7 @@ import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
|||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The default implementation of an {@link OidcUser}.
|
||||
|
@ -35,6 +36,7 @@ import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
|||
*
|
||||
* @author Joe Grandja
|
||||
* @author Vedran Pavic
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.0
|
||||
* @see OidcUser
|
||||
* @see DefaultOAuth2User
|
||||
|
@ -54,7 +56,9 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
* Constructs a {@code DefaultOidcUser} using the provided parameters.
|
||||
* @param authorities the authorities granted to the user
|
||||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken) {
|
||||
this(authorities, idToken, IdTokenClaimNames.SUB);
|
||||
}
|
||||
|
@ -65,7 +69,9 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @param nameAttributeKey the key used to access the user's "name" from
|
||||
* {@link #getAttributes()}
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
|
||||
String nameAttributeKey) {
|
||||
this(authorities, idToken, null, nameAttributeKey);
|
||||
|
@ -77,7 +83,9 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||
* may be {@code null}
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
|
||||
OidcUserInfo userInfo) {
|
||||
this(authorities, idToken, userInfo, IdTokenClaimNames.SUB);
|
||||
|
@ -91,7 +99,9 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
* may be {@code null}
|
||||
* @param nameAttributeKey the key used to access the user's "name" from
|
||||
* {@link #getAttributes()}
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
|
||||
OidcUserInfo userInfo, String nameAttributeKey) {
|
||||
super(authorities, OidcUserAuthority.collectClaims(idToken, userInfo), nameAttributeKey);
|
||||
|
@ -99,6 +109,34 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
this.userInfo = userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code DefaultOidcUser} using the provided parameters.
|
||||
* @param authorities the authorities granted to the user
|
||||
* @param attributes the attributes about the user
|
||||
* @param nameAttributeKey the key used to access the user's "name" from
|
||||
* {@link #getAttributes()} - preserved for backwards compatibility
|
||||
* @param username the user's name
|
||||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||
* may be {@code null}
|
||||
*/
|
||||
private DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
|
||||
String nameAttributeKey, String username, OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||
super(authorities, attributes, nameAttributeKey, username);
|
||||
this.idToken = idToken;
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code DefaultOidcUser} builder with the username.
|
||||
* @param username the user's name
|
||||
* @return a new {@code Builder}
|
||||
* @since 7.0
|
||||
*/
|
||||
public static Builder withUsername(String username) {
|
||||
return new Builder(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.getAttributes();
|
||||
|
@ -114,4 +152,56 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
|
|||
return this.userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link DefaultOidcUser}.
|
||||
*
|
||||
* @since 7.0
|
||||
*/
|
||||
public static final class Builder extends DefaultOAuth2User.Builder {
|
||||
|
||||
private OidcIdToken idToken;
|
||||
|
||||
private OidcUserInfo userInfo;
|
||||
|
||||
private Builder(String username) {
|
||||
super(username);
|
||||
}
|
||||
|
||||
public Builder idToken(OidcIdToken idToken) {
|
||||
this.idToken = idToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder userInfo(OidcUserInfo userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder authorities(Collection<? extends GrantedAuthority> authorities) {
|
||||
super.authorities(authorities);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder attributes(Map<String, Object> attributes) {
|
||||
super.attributes(attributes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultOidcUser build() {
|
||||
Assert.notNull(this.idToken, "idToken cannot be null");
|
||||
|
||||
if (this.attributes == null) {
|
||||
this.attributes = OidcUserAuthority.collectClaims(this.idToken, this.userInfo);
|
||||
}
|
||||
|
||||
Assert.notEmpty(this.attributes, "attributes cannot be empty");
|
||||
return new DefaultOidcUser(this.authorities, this.attributes, null, this.username, this.idToken,
|
||||
this.userInfo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -32,6 +32,7 @@ import org.springframework.util.Assert;
|
|||
* A {@link GrantedAuthority} that may be associated to an {@link OidcUser}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.0
|
||||
* @see OidcUser
|
||||
*/
|
||||
|
@ -47,7 +48,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
/**
|
||||
* Constructs a {@code OidcUserAuthority} using the provided parameters.
|
||||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OidcUserAuthority(OidcIdToken idToken) {
|
||||
this(idToken, null);
|
||||
}
|
||||
|
@ -58,7 +61,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||
* may be {@code null}
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||
this("OIDC_USER", idToken, userInfo);
|
||||
}
|
||||
|
@ -72,7 +77,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||
* the attributes
|
||||
* @since 6.4
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo, @Nullable String userNameAttributeName) {
|
||||
this("OIDC_USER", idToken, userInfo, userNameAttributeName);
|
||||
}
|
||||
|
@ -83,7 +90,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||
* may be {@code null}
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||
this(authority, idToken, userInfo, IdTokenClaimNames.SUB);
|
||||
}
|
||||
|
@ -97,7 +106,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||
* the attributes
|
||||
* @since 6.4
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo,
|
||||
@Nullable String userNameAttributeName) {
|
||||
super(authority, collectClaims(idToken, userInfo), userNameAttributeName);
|
||||
|
@ -105,6 +116,33 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
this.userInfo = userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code OidcUserAuthority} using the provided parameters. This
|
||||
* constructor is used by the Builder pattern.
|
||||
* @param username the username
|
||||
* @param authority the authority granted to the user
|
||||
* @param attributes the attributes about the user
|
||||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||
* may be {@code null}
|
||||
*/
|
||||
private OidcUserAuthority(String username, String authority, Map<String, Object> attributes, OidcIdToken idToken,
|
||||
OidcUserInfo userInfo) {
|
||||
super(username, authority, attributes);
|
||||
this.idToken = idToken;
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code OidcUserAuthority} builder with the username.
|
||||
* @param username the username
|
||||
* @return a new {@code Builder}
|
||||
* @since 7.0
|
||||
*/
|
||||
public static Builder withUsername(String username) {
|
||||
return new Builder(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OidcIdToken ID Token} containing claims about the user.
|
||||
* @return the {@link OidcIdToken} containing claims about the user.
|
||||
|
@ -159,4 +197,66 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
|||
return claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OidcUserAuthority}.
|
||||
*
|
||||
* @since 7.0
|
||||
*/
|
||||
public static final class Builder extends OAuth2UserAuthority.Builder {
|
||||
|
||||
private OidcIdToken idToken;
|
||||
|
||||
private OidcUserInfo userInfo;
|
||||
|
||||
private Builder(String username) {
|
||||
super(username);
|
||||
this.authority = "OIDC_USER";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OidcIdToken ID Token} containing claims about the user.
|
||||
* @param idToken the {@link OidcIdToken ID Token}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder idToken(OidcIdToken idToken) {
|
||||
this.idToken = idToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OidcUserInfo UserInfo} containing claims about the user.
|
||||
* @param userInfo the {@link OidcUserInfo UserInfo}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder userInfo(OidcUserInfo userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder authority(String authority) {
|
||||
super.authority(authority);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder attributes(Map<String, Object> attributes) {
|
||||
super.attributes(attributes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcUserAuthority build() {
|
||||
Assert.notNull(this.idToken, "idToken cannot be null");
|
||||
|
||||
if (this.attributes == null) {
|
||||
this.attributes = collectClaims(this.idToken, this.userInfo);
|
||||
}
|
||||
|
||||
Assert.notEmpty(this.attributes, "attributes cannot be empty");
|
||||
return new OidcUserAuthority(this.username, this.authority, this.attributes, this.idToken, this.userInfo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,16 +36,18 @@ import org.springframework.util.Assert;
|
|||
* The default implementation of an {@link OAuth2User}.
|
||||
*
|
||||
* <p>
|
||||
* User attribute names are <b>not</b> standardized between providers and therefore it is
|
||||
* required to supply the <i>key</i> for the user's "name" attribute to one of
|
||||
* the constructors. The <i>key</i> will be used for accessing the "name" of the
|
||||
* {@code Principal} (user) via {@link #getAttributes()} and returning it from
|
||||
* {@link #getName()}.
|
||||
* User attribute names are <b>not</b> standardized between providers. The recommended
|
||||
* approach is to use {@link #withUsername(String)} builder pattern to directly specify
|
||||
* the username, eliminating the need to determine attribute keys. Alternatively, when
|
||||
* using the deprecated constructors, it is required to supply the <i>key</i> for the
|
||||
* user's "name" attribute, which will be used for accessing the
|
||||
* "name" of the {@code Principal} (user) via {@link #getAttributes()} and
|
||||
* returning it from {@link #getName()}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Eddú Meléndez
|
||||
* @author Park Hyojong
|
||||
* @author YooBin Yoon
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.0
|
||||
* @see OAuth2User
|
||||
*/
|
||||
|
@ -57,6 +59,7 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
|||
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
@Deprecated
|
||||
private final String nameAttributeKey;
|
||||
|
||||
private final String username;
|
||||
|
@ -86,15 +89,14 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code DefaultOAuth2User} using the provided parameters. This
|
||||
* constructor is used by Jackson for deserialization.
|
||||
* Constructs a {@code DefaultOAuth2User} using the provided parameters.
|
||||
* @param authorities the authorities granted to the user
|
||||
* @param attributes the attributes about the user
|
||||
* @param nameAttributeKey the key used to access the user's "name" from
|
||||
* {@link #getAttributes()} - preserved for backwards compatibility
|
||||
* @param username the user's name
|
||||
*/
|
||||
private DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
|
||||
protected DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
|
||||
String nameAttributeKey, String username) {
|
||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||
|
||||
|
@ -103,7 +105,7 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
|||
: Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES));
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
this.nameAttributeKey = nameAttributeKey;
|
||||
this.username = (username != null) ? username : attributes.get(nameAttributeKey).toString();
|
||||
this.username = username;
|
||||
|
||||
Assert.hasText(this.username, "username cannot be empty");
|
||||
}
|
||||
|
@ -112,47 +114,12 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
|||
* Creates a new {@code DefaultOAuth2User} builder with the username.
|
||||
* @param username the user's name
|
||||
* @return a new {@code Builder}
|
||||
* @since 6.5
|
||||
* @since 7.0
|
||||
*/
|
||||
public static Builder withUsername(String username) {
|
||||
return new Builder(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link DefaultOAuth2User}.
|
||||
*
|
||||
* @since 6.5
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final String username;
|
||||
|
||||
private Collection<? extends GrantedAuthority> authorities;
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
private Builder(String username) {
|
||||
Assert.hasText(username, "username cannot be empty");
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Builder authorities(Collection<? extends GrantedAuthority> authorities) {
|
||||
this.authorities = authorities;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder attributes(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOAuth2User build() {
|
||||
Assert.notEmpty(this.attributes, "attributes cannot be empty");
|
||||
return new DefaultOAuth2User(this.authorities, this.attributes, null, this.username);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.username;
|
||||
|
@ -214,4 +181,39 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link DefaultOAuth2User}.
|
||||
*
|
||||
* @since 7.0
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
protected final String username;
|
||||
|
||||
protected Collection<? extends GrantedAuthority> authorities;
|
||||
|
||||
protected Map<String, Object> attributes;
|
||||
|
||||
protected Builder(String username) {
|
||||
Assert.hasText(username, "username cannot be empty");
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Builder authorities(Collection<? extends GrantedAuthority> authorities) {
|
||||
this.authorities = authorities;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder attributes(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultOAuth2User build() {
|
||||
Assert.notEmpty(this.attributes, "attributes cannot be empty");
|
||||
return new DefaultOAuth2User(this.authorities, this.attributes, null, this.username);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -31,6 +31,7 @@ import org.springframework.util.Assert;
|
|||
* A {@link GrantedAuthority} that may be associated to an {@link OAuth2User}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
* @since 5.0
|
||||
* @see OAuth2User
|
||||
*/
|
||||
|
@ -42,13 +43,18 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
@Deprecated
|
||||
private final String userNameAttributeName;
|
||||
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults
|
||||
* {@link #getAuthority()} to {@code OAUTH2_USER}.
|
||||
* @param attributes the attributes about the user
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2UserAuthority(Map<String, Object> attributes) {
|
||||
this("OAUTH2_USER", attributes);
|
||||
}
|
||||
|
@ -60,7 +66,9 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||
* the attributes
|
||||
* @since 6.4
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2UserAuthority(Map<String, Object> attributes, @Nullable String userNameAttributeName) {
|
||||
this("OAUTH2_USER", attributes, userNameAttributeName);
|
||||
}
|
||||
|
@ -69,7 +77,9 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
* Constructs a {@code OAuth2UserAuthority} using the provided parameters.
|
||||
* @param authority the authority granted to the user
|
||||
* @param attributes the attributes about the user
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2UserAuthority(String authority, Map<String, Object> attributes) {
|
||||
this(authority, attributes, null);
|
||||
}
|
||||
|
@ -81,13 +91,43 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||
* the attributes
|
||||
* @since 6.4
|
||||
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OAuth2UserAuthority(String authority, Map<String, Object> attributes, String userNameAttributeName) {
|
||||
Assert.hasText(authority, "authority cannot be empty");
|
||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||
this.authority = authority;
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
this.userNameAttributeName = userNameAttributeName;
|
||||
this.username = (userNameAttributeName != null && attributes.get(userNameAttributeName) != null)
|
||||
? attributes.get(userNameAttributeName).toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code OAuth2UserAuthority} using the provided parameters.
|
||||
* @param username the username
|
||||
* @param authority the authority granted to the user
|
||||
* @param attributes the attributes about the user
|
||||
*/
|
||||
protected OAuth2UserAuthority(String username, String authority, Map<String, Object> attributes) {
|
||||
Assert.hasText(username, "username cannot be empty");
|
||||
Assert.hasText(authority, "authority cannot be empty");
|
||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||
this.username = username;
|
||||
this.authority = authority;
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
this.userNameAttributeName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code OAuth2UserAuthority} builder with the username.
|
||||
* @param username the username
|
||||
* @return a new {@code Builder}
|
||||
* @since 7.0
|
||||
*/
|
||||
public static Builder withUsername(String username) {
|
||||
return new Builder(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -107,12 +147,26 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
* Returns the attribute name used to access the user's name from the attributes.
|
||||
* @return the attribute name used to access the user's name from the attributes
|
||||
* @since 6.4
|
||||
* @deprecated Use {@link #getUsername()} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public String getUserNameAttributeName() {
|
||||
return this.userNameAttributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username of the OAuth2 user.
|
||||
* <p>
|
||||
* This method provides direct access to the username without requiring knowledge of
|
||||
* the attribute structure or SpEL expressions used to extract it.
|
||||
* @return the username
|
||||
* @since 7.0
|
||||
*/
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
|
@ -125,6 +179,9 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
if (!this.getAuthority().equals(that.getAuthority())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(this.username, that.username)) {
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> thatAttributes = that.getAttributes();
|
||||
if (getAttributes().size() != thatAttributes.size()) {
|
||||
return false;
|
||||
|
@ -150,7 +207,7 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.getAuthority().hashCode();
|
||||
result = 31 * result;
|
||||
result = 31 * result + Objects.hashCode(this.username);
|
||||
for (Map.Entry<String, Object> e : getAttributes().entrySet()) {
|
||||
Object key = e.getKey();
|
||||
Object value = convertURLIfNecessary(e.getValue());
|
||||
|
@ -172,4 +229,39 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
|||
return (value instanceof URL) ? ((URL) value).toExternalForm() : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2UserAuthority}.
|
||||
*
|
||||
* @since 7.0
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
protected final String username;
|
||||
|
||||
protected String authority = "OAUTH2_USER";
|
||||
|
||||
protected Map<String, Object> attributes;
|
||||
|
||||
protected Builder(String username) {
|
||||
Assert.hasText(username, "username cannot be empty");
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Builder authority(String authority) {
|
||||
this.authority = authority;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder attributes(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuth2UserAuthority build() {
|
||||
Assert.notEmpty(this.attributes, "attributes cannot be empty");
|
||||
return new OAuth2UserAuthority(this.username, this.authority, this.attributes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
|||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
*/
|
||||
public class DefaultOidcUserTests {
|
||||
|
||||
|
@ -147,4 +148,37 @@ public class DefaultOidcUserTests {
|
|||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOidcUser.withUsername(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameIsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOidcUser.withUsername(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderWhenIdTokenIsNotSetThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOidcUser.withUsername(SUBJECT).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderWhenAllParametersProvidedAndValidThenCreated() {
|
||||
String username = "custom-username";
|
||||
DefaultOidcUser user = DefaultOidcUser.withUsername(username)
|
||||
.authorities(AUTHORITIES)
|
||||
.idToken(ID_TOKEN)
|
||||
.userInfo(USER_INFO)
|
||||
.build();
|
||||
|
||||
assertThat(user.getName()).isEqualTo(username);
|
||||
assertThat(user.getIdToken()).isEqualTo(ID_TOKEN);
|
||||
assertThat(user.getUserInfo()).isEqualTo(USER_INFO);
|
||||
assertThat(user.getAuthorities()).hasSize(1);
|
||||
assertThat(user.getAuthorities().iterator().next()).isEqualTo(AUTHORITY);
|
||||
assertThat(user.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB,
|
||||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
|||
* Tests for {@link OidcUserAuthority}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
*/
|
||||
public class OidcUserAuthorityTests {
|
||||
|
||||
|
@ -84,4 +85,43 @@ public class OidcUserAuthorityTests {
|
|||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> OidcUserAuthority.withUsername(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameIsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> OidcUserAuthority.withUsername(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderWhenIdTokenIsNotSetThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> OidcUserAuthority.withUsername(SUBJECT).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderWhenAllParametersProvidedAndValidThenCreated() {
|
||||
String username = SUBJECT;
|
||||
OidcUserAuthority authority = OidcUserAuthority.withUsername(username)
|
||||
.idToken(ID_TOKEN)
|
||||
.userInfo(USER_INFO)
|
||||
.build();
|
||||
|
||||
assertThat(authority.getUsername()).isEqualTo(username);
|
||||
assertThat(authority.getAuthority()).isEqualTo("OIDC_USER");
|
||||
assertThat(authority.getIdToken()).isEqualTo(ID_TOKEN);
|
||||
assertThat(authority.getUserInfo()).isEqualTo(USER_INFO);
|
||||
assertThat(authority.getAttributes()).containsOnlyKeys(IdTokenClaimNames.ISS, IdTokenClaimNames.SUB,
|
||||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUsernameWhenBuiltWithUsernameThenReturnsUsername() {
|
||||
String username = SUBJECT;
|
||||
OidcUserAuthority authority = OidcUserAuthority.withUsername(username).idToken(ID_TOKEN).build();
|
||||
|
||||
assertThat(authority.getUsername()).isEqualTo(username);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -145,6 +145,11 @@ public class DefaultOAuth2UserTests {
|
|||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOAuth2User.withUsername((String) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenCreatedThenIsSerializable() {
|
||||
DefaultOAuth2User user = DefaultOAuth2User.withUsername("directUser")
|
||||
|
@ -215,4 +220,48 @@ public class DefaultOAuth2UserTests {
|
|||
assertThat(user.getAttributes()).isEqualTo(ATTRIBUTES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenNestedAttributesThenUsernameExtractedCorrectly() {
|
||||
Map<String, Object> nestedAttributes = new HashMap<>();
|
||||
Map<String, Object> userData = new HashMap<>();
|
||||
userData.put("name", "nestedUser");
|
||||
userData.put("id", "123");
|
||||
nestedAttributes.put("data", userData);
|
||||
nestedAttributes.put("other", "value");
|
||||
|
||||
DefaultOAuth2User user = DefaultOAuth2User.withUsername("nestedUser")
|
||||
.authorities(AUTHORITIES)
|
||||
.attributes(nestedAttributes)
|
||||
.build();
|
||||
|
||||
assertThat(user.getName()).isEqualTo("nestedUser");
|
||||
assertThat(user.getAttributes()).hasSize(2);
|
||||
assertThat(user.getAttributes().get("data")).isEqualTo(userData);
|
||||
assertThat(user.getAttributes().get("other")).isEqualTo("value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenComplexNestedAttributesThenCorrectlyHandled() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
Map<String, Object> profile = new HashMap<>();
|
||||
Map<String, Object> socialMedia = new HashMap<>();
|
||||
|
||||
socialMedia.put("twitter", "twitterUser");
|
||||
socialMedia.put("github", "githubUser");
|
||||
profile.put("social", socialMedia);
|
||||
profile.put("email", "user@example.com");
|
||||
attributes.put("profile", profile);
|
||||
attributes.put("id", "user123");
|
||||
|
||||
DefaultOAuth2User user = DefaultOAuth2User.withUsername("customUsername")
|
||||
.authorities(AUTHORITIES)
|
||||
.attributes(attributes)
|
||||
.build();
|
||||
|
||||
assertThat(user.getName()).isEqualTo("customUsername");
|
||||
assertThat(user.getAttributes()).isEqualTo(attributes);
|
||||
assertThat(((Map<?, ?>) ((Map<?, ?>) user.getAttribute("profile")).get("social")).get("twitter"))
|
||||
.isEqualTo("twitterUser");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -30,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
|||
* Tests for {@link OAuth2UserAuthority}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Yoobin Yoon
|
||||
*/
|
||||
public class OAuth2UserAuthorityTests {
|
||||
|
||||
|
@ -94,4 +95,37 @@ public class OAuth2UserAuthorityTests {
|
|||
assertThat(AUTHORITY_WITH_STRINGURL.hashCode()).isEqualTo(AUTHORITY_WITH_OBJECTURL.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameIsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> OAuth2UserAuthority.withUsername(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withUsernameWhenUsernameIsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> OAuth2UserAuthority.withUsername(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderWhenAttributesIsNotSetThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> OAuth2UserAuthority.withUsername("john_doe").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void builderWhenAllParametersProvidedAndValidThenCreated() {
|
||||
String username = "john_doe";
|
||||
OAuth2UserAuthority authority = OAuth2UserAuthority.withUsername(username).attributes(ATTRIBUTES).build();
|
||||
|
||||
assertThat(authority.getUsername()).isEqualTo(username);
|
||||
assertThat(authority.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||
assertThat(authority.getAttributes()).isEqualTo(ATTRIBUTES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUsernameWhenBuiltWithUsernameThenReturnsUsername() {
|
||||
String username = "john_doe";
|
||||
OAuth2UserAuthority authority = OAuth2UserAuthority.withUsername(username).attributes(ATTRIBUTES).build();
|
||||
|
||||
assertThat(authority.getUsername()).isEqualTo(username);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue