Browse Source

Implement User Info Endpoint

Closes gh-176
pull/471/head
Steve Riesenberg 4 years ago committed by Joe Grandja
parent
commit
8e8e6d1b17
  1. 19
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java
  2. 112
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoEndpointConfigurer.java
  3. 105
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcUserInfoHttpMessageConverter.java
  4. 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java
  5. 22
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java
  6. 11
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/DefaultUserInfoClaimsMapper.java
  7. 9
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/UserInfoClaimsMapper.java
  8. 199
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationProvider.java
  9. 88
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationToken.java
  10. 157
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilter.java
  11. 308
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoTests.java
  12. 224
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcUserInfoHttpMessageConverterTests.java
  13. 13
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java
  14. 285
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationProviderTests.java
  15. 60
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationTokenTests.java
  16. 254
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilterTests.java

19
oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java

@ -36,9 +36,11 @@ import org.springframework.security.web.util.matcher.RequestMatcher; @@ -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 { @@ -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 { @@ -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 { @@ -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);
}

112
oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoEndpointConfigurer.java

@ -0,0 +1,112 @@ @@ -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;
}
}

105
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcUserInfoHttpMessageConverter.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.AbstractHttpMessageConverter;
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 @@ -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 @@ -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);
}

5
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java

@ -94,6 +94,11 @@ public final class ConfigurationSettingNames { @@ -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() {
}

22
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java

@ -97,6 +97,15 @@ public final class ProviderSettings extends AbstractSettings { @@ -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 { @@ -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 { @@ -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}.
*

11
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/DefaultUserInfoClaimsMapper.java

@ -1,11 +0,0 @@ @@ -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
}
}

9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/UserInfoClaimsMapper.java

@ -1,9 +0,0 @@ @@ -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);
}

199
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationProvider.java

@ -0,0 +1,199 @@ @@ -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;
}
}
}

88
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationToken.java

@ -0,0 +1,88 @@ @@ -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;
}
}

157
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilter.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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;
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;
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
/**
* 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);
if (authenticationDetails instanceof OAuth2AccessToken) {
oidcUserInfo = getUserInfoClaimsRequestedByScope(oidcUserInfo, ((OAuth2AccessToken) authenticationDetails).getScopes());
} else {
oidcUserInfo = OidcUserInfo.builder()
.subject(oidcUserInfo.getSubject())
.build();
}
try {
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.oidcUserInfoHttpMessageConverter.write(
oidcUserInfo, MediaType.APPLICATION_JSON, httpResponse);
}
OidcUserInfoAuthenticationToken userInfoAuthentication = new OidcUserInfoAuthenticationToken(principal);
private OidcUserInfo getUserInfoClaimsRequestedByScope(OidcUserInfo userInfo, Set<String> scopes) {
Set<String> scopeRequestedClaimNames = getScopeRequestedClaimNames(scopes);
OidcUserInfoAuthenticationToken userInfoAuthenticationResult =
(OidcUserInfoAuthenticationToken) this.authenticationManager.authenticate(userInfoAuthentication);
Map<String, Object> scopeRequestedClaims = userInfo.getClaims().entrySet().stream()
.filter(claim -> scopeRequestedClaimNames.contains(claim.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
sendUserInfoResponse(response, userInfoAuthenticationResult.getUserInfo());
return new OidcUserInfo(scopeRequestedClaims);
}
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);
} 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();
}
}
return scopeRequestedClaimNames;
private void sendUserInfoResponse(HttpServletResponse response, OidcUserInfo userInfo) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.userInfoHttpMessageConverter.write(userInfo, MediaType.APPLICATION_JSON, httpResponse);
}
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);
}
}

308
oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcUserInfoTests.java

@ -0,0 +1,308 @@ @@ -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);
}
}
}

224
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcUserInfoHttpMessageConverterTests.java

@ -0,0 +1,224 @@ @@ -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();
}
}

13
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java

@ -38,6 +38,7 @@ public class ProviderSettingsTests { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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()

285
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationProviderTests.java

@ -0,0 +1,285 @@ @@ -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();
}
}

60
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcUserInfoAuthenticationTokenTests.java

@ -0,0 +1,60 @@ @@ -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();
}
}

254
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcUserInfoEndpointFilterTests.java

@ -0,0 +1,254 @@ @@ -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\"");
}
}
Loading…
Cancel
Save