Implement User Info Endpoint
Closes gh-176
This commit is contained in:
committed by
Joe Grandja
parent
9667229429
commit
8e8e6d1b17
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
|
||||
import static org.springframework.test.web.servlet.ResultMatcher.matchAll;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Integration tests for the OpenID Connect 1.0 UserInfo endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoTests {
|
||||
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private JwtEncoder jwtEncoder;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoRequestGetThenUserInfoResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(userInfoResponse());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoRequestPostThenUserInfoResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(post(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(userInfoResponse());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenSignedJwtAndCustomUserInfoMapperThenUserInfoResponse() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(userInfoResponse());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static ResultMatcher userInfoResponse() {
|
||||
// @formatter:off
|
||||
return matchAll(
|
||||
jsonPath("sub").value("user1"),
|
||||
jsonPath("name").value("First Last"),
|
||||
jsonPath("given_name").value("First"),
|
||||
jsonPath("family_name").value("Last"),
|
||||
jsonPath("middle_name").value("Middle"),
|
||||
jsonPath("nickname").value("User"),
|
||||
jsonPath("preferred_username").value("user"),
|
||||
jsonPath("profile").value("https://example.com/user1"),
|
||||
jsonPath("picture").value("https://example.com/user1.jpg"),
|
||||
jsonPath("website").value("https://example.com"),
|
||||
jsonPath("email").value("user1@example.com"),
|
||||
jsonPath("email_verified").value("true"),
|
||||
jsonPath("gender").value("female"),
|
||||
jsonPath("birthdate").value("1970-01-01"),
|
||||
jsonPath("zoneinfo").value("Europe/Paris"),
|
||||
jsonPath("locale").value("en-US"),
|
||||
jsonPath("phone_number").value("+1 (604) 555-1234;ext=5678"),
|
||||
jsonPath("phone_number_verified").value("false"),
|
||||
jsonPath("address").value("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"),
|
||||
jsonPath("updated_at").value("1970-01-01T00:00:00Z")
|
||||
);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private OAuth2Authorization createAuthorization() {
|
||||
JoseHeader headers = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
// @formatter:off
|
||||
JwtClaimsSet claimSet = JwtClaimsSet.builder()
|
||||
.claims(claims -> claims.putAll(createUserInfo().getClaims()))
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.jwtEncoder.encode(headers, claimSet);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Set<String> scopes = new HashSet<>(Arrays.asList(
|
||||
OidcScopes.OPENID, OidcScopes.ADDRESS, OidcScopes.EMAIL, OidcScopes.PHONE, OidcScopes.PROFILE));
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||
OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), now, now.plusSeconds(300), scopes);
|
||||
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
|
||||
.claims(claims -> claims.putAll(createUserInfo().getClaims()))
|
||||
.build();
|
||||
|
||||
return TestOAuth2Authorizations.authorization()
|
||||
.accessToken(accessToken)
|
||||
.token(idToken)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
// @formatter:off
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class CustomUserInfoConfiguration extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
// Custom User Info Mapper that retrieves claims from a signed JWT
|
||||
Function<OAuth2AuthenticationContext, OidcUserInfo> userInfoMapper = context -> {
|
||||
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
|
||||
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
|
||||
|
||||
return new OidcUserInfo(principal.getToken().getClaims());
|
||||
};
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.apply(authorizationServerConfigurer)
|
||||
.oidc(oidc -> oidc
|
||||
.userInfoEndpoint(userInfo -> userInfo
|
||||
.userInfoMapper(userInfoMapper)
|
||||
)
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.apply(authorizationServerConfigurer);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService() {
|
||||
return new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return new ImmutableJWKSet<>(new JWKSet(TestJwks.DEFAULT_RSA_JWK));
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return new NimbusJwsEncoder(jwkSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.oidc.http.converter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.mock.http.MockHttpOutputMessage;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoHttpMessageConverter}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoHttpMessageConverterTests {
|
||||
private final OidcUserInfoHttpMessageConverter messageConverter = new OidcUserInfoHttpMessageConverter();
|
||||
|
||||
@Test
|
||||
public void supportsWhenOidcUserInfoThenTrue() {
|
||||
assertThat(this.messageConverter.supports(OidcUserInfo.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setUserInfoConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setUserInfoConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setUserInfoParametersConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setUserInfoParametersConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenValidParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String userInfoResponse = "{\n" +
|
||||
" \"sub\": \"user1\",\n" +
|
||||
" \"name\": \"First Last\",\n" +
|
||||
" \"given_name\": \"First\",\n" +
|
||||
" \"family_name\": \"Last\",\n" +
|
||||
" \"middle_name\": \"Middle\",\n" +
|
||||
" \"nickname\": \"User\",\n" +
|
||||
" \"preferred_username\": \"user\",\n" +
|
||||
" \"profile\": \"https://example.com/user1\",\n" +
|
||||
" \"picture\": \"https://example.com/user1.jpg\",\n" +
|
||||
" \"website\": \"https://example.com\",\n" +
|
||||
" \"email\": \"user1@example.com\",\n" +
|
||||
" \"email_verified\": \"true\",\n" +
|
||||
" \"gender\": \"female\",\n" +
|
||||
" \"birthdate\": \"1970-01-01\",\n" +
|
||||
" \"zoneinfo\": \"Europe/Paris\",\n" +
|
||||
" \"locale\": \"en-US\",\n" +
|
||||
" \"phone_number\": \"+1 (604) 555-1234;ext=5678\",\n" +
|
||||
" \"phone_number_verified\": \"false\",\n" +
|
||||
" \"address\": {\n" +
|
||||
" \"formatted\": \"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\",\n" +
|
||||
" \"street_address\": \"Champ de Mars\\n5 Av. Anatole France\",\n" +
|
||||
" \"locality\": \"Paris\",\n" +
|
||||
" \"postal_code\": \"75007\",\n" +
|
||||
" \"country\": \"France\"\n" +
|
||||
" },\n" +
|
||||
" \"updated_at\": 1607633867\n" +
|
||||
"}\n";
|
||||
// @formatter:on
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(userInfoResponse.getBytes(), HttpStatus.OK);
|
||||
OidcUserInfo oidcUserInfo = this.messageConverter.readInternal(OidcUserInfo.class, response);
|
||||
|
||||
assertThat(oidcUserInfo.getSubject()).isEqualTo("user1");
|
||||
assertThat(oidcUserInfo.getFullName()).isEqualTo("First Last");
|
||||
assertThat(oidcUserInfo.getGivenName()).isEqualTo("First");
|
||||
assertThat(oidcUserInfo.getFamilyName()).isEqualTo("Last");
|
||||
assertThat(oidcUserInfo.getMiddleName()).isEqualTo("Middle");
|
||||
assertThat(oidcUserInfo.getNickName()).isEqualTo("User");
|
||||
assertThat(oidcUserInfo.getPreferredUsername()).isEqualTo("user");
|
||||
assertThat(oidcUserInfo.getProfile()).isEqualTo("https://example.com/user1");
|
||||
assertThat(oidcUserInfo.getPicture()).isEqualTo("https://example.com/user1.jpg");
|
||||
assertThat(oidcUserInfo.getWebsite()).isEqualTo("https://example.com");
|
||||
assertThat(oidcUserInfo.getEmail()).isEqualTo("user1@example.com");
|
||||
assertThat(oidcUserInfo.getEmailVerified()).isTrue();
|
||||
assertThat(oidcUserInfo.getGender()).isEqualTo("female");
|
||||
assertThat(oidcUserInfo.getBirthdate()).isEqualTo("1970-01-01");
|
||||
assertThat(oidcUserInfo.getZoneInfo()).isEqualTo("Europe/Paris");
|
||||
assertThat(oidcUserInfo.getLocale()).isEqualTo("en-US");
|
||||
assertThat(oidcUserInfo.getPhoneNumber()).isEqualTo("+1 (604) 555-1234;ext=5678");
|
||||
assertThat(oidcUserInfo.getPhoneNumberVerified()).isFalse();
|
||||
assertThat(oidcUserInfo.getAddress().getFormatted()).isEqualTo("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance");
|
||||
assertThat(oidcUserInfo.getAddress().getStreetAddress()).isEqualTo("Champ de Mars\n5 Av. Anatole France");
|
||||
assertThat(oidcUserInfo.getAddress().getLocality()).isEqualTo("Paris");
|
||||
assertThat(oidcUserInfo.getAddress().getPostalCode()).isEqualTo("75007");
|
||||
assertThat(oidcUserInfo.getAddress().getCountry()).isEqualTo("France");
|
||||
assertThat(oidcUserInfo.getUpdatedAt()).isEqualTo(Instant.ofEpochSecond(1607633867));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenFailingConverterThenThrowException() {
|
||||
String errorMessage = "this is not a valid converter";
|
||||
this.messageConverter.setUserInfoConverter(source -> {
|
||||
throw new RuntimeException(errorMessage);
|
||||
});
|
||||
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.readInternal(OidcUserInfo.class, response))
|
||||
.withMessageContaining("An error occurred reading the UserInfo")
|
||||
.withMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenInvalidResponseThenThrowException() {
|
||||
String providerConfigurationResponse = "{}";
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK);
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.readInternal(OidcUserInfo.class, response))
|
||||
.withMessageContaining("An error occurred reading the UserInfo")
|
||||
.withMessageContaining("claims cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInternalWhenOidcUserInfoThenSuccess() {
|
||||
OidcUserInfo userInfo = createUserInfo();
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
|
||||
this.messageConverter.writeInternal(userInfo, outputMessage);
|
||||
|
||||
String userInfoResponse = outputMessage.getBodyAsString();
|
||||
assertThat(userInfoResponse).contains("\"sub\":\"user1\"");
|
||||
assertThat(userInfoResponse).contains("\"name\":\"First Last\"");
|
||||
assertThat(userInfoResponse).contains("\"given_name\":\"First\"");
|
||||
assertThat(userInfoResponse).contains("\"family_name\":\"Last\"");
|
||||
assertThat(userInfoResponse).contains("\"middle_name\":\"Middle\"");
|
||||
assertThat(userInfoResponse).contains("\"nickname\":\"User\"");
|
||||
assertThat(userInfoResponse).contains("\"preferred_username\":\"user\"");
|
||||
assertThat(userInfoResponse).contains("\"profile\":\"https://example.com/user1\"");
|
||||
assertThat(userInfoResponse).contains("\"picture\":\"https://example.com/user1.jpg\"");
|
||||
assertThat(userInfoResponse).contains("\"website\":\"https://example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email\":\"user1@example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email_verified\":true");
|
||||
assertThat(userInfoResponse).contains("\"gender\":\"female\"");
|
||||
assertThat(userInfoResponse).contains("\"birthdate\":\"1970-01-01\"");
|
||||
assertThat(userInfoResponse).contains("\"zoneinfo\":\"Europe/Paris\"");
|
||||
assertThat(userInfoResponse).contains("\"locale\":\"en-US\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number\":\"+1 (604) 555-1234;ext=5678\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number_verified\":false");
|
||||
assertThat(userInfoResponse).contains("\"address\":");
|
||||
assertThat(userInfoResponse).contains("\"formatted\":\"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\"");
|
||||
assertThat(userInfoResponse).contains("\"updated_at\":1607633867");
|
||||
assertThat(userInfoResponse).contains("\"custom_claim\":\"value\"");
|
||||
assertThat(userInfoResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInternalWhenWriteFailsThenThrowsException() {
|
||||
String errorMessage = "this is not a valid converter";
|
||||
Converter<OidcUserInfo, Map<String, Object>> failingConverter = source -> {
|
||||
throw new RuntimeException(errorMessage);
|
||||
};
|
||||
this.messageConverter.setUserInfoParametersConverter(failingConverter);
|
||||
|
||||
OidcUserInfo userInfo = createUserInfo();
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotWritableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.writeInternal(userInfo, outputMessage))
|
||||
.withMessageContaining("An error occurred writing the UserInfo response")
|
||||
.withMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.claim("phone_number_verified", false)
|
||||
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
|
||||
.claim(StandardClaimNames.UPDATED_AT, Instant.ofEpochSecond(1607633867))
|
||||
.claim("custom_claim", "value")
|
||||
.claim("custom_collection_claim", Arrays.asList("value1", "value2"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class ProviderSettingsTests {
|
||||
assertThat(providerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
|
||||
assertThat(providerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
|
||||
assertThat(providerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
|
||||
assertThat(providerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,6 +49,7 @@ public class ProviderSettingsTests {
|
||||
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
|
||||
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
|
||||
String oidcClientRegistrationEndpoint = "/connect/v1/register";
|
||||
String oidcUserInfoEndpoint = "/connect/v1/userinfo";
|
||||
String issuer = "https://example.com:9000";
|
||||
|
||||
ProviderSettings providerSettings = ProviderSettings.builder()
|
||||
@@ -59,6 +61,7 @@ public class ProviderSettingsTests {
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint)
|
||||
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint)
|
||||
.oidcUserInfoEndpoint(oidcUserInfoEndpoint)
|
||||
.build();
|
||||
|
||||
assertThat(providerSettings.getIssuer()).isEqualTo(issuer);
|
||||
@@ -68,6 +71,7 @@ public class ProviderSettingsTests {
|
||||
assertThat(providerSettings.getTokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
|
||||
assertThat(providerSettings.getTokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
|
||||
assertThat(providerSettings.getOidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
|
||||
assertThat(providerSettings.getOidcUserInfoEndpoint()).isEqualTo(oidcUserInfoEndpoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,7 +81,7 @@ public class ProviderSettingsTests {
|
||||
.settings(settings -> settings.put("name2", "value2"))
|
||||
.build();
|
||||
|
||||
assertThat(providerSettings.getSettings()).hasSize(8);
|
||||
assertThat(providerSettings.getSettings()).hasSize(9);
|
||||
assertThat(providerSettings.<String>getSetting("name1")).isEqualTo("value1");
|
||||
assertThat(providerSettings.<String>getSetting("name2")).isEqualTo("value2");
|
||||
}
|
||||
@@ -124,6 +128,13 @@ public class ProviderSettingsTests {
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oidcUserInfoEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ProviderSettings.builder().oidcUserInfoEndpoint(null))
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoAuthenticationProvider}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoAuthenticationProviderTests {
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private OidcUserInfoAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authenticationProvider = new OidcUserInfoAuthenticationProvider(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoAuthenticationProvider(null))
|
||||
.withMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setUserInfoMapperWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authenticationProvider.setUserInfoMapper(null))
|
||||
.withMessage("userInfoMapper cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOidcUserInfoAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OidcUserInfoAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotOfExpectedTypeThenThrowOAuth2AuthenticationException() {
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(
|
||||
new UsernamePasswordAuthenticationToken(null, null));
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verifyNoInteractions(this.authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
principal.setAuthenticated(false);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verifyNoInteractions(this.authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
|
||||
authorization = OidcAuthenticationProviderUtils.invalidate(authorization,
|
||||
authorization.getAccessToken().getToken());
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(TestOAuth2Authorizations.authorization().build());
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenIdTokenNullThenThrowOAuth2AuthenticationException() {
|
||||
String tokenValue = "token";
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
|
||||
.token(createAuthorization(tokenValue).getAccessToken().getToken())
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnUserInfo() {
|
||||
String tokenValue = "access-token";
|
||||
when(this.authorizationService.findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(createAuthorization(tokenValue));
|
||||
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken(tokenValue);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
OidcUserInfoAuthenticationToken authenticationResult =
|
||||
(OidcUserInfoAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||
|
||||
assertThat(authenticationResult.getPrincipal()).isEqualTo(principal);
|
||||
assertThat(authenticationResult.getCredentials()).isEqualTo("");
|
||||
assertThat(authenticationResult.isAuthenticated()).isTrue();
|
||||
|
||||
OidcUserInfo userInfo = authenticationResult.getUserInfo();
|
||||
assertThat(userInfo.getClaims()).hasSize(20);
|
||||
assertThat(userInfo.getSubject()).isEqualTo("user1");
|
||||
assertThat(userInfo.getFullName()).isEqualTo("First Last");
|
||||
assertThat(userInfo.getGivenName()).isEqualTo("First");
|
||||
assertThat(userInfo.getFamilyName()).isEqualTo("Last");
|
||||
assertThat(userInfo.getMiddleName()).isEqualTo("Middle");
|
||||
assertThat(userInfo.getNickName()).isEqualTo("User");
|
||||
assertThat(userInfo.getPreferredUsername()).isEqualTo("user");
|
||||
assertThat(userInfo.getProfile()).isEqualTo("https://example.com/user1");
|
||||
assertThat(userInfo.getPicture()).isEqualTo("https://example.com/user1.jpg");
|
||||
assertThat(userInfo.getWebsite()).isEqualTo("https://example.com");
|
||||
assertThat(userInfo.getEmail()).isEqualTo("user1@example.com");
|
||||
assertThat(userInfo.getEmailVerified()).isEqualTo(true);
|
||||
assertThat(userInfo.getGender()).isEqualTo("female");
|
||||
assertThat(userInfo.getBirthdate()).isEqualTo("1970-01-01");
|
||||
assertThat(userInfo.getZoneInfo()).isEqualTo("Europe/Paris");
|
||||
assertThat(userInfo.getLocale()).isEqualTo("en-US");
|
||||
assertThat(userInfo.getPhoneNumber()).isEqualTo("+1 (604) 555-1234;ext=5678");
|
||||
assertThat(userInfo.getPhoneNumberVerified()).isEqualTo(false);
|
||||
assertThat(userInfo.getClaimAsString(StandardClaimNames.ADDRESS))
|
||||
.isEqualTo("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance");
|
||||
assertThat(userInfo.getUpdatedAt()).isEqualTo(Instant.parse("1970-01-01T00:00:00Z"));
|
||||
|
||||
verify(this.authorizationService).findByToken(eq(tokenValue), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
private static OAuth2Authorization createAuthorization(String tokenValue) {
|
||||
Instant now = Instant.now();
|
||||
Set<String> scopes = new HashSet<>(Arrays.asList(
|
||||
OidcScopes.OPENID, OidcScopes.ADDRESS, OidcScopes.EMAIL, OidcScopes.PHONE, OidcScopes.PROFILE));
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||
OAuth2AccessToken.TokenType.BEARER, tokenValue, now, now.plusSeconds(300), scopes);
|
||||
OidcIdToken idToken = new OidcIdToken("id-token", now, now.plusSeconds(900), createUserInfo().getClaims());
|
||||
|
||||
return TestOAuth2Authorizations.authorization()
|
||||
.token(accessToken)
|
||||
.token(idToken)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static JwtAuthenticationToken createJwtAuthenticationToken(String tokenValue) {
|
||||
Instant now = Instant.now();
|
||||
// @formatter:off
|
||||
Jwt jwt = Jwt.withTokenValue(tokenValue)
|
||||
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
|
||||
.issuedAt(now)
|
||||
.expiresAt(now.plusSeconds(300))
|
||||
.claim(StandardClaimNames.SUB, "user")
|
||||
.build();
|
||||
// @formatter:on
|
||||
return new JwtAuthenticationToken(jwt, Collections.emptyList());
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoAuthenticationToken}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoAuthenticationTokenTests {
|
||||
@Test
|
||||
public void constructorWhenPrincipalNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoAuthenticationToken(null))
|
||||
.withMessage("principal cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenPrincipalProvidedThenCreated() {
|
||||
UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken(null, null);
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
assertThat(authentication.getPrincipal()).isEqualTo(principal);
|
||||
assertThat(authentication.getUserInfo()).isNull();
|
||||
assertThat(authentication.isAuthenticated()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenPrincipalAndUserInfoProvidedThenCreated() {
|
||||
UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken(null, null);
|
||||
OidcUserInfo userInfo = new OidcUserInfo(Collections.singletonMap(StandardClaimNames.SUB, "user"));
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal, userInfo);
|
||||
assertThat(authentication.getPrincipal()).isEqualTo(principal);
|
||||
assertThat(authentication.getUserInfo()).isEqualTo(userInfo);
|
||||
assertThat(authentication.isAuthenticated()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.web;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcUserInfoEndpointFilter}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class OidcUserInfoEndpointFilterTests {
|
||||
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoEndpointFilter(null))
|
||||
.withMessage("authenticationManager cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenUserInfoEndpointUriIsEmptyThenThrowIllegalArgumentException() {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcUserInfoEndpointFilter(authenticationManager, ""))
|
||||
.withMessage("userInfoEndpointUri cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotUserInfoRequestThenNotProcessed() throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI);
|
||||
|
||||
String requestUri = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestPutThenNotProcessed() throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("PUT", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(authenticationManager);
|
||||
verify(filterChain).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestGetThenSuccess() throws Exception {
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken();
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken authenticationResult = new OidcUserInfoAuthenticationToken(principal, createUserInfo());
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any())).thenReturn(authenticationResult);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(authenticationManager).authenticate(any());
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertUserInfoResponse(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestPostThenSuccess() throws Exception {
|
||||
JwtAuthenticationToken principal = createJwtAuthenticationToken();
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal, createUserInfo());
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any())).thenReturn(authentication);
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(authenticationManager).authenticate(any());
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertUserInfoResponse(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthenticationNullThenInvalidRequestError() throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any(Authentication.class)))
|
||||
.thenReturn(new UsernamePasswordAuthenticationToken("user", "password"));
|
||||
OidcUserInfoEndpointFilter userInfoEndpointFilter = new OidcUserInfoEndpointFilter(authenticationManager);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
userInfoEndpointFilter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
OAuth2Error error = readError(response);
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
assertThat(error.getDescription()).isEqualTo("OpenID Connect 1.0 UserInfo Error: principal cannot be null");
|
||||
}
|
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
|
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
||||
response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
|
||||
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
|
||||
}
|
||||
|
||||
private JwtAuthenticationToken createJwtAuthenticationToken() {
|
||||
Instant now = Instant.now();
|
||||
// @formatter:off
|
||||
Jwt jwt = Jwt.withTokenValue("token")
|
||||
.header(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName())
|
||||
.issuedAt(now)
|
||||
.expiresAt(now.plusSeconds(300))
|
||||
.claim(StandardClaimNames.SUB, "user")
|
||||
.build();
|
||||
// @formatter:on
|
||||
return new JwtAuthenticationToken(jwt, Collections.emptyList());
|
||||
}
|
||||
|
||||
private static OidcUserInfo createUserInfo() {
|
||||
return OidcUserInfo.builder()
|
||||
.subject("user1")
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername("user")
|
||||
.profile("https://example.com/user1")
|
||||
.picture("https://example.com/user1.jpg")
|
||||
.website("https://example.com")
|
||||
.email("user1@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void assertUserInfoResponse(String userInfoResponse) {
|
||||
assertThat(userInfoResponse).contains("\"sub\":\"user1\"");
|
||||
assertThat(userInfoResponse).contains("\"name\":\"First Last\"");
|
||||
assertThat(userInfoResponse).contains("\"given_name\":\"First\"");
|
||||
assertThat(userInfoResponse).contains("\"family_name\":\"Last\"");
|
||||
assertThat(userInfoResponse).contains("\"middle_name\":\"Middle\"");
|
||||
assertThat(userInfoResponse).contains("\"nickname\":\"User\"");
|
||||
assertThat(userInfoResponse).contains("\"preferred_username\":\"user\"");
|
||||
assertThat(userInfoResponse).contains("\"profile\":\"https://example.com/user1\"");
|
||||
assertThat(userInfoResponse).contains("\"picture\":\"https://example.com/user1.jpg\"");
|
||||
assertThat(userInfoResponse).contains("\"website\":\"https://example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email\":\"user1@example.com\"");
|
||||
assertThat(userInfoResponse).contains("\"email_verified\":true");
|
||||
assertThat(userInfoResponse).contains("\"gender\":\"female\"");
|
||||
assertThat(userInfoResponse).contains("\"birthdate\":\"1970-01-01\"");
|
||||
assertThat(userInfoResponse).contains("\"zoneinfo\":\"Europe/Paris\"");
|
||||
assertThat(userInfoResponse).contains("\"locale\":\"en-US\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number\":\"+1 (604) 555-1234;ext=5678\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number_verified\":\"false\"");
|
||||
assertThat(userInfoResponse).contains("\"address\":\"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\"");
|
||||
assertThat(userInfoResponse).contains("\"updated_at\":\"1970-01-01T00:00:00Z\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user