Implement User Info Endpoint
Closes gh-176
This commit is contained in:
committed by
Joe Grandja
parent
9667229429
commit
8e8e6d1b17
@@ -36,9 +36,11 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
* @since 0.2.0
|
||||
* @see OAuth2AuthorizationServerConfigurer#oidc
|
||||
* @see OidcClientRegistrationEndpointConfigurer
|
||||
* @see OidcUserInfoEndpointConfigurer
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
*/
|
||||
public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
private final OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer;
|
||||
private OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer;
|
||||
private RequestMatcher requestMatcher;
|
||||
|
||||
@@ -47,6 +49,7 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
*/
|
||||
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
this.userInfoEndpointConfigurer = new OidcUserInfoEndpointConfigurer(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,8 +66,20 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @param userInfoEndpointCustomizer the {@link Customizer} providing access to the {@link OidcUserInfoEndpointConfigurer}
|
||||
* @return the {@link OidcConfigurer} for further configuration
|
||||
*/
|
||||
public OidcConfigurer userInfoEndpoint(Customizer<OidcUserInfoEndpointConfigurer> userInfoEndpointCustomizer) {
|
||||
userInfoEndpointCustomizer.customize(this.userInfoEndpointConfigurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
this.userInfoEndpointConfigurer.init(builder);
|
||||
if (this.clientRegistrationEndpointConfigurer != null) {
|
||||
this.clientRegistrationEndpointConfigurer.init(builder);
|
||||
}
|
||||
@@ -75,14 +90,16 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
"/.well-known/openid-configuration", HttpMethod.GET.name()));
|
||||
}
|
||||
requestMatchers.add(this.userInfoEndpointConfigurer.getRequestMatcher());
|
||||
if (this.clientRegistrationEndpointConfigurer != null) {
|
||||
requestMatchers.add(this.clientRegistrationEndpointConfigurer.getRequestMatcher());
|
||||
}
|
||||
this.requestMatcher = !requestMatchers.isEmpty() ? new OrRequestMatcher(requestMatchers) : request -> false;
|
||||
this.requestMatcher = requestMatchers.size() > 1 ? new OrRequestMatcher(requestMatchers) : requestMatchers.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
this.userInfoEndpointConfigurer.configure(builder);
|
||||
if (this.clientRegistrationEndpointConfigurer != null) {
|
||||
this.clientRegistrationEndpointConfigurer.configure(builder);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see OidcConfigurer#userInfoEndpoint
|
||||
* @see OidcUserInfoEndpointFilter
|
||||
*/
|
||||
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private Function<OAuth2AuthenticationContext, OidcUserInfo> userInfoMapper;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OidcUserInfoEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Function} used to extract claims from an {@link OAuth2AuthenticationContext}
|
||||
* to an instance of {@link OidcUserInfo}.
|
||||
*
|
||||
* <p>
|
||||
* The {@link OAuth2AuthenticationContext} gives the mapper access to the {@link OidcUserInfoAuthenticationToken}.
|
||||
* In addition, the following context attributes are supported:
|
||||
* <ul>
|
||||
* <li>{@code OAuth2Token.class} - The {@link OAuth2Token} containing the bearer token used to make the request.</li>
|
||||
* <li>{@code OAuth2Authorization.class} - The {@link OAuth2Authorization} containing the {@link OidcIdToken} and
|
||||
* {@link OAuth2AccessToken} associated with the bearer token used to make the request.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param userInfoMapper the {@link Function} used to extract claims from an {@link OAuth2AuthenticationContext} to an instance of {@link OidcUserInfo}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer userInfoMapper(Function<OAuth2AuthenticationContext, OidcUserInfo> userInfoMapper) {
|
||||
this.userInfoMapper = userInfoMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void init(B builder) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
String userInfoEndpointUri = providerSettings.getOidcUserInfoEndpoint();
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
|
||||
|
||||
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
|
||||
new OidcUserInfoAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(builder));
|
||||
if (this.userInfoMapper != null) {
|
||||
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
|
||||
}
|
||||
builder.authenticationProvider(postProcess(oidcUserInfoAuthenticationProvider));
|
||||
}
|
||||
|
||||
@Override
|
||||
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
|
||||
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
|
||||
|
||||
OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getOidcUserInfoEndpoint());
|
||||
builder.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,44 +15,46 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc.http.converter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link HttpMessageConverter} for an {@link OidcUserInfo OIDC User Info Response}.
|
||||
* A {@link HttpMessageConverter} for an {@link OidcUserInfo OpenID Connect UserInfo Request and Response}.
|
||||
*
|
||||
* @author Ido Salomon
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see AbstractHttpMessageConverter
|
||||
* @see OidcUserInfo
|
||||
* @since 0.1.1
|
||||
*/
|
||||
public class OidcUserInfoHttpMessageConverter extends AbstractHttpMessageConverter<OidcUserInfo> {
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
};
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
private final GenericHttpMessageConverter<Object> jsonMessageConverter =
|
||||
HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
private Converter<Map<String, Object>, OidcUserInfo> oidcUserInfoConverter = new OidcUserInfoConverter();
|
||||
private Converter<OidcUserInfo, Map<String, Object>> oidcUserInfoParametersConverter = OidcUserInfo::getClaims;
|
||||
private Converter<Map<String, Object>, OidcUserInfo> userInfoConverter = new MapOidcUserInfoConverter();
|
||||
private Converter<OidcUserInfo, Map<String, Object>> userInfoParametersConverter = OidcUserInfo::getClaims;
|
||||
|
||||
public OidcUserInfoHttpMessageConverter() {
|
||||
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
||||
@@ -68,12 +70,12 @@ public class OidcUserInfoHttpMessageConverter extends AbstractHttpMessageConvert
|
||||
protected OidcUserInfo readInternal(Class<? extends OidcUserInfo> clazz, HttpInputMessage inputMessage)
|
||||
throws HttpMessageNotReadableException {
|
||||
try {
|
||||
Map<String, Object> oidcUserInfoParameters =
|
||||
Map<String, Object> userInfoParameters =
|
||||
(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
|
||||
return this.oidcUserInfoConverter.convert(oidcUserInfoParameters);
|
||||
return this.userInfoConverter.convert(userInfoParameters);
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException(
|
||||
"An error occurred reading the OIDC User Info: " + ex.getMessage(), ex, inputMessage);
|
||||
"An error occurred reading the UserInfo: " + ex.getMessage(), ex, inputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,77 +83,82 @@ public class OidcUserInfoHttpMessageConverter extends AbstractHttpMessageConvert
|
||||
protected void writeInternal(OidcUserInfo oidcUserInfo, HttpOutputMessage outputMessage)
|
||||
throws HttpMessageNotWritableException {
|
||||
try {
|
||||
Map<String, Object> oidcUserInfoResponseParameters =
|
||||
this.oidcUserInfoParametersConverter.convert(oidcUserInfo);
|
||||
Map<String, Object> userInfoResponseParameters =
|
||||
this.userInfoParametersConverter.convert(oidcUserInfo);
|
||||
this.jsonMessageConverter.write(
|
||||
oidcUserInfoResponseParameters,
|
||||
userInfoResponseParameters,
|
||||
STRING_OBJECT_MAP.getType(),
|
||||
MediaType.APPLICATION_JSON,
|
||||
outputMessage
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotWritableException(
|
||||
"An error occurred writing the OIDC User Info response: " + ex.getMessage(), ex);
|
||||
"An error occurred writing the UserInfo response: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting the OIDC User Info parameters
|
||||
* Sets the {@link Converter} used for converting the UserInfo parameters
|
||||
* to an {@link OidcUserInfo}.
|
||||
*
|
||||
* @param oidcUserInfoConverter the {@link Converter} used for converting to an
|
||||
* {@link OidcUserInfo}
|
||||
* @param userInfoConverter the {@link Converter} used for converting to an
|
||||
* {@link OidcUserInfo}
|
||||
*/
|
||||
public final void setOidcUserInfoConverter(Converter<Map<String, Object>, OidcUserInfo> oidcUserInfoConverter) {
|
||||
Assert.notNull(oidcUserInfoConverter, "oidcUserInfoConverter cannot be null");
|
||||
this.oidcUserInfoConverter = oidcUserInfoConverter;
|
||||
public final void setUserInfoConverter(Converter<Map<String, Object>, OidcUserInfo> userInfoConverter) {
|
||||
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
|
||||
this.userInfoConverter = userInfoConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting the {@link OidcUserInfo} to a
|
||||
* {@code Map} representation of the OIDC User Info.
|
||||
* {@code Map} representation of the UserInfo.
|
||||
*
|
||||
* @param oidcUserInfoParametersConverter the {@link Converter} used for converting to a
|
||||
* {@code Map} representation of the OIDC User Info
|
||||
* @param userInfoParametersConverter the {@link Converter} used for converting to a
|
||||
* {@code Map} representation of the UserInfo
|
||||
*/
|
||||
public final void setOidcUserInfoParametersConverter(
|
||||
Converter<OidcUserInfo, Map<String, Object>> oidcUserInfoParametersConverter) {
|
||||
Assert.notNull(oidcUserInfoParametersConverter, "oidcUserInfoParametersConverter cannot be null");
|
||||
this.oidcUserInfoParametersConverter = oidcUserInfoParametersConverter;
|
||||
public final void setUserInfoParametersConverter(
|
||||
Converter<OidcUserInfo, Map<String, Object>> userInfoParametersConverter) {
|
||||
Assert.notNull(userInfoParametersConverter, "userInfoParametersConverter cannot be null");
|
||||
this.userInfoParametersConverter = userInfoParametersConverter;
|
||||
}
|
||||
|
||||
private static final class OidcUserInfoConverter implements Converter<Map<String, Object>, OidcUserInfo> {
|
||||
private static final class MapOidcUserInfoConverter implements Converter<Map<String, Object>, OidcUserInfo> {
|
||||
|
||||
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
|
||||
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
|
||||
private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class);
|
||||
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
|
||||
private static final TypeDescriptor STRING_OBJECT_MAP_DESCRIPTOR = TypeDescriptor.map(Map.class, STRING_TYPE_DESCRIPTOR, OBJECT_TYPE_DESCRIPTOR);
|
||||
private final ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
private OidcUserInfoConverter() {
|
||||
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
|
||||
private MapOidcUserInfoConverter() {
|
||||
Converter<Object, ?> booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> mapConverter = getConverter(STRING_OBJECT_MAP_DESCRIPTOR);
|
||||
|
||||
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
|
||||
claimConverters.put(StandardClaimNames.SUB, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PROFILE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.ADDRESS, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.BIRTHDATE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.EMAIL, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimConverters.put(StandardClaimNames.NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.GIVEN_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.MIDDLE_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.FAMILY_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.MIDDLE_NAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.NICKNAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PREFERRED_USERNAME, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.LOCALE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.GENDER, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PHONE_NUMBER, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PROFILE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PICTURE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.ZONEINFO, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.WEBSITE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.UPDATED_AT, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.EMAIL, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimConverters.put(StandardClaimNames.GENDER, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.BIRTHDATE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.ZONEINFO, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.LOCALE, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PHONE_NUMBER, stringConverter);
|
||||
claimConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
|
||||
claimConverters.put(StandardClaimNames.ADDRESS, mapConverter);
|
||||
claimConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
|
||||
|
||||
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,11 @@ public final class ConfigurationSettingNames {
|
||||
*/
|
||||
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-client-registration-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OpenID Connect 1.0 UserInfo endpoint.
|
||||
*/
|
||||
public static final String OIDC_USER_INFO_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-user-info-endpoint");
|
||||
|
||||
private Provider() {
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,15 @@ public final class ProviderSettings extends AbstractSettings {
|
||||
return getSetting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OpenID Connect 1.0 UserInfo endpoint. The default is {@code /userinfo}.
|
||||
*
|
||||
* @return the OpenID Connect 1.0 User Info endpoint
|
||||
*/
|
||||
public String getOidcUserInfoEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the default settings.
|
||||
*
|
||||
@@ -109,7 +118,8 @@ public final class ProviderSettings extends AbstractSettings {
|
||||
.jwkSetEndpoint("/oauth2/jwks")
|
||||
.tokenRevocationEndpoint("/oauth2/revoke")
|
||||
.tokenIntrospectionEndpoint("/oauth2/introspect")
|
||||
.oidcClientRegistrationEndpoint("/connect/register");
|
||||
.oidcClientRegistrationEndpoint("/connect/register")
|
||||
.oidcUserInfoEndpoint("/userinfo");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,6 +212,16 @@ public final class ProviderSettings extends AbstractSettings {
|
||||
return setting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OpenID Connect 1.0 UserInfo endpoint.
|
||||
*
|
||||
* @param oidcUserInfoEndpoint the OpenID Connect 1.0 User Info endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder oidcUserInfoEndpoint(String oidcUserInfoEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT, oidcUserInfoEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link ProviderSettings}.
|
||||
*
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.springframework.security.oauth2.server.authorization.oidc;
|
||||
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
|
||||
public class DefaultUserInfoClaimsMapper implements UserInfoClaimsMapper {
|
||||
|
||||
public OidcUserInfo map(Object principal) {
|
||||
return null; // TODO
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.springframework.security.oauth2.server.authorization.oidc;
|
||||
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
|
||||
public interface UserInfoClaimsMapper {
|
||||
|
||||
OidcUserInfo map(Object principal);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfo">5.3. UserInfo Endpoint</a>
|
||||
*/
|
||||
public final class OidcUserInfoAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
private Function<OAuth2AuthenticationContext, OidcUserInfo> userInfoMapper = new DefaultOidcUserInfoMapper();
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcUserInfoAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public OidcUserInfoAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OidcUserInfoAuthenticationToken userInfoAuthentication =
|
||||
(OidcUserInfoAuthenticationToken) authentication;
|
||||
|
||||
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
|
||||
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(userInfoAuthentication.getPrincipal().getClass())) {
|
||||
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) userInfoAuthentication.getPrincipal();
|
||||
}
|
||||
if (accessTokenAuthentication == null || !accessTokenAuthentication.isAuthenticated()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
|
||||
if (!authorizedAccessToken.isActive()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
if (!authorizedAccessToken.getToken().getScopes().contains(OidcScopes.OPENID)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OidcIdToken> idToken = authorization.getToken(OidcIdToken.class);
|
||||
if (idToken == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
Map<Object, Object> context = new HashMap<>();
|
||||
context.put(OAuth2Token.class, accessTokenAuthentication.getToken());
|
||||
context.put(OAuth2Authorization.class, authorization);
|
||||
OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext(
|
||||
userInfoAuthentication, context);
|
||||
|
||||
OidcUserInfo userInfo = this.userInfoMapper.apply(authenticationContext);
|
||||
return new OidcUserInfoAuthenticationToken(accessTokenAuthentication, userInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OidcUserInfoAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Function} used when mapping from an {@link OAuth2AuthenticationContext}
|
||||
* to an instance of {@link OidcUserInfo} for the UserInfo response.
|
||||
*
|
||||
* <p>
|
||||
* The {@link OAuth2AuthenticationContext} gives the mapper access to the {@link OidcUserInfoAuthenticationToken}.
|
||||
* In addition, the following context attributes are supported:
|
||||
* <ul>
|
||||
* <li>{@code OAuth2Token.class} - The {@link OAuth2Token} containing the bearer token used to make the request.</li>
|
||||
* <li>{@code OAuth2Authorization.class} - The {@link OAuth2Authorization} containing the {@link OidcIdToken} and
|
||||
* {@link OAuth2AccessToken} associated with the bearer token used to make the request.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param userInfoMapper the {@link Function} used when mapping from an {@link OAuth2AuthenticationContext}
|
||||
*/
|
||||
public void setUserInfoMapper(Function<OAuth2AuthenticationContext, OidcUserInfo> userInfoMapper) {
|
||||
Assert.notNull(userInfoMapper, "userInfoMapper cannot be null");
|
||||
this.userInfoMapper = userInfoMapper;
|
||||
}
|
||||
|
||||
private static final class DefaultOidcUserInfoMapper implements Function<OAuth2AuthenticationContext, OidcUserInfo> {
|
||||
|
||||
private static final List<String> EMAIL_CLAIMS = Arrays.asList(
|
||||
StandardClaimNames.EMAIL,
|
||||
StandardClaimNames.EMAIL_VERIFIED
|
||||
);
|
||||
private static final List<String> PHONE_CLAIMS = Arrays.asList(
|
||||
StandardClaimNames.PHONE_NUMBER,
|
||||
StandardClaimNames.PHONE_NUMBER_VERIFIED
|
||||
);
|
||||
private static final List<String> PROFILE_CLAIMS = Arrays.asList(
|
||||
StandardClaimNames.NAME,
|
||||
StandardClaimNames.FAMILY_NAME,
|
||||
StandardClaimNames.GIVEN_NAME,
|
||||
StandardClaimNames.MIDDLE_NAME,
|
||||
StandardClaimNames.NICKNAME,
|
||||
StandardClaimNames.PREFERRED_USERNAME,
|
||||
StandardClaimNames.PROFILE,
|
||||
StandardClaimNames.PICTURE,
|
||||
StandardClaimNames.WEBSITE,
|
||||
StandardClaimNames.GENDER,
|
||||
StandardClaimNames.BIRTHDATE,
|
||||
StandardClaimNames.ZONEINFO,
|
||||
StandardClaimNames.LOCALE,
|
||||
StandardClaimNames.UPDATED_AT
|
||||
);
|
||||
|
||||
@Override
|
||||
public OidcUserInfo apply(OAuth2AuthenticationContext authenticationContext) {
|
||||
OAuth2Authorization authorization = authenticationContext.get(OAuth2Authorization.class);
|
||||
OidcIdToken idToken = authorization.getToken(OidcIdToken.class).getToken();
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
Map<String, Object> scopeRequestedClaims = getClaimsRequestedByScope(idToken.getClaims(),
|
||||
accessToken.getScopes());
|
||||
|
||||
return new OidcUserInfo(scopeRequestedClaims);
|
||||
}
|
||||
|
||||
private Map<String, Object> getClaimsRequestedByScope(Map<String, Object> claims, Set<String> requestedScopes) {
|
||||
Set<String> scopeRequestedClaimNames = new HashSet<>(32);
|
||||
scopeRequestedClaimNames.add(StandardClaimNames.SUB);
|
||||
|
||||
if (requestedScopes.contains(OidcScopes.ADDRESS)) {
|
||||
scopeRequestedClaimNames.add(StandardClaimNames.ADDRESS);
|
||||
}
|
||||
if (requestedScopes.contains(OidcScopes.EMAIL)) {
|
||||
scopeRequestedClaimNames.addAll(EMAIL_CLAIMS);
|
||||
}
|
||||
if (requestedScopes.contains(OidcScopes.PHONE)) {
|
||||
scopeRequestedClaimNames.addAll(PHONE_CLAIMS);
|
||||
}
|
||||
if (requestedScopes.contains(OidcScopes.PROFILE)) {
|
||||
scopeRequestedClaimNames.addAll(PROFILE_CLAIMS);
|
||||
}
|
||||
|
||||
Map<String, Object> requestedClaims = new HashMap<>(claims);
|
||||
requestedClaims.keySet().removeIf(claimName -> !scopeRequestedClaimNames.contains(claimName));
|
||||
|
||||
return requestedClaims;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.Version;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation used for OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OidcUserInfo
|
||||
* @see OidcUserInfoAuthenticationProvider
|
||||
*/
|
||||
public class OidcUserInfoAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
|
||||
private final Authentication principal;
|
||||
private final OidcUserInfo userInfo;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcUserInfoAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param principal the authenticated principal
|
||||
*/
|
||||
public OidcUserInfoAuthenticationToken(Authentication principal) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
this.principal = principal;
|
||||
this.userInfo = null;
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcUserInfoAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param principal the authenticated principal
|
||||
* @param userInfo the UserInfo claims
|
||||
*/
|
||||
public OidcUserInfoAuthenticationToken(Authentication principal, OidcUserInfo userInfo) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
Assert.notNull(userInfo, "userInfo cannot be null");
|
||||
this.principal = principal;
|
||||
this.userInfo = userInfo;
|
||||
setAuthenticated(principal.isAuthenticated());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UserInfo claims.
|
||||
*
|
||||
* @return the UserInfo claims
|
||||
*/
|
||||
public OidcUserInfo getUserInfo() {
|
||||
return this.userInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,11 +16,6 @@
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
@@ -28,116 +23,120 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.http.converter.OidcUserInfoHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.UserInfoClaimsMapper;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that processes OpenID User Info requests.
|
||||
* A {@code Filter} that processes OpenID Connect 1.0 UserInfo Requests.
|
||||
*
|
||||
* @author Ido Salomon
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.2.1
|
||||
* @see OidcUserInfo
|
||||
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest">5.3.1. UserInfo Request</a>
|
||||
* @since 0.1.1
|
||||
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfo">5.3. UserInfo Endpoint</a>
|
||||
*/
|
||||
public class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
|
||||
public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* The default endpoint {@code URI} for OpenID User Info requests.
|
||||
* The default endpoint {@code URI} for OpenID Connect 1.0 UserInfo Requests.
|
||||
*/
|
||||
public static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
|
||||
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
|
||||
|
||||
private final RequestMatcher requestMatcher;
|
||||
private final OidcUserInfoHttpMessageConverter oidcUserInfoHttpMessageConverter =
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RequestMatcher userInfoEndpointMatcher;
|
||||
|
||||
private final HttpMessageConverter<OidcUserInfo> userInfoHttpMessageConverter =
|
||||
new OidcUserInfoHttpMessageConverter();
|
||||
private final UserInfoClaimsMapper userInfoClaimsMapper;
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
public OidcUserInfoEndpointFilter(UserInfoClaimsMapper userInfoClaimsMapper) {
|
||||
AntPathRequestMatcher userInfoGetMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OIDC_USER_INFO_ENDPOINT_URI,
|
||||
HttpMethod.GET.name()
|
||||
);
|
||||
AntPathRequestMatcher userInfoPostMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OIDC_USER_INFO_ENDPOINT_URI,
|
||||
HttpMethod.POST.name()
|
||||
);
|
||||
this.requestMatcher = new OrRequestMatcher(userInfoGetMatcher, userInfoPostMatcher);
|
||||
this.userInfoClaimsMapper = userInfoClaimsMapper;
|
||||
/**
|
||||
* Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param authenticationManager the authentication manager
|
||||
*/
|
||||
public OidcUserInfoEndpointFilter(AuthenticationManager authenticationManager) {
|
||||
this(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters.
|
||||
*
|
||||
* @param authenticationManager the authentication manager
|
||||
* @param userInfoEndpointUri the endpoint {@code URI} for OpenID Connect 1.0 UserInfo Requests
|
||||
*/
|
||||
public OidcUserInfoEndpointFilter(AuthenticationManager authenticationManager, String userInfoEndpointUri) {
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
Assert.hasText(userInfoEndpointUri, "userInfoEndpointUri cannot be empty");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.userInfoEndpointMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!this.requestMatcher.matches(request)) {
|
||||
if (!this.userInfoEndpointMatcher.matches(request)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
Object authenticationDetails = authentication.getDetails();
|
||||
Object principal = authentication.getPrincipal();
|
||||
OidcUserInfo oidcUserInfo = userInfoClaimsMapper.map(principal);
|
||||
try {
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authenticationDetails instanceof OAuth2AccessToken) {
|
||||
oidcUserInfo = getUserInfoClaimsRequestedByScope(oidcUserInfo, ((OAuth2AccessToken) authenticationDetails).getScopes());
|
||||
} else {
|
||||
oidcUserInfo = OidcUserInfo.builder()
|
||||
.subject(oidcUserInfo.getSubject())
|
||||
.build();
|
||||
OidcUserInfoAuthenticationToken userInfoAuthentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken userInfoAuthenticationResult =
|
||||
(OidcUserInfoAuthenticationToken) this.authenticationManager.authenticate(userInfoAuthentication);
|
||||
|
||||
sendUserInfoResponse(response, userInfoAuthenticationResult.getUserInfo());
|
||||
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
sendErrorResponse(response, ex.getError());
|
||||
} catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"OpenID Connect 1.0 UserInfo Error: " + ex.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError");
|
||||
sendErrorResponse(response, error);
|
||||
} finally {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendUserInfoResponse(HttpServletResponse response, OidcUserInfo userInfo) throws IOException {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.oidcUserInfoHttpMessageConverter.write(
|
||||
oidcUserInfo, MediaType.APPLICATION_JSON, httpResponse);
|
||||
this.userInfoHttpMessageConverter.write(userInfo, MediaType.APPLICATION_JSON, httpResponse);
|
||||
}
|
||||
|
||||
private OidcUserInfo getUserInfoClaimsRequestedByScope(OidcUserInfo userInfo, Set<String> scopes) {
|
||||
Set<String> scopeRequestedClaimNames = getScopeRequestedClaimNames(scopes);
|
||||
|
||||
Map<String, Object> scopeRequestedClaims = userInfo.getClaims().entrySet().stream()
|
||||
.filter(claim -> scopeRequestedClaimNames.contains(claim.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
return new OidcUserInfo(scopeRequestedClaims);
|
||||
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
|
||||
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) {
|
||||
httpStatus = HttpStatus.UNAUTHORIZED;
|
||||
} else if (error.getErrorCode().equals(OAuth2ErrorCodes.INSUFFICIENT_SCOPE)) {
|
||||
httpStatus = HttpStatus.FORBIDDEN;
|
||||
}
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(httpStatus);
|
||||
this.errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
|
||||
private Set<String> getScopeRequestedClaimNames(Set<String> scopes) {
|
||||
Set<String> scopeRequestedClaimNames = new HashSet<>(Arrays.asList(StandardClaimNames.SUB));
|
||||
Set<String> profileClaimNames = new HashSet<>(Arrays.asList(StandardClaimNames.NAME,
|
||||
StandardClaimNames.FAMILY_NAME, StandardClaimNames.GIVEN_NAME, StandardClaimNames.MIDDLE_NAME,
|
||||
StandardClaimNames.NICKNAME, StandardClaimNames.PREFERRED_USERNAME, StandardClaimNames.PROFILE,
|
||||
StandardClaimNames.PICTURE, StandardClaimNames.WEBSITE, StandardClaimNames.GENDER,
|
||||
StandardClaimNames.BIRTHDATE, StandardClaimNames.ZONEINFO, StandardClaimNames.LOCALE, StandardClaimNames.UPDATED_AT));
|
||||
Set<String> emailClaimNames = new HashSet<>(Arrays.asList(StandardClaimNames.EMAIL, StandardClaimNames.EMAIL_VERIFIED));
|
||||
String addressClaimName = StandardClaimNames.ADDRESS;
|
||||
Set<String> phoneClaimNames = new HashSet<>(Arrays.asList(StandardClaimNames.PHONE_NUMBER, StandardClaimNames.PHONE_NUMBER_VERIFIED));
|
||||
|
||||
if (scopes.contains(OidcScopes.ADDRESS)) {
|
||||
scopeRequestedClaimNames.add(addressClaimName);
|
||||
}
|
||||
if (scopes.contains(OidcScopes.EMAIL)) {
|
||||
scopeRequestedClaimNames.addAll(emailClaimNames);
|
||||
}
|
||||
if (scopes.contains(OidcScopes.PHONE)) {
|
||||
scopeRequestedClaimNames.addAll(phoneClaimNames);
|
||||
}
|
||||
if (scopes.contains(OidcScopes.PROFILE)) {
|
||||
scopeRequestedClaimNames.addAll(profileClaimNames);
|
||||
}
|
||||
|
||||
return scopeRequestedClaimNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
|
||||
import static org.springframework.test.web.servlet.ResultMatcher.matchAll;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Integration tests for the OpenID Connect 1.0 UserInfo endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoTests {
|
||||
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private JwtEncoder jwtEncoder;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoRequestGetThenUserInfoResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(userInfoResponse());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoRequestPostThenUserInfoResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(post(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(userInfoResponse());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenSignedJwtAndCustomUserInfoMapperThenUserInfoResponse() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(userInfoResponse());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static ResultMatcher userInfoResponse() {
|
||||
// @formatter:off
|
||||
return matchAll(
|
||||
jsonPath("sub").value("user1"),
|
||||
jsonPath("name").value("First Last"),
|
||||
jsonPath("given_name").value("First"),
|
||||
jsonPath("family_name").value("Last"),
|
||||
jsonPath("middle_name").value("Middle"),
|
||||
jsonPath("nickname").value("User"),
|
||||
jsonPath("preferred_username").value("user"),
|
||||
jsonPath("profile").value("https://example.com/user1"),
|
||||
jsonPath("picture").value("https://example.com/user1.jpg"),
|
||||
jsonPath("website").value("https://example.com"),
|
||||
jsonPath("email").value("user1@example.com"),
|
||||
jsonPath("email_verified").value("true"),
|
||||
jsonPath("gender").value("female"),
|
||||
jsonPath("birthdate").value("1970-01-01"),
|
||||
jsonPath("zoneinfo").value("Europe/Paris"),
|
||||
jsonPath("locale").value("en-US"),
|
||||
jsonPath("phone_number").value("+1 (604) 555-1234;ext=5678"),
|
||||
jsonPath("phone_number_verified").value("false"),
|
||||
jsonPath("address").value("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"),
|
||||
jsonPath("updated_at").value("1970-01-01T00:00:00Z")
|
||||
);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private OAuth2Authorization createAuthorization() {
|
||||
JoseHeader headers = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
// @formatter:off
|
||||
JwtClaimsSet claimSet = JwtClaimsSet.builder()
|
||||
.claims(claims -> claims.putAll(createUserInfo().getClaims()))
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.jwtEncoder.encode(headers, claimSet);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Set<String> scopes = new HashSet<>(Arrays.asList(
|
||||
OidcScopes.OPENID, OidcScopes.ADDRESS, OidcScopes.EMAIL, OidcScopes.PHONE, OidcScopes.PROFILE));
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||
OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), now, now.plusSeconds(300), scopes);
|
||||
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
|
||||
.claims(claims -> claims.putAll(createUserInfo().getClaims()))
|
||||
.build();
|
||||
|
||||
return TestOAuth2Authorizations.authorization()
|
||||
.accessToken(accessToken)
|
||||
.token(idToken)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
// @formatter:off
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class CustomUserInfoConfiguration extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
// Custom User Info Mapper that retrieves claims from a signed JWT
|
||||
Function<OAuth2AuthenticationContext, OidcUserInfo> userInfoMapper = context -> {
|
||||
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
|
||||
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
|
||||
|
||||
return new OidcUserInfo(principal.getToken().getClaims());
|
||||
};
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.apply(authorizationServerConfigurer)
|
||||
.oidc(oidc -> oidc
|
||||
.userInfoEndpoint(userInfo -> userInfo
|
||||
.userInfoMapper(userInfoMapper)
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.apply(authorizationServerConfigurer);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService() {
|
||||
return new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return new ImmutableJWKSet<>(new JWKSet(TestJwks.DEFAULT_RSA_JWK));
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return new NimbusJwsEncoder(jwkSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc.http.converter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.mock.http.MockHttpOutputMessage;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoHttpMessageConverter}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoHttpMessageConverterTests {
|
||||
private final OidcUserInfoHttpMessageConverter messageConverter = new OidcUserInfoHttpMessageConverter();
|
||||
|
||||
@Test
|
||||
public void supportsWhenOidcUserInfoThenTrue() {
|
||||
assertThat(this.messageConverter.supports(OidcUserInfo.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setUserInfoConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setUserInfoConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setUserInfoParametersConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setUserInfoParametersConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenValidParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n" +
|
||||
" \"sub\": \"user1\",\n" +
|
||||
" \"name\": \"First Last\",\n" +
|
||||
" \"given_name\": \"First\",\n" +
|
||||
" \"family_name\": \"Last\",\n" +
|
||||
" \"middle_name\": \"Middle\",\n" +
|
||||
" \"nickname\": \"User\",\n" +
|
||||
" \"preferred_username\": \"user\",\n" +
|
||||
" \"profile\": \"https://example.com/user1\",\n" +
|
||||
" \"picture\": \"https://example.com/user1.jpg\",\n" +
|
||||
" \"website\": \"https://example.com\",\n" +
|
||||
" \"email\": \"user1@example.com\",\n" +
|
||||
" \"email_verified\": \"true\",\n" +
|
||||
" \"gender\": \"female\",\n" +
|
||||
" \"birthdate\": \"1970-01-01\",\n" +
|
||||
" \"zoneinfo\": \"Europe/Paris\",\n" +
|
||||
" \"locale\": \"en-US\",\n" +
|
||||
" \"phone_number\": \"+1 (604) 555-1234;ext=5678\",\n" +
|
||||
" \"phone_number_verified\": \"false\",\n" +
|
||||
" \"address\": {\n" +
|
||||
" \"formatted\": \"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\",\n" +
|
||||
" \"street_address\": \"Champ de Mars\\n5 Av. Anatole France\",\n" +
|
||||
" \"locality\": \"Paris\",\n" +
|
||||
" \"postal_code\": \"75007\",\n" +
|
||||
" \"country\": \"France\"\n" +
|
||||
" },\n" +
|
||||
" \"updated_at\": 1607633867\n" +
|
||||
"}\n";
|
||||
// @formatter:on
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(userInfoResponse.getBytes(), HttpStatus.OK);
|
||||
OidcUserInfo oidcUserInfo = this.messageConverter.readInternal(OidcUserInfo.class, response);
|
||||
|
||||
assertThat(oidcUserInfo.getSubject()).isEqualTo("user1");
|
||||
assertThat(oidcUserInfo.getFullName()).isEqualTo("First Last");
|
||||
assertThat(oidcUserInfo.getGivenName()).isEqualTo("First");
|
||||
assertThat(oidcUserInfo.getFamilyName()).isEqualTo("Last");
|
||||
assertThat(oidcUserInfo.getMiddleName()).isEqualTo("Middle");
|
||||
assertThat(oidcUserInfo.getNickName()).isEqualTo("User");
|
||||
assertThat(oidcUserInfo.getPreferredUsername()).isEqualTo("user");
|
||||
assertThat(oidcUserInfo.getProfile()).isEqualTo("https://example.com/user1");
|
||||
assertThat(oidcUserInfo.getPicture()).isEqualTo("https://example.com/user1.jpg");
|
||||
assertThat(oidcUserInfo.getWebsite()).isEqualTo("https://example.com");
|
||||
assertThat(oidcUserInfo.getEmail()).isEqualTo("user1@example.com");
|
||||
assertThat(oidcUserInfo.getEmailVerified()).isTrue();
|
||||
assertThat(oidcUserInfo.getGender()).isEqualTo("female");
|
||||
assertThat(oidcUserInfo.getBirthdate()).isEqualTo("1970-01-01");
|
||||
assertThat(oidcUserInfo.getZoneInfo()).isEqualTo("Europe/Paris");
|
||||
assertThat(oidcUserInfo.getLocale()).isEqualTo("en-US");
|
||||
assertThat(oidcUserInfo.getPhoneNumber()).isEqualTo("+1 (604) 555-1234;ext=5678");
|
||||
assertThat(oidcUserInfo.getPhoneNumberVerified()).isFalse();
|
||||
assertThat(oidcUserInfo.getAddress().getFormatted()).isEqualTo("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance");
|
||||
assertThat(oidcUserInfo.getAddress().getStreetAddress()).isEqualTo("Champ de Mars\n5 Av. Anatole France");
|
||||
assertThat(oidcUserInfo.getAddress().getLocality()).isEqualTo("Paris");
|
||||
assertThat(oidcUserInfo.getAddress().getPostalCode()).isEqualTo("75007");
|
||||
assertThat(oidcUserInfo.getAddress().getCountry()).isEqualTo("France");
|
||||
assertThat(oidcUserInfo.getUpdatedAt()).isEqualTo(Instant.ofEpochSecond(1607633867));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenFailingConverterThenThrowException() {
|
||||
String errorMessage = "this is not a valid converter";
|
||||
this.messageConverter.setUserInfoConverter(source -> {
|
||||
throw new RuntimeException(errorMessage);
|
||||
});
|
||||
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.readInternal(OidcUserInfo.class, response))
|
||||
.withMessageContaining("An error occurred reading the UserInfo")
|
||||
.withMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenInvalidResponseThenThrowException() {
|
||||
String providerConfigurationResponse = "{}";
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK);
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.readInternal(OidcUserInfo.class, response))
|
||||
.withMessageContaining("An error occurred reading the UserInfo")
|
||||
.withMessageContaining("claims cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInternalWhenOidcUserInfoThenSuccess() {
|
||||
OidcUserInfo userInfo = createUserInfo();
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
|
||||
this.messageConverter.writeInternal(userInfo, outputMessage);
|
||||
|
||||
String userInfoResponse = outputMessage.getBodyAsString();
|
||||
assertThat(userInfoResponse).contains("\"sub\":\"user1\"");
|
||||
assertThat(userInfoResponse).contains("\"name\":\"First Last\"");
|
||||
assertThat(userInfoResponse).contains("\"given_name\":\"First\"");
|
||||
assertThat(userInfoResponse).contains("\"family_name\":\"Last\"");
|
||||
assertThat(userInfoResponse).contains("\"middle_name\":\"Middle\"");
|
||||
assertThat(userInfoResponse).contains("\"nickname\":\"User\"");
|
||||
assertThat(userInfoResponse).contains("\"preferred_username\":\"user\"");
|
||||
assertThat(userInfoResponse).contains("\"profile\":\"https://example.com/user1\"");
|
||||
assertThat(userInfoResponse).contains("\"picture\":\"https://example.com/user1.jpg\"");
|
||||
assertThat(userInfoResponse).contains("\"website\":\"https://example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email\":\"user1@example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email_verified\":true");
|
||||
assertThat(userInfoResponse).contains("\"gender\":\"female\"");
|
||||
assertThat(userInfoResponse).contains("\"birthdate\":\"1970-01-01\"");
|
||||
assertThat(userInfoResponse).contains("\"zoneinfo\":\"Europe/Paris\"");
|
||||
assertThat(userInfoResponse).contains("\"locale\":\"en-US\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number\":\"+1 (604) 555-1234;ext=5678\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number_verified\":false");
|
||||
assertThat(userInfoResponse).contains("\"address\":");
|
||||
assertThat(userInfoResponse).contains("\"formatted\":\"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\"");
|
||||
assertThat(userInfoResponse).contains("\"updated_at\":1607633867");
|
||||
assertThat(userInfoResponse).contains("\"custom_claim\":\"value\"");
|
||||
assertThat(userInfoResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInternalWhenWriteFailsThenThrowsException() {
|
||||
String errorMessage = "this is not a valid converter";
|
||||
Converter<OidcUserInfo, Map<String, Object>> failingConverter = source -> {
|
||||
throw new RuntimeException(errorMessage);
|
||||
};
|
||||
this.messageConverter.setUserInfoParametersConverter(failingConverter);
|
||||
|
||||
OidcUserInfo userInfo = createUserInfo();
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotWritableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.writeInternal(userInfo, outputMessage))
|
||||
.withMessageContaining("An error occurred writing the UserInfo response")
|
||||
.withMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.claim("phone_number_verified", false)
|
||||
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
|
||||
.claim(StandardClaimNames.UPDATED_AT, Instant.ofEpochSecond(1607633867))
|
||||
.claim("custom_claim", "value")
|
||||
.claim("custom_collection_claim", Arrays.asList("value1", "value2"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class ProviderSettingsTests {
|
||||
assertThat(providerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
|
||||
assertThat(providerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
|
||||
assertThat(providerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
|
||||
assertThat(providerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,6 +49,7 @@ public class ProviderSettingsTests {
|
||||
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
|
||||
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
|
||||
String oidcClientRegistrationEndpoint = "/connect/v1/register";
|
||||
String oidcUserInfoEndpoint = "/connect/v1/userinfo";
|
||||
String issuer = "https://example.com:9000";
|
||||
|
||||
ProviderSettings providerSettings = ProviderSettings.builder()
|
||||
@@ -59,6 +61,7 @@ public class ProviderSettingsTests {
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint)
|
||||
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint)
|
||||
.oidcUserInfoEndpoint(oidcUserInfoEndpoint)
|
||||
.build();
|
||||
|
||||
assertThat(providerSettings.getIssuer()).isEqualTo(issuer);
|
||||
@@ -68,6 +71,7 @@ public class ProviderSettingsTests {
|
||||
assertThat(providerSettings.getTokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
|
||||
assertThat(providerSettings.getTokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
|
||||
assertThat(providerSettings.getOidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
|
||||
assertThat(providerSettings.getOidcUserInfoEndpoint()).isEqualTo(oidcUserInfoEndpoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,7 +81,7 @@ public class ProviderSettingsTests {
|
||||
.settings(settings -> settings.put("name2", "value2"))
|
||||
.build();
|
||||
|
||||
assertThat(providerSettings.getSettings()).hasSize(8);
|
||||
assertThat(providerSettings.getSettings()).hasSize(9);
|
||||
assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1");
|
||||
assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2");
|
||||
}
|
||||
@@ -124,6 +128,13 @@ public class ProviderSettingsTests {
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oidcUserInfoEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ProviderSettings.builder().oidcUserInfoEndpoint(null))
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoAuthenticationProvider}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoAuthenticationProviderTests {
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private OidcUserInfoAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authenticationProvider = new OidcUserInfoAuthenticationProvider(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoAuthenticationProvider(null))
|
||||
.withMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setUserInfoMapperWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authenticationProvider.setUserInfoMapper(null))
|
||||
.withMessage("userInfoMapper cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOidcUserInfoAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OidcUserInfoAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotOfExpectedTypeThenThrowOAuth2AuthenticationException() {
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(
|
||||
new UsernamePasswordAuthenticationToken(null, null));
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verifyNoInteractions(this.authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
principal.setAuthenticated(false);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verifyNoInteractions(this.authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
|
||||
authorization = OidcAuthenticationProviderUtils.invalidate(authorization,
|
||||
authorization.getAccessToken().getToken());
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(TestOAuth2Authorizations.authorization().build());
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenIdTokenNullThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
|
||||
.token(createAuthorization(tokenValue).getAccessToken().getToken())
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnUserInfo() {
|
||||
String tokenValue = "access-token";
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(createAuthorization(tokenValue));
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
OidcUserInfoAuthenticationToken authenticationResult =
|
||||
(OidcUserInfoAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||
|
||||
assertThat(authenticationResult.getPrincipal()).isEqualTo(principal);
|
||||
assertThat(authenticationResult.getCredentials()).isEqualTo("");
|
||||
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||
|
||||
OidcUserInfo userInfo = authenticationResult.getUserInfo();
|
||||
assertThat(userInfo.getClaims()).hasSize(20);
|
||||
assertThat(userInfo.getSubject()).isEqualTo("user1");
|
||||
assertThat(userInfo.getFullName()).isEqualTo("First Last");
|
||||
assertThat(userInfo.getGivenName()).isEqualTo("First");
|
||||
assertThat(userInfo.getFamilyName()).isEqualTo("Last");
|
||||
assertThat(userInfo.getMiddleName()).isEqualTo("Middle");
|
||||
assertThat(userInfo.getNickName()).isEqualTo("User");
|
||||
assertThat(userInfo.getPreferredUsername()).isEqualTo("user");
|
||||
assertThat(userInfo.getProfile()).isEqualTo("https://example.com/user1");
|
||||
assertThat(userInfo.getPicture()).isEqualTo("https://example.com/user1.jpg");
|
||||
assertThat(userInfo.getWebsite()).isEqualTo("https://example.com");
|
||||
assertThat(userInfo.getEmail()).isEqualTo("user1@example.com");
|
||||
assertThat(userInfo.getEmailVerified()).isEqualTo(true);
|
||||
assertThat(userInfo.getGender()).isEqualTo("female");
|
||||
assertThat(userInfo.getBirthdate()).isEqualTo("1970-01-01");
|
||||
assertThat(userInfo.getZoneInfo()).isEqualTo("Europe/Paris");
|
||||
assertThat(userInfo.getLocale()).isEqualTo("en-US");
|
||||
assertThat(userInfo.getPhoneNumber()).isEqualTo("+1 (604) 555-1234;ext=5678");
|
||||
assertThat(userInfo.getPhoneNumberVerified()).isEqualTo(false);
|
||||
assertThat(userInfo.getClaimAsString(StandardClaimNames.ADDRESS))
|
||||
.isEqualTo("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance");
|
||||
assertThat(userInfo.getUpdatedAt()).isEqualTo(Instant.parse("1970-01-01T00:00:00Z"));
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
private static OAuth2Authorization createAuthorization(String tokenValue) {
|
||||
Instant now = Instant.now();
|
||||
Set<String> scopes = new HashSet<>(Arrays.asList(
|
||||
OidcScopes.OPENID, OidcScopes.ADDRESS, OidcScopes.EMAIL, OidcScopes.PHONE, OidcScopes.PROFILE));
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||
OAuth2AccessToken.TokenType.BEARER, tokenValue, now, now.plusSeconds(300), scopes);
|
||||
OidcIdToken idToken = new OidcIdToken("id-token", now, now.plusSeconds(900), createUserInfo().getClaims());
|
||||
|
||||
return TestOAuth2Authorizations.authorization()
|
||||
.token(accessToken)
|
||||
.token(idToken)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static JwtAuthenticationToken createJwtAuthenticationToken(String tokenValue) {
|
||||
Instant now = Instant.now();
|
||||
// @formatter:off
|
||||
Jwt jwt = Jwt.withTokenValue(tokenValue)
|
||||
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
|
||||
.issuedAt(now)
|
||||
.expiresAt(now.plusSeconds(300))
|
||||
.claim(StandardClaimNames.SUB, "user")
|
||||
.build();
|
||||
// @formatter:on
|
||||
return new JwtAuthenticationToken(jwt, Collections.emptyList());
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoAuthenticationToken}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoAuthenticationTokenTests {
|
||||
@Test
|
||||
public void constructorWhenPrincipalNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoAuthenticationToken(null))
|
||||
.withMessage("principal cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenPrincipalProvidedThenCreated() {
|
||||
UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken(null, null);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
assertThat(authentication.getPrincipal()).isEqualTo(principal);
|
||||
assertThat(authentication.getUserInfo()).isNull();
|
||||
assertThat(authentication.isAuthenticated()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenPrincipalAndUserInfoProvidedThenCreated() {
|
||||
UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken(null, null);
|
||||
OidcUserInfo userInfo = new OidcUserInfo(Collections.singletonMap(StandardClaimNames.SUB, "user"));
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal, userInfo);
|
||||
assertThat(authentication.getPrincipal()).isEqualTo(principal);
|
||||
assertThat(authentication.getUserInfo()).isEqualTo(userInfo);
|
||||
assertThat(authentication.isAuthenticated()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.web;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoEndpointFilter}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoEndpointFilterTests {
|
||||
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoEndpointFilter(null))
|
||||
.withMessage("authenticationManager cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenUserInfoEndpointUriIsEmptyThenThrowIllegalArgumentException() {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoEndpointFilter(authenticationManager, ""))
|
||||
.withMessage("userInfoEndpointUri cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotUserInfoRequestThenNotProcessed() throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI);
|
||||
|
||||
String requestUri = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestPutThenNotProcessed() throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("PUT", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(authenticationManager);
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestGetThenSuccess() throws Exception {
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken();
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken authenticationResult = new OidcUserInfoAuthenticationToken(principal, createUserInfo());
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any())).thenReturn(authenticationResult);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(authenticationManager).authenticate(any());
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertUserInfoResponse(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestPostThenSuccess() throws Exception {
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken();
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal, createUserInfo());
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any())).thenReturn(authentication);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(authenticationManager).authenticate(any());
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertUserInfoResponse(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthenticationNullThenInvalidRequestError() throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any(Authentication.class)))
|
||||
.thenReturn(new UsernamePasswordAuthenticationToken("user", "password"));
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
OAuth2Error error = readError(response);
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
assertThat(error.getDescription()).isEqualTo("OpenID Connect 1.0 UserInfo Error: principal cannot be null");
|
||||
}
|
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
|
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
||||
response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
|
||||
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
|
||||
}
|
||||
|
||||
private JwtAuthenticationToken createJwtAuthenticationToken() {
|
||||
Instant now = Instant.now();
|
||||
// @formatter:off
|
||||
Jwt jwt = Jwt.withTokenValue("token")
|
||||
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
|
||||
.issuedAt(now)
|
||||
.expiresAt(now.plusSeconds(300))
|
||||
.claim(StandardClaimNames.SUB, "user")
|
||||
.build();
|
||||
// @formatter:on
|
||||
return new JwtAuthenticationToken(jwt, Collections.emptyList());
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void assertUserInfoResponse(String userInfoResponse) {
|
||||
assertThat(userInfoResponse).contains("\"sub\":\"user1\"");
|
||||
assertThat(userInfoResponse).contains("\"name\":\"First Last\"");
|
||||
assertThat(userInfoResponse).contains("\"given_name\":\"First\"");
|
||||
assertThat(userInfoResponse).contains("\"family_name\":\"Last\"");
|
||||
assertThat(userInfoResponse).contains("\"middle_name\":\"Middle\"");
|
||||
assertThat(userInfoResponse).contains("\"nickname\":\"User\"");
|
||||
assertThat(userInfoResponse).contains("\"preferred_username\":\"user\"");
|
||||
assertThat(userInfoResponse).contains("\"profile\":\"https://example.com/user1\"");
|
||||
assertThat(userInfoResponse).contains("\"picture\":\"https://example.com/user1.jpg\"");
|
||||
assertThat(userInfoResponse).contains("\"website\":\"https://example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email\":\"user1@example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email_verified\":true");
|
||||
assertThat(userInfoResponse).contains("\"gender\":\"female\"");
|
||||
assertThat(userInfoResponse).contains("\"birthdate\":\"1970-01-01\"");
|
||||
assertThat(userInfoResponse).contains("\"zoneinfo\":\"Europe/Paris\"");
|
||||
assertThat(userInfoResponse).contains("\"locale\":\"en-US\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number\":\"+1 (604) 555-1234;ext=5678\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number_verified\":\"false\"");
|
||||
assertThat(userInfoResponse).contains("\"address\":\"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\"");
|
||||
assertThat(userInfoResponse).contains("\"updated_at\":\"1970-01-01T00:00:00Z\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user