Implement User Info Endpoint

Closes gh-176
This commit is contained in:
Steve Riesenberg
2021-09-21 14:31:32 -05:00
committed by Joe Grandja
parent 9667229429
commit 8e8e6d1b17
16 changed files with 1721 additions and 152 deletions

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -38,6 +38,7 @@ public class ProviderSettingsTests {
assertThat(providerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
assertThat(providerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
assertThat(providerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
assertThat(providerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
}
@Test
@@ -48,6 +49,7 @@ public class ProviderSettingsTests {
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
String oidcClientRegistrationEndpoint = "/connect/v1/register";
String oidcUserInfoEndpoint = "/connect/v1/userinfo";
String issuer = "https://example.com:9000";
ProviderSettings providerSettings = ProviderSettings.builder()
@@ -59,6 +61,7 @@ public class ProviderSettingsTests {
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint)
.oidcUserInfoEndpoint(oidcUserInfoEndpoint)
.build();
assertThat(providerSettings.getIssuer()).isEqualTo(issuer);
@@ -68,6 +71,7 @@ public class ProviderSettingsTests {
assertThat(providerSettings.getTokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
assertThat(providerSettings.getTokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
assertThat(providerSettings.getOidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
assertThat(providerSettings.getOidcUserInfoEndpoint()).isEqualTo(oidcUserInfoEndpoint);
}
@Test
@@ -77,7 +81,7 @@ public class ProviderSettingsTests {
.settings(settings -> settings.put("name2", "value2"))
.build();
assertThat(providerSettings.getSettings()).hasSize(8);
assertThat(providerSettings.getSettings()).hasSize(9);
assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2");
}
@@ -124,6 +128,13 @@ public class ProviderSettingsTests {
.withMessage("value cannot be null");
}
@Test
public void oidcUserInfoEndpointWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ProviderSettings.builder().oidcUserInfoEndpoint(null))
.withMessage("value cannot be null");
}
@Test
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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\"");
}
}