16 changed files with 1720 additions and 151 deletions
@ -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; |
||||
} |
||||
} |
||||
@ -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
|
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
|
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue