Merge 5d237a7822
into 725745defd
This commit is contained in:
commit
5f4c9bfb08
|
@ -36,12 +36,13 @@ public final class ClientRegistration {
|
||||||
private String uri; <14>
|
private String uri; <14>
|
||||||
private AuthenticationMethod authenticationMethod; <15>
|
private AuthenticationMethod authenticationMethod; <15>
|
||||||
private String userNameAttributeName; <16>
|
private String userNameAttributeName; <16>
|
||||||
|
private String usernameExpression; <17>
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class ClientSettings {
|
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.
|
<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.
|
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
||||||
The supported values are *header*, *form* and *query*.
|
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.
|
<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> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
|
<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].
|
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].
|
||||||
|
|
||||||
|
|
|
@ -37,12 +37,13 @@ public final class ClientRegistration {
|
||||||
private String uri; <14>
|
private String uri; <14>
|
||||||
private AuthenticationMethod authenticationMethod; <15>
|
private AuthenticationMethod authenticationMethod; <15>
|
||||||
private String userNameAttributeName; <16>
|
private String userNameAttributeName; <16>
|
||||||
|
private String usernameExpression; <17>
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class ClientSettings {
|
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.
|
<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.
|
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
|
||||||
The supported values are *header*, *form*, and *query*.
|
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.
|
<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> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
|
<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].
|
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].
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,9 @@ Below are the highlights of the release, or you can view https://github.com/spri
|
||||||
|
|
||||||
* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
|
* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
|
||||||
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
|
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
|
||||||
|
|
||||||
|
== OAuth 2.0
|
||||||
|
|
||||||
|
=== Username Expression Support for Nested Attributes - https://github.com/spring-projects/spring-security/pull/16390[gh-16390]
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -32,6 +32,7 @@ import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||||
* This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}.
|
* This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author YooBin Yoon
|
||||||
* @since 5.3
|
* @since 5.3
|
||||||
* @see DefaultOAuth2User
|
* @see DefaultOAuth2User
|
||||||
* @see OAuth2ClientJackson2Module
|
* @see OAuth2ClientJackson2Module
|
||||||
|
@ -45,7 +46,7 @@ abstract class DefaultOAuth2UserMixin {
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
DefaultOAuth2UserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
|
DefaultOAuth2UserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
|
||||||
@JsonProperty("attributes") Map<String, Object> attributes,
|
@JsonProperty("attributes") Map<String, Object> attributes,
|
||||||
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
|
@JsonProperty("nameAttributeKey") String nameAttributeKey, @JsonProperty("username") String username) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||||
@JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true)
|
@JsonIgnoreProperties(value = { "attributes", "username" }, ignoreUnknown = true)
|
||||||
abstract class DefaultOidcUserMixin {
|
abstract class DefaultOidcUserMixin {
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.springframework.util.StringUtils;
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
* @author Michael Sosa
|
* @author Michael Sosa
|
||||||
|
* @author Yoobin Yoon
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2">Section 2
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2">Section 2
|
||||||
* Client Registration</a>
|
* Client Registration</a>
|
||||||
|
@ -299,8 +300,11 @@ public final class ClientRegistration implements Serializable {
|
||||||
|
|
||||||
private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;
|
private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
private String userNameAttributeName;
|
private String userNameAttributeName;
|
||||||
|
|
||||||
|
private String usernameExpression;
|
||||||
|
|
||||||
UserInfoEndpoint() {
|
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
|
* @deprecated Use {@link #getUsernameExpression()} instead
|
||||||
* info response.
|
|
||||||
* @return the attribute name used to access the user's name from the user
|
|
||||||
* info response
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public String getUserNameAttributeName() {
|
public String getUserNameAttributeName() {
|
||||||
return this.userNameAttributeName;
|
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;
|
private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
private String userNameAttributeName;
|
private String userNameAttributeName;
|
||||||
|
|
||||||
|
private String usernameExpression;
|
||||||
|
|
||||||
private String jwkSetUri;
|
private String jwkSetUri;
|
||||||
|
|
||||||
private String issuerUri;
|
private String issuerUri;
|
||||||
|
@ -399,6 +414,7 @@ public final class ClientRegistration implements Serializable {
|
||||||
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
|
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
|
||||||
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
|
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
|
||||||
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
|
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
|
||||||
|
this.usernameExpression = clientRegistration.providerDetails.userInfoEndpoint.usernameExpression;
|
||||||
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
|
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
|
||||||
this.issuerUri = clientRegistration.providerDetails.issuerUri;
|
this.issuerUri = clientRegistration.providerDetails.issuerUri;
|
||||||
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
|
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
|
* Sets the username attribute name. This method automatically converts the
|
||||||
* response.
|
* attribute name to a SpEL expression for backward compatibility.
|
||||||
* @param userNameAttributeName the attribute name used to access the user's name
|
*
|
||||||
* from the user info response
|
* <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}
|
* @return the {@link Builder}
|
||||||
*/
|
*/
|
||||||
public Builder userNameAttributeName(String userNameAttributeName) {
|
public Builder userNameAttributeName(String userNameAttributeName) {
|
||||||
this.userNameAttributeName = 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -672,7 +717,10 @@ public final class ClientRegistration implements Serializable {
|
||||||
providerDetails.tokenUri = this.tokenUri;
|
providerDetails.tokenUri = this.tokenUri;
|
||||||
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
|
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
|
||||||
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
|
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
|
||||||
|
|
||||||
|
providerDetails.userInfoEndpoint.usernameExpression = this.usernameExpression;
|
||||||
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
|
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
|
||||||
|
|
||||||
providerDetails.jwkSetUri = this.jwkSetUri;
|
providerDetails.jwkSetUri = this.jwkSetUri;
|
||||||
providerDetails.issuerUri = this.issuerUri;
|
providerDetails.issuerUri = this.issuerUri;
|
||||||
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.context.expression.MapAccessor;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.convert.converter.Converter;
|
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.RequestEntity;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
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
|
* An implementation of an {@link OAuth2UserService} that supports standard OAuth 2.0
|
||||||
* Provider's.
|
* Provider's.
|
||||||
* <p>
|
* <p>
|
||||||
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
|
* For standard OAuth 2.0 Provider's, the username expression used to extract the user's
|
||||||
* from the UserInfo response is required and therefore must be available via
|
* name from the UserInfo response is required and therefore must be available via
|
||||||
* {@link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName()
|
* {@link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUsernameExpression()
|
||||||
* UserInfoEndpoint.getUserNameAttributeName()}.
|
* UserInfoEndpoint.getUsernameExpression()}.
|
||||||
* <p>
|
* <p>
|
||||||
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and
|
* <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
|
* therefore will vary. Please consult the provider's API documentation for the set of
|
||||||
* supported user attribute names.
|
* supported user attribute names.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Yoobin Yoon
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see OAuth2UserService
|
* @see OAuth2UserService
|
||||||
* @see OAuth2UserRequest
|
* @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_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<>() {
|
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,13 +99,67 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
|
||||||
@Override
|
@Override
|
||||||
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
|
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
Assert.notNull(userRequest, "userRequest cannot be null");
|
Assert.notNull(userRequest, "userRequest cannot be null");
|
||||||
String userNameAttributeName = getUserNameAttributeName(userRequest);
|
String usernameExpression = getUsernameExpression(userRequest);
|
||||||
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
|
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
|
||||||
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
|
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
|
||||||
OAuth2AccessToken token = userRequest.getAccessToken();
|
OAuth2AccessToken token = userRequest.getAccessToken();
|
||||||
Map<String, Object> attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
|
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,
|
private Collection<GrantedAuthority> getAuthorities(OAuth2AccessToken token, Map<String, Object> attributes,
|
||||||
String userNameAttributeName) {
|
String username) {
|
||||||
Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
|
Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
|
||||||
authorities.add(new OAuth2UserAuthority(attributes, userNameAttributeName));
|
authorities.add(OAuth2UserAuthority.withUsername(username).attributes(attributes).build());
|
||||||
|
|
||||||
for (String authority : token.getScopes()) {
|
for (String authority : token.getScopes()) {
|
||||||
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 net.minidev.json.JSONObject;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.context.expression.MapAccessor;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.convert.converter.Converter;
|
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.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatusCode;
|
import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.MediaType;
|
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
|
* An implementation of an {@link ReactiveOAuth2UserService} that supports standard OAuth
|
||||||
* 2.0 Provider's.
|
* 2.0 Provider's.
|
||||||
* <p>
|
* <p>
|
||||||
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
|
* For standard OAuth 2.0 Provider's, the username expression used to extract the user's
|
||||||
* from the UserInfo response is required and therefore must be available via
|
* name from the UserInfo response is required and therefore must be available via
|
||||||
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName()
|
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint#getUsernameExpression()
|
||||||
* UserInfoEndpoint.getUserNameAttributeName()}.
|
* UserInfoEndpoint.getUsernameExpression()}.
|
||||||
* <p>
|
* <p>
|
||||||
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and
|
* <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
|
* therefore will vary. Please consult the provider's API documentation for the set of
|
||||||
* supported user attribute names.
|
* supported user attribute names.
|
||||||
*
|
*
|
||||||
* @author Rob Winch
|
* @author Rob Winch
|
||||||
|
* @author Yoobin Yoon
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
* @see ReactiveOAuth2UserService
|
* @see ReactiveOAuth2UserService
|
||||||
* @see OAuth2UserRequest
|
* @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 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<>() {
|
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,17 +108,7 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
||||||
null);
|
null);
|
||||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
}
|
}
|
||||||
String userNameAttributeName = userRequest.getClientRegistration()
|
String usernameExpression = getUsernameExpression(userRequest);
|
||||||
.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());
|
|
||||||
}
|
|
||||||
AuthenticationMethod authenticationMethod = userRequest.getClientRegistration()
|
AuthenticationMethod authenticationMethod = userRequest.getClientRegistration()
|
||||||
.getProviderDetails()
|
.getProviderDetails()
|
||||||
.getUserInfoEndpoint()
|
.getUserInfoEndpoint()
|
||||||
|
@ -130,15 +129,20 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
||||||
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP)
|
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP)
|
||||||
.mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes));
|
.mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes));
|
||||||
return userAttributes.map((attrs) -> {
|
return userAttributes.map((attrs) -> {
|
||||||
GrantedAuthority authority = new OAuth2UserAuthority(attrs, userNameAttributeName);
|
String username = evaluateUsername(attrs, usernameExpression);
|
||||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||||
authorities.add(authority);
|
authorities.add(OAuth2UserAuthority.withUsername(username)
|
||||||
|
.attributes(attrs)
|
||||||
|
.build());
|
||||||
OAuth2AccessToken token = userRequest.getAccessToken();
|
OAuth2AccessToken token = userRequest.getAccessToken();
|
||||||
for (String scope : token.getScopes()) {
|
for (String scope : token.getScopes()) {
|
||||||
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
|
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 ||
|
.onErrorMap((ex) -> (ex instanceof UnsupportedMediaTypeException ||
|
||||||
ex.getCause() instanceof UnsupportedMediaTypeException), (ex) -> {
|
ex.getCause() instanceof UnsupportedMediaTypeException), (ex) -> {
|
||||||
|
@ -168,6 +172,47 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
|
||||||
// @formatter:on
|
// @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,
|
private WebClient.RequestHeadersSpec<?> getRequestHeaderSpec(OAuth2UserRequest userRequest, String userInfoUri,
|
||||||
AuthenticationMethod authenticationMethod) {
|
AuthenticationMethod authenticationMethod) {
|
||||||
if (AuthenticationMethod.FORM.equals(authenticationMethod)) {
|
if (AuthenticationMethod.FORM.equals(authenticationMethod)) {
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -194,7 +194,8 @@ public class OAuth2AuthenticationTokenMixinTests {
|
||||||
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
|
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
|
||||||
" \"username\": \"user\"\n" +
|
" \"username\": \"user\"\n" +
|
||||||
" },\n" +
|
" },\n" +
|
||||||
" \"nameAttributeKey\": \"username\"\n" +
|
" \"nameAttributeKey\": \"username\",\n" +
|
||||||
|
" \"username\": \"" + oauth2User.getName() + "\"\n" +
|
||||||
" }";
|
" }";
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
@ -247,11 +248,12 @@ public class OAuth2AuthenticationTokenMixinTests {
|
||||||
return "{\n" +
|
return "{\n" +
|
||||||
" \"@class\": \"org.springframework.security.oauth2.core.user.OAuth2UserAuthority\",\n" +
|
" \"@class\": \"org.springframework.security.oauth2.core.user.OAuth2UserAuthority\",\n" +
|
||||||
" \"authority\": \"" + oauth2UserAuthority.getAuthority() + "\",\n" +
|
" \"authority\": \"" + oauth2UserAuthority.getAuthority() + "\",\n" +
|
||||||
" \"userNameAttributeName\": \"username\",\n" +
|
|
||||||
" \"attributes\": {\n" +
|
" \"attributes\": {\n" +
|
||||||
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
|
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
|
||||||
" \"username\": \"user\"\n" +
|
" \"username\": \"user\"\n" +
|
||||||
" }\n" +
|
" },\n" +
|
||||||
|
" \"userNameAttributeName\": \"username\",\n" +
|
||||||
|
" \"username\": \"user\"\n" +
|
||||||
" }";
|
" }";
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
@ -261,9 +263,10 @@ public class OAuth2AuthenticationTokenMixinTests {
|
||||||
return "{\n" +
|
return "{\n" +
|
||||||
" \"@class\": \"org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority\",\n" +
|
" \"@class\": \"org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority\",\n" +
|
||||||
" \"authority\": \"" + oidcUserAuthority.getAuthority() + "\",\n" +
|
" \"authority\": \"" + oidcUserAuthority.getAuthority() + "\",\n" +
|
||||||
" \"userNameAttributeName\": \"" + oidcUserAuthority.getUserNameAttributeName() + "\",\n" +
|
|
||||||
" \"idToken\": " + asJson(oidcUserAuthority.getIdToken()) + ",\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
|
// @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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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());
|
.isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod());
|
||||||
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo(
|
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo(
|
||||||
expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
|
expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
|
||||||
|
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression())
|
||||||
|
.isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUsernameExpression());
|
||||||
assertThat(clientRegistration.getProviderDetails().getJwkSetUri())
|
assertThat(clientRegistration.getProviderDetails().getJwkSetUri())
|
||||||
.isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri());
|
.isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri());
|
||||||
assertThat(clientRegistration.getProviderDetails().getIssuerUri())
|
assertThat(clientRegistration.getProviderDetails().getIssuerUri())
|
||||||
|
@ -306,6 +308,8 @@ public class OAuth2AuthorizedClientMixinTests {
|
||||||
.map((key) -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"")
|
.map((key) -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"")
|
||||||
.collect(Collectors.joining(","));
|
.collect(Collectors.joining(","));
|
||||||
}
|
}
|
||||||
|
String usernameExpression = (userInfoEndpoint.getUsernameExpression() != null)
|
||||||
|
? "\"" + userInfoEndpoint.getUsernameExpression() + "\"" : null;
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
return "{\n" +
|
return "{\n" +
|
||||||
" \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" +
|
" \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" +
|
||||||
|
@ -333,7 +337,8 @@ public class OAuth2AuthorizedClientMixinTests {
|
||||||
" \"authenticationMethod\": {\n" +
|
" \"authenticationMethod\": {\n" +
|
||||||
" \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" +
|
" \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" +
|
||||||
" },\n" +
|
" },\n" +
|
||||||
" \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" +
|
" \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + ",\n" +
|
||||||
|
" \"usernameExpression\": " + usernameExpression + "\n" +
|
||||||
" },\n" +
|
" },\n" +
|
||||||
" \"jwkSetUri\": " + ((providerDetails.getJwkSetUri() != null) ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" +
|
" \"jwkSetUri\": " + ((providerDetails.getJwkSetUri() != null) ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" +
|
||||||
" \"issuerUri\": " + ((providerDetails.getIssuerUri() != null) ? "\"" + providerDetails.getIssuerUri() + "\"" : 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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}.
|
* Tests for {@link ClientRegistration}.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Yoobin Yoon
|
||||||
*/
|
*/
|
||||||
public class ClientRegistrationTests {
|
public class ClientRegistrationTests {
|
||||||
|
|
||||||
|
@ -716,6 +717,7 @@ public class ClientRegistrationTests {
|
||||||
.isEqualTo(updatedUserInfoEndpoint.getAuthenticationMethod());
|
.isEqualTo(updatedUserInfoEndpoint.getAuthenticationMethod());
|
||||||
assertThat(userInfoEndpoint.getUserNameAttributeName())
|
assertThat(userInfoEndpoint.getUserNameAttributeName())
|
||||||
.isEqualTo(updatedUserInfoEndpoint.getUserNameAttributeName());
|
.isEqualTo(updatedUserInfoEndpoint.getUserNameAttributeName());
|
||||||
|
assertThat(userInfoEndpoint.getUsernameExpression()).isEqualTo(updatedUserInfoEndpoint.getUsernameExpression());
|
||||||
assertThat(providerDetails.getJwkSetUri()).isEqualTo(updatedProviderDetails.getJwkSetUri());
|
assertThat(providerDetails.getJwkSetUri()).isEqualTo(updatedProviderDetails.getJwkSetUri());
|
||||||
assertThat(providerDetails.getIssuerUri()).isEqualTo(updatedProviderDetails.getIssuerUri());
|
assertThat(providerDetails.getIssuerUri()).isEqualTo(updatedProviderDetails.getIssuerUri());
|
||||||
assertThat(providerDetails.getConfigurationMetadata())
|
assertThat(providerDetails.getConfigurationMetadata())
|
||||||
|
@ -802,6 +804,84 @@ public class ClientRegistrationTests {
|
||||||
assertThat(clientRegistration.getClientSettings().isRequireProofKey()).isTrue();
|
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
|
@ParameterizedTest
|
||||||
@MethodSource("invalidPkceGrantTypes")
|
@MethodSource("invalidPkceGrantTypes")
|
||||||
void buildWhenInvalidGrantTypeForPkceThenException(AuthorizationGrantType invalidGrantType) {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 Joe Grandja
|
||||||
* @author Eddú Meléndez
|
* @author Eddú Meléndez
|
||||||
|
* @author Yoobin Yoon
|
||||||
*/
|
*/
|
||||||
public class DefaultOAuth2UserServiceTests {
|
public class DefaultOAuth2UserServiceTests {
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
assertThatExceptionOfType(OAuth2AuthenticationException.class)
|
||||||
.isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
.isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)))
|
||||||
.withMessageContaining("missing_user_name_attribute");
|
.withMessageContaining("invalid_user_info_response");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -153,10 +154,13 @@ public class DefaultOAuth2UserServiceTests {
|
||||||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||||
assertThat(user.getAuthorities()).hasSize(1);
|
assertThat(user.getAuthorities()).hasSize(1);
|
||||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
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.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name");
|
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -194,10 +198,13 @@ public class DefaultOAuth2UserServiceTests {
|
||||||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||||
assertThat(user.getAuthorities()).hasSize(1);
|
assertThat(user.getAuthorities()).hasSize(1);
|
||||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
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.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name");
|
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -421,6 +428,134 @@ public class DefaultOAuth2UserServiceTests {
|
||||||
.isThrownBy(() -> this.userService.setAttributesConverter(null));
|
.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) {
|
private DefaultOAuth2UserService withMockResponse(Map<String, Object> response) {
|
||||||
ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(response, HttpStatus.OK);
|
ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(response, HttpStatus.OK);
|
||||||
Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 Rob Winch
|
||||||
* @author Eddú Meléndez
|
* @author Eddú Meléndez
|
||||||
|
* @author Yoobin Yoon
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
*/
|
*/
|
||||||
public class DefaultReactiveOAuth2UserServiceTests {
|
public class DefaultReactiveOAuth2UserServiceTests {
|
||||||
|
@ -104,19 +105,6 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
||||||
.verify();
|
.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
|
@Test
|
||||||
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
|
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
|
@ -141,10 +129,13 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
||||||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||||
assertThat(user.getAuthorities()).hasSize(1);
|
assertThat(user.getAuthorities()).hasSize(1);
|
||||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
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.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("id");
|
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// gh-9336
|
// gh-9336
|
||||||
|
@ -201,10 +192,13 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
||||||
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
|
||||||
assertThat(user.getAuthorities()).hasSize(1);
|
assertThat(user.getAuthorities()).hasSize(1);
|
||||||
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
|
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.getAuthority()).isEqualTo("OAUTH2_USER");
|
||||||
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
|
||||||
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name");
|
assertThat(userAuthority.getUsername()).isEqualTo("user1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// gh-5500
|
// gh-5500
|
||||||
|
@ -338,6 +332,66 @@ public class DefaultReactiveOAuth2UserServiceTests {
|
||||||
.isThrownBy(() -> this.userService.setAttributesConverter(null));
|
.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) {
|
private DefaultReactiveOAuth2UserService withMockResponse(Map<String, Object> body) {
|
||||||
WebClient real = WebClient.builder().build();
|
WebClient real = WebClient.builder().build();
|
||||||
WebClient.RequestHeadersUriSpec spec = spy(real.post());
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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}.
|
* A {@link GrantedAuthority} that may be associated to an {@link OidcUser}.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Yoobin Yoon
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see OidcUser
|
* @see OidcUser
|
||||||
*/
|
*/
|
||||||
|
@ -47,7 +48,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
||||||
/**
|
/**
|
||||||
* Constructs a {@code OidcUserAuthority} using the provided parameters.
|
* Constructs a {@code OidcUserAuthority} using the provided parameters.
|
||||||
* @param idToken the {@link OidcIdToken ID Token} containing claims about the user
|
* @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) {
|
public OidcUserAuthority(OidcIdToken idToken) {
|
||||||
this(idToken, null);
|
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 idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||||
* may be {@code null}
|
* may be {@code null}
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo) {
|
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
this("OIDC_USER", idToken, 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
|
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||||
* the attributes
|
* the attributes
|
||||||
* @since 6.4
|
* @since 6.4
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo, @Nullable String userNameAttributeName) {
|
public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo, @Nullable String userNameAttributeName) {
|
||||||
this("OIDC_USER", idToken, userInfo, 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 idToken the {@link OidcIdToken ID Token} containing claims about the user
|
||||||
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
* @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user,
|
||||||
* may be {@code null}
|
* may be {@code null}
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo) {
|
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo) {
|
||||||
this(authority, idToken, userInfo, IdTokenClaimNames.SUB);
|
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
|
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||||
* the attributes
|
* the attributes
|
||||||
* @since 6.4
|
* @since 6.4
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo,
|
public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo,
|
||||||
@Nullable String userNameAttributeName) {
|
@Nullable String userNameAttributeName) {
|
||||||
super(authority, collectClaims(idToken, userInfo), userNameAttributeName);
|
super(authority, collectClaims(idToken, userInfo), userNameAttributeName);
|
||||||
|
@ -105,6 +116,33 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
|
||||||
this.userInfo = userInfo;
|
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.
|
* Returns the {@link OidcIdToken ID Token} containing claims about the user.
|
||||||
* @return the {@link OidcIdToken} 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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -45,6 +45,7 @@ import org.springframework.util.Assert;
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
* @author Eddú Meléndez
|
* @author Eddú Meléndez
|
||||||
* @author Park Hyojong
|
* @author Park Hyojong
|
||||||
|
* @author YooBin Yoon
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see OAuth2User
|
* @see OAuth2User
|
||||||
*/
|
*/
|
||||||
|
@ -58,13 +59,17 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
||||||
|
|
||||||
private final String nameAttributeKey;
|
private final String nameAttributeKey;
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a {@code DefaultOAuth2User} using the provided parameters.
|
* Constructs a {@code DefaultOAuth2User} using the provided parameters.
|
||||||
* @param authorities the authorities granted to the user
|
* @param authorities the authorities granted to the user
|
||||||
* @param attributes the attributes about the user
|
* @param attributes the attributes about the user
|
||||||
* @param nameAttributeKey the key used to access the user's "name" from
|
* @param nameAttributeKey the key used to access the user's "name" from
|
||||||
* {@link #getAttributes()}
|
* {@link #getAttributes()}
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
|
public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
|
||||||
String nameAttributeKey) {
|
String nameAttributeKey) {
|
||||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||||
|
@ -77,11 +82,45 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
||||||
: Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES));
|
: Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES));
|
||||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||||
this.nameAttributeKey = nameAttributeKey;
|
this.nameAttributeKey = nameAttributeKey;
|
||||||
|
this.username = attributes.get(nameAttributeKey).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code DefaultOAuth2User} using the provided parameters. This
|
||||||
|
* constructor is used by Jackson for deserialization.
|
||||||
|
* @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,
|
||||||
|
String nameAttributeKey, String username) {
|
||||||
|
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||||
|
|
||||||
|
this.authorities = (authorities != null)
|
||||||
|
? Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities)))
|
||||||
|
: Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES));
|
||||||
|
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||||
|
this.nameAttributeKey = nameAttributeKey;
|
||||||
|
this.username = username;
|
||||||
|
|
||||||
|
Assert.hasText(this.username, "username cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code DefaultOAuth2User} 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
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.getAttribute(this.nameAttributeKey).toString();
|
return this.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -140,4 +179,39 @@ public class DefaultOAuth2User implements OAuth2User, Serializable {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for {@link DefaultOAuth2User}.
|
||||||
|
*
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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}.
|
* A {@link GrantedAuthority} that may be associated to an {@link OAuth2User}.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Yoobin Yoon
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see OAuth2User
|
* @see OAuth2User
|
||||||
*/
|
*/
|
||||||
|
@ -42,13 +43,18 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
||||||
|
|
||||||
private final Map<String, Object> attributes;
|
private final Map<String, Object> attributes;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
private final String userNameAttributeName;
|
private final String userNameAttributeName;
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults
|
* Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults
|
||||||
* {@link #getAuthority()} to {@code OAUTH2_USER}.
|
* {@link #getAuthority()} to {@code OAUTH2_USER}.
|
||||||
* @param attributes the attributes about the user
|
* @param attributes the attributes about the user
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OAuth2UserAuthority(Map<String, Object> attributes) {
|
public OAuth2UserAuthority(Map<String, Object> attributes) {
|
||||||
this("OAUTH2_USER", 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
|
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||||
* the attributes
|
* the attributes
|
||||||
* @since 6.4
|
* @since 6.4
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OAuth2UserAuthority(Map<String, Object> attributes, @Nullable String userNameAttributeName) {
|
public OAuth2UserAuthority(Map<String, Object> attributes, @Nullable String userNameAttributeName) {
|
||||||
this("OAUTH2_USER", attributes, userNameAttributeName);
|
this("OAUTH2_USER", attributes, userNameAttributeName);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +77,9 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
||||||
* Constructs a {@code OAuth2UserAuthority} using the provided parameters.
|
* Constructs a {@code OAuth2UserAuthority} using the provided parameters.
|
||||||
* @param authority the authority granted to the user
|
* @param authority the authority granted to the user
|
||||||
* @param attributes the attributes about 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) {
|
public OAuth2UserAuthority(String authority, Map<String, Object> attributes) {
|
||||||
this(authority, attributes, null);
|
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
|
* @param userNameAttributeName the attribute name used to access the user's name from
|
||||||
* the attributes
|
* the attributes
|
||||||
* @since 6.4
|
* @since 6.4
|
||||||
|
* @deprecated Use {@link #withUsername(String)} builder pattern instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public OAuth2UserAuthority(String authority, Map<String, Object> attributes, String userNameAttributeName) {
|
public OAuth2UserAuthority(String authority, Map<String, Object> attributes, String userNameAttributeName) {
|
||||||
Assert.hasText(authority, "authority cannot be empty");
|
Assert.hasText(authority, "authority cannot be empty");
|
||||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||||
this.authority = authority;
|
this.authority = authority;
|
||||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||||
this.userNameAttributeName = userNameAttributeName;
|
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
|
@Override
|
||||||
|
@ -107,12 +147,26 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
||||||
* Returns the attribute name used to access the user's name from the attributes.
|
* 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
|
* @return the attribute name used to access the user's name from the attributes
|
||||||
* @since 6.4
|
* @since 6.4
|
||||||
|
* @deprecated Use {@link #getUsername()} instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getUserNameAttributeName() {
|
public String getUserNameAttributeName() {
|
||||||
return this.userNameAttributeName;
|
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
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (this == obj) {
|
if (this == obj) {
|
||||||
|
@ -125,6 +179,9 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
||||||
if (!this.getAuthority().equals(that.getAuthority())) {
|
if (!this.getAuthority().equals(that.getAuthority())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!Objects.equals(this.username, that.username)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Map<String, Object> thatAttributes = that.getAttributes();
|
Map<String, Object> thatAttributes = that.getAttributes();
|
||||||
if (getAttributes().size() != thatAttributes.size()) {
|
if (getAttributes().size() != thatAttributes.size()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -150,7 +207,7 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = this.getAuthority().hashCode();
|
int result = this.getAuthority().hashCode();
|
||||||
result = 31 * result;
|
result = 31 * result + Objects.hashCode(this.username);
|
||||||
for (Map.Entry<String, Object> e : getAttributes().entrySet()) {
|
for (Map.Entry<String, Object> e : getAttributes().entrySet()) {
|
||||||
Object key = e.getKey();
|
Object key = e.getKey();
|
||||||
Object value = convertURLIfNecessary(e.getValue());
|
Object value = convertURLIfNecessary(e.getValue());
|
||||||
|
@ -172,4 +229,39 @@ public class OAuth2UserAuthority implements GrantedAuthority {
|
||||||
return (value instanceof URL) ? ((URL) value).toExternalForm() : value;
|
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-2017 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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}.
|
* Tests for {@link OidcUserAuthority}.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Yoobin Yoon
|
||||||
*/
|
*/
|
||||||
public class OidcUserAuthorityTests {
|
public class OidcUserAuthorityTests {
|
||||||
|
|
||||||
|
@ -84,4 +85,43 @@ public class OidcUserAuthorityTests {
|
||||||
StandardClaimNames.NAME, StandardClaimNames.EMAIL);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.security.oauth2.core.user;
|
package org.springframework.security.oauth2.core.user;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
* @author Vedran Pavic
|
* @author Vedran Pavic
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
* @author Park Hyojong
|
* @author Park Hyojong
|
||||||
|
* @author Yoobin Yoon
|
||||||
*/
|
*/
|
||||||
public class DefaultOAuth2UserTests {
|
public class DefaultOAuth2UserTests {
|
||||||
|
|
||||||
|
@ -109,4 +111,157 @@ public class DefaultOAuth2UserTests {
|
||||||
SerializationUtils.serialize(user);
|
SerializationUtils.serialize(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenValidParametersThenCreated() {
|
||||||
|
String directUsername = "directUser";
|
||||||
|
DefaultOAuth2User user = DefaultOAuth2User.withUsername(directUsername)
|
||||||
|
.authorities(AUTHORITIES)
|
||||||
|
.attributes(ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(user.getName()).isEqualTo(directUsername);
|
||||||
|
assertThat(user.getAuthorities()).hasSize(1);
|
||||||
|
assertThat(user.getAuthorities().iterator().next()).isEqualTo(AUTHORITY);
|
||||||
|
assertThat(user.getAttributes()).containsOnlyKeys(ATTRIBUTE_NAME_KEY);
|
||||||
|
assertThat(user.getAttributes().get(ATTRIBUTE_NAME_KEY)).isEqualTo(USERNAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenUsernameIsEmptyThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOAuth2User.withUsername(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenAttributesIsNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(
|
||||||
|
() -> DefaultOAuth2User.withUsername("username").authorities(AUTHORITIES).attributes(null).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenAttributesIsEmptyThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOAuth2User.withUsername("username")
|
||||||
|
.authorities(AUTHORITIES)
|
||||||
|
.attributes(Collections.emptyMap())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenUsernameNullThenThrowIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> DefaultOAuth2User.withUsername((String) null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenCreatedThenIsSerializable() {
|
||||||
|
DefaultOAuth2User user = DefaultOAuth2User.withUsername("directUser")
|
||||||
|
.authorities(AUTHORITIES)
|
||||||
|
.attributes(ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
SerializationUtils.serialize(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenUsernameProvidedThenTakesPrecedenceOverAttributes() {
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put("username", "fromAttributes");
|
||||||
|
attributes.put("id", "123");
|
||||||
|
|
||||||
|
DefaultOAuth2User user = DefaultOAuth2User.withUsername("directUsername")
|
||||||
|
.authorities(AUTHORITIES)
|
||||||
|
.attributes(attributes)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(user.getName()).isEqualTo("directUsername");
|
||||||
|
assertThat((String) user.getAttribute("username")).isEqualTo("fromAttributes");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructorWhenSimpleAttributeKeyThenWorksAsUsual() {
|
||||||
|
DefaultOAuth2User user = new DefaultOAuth2User(AUTHORITIES, ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||||
|
|
||||||
|
assertThat(user.getName()).isEqualTo(USERNAME);
|
||||||
|
assertThat(user.getAttributes()).containsOnlyKeys(ATTRIBUTE_NAME_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameAndDeprecatedConstructorWhenSameDataThenEqual() {
|
||||||
|
DefaultOAuth2User user1 = new DefaultOAuth2User(AUTHORITIES, ATTRIBUTES, ATTRIBUTE_NAME_KEY);
|
||||||
|
DefaultOAuth2User user2 = DefaultOAuth2User.withUsername(USERNAME)
|
||||||
|
.authorities(AUTHORITIES)
|
||||||
|
.attributes(ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(user1.getName()).isEqualTo(user2.getName());
|
||||||
|
assertThat(user1.getAuthorities()).isEqualTo(user2.getAuthorities());
|
||||||
|
assertThat(user1.getAttributes()).isEqualTo(user2.getAttributes());
|
||||||
|
assertThat(user1).isEqualTo(user2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenAuthoritiesIsNullThenCreatedWithEmptyAuthorities() {
|
||||||
|
DefaultOAuth2User user = DefaultOAuth2User.withUsername("testUser")
|
||||||
|
.authorities(null)
|
||||||
|
.attributes(ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(user.getName()).isEqualTo("testUser");
|
||||||
|
assertThat(user.getAuthorities()).isEmpty();
|
||||||
|
assertThat(user.getAttributes()).isEqualTo(ATTRIBUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withUsernameWhenAuthoritiesIsEmptyThenCreated() {
|
||||||
|
DefaultOAuth2User user = DefaultOAuth2User.withUsername("testUser")
|
||||||
|
.authorities(Collections.emptySet())
|
||||||
|
.attributes(ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(user.getName()).isEqualTo("testUser");
|
||||||
|
assertThat(user.getAuthorities()).isEmpty();
|
||||||
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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}.
|
* Tests for {@link OAuth2UserAuthority}.
|
||||||
*
|
*
|
||||||
* @author Joe Grandja
|
* @author Joe Grandja
|
||||||
|
* @author Yoobin Yoon
|
||||||
*/
|
*/
|
||||||
public class OAuth2UserAuthorityTests {
|
public class OAuth2UserAuthorityTests {
|
||||||
|
|
||||||
|
@ -94,4 +95,37 @@ public class OAuth2UserAuthorityTests {
|
||||||
assertThat(AUTHORITY_WITH_STRINGURL.hashCode()).isEqualTo(AUTHORITY_WITH_OBJECTURL.hashCode());
|
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