Implement OpenID client registration endpoint
See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration Closes gh-57
This commit is contained in:
@@ -22,7 +22,6 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
@@ -26,7 +22,6 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -37,16 +32,20 @@ import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
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.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -15,14 +15,6 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
@@ -30,7 +22,6 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -45,6 +36,7 @@ import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
@@ -52,21 +44,28 @@ import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
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.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
@@ -27,7 +23,6 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -38,12 +33,12 @@ import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
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.core.OAuth2TokenType;
|
||||
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;
|
||||
@@ -53,6 +48,10 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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 com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
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.MockHttpServletResponse;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.core.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
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 OpenID Connect 1.0 Client Registration Endpoint.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.1
|
||||
*/
|
||||
public class OidcClientRegistrationTests {
|
||||
private static final OidcClientRegistration.Builder OIDC_CLIENT_REGISTRATION = OidcClientRegistration.builder()
|
||||
.redirectUri("https://localhost:8080/client")
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.BASIC.getValue())
|
||||
.scope("test");
|
||||
|
||||
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
|
||||
private static final OidcClientRegistrationHttpMessageConverter clientRegistrationHttpMessageConverter =
|
||||
new OidcClientRegistrationHttpMessageConverter();
|
||||
|
||||
private static final OAuth2TokenType ACCESS_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static OAuth2AuthorizationService authorizationService;
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
private static NimbusJwtDecoder jwtDecoder;
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(registeredClientRepository);
|
||||
reset(authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenAuthenticatedThenResponseIncludesRegisteredClientDetails() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationEnabledClientRegistration.class).autowire();
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2()
|
||||
.scope("client.create").build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
// get access token
|
||||
MvcResult mvcResult = this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.param(OAuth2ParameterNames.SCOPE, "client.create")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").value("client.create"))
|
||||
.andReturn();
|
||||
|
||||
//assert get access token
|
||||
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(authorizationService).save(authorizationCaptor.capture());
|
||||
OAuth2Authorization authorization = authorizationCaptor.getValue();
|
||||
MockHttpServletResponse servletResponse = mvcResult.getResponse();
|
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
||||
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
|
||||
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
|
||||
String tokenValue = accessTokenResponse.getAccessToken().getTokenValue();
|
||||
|
||||
// prepare register client request
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorization.getToken(OAuth2AccessToken.class).getToken().getTokenValue()),
|
||||
eq(ACCESS_TOKEN_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
doNothing().when(registeredClientRepository).saveClient(any(RegisteredClient.class));
|
||||
mvcResult = this.mvc.perform(post("/connect/register")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenValue)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(convertToByteArray(OIDC_CLIENT_REGISTRATION.build())))
|
||||
.andExpect(status().isCreated()).andReturn();
|
||||
|
||||
servletResponse = mvcResult.getResponse();
|
||||
httpResponse = new MockClientHttpResponse(
|
||||
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
|
||||
|
||||
OidcClientRegistration result = clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
||||
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getClaimAsString("client_id")).isNotEmpty();
|
||||
assertThat(result.getClaimAsString("client_id_issued_at")).isNotEmpty();
|
||||
assertThat(result.getClaimAsString("client_secret")).isNotEmpty();
|
||||
assertThat(result.getClaimAsString("client_secret_expires_at")).isNotNull().isEqualTo("0.0");
|
||||
assertThat(result.getRedirectUris()).isNotEmpty().containsExactly("https://localhost:8080/client");
|
||||
assertThat(result.getResponseTypes()).isNotEmpty().containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(result.getGrantTypes()).isNotEmpty().containsExactly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(result.getTokenEndpointAuthenticationMethod()).isNotEmpty().isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
assertThat(result.getScope()).isNotEmpty().isEqualTo("test");
|
||||
}
|
||||
|
||||
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
|
||||
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
|
||||
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
|
||||
String credentialsString = clientId + ":" + secret;
|
||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(encodedBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] convertToByteArray(OidcClientRegistration clientRegistration) throws JsonProcessingException {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
return objectMapper
|
||||
.writerFor(Map.class)
|
||||
.writeValueAsBytes(clientRegistration.getClaims());
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService() {
|
||||
return authorizationService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return jwkSource;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationEnabledClientRegistration extends AuthorizationServerConfiguration{
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return jwtDecoder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ProviderSettings providerSettings() {
|
||||
return new ProviderSettings().isOidClientRegistrationEndpointEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
@@ -273,6 +274,11 @@ public class OidcTests {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder(){
|
||||
return jwtDecoder;
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcClientRegistration}
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.1
|
||||
*/
|
||||
public class OidcClientRegistrationTests {
|
||||
|
||||
private final OidcClientRegistration.Builder clientRegistrationBuilder =
|
||||
OidcClientRegistration.builder();
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.scope("test read")
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.BASIC.getValue())
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.contains(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
|
||||
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
|
||||
);
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isEqualTo("test read");
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isNull();
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndAuthorizationGrantTypeButMissingResponseTypeThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndEmptyGrantTypeListButMissingResponseTypeThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.grantTypes(List::clear)
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndResponseTypeButMissingAuthorizationGrantTypeThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndEmptyResponseTypeListButMissingAuthorizationGrantTypeThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.responseTypes(List::clear)
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.containsOnly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndEmptyScopeThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllRequiredClaimsAndEmptyTokenEndpointAuthMethodThenCreated() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenClaimsProvidedThenCreated() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Collections.singletonList("http://client.example.com"));
|
||||
claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
|
||||
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
|
||||
));
|
||||
claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES,
|
||||
Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
|
||||
claims.put(OidcClientMetadataClaimNames.SCOPE, "test read");
|
||||
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.BASIC.getValue());
|
||||
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(claims).build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("http://client.example.com");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.contains(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
|
||||
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
|
||||
);
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isEqualTo("test read");
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenRedirectUriProvidedWithUrlThenCreated() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Arrays.asList(
|
||||
url("http://client.example.com"),
|
||||
url("http://client.example.com/authorized")
|
||||
)
|
||||
);
|
||||
claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
|
||||
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
|
||||
));
|
||||
claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES,
|
||||
Collections.singletonList(OAuth2AuthorizationResponseType.CODE.getValue()));
|
||||
claims.put(OidcClientMetadataClaimNames.SCOPE, "test read");
|
||||
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.BASIC.getValue());
|
||||
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(claims).build();
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.contains("http://client.example.com", "http://client.example.com/authorized");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.contains(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
|
||||
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
|
||||
);
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isEqualTo("test read");
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withClaimsNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> OidcClientRegistration.withClaims(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withClaimsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> OidcClientRegistration.withClaims(Collections.emptyMap()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("claims cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenNullRedirectUriThenThrowIllegalArgumentException() {
|
||||
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
|
||||
.redirectUris((claims) -> claims.remove(OidcClientMetadataClaimNames.REDIRECT_URIS));
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("redirect_uris must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenNullRedirectUriClaimThenThrowIllegalArgumentException() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, null);
|
||||
OidcClientRegistration.Builder builder = OidcClientRegistration.withClaims(claims);
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("redirect_uris cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenEmptyRedirectUriListThenThrowIllegalArgumentException() {
|
||||
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
|
||||
.redirectUris(List::clear);
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("redirect_uris must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenRedirectUriNotOfTypeListThenThrowIllegalArgumentException() {
|
||||
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
|
||||
.claims(claims -> claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, "http://client.example.com"));
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("redirect_uris must be of type list");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenRedirectUriNotUrlThenThrowIllegalArgumentException() {
|
||||
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
|
||||
.redirectUri("not url");
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("redirect_uri must be a valid URL");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenResponseTypesNotOfTypeListThenThrowIllegalArgumentException() {
|
||||
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
|
||||
.redirectUri("http://client.example.com")
|
||||
.claims(claims -> claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, OAuth2AuthorizationResponseType.CODE.getValue()));
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("response_types must be of type List");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenGrantTypesNotOfTypeListThenThrowIllegalArgumentException() {
|
||||
OidcClientRegistration.Builder builder = this.clientRegistrationBuilder
|
||||
.redirectUri("http://client.example.com")
|
||||
.claims(claims -> claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
|
||||
|
||||
assertThatThrownBy(builder::build)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("grant_types must be of type List");
|
||||
}
|
||||
|
||||
private static URL url(String urlString) {
|
||||
try {
|
||||
return new URL(urlString);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("urlString must be a valid URL and valid URI");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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 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.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.1
|
||||
*/
|
||||
public class OidcClientRegistrationHttpMessageConverterTest {
|
||||
private final OidcClientRegistrationHttpMessageConverter messageConverter =
|
||||
new OidcClientRegistrationHttpMessageConverter();
|
||||
|
||||
@Test
|
||||
public void supportsWhenOidcClientRegistrationThenTrue() {
|
||||
assertThat(this.messageConverter.supports(OidcClientRegistration.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setClientRegistrationReadConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.messageConverter.setClientRegistrationConverter(null))
|
||||
.withMessageContaining("clientRegistrationConverter cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setClientRegistrationWriteConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.messageConverter.setClientRegistrationParametersConverter(null))
|
||||
.withMessageContaining("clientRegistrationParametersConverter cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenRequiredParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String clientRegistrationResponse = "{\n"
|
||||
+ " \"redirect_uris\": [\n"
|
||||
+ " \"https://client.example.org/callback\"\n"
|
||||
+ " ]\n"
|
||||
+ "}\n";
|
||||
// @formatter:on
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationResponse.getBytes(), HttpStatus.OK);
|
||||
OidcClientRegistration clientRegistration = this.messageConverter
|
||||
.readInternal(OidcClientRegistration.class, response);
|
||||
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("https://client.example.org/callback");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.containsOnly(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
|
||||
);
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isNull();
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenValidParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String clientRegistrationResponse = "{\n"
|
||||
+" \"redirect_uris\": [\n"
|
||||
+ " \"https://client.example.org/callback\"\n"
|
||||
+ " ],\n"
|
||||
+" \"grant_types\": [\n"
|
||||
+" \"client_credentials\",\n"
|
||||
+" \"authorization_code\"\n"
|
||||
+" ],\n"
|
||||
+" \"response_types\":[\n"
|
||||
+" \"code\"\n"
|
||||
+" ],\n"
|
||||
+" \"client_name\": \"My Example\",\n"
|
||||
+" \"scope\": \"read write\",\n"
|
||||
+" \"token_endpoint_auth_method\": \"basic\"\n"
|
||||
+"}\n";
|
||||
// @formatter:on
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationResponse.getBytes(), HttpStatus.OK);
|
||||
|
||||
OidcClientRegistration clientRegistration = this.messageConverter
|
||||
.readInternal(OidcClientRegistration.class, response);
|
||||
assertThat(clientRegistration.getRedirectUris())
|
||||
.containsOnly("https://client.example.org/callback");
|
||||
assertThat(clientRegistration.getGrantTypes())
|
||||
.contains(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
|
||||
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()
|
||||
);
|
||||
assertThat(clientRegistration.getResponseTypes())
|
||||
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistration.getScope())
|
||||
.isEqualTo("read write");
|
||||
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(ClientAuthenticationMethod.BASIC.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenFailingConverterThenThrowException() {
|
||||
String errorMessage = "this is not a valid converter";
|
||||
this.messageConverter.setClientRegistrationConverter(source -> {
|
||||
throw new RuntimeException(errorMessage);
|
||||
});
|
||||
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.readInternal(OidcClientRegistration.class, response))
|
||||
.withMessageContaining("An error occurred reading the OpenID Client Registration Request")
|
||||
.withMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenInvalidClientRegistrationThenThrowException() {
|
||||
String clientRegistrationResponse = "{ \"redirect_uris\": null }";
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationResponse.getBytes(), HttpStatus.OK);
|
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class)
|
||||
.isThrownBy(() -> this.messageConverter.readInternal(OidcClientRegistration.class, response))
|
||||
.withMessageContaining("An error occurred reading the OpenID Client Registration Request")
|
||||
.withMessageContaining("redirect_uris cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInternalWhenClientRegistrationThenSuccess() {
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com/callback")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.scope("test read")
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.BASIC.getValue())
|
||||
.build();
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
|
||||
this.messageConverter.writeInternal(clientRegistration, outputMessage);
|
||||
String clientRegistrationResponse = outputMessage.getBodyAsString();
|
||||
assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"http://client.example.com/callback\"]");
|
||||
assertThat(clientRegistrationResponse).contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]");
|
||||
assertThat(clientRegistrationResponse).contains("\"response_types\":[\"code\"]");
|
||||
assertThat(clientRegistrationResponse).contains("\"scope\":\"test read\"");
|
||||
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"basic\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeInternalWhenWriteFailsThenThrowsException() {
|
||||
String errorMessage = "this is not a valid converter";
|
||||
Converter<OidcClientRegistration, Map<String, Object>> failingConverter =
|
||||
source -> {
|
||||
throw new RuntimeException(errorMessage);
|
||||
};
|
||||
this.messageConverter.setClientRegistrationParametersConverter(failingConverter);
|
||||
|
||||
OidcClientRegistration clientRegistration =
|
||||
OidcClientRegistration.builder()
|
||||
.redirectUri("http://client.example.com")
|
||||
.build();
|
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
|
||||
|
||||
assertThatThrownBy(() -> this.messageConverter.writeInternal(clientRegistration, outputMessage))
|
||||
.isInstanceOf(HttpMessageNotWritableException.class)
|
||||
.hasMessageContaining("An error occurred writing the OpenID Client Registration response")
|
||||
.hasMessageContaining(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.authentication;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
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.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 java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.when;
|
||||
|
||||
/**
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.1
|
||||
*/
|
||||
public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private OidcClientRegistrationAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authenticationProvider = new OidcClientRegistrationAuthenticationProvider(this.authorizationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OidcClientRegistrationAuthenticationProvider(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeJwtAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(JwtAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() {
|
||||
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_client.create");
|
||||
|
||||
when(authorizationService.findByToken(
|
||||
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(null);
|
||||
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenInvalidatedThenThrowOAuth2AuthenticationException() {
|
||||
|
||||
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_client.create");
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
"client-registration-token", Instant.now().minusSeconds(120), Instant.now().plusSeconds(1000));
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
|
||||
.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true))
|
||||
.build();
|
||||
|
||||
when(authorizationService.findByToken(
|
||||
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenWithoutClientCreateScopeThenThrowOAuth2AuthenticationException() {
|
||||
|
||||
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_scope1");
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
"client-registration-token", Instant.now().minusSeconds(120), Instant.now().plusSeconds(1000),
|
||||
new HashSet<>(Collections.singletonList("scope1")));
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
|
||||
.token(accessToken)
|
||||
.build();
|
||||
|
||||
when(authorizationService.findByToken(
|
||||
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenInvalidated() {
|
||||
JwtAuthenticationToken authentication = buildJwtAuthenticationToken("client-registration-token", "SCOPE_client.create");
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
"client-registration-token", Instant.now().minusSeconds(120), Instant.now().plusSeconds(1000),
|
||||
new HashSet<>(Collections.singletonList("client.create")));
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization()
|
||||
.token(accessToken)
|
||||
.build();
|
||||
|
||||
when(authorizationService.findByToken(
|
||||
eq("client-registration-token"), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
authenticationProvider.authenticate(authentication);
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(authorizationService).save(authorizationCaptor.capture());
|
||||
|
||||
OAuth2Authorization capturedAuthorization = authorizationCaptor.getValue();
|
||||
|
||||
assertThat(capturedAuthorization.getAccessToken()).isNotNull();
|
||||
assertThat(capturedAuthorization.getAccessToken().isInvalidated()).isTrue();
|
||||
}
|
||||
|
||||
private static JwtAuthenticationToken buildJwtAuthenticationToken(String tokenValue, String... authorities) {
|
||||
Jwt jwt = Jwt.withTokenValue(tokenValue)
|
||||
.header("alg", "none")
|
||||
.claim("sub", "client")
|
||||
.build();
|
||||
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(authorities);
|
||||
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, grantedAuthorities);
|
||||
jwtAuthenticationToken.setAuthenticated(true);
|
||||
return jwtAuthenticationToken;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
package org.springframework.security.oauth2.server.authorization.client;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -112,4 +114,77 @@ public class InMemoryRegisteredClientRepositoryTests {
|
||||
public void findByClientIdWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.clients.findByClientId(null)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveNullRegisteredClientThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.clients.saveClient(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("registeredClient cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveRegisteredClientThenReturnsSavedRegisteredClientWhenSearchedById() {
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("new-client")
|
||||
.clientId("new-client")
|
||||
.clientSecret("secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
||||
.redirectUri("https://newclient.com")
|
||||
.scope("scope1").build();
|
||||
|
||||
this.clients.saveClient(registeredClient);
|
||||
|
||||
RegisteredClient savedClient = this.clients.findById("new-client");
|
||||
|
||||
assertThat(savedClient).isNotNull().isEqualTo(registeredClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveRegisteredClientThenReturnsSavedRegisteredClientWhenSearchedByClientId() {
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("id1")
|
||||
.clientId("new-client-id")
|
||||
.clientSecret("secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
||||
.redirectUri("https://newclient.com")
|
||||
.scope("scope1").build();
|
||||
|
||||
this.clients.saveClient(registeredClient);
|
||||
|
||||
RegisteredClient savedClient = this.clients.findByClientId("new-client-id");
|
||||
|
||||
assertThat(savedClient).isNotNull().isEqualTo(registeredClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveRegisteredClientWithExistingIdThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> {
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("registration-1")
|
||||
.clientId("new-client")
|
||||
.clientSecret("secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
||||
.redirectUri("https://newclient.com")
|
||||
.scope("scope1").build();
|
||||
|
||||
this.clients.saveClient(registeredClient);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Registered client must be unique. Found duplicate identifier");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveRegisteredClientWithExistingClientIdThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> {
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("new-client")
|
||||
.clientId("client-1")
|
||||
.clientSecret("secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
|
||||
.redirectUri("https://newclient.com")
|
||||
.scope("scope1").build();
|
||||
|
||||
this.clients.saveClient(registeredClient);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Registered client must be unique. Found duplicate client identifier");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ public class ProviderSettingsTests {
|
||||
assertThat(providerSettings.jwkSetEndpoint()).isEqualTo("/oauth2/jwks");
|
||||
assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
|
||||
assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
|
||||
assertThat(providerSettings.oidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
|
||||
assertThat(providerSettings.isOidClientRegistrationEndpointEnabled()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -47,6 +49,7 @@ public class ProviderSettingsTests {
|
||||
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
|
||||
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
|
||||
String issuer = "https://example.com:9000";
|
||||
String oidcClientRegistrationEndpoint = "/connect/v1/register";
|
||||
|
||||
ProviderSettings providerSettings = new ProviderSettings()
|
||||
.issuer(issuer)
|
||||
@@ -54,7 +57,10 @@ public class ProviderSettingsTests {
|
||||
.tokenEndpoint(tokenEndpoint)
|
||||
.jwkSetEndpoint(jwkSetEndpoint)
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint)
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint);
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint)
|
||||
.isOidClientRegistrationEndpointEnabled(true)
|
||||
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint);
|
||||
|
||||
assertThat(providerSettings.issuer()).isEqualTo(issuer);
|
||||
assertThat(providerSettings.authorizationEndpoint()).isEqualTo(authorizationEndpoint);
|
||||
@@ -62,6 +68,8 @@ public class ProviderSettingsTests {
|
||||
assertThat(providerSettings.jwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
|
||||
assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
|
||||
assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
|
||||
assertThat(providerSettings.oidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
|
||||
assertThat(providerSettings.isOidClientRegistrationEndpointEnabled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,7 +78,7 @@ public class ProviderSettingsTests {
|
||||
.setting("name1", "value1")
|
||||
.settings(settings -> settings.put("name2", "value2"));
|
||||
|
||||
assertThat(providerSettings.settings()).hasSize(7);
|
||||
assertThat(providerSettings.settings()).hasSize(9);
|
||||
assertThat(providerSettings.<String>setting("name1")).isEqualTo("value1");
|
||||
assertThat(providerSettings.<String>setting("name2")).isEqualTo("value2");
|
||||
}
|
||||
@@ -115,6 +123,15 @@ public class ProviderSettingsTests {
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oidcClientRegistrationEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
ProviderSettings settings = new ProviderSettings();
|
||||
assertThatThrownBy(() -> settings.oidcClientRegistrationEndpoint(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("value cannot be null");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
ProviderSettings settings = new ProviderSettings();
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* 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 com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.AdditionalAnswers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
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.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcClientRegistrationEndpointFilter}
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @since 0.1.1
|
||||
*/
|
||||
public class OidcClientRegistrationEndpointFilterTests {
|
||||
|
||||
private static final OidcClientRegistration.Builder OIDC_CLIENT_REGISTRATION = OidcClientRegistration.builder()
|
||||
.redirectUri("https://localhost:8080/client")
|
||||
.responseType("code")
|
||||
.grantType("authorization_code")
|
||||
.tokenEndpointAuthenticationMethod("basic")
|
||||
.scope("test");
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
new OAuth2ErrorHttpMessageConverter();
|
||||
private static RegisteredClientRepository registeredClientRepository;
|
||||
private static AuthenticationManager authenticationManager;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
authenticationManager = mock(AuthenticationManager.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(registeredClientRepository);
|
||||
reset(authenticationManager);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(null,
|
||||
authenticationManager))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("registeredClientRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
|
||||
|
||||
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(registeredClientRepository, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authenticationManager cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenOidcClientRegistrationUriNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("oidcClientRegistrationUri cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenOidcClientRegistrationUriEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager, ""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("oidcClientRegistrationUri cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotClientRegistrationRequestThenNotProcessed() throws Exception {
|
||||
OidcClientRegistrationEndpointFilter filter =
|
||||
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
|
||||
|
||||
String requestUri = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenClientRegistrationRequestGetThenNotProcessed() throws Exception {
|
||||
|
||||
OidcClientRegistrationEndpointFilter filter =
|
||||
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
|
||||
|
||||
String requestUri = OidcClientRegistrationEndpointFilter.DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthenticationManagerThrowsOAuth2AuthenticationExceptionThenBadRequest() throws Exception {
|
||||
|
||||
setSecurityContext("client-registration-token", true, "SCOPE_client.create");
|
||||
|
||||
when(authenticationManager.authenticate(any(JwtAuthenticationToken.class)))
|
||||
.thenThrow(new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)));
|
||||
|
||||
OidcClientRegistrationEndpointFilter filter =
|
||||
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
|
||||
|
||||
String requestUri = OidcClientRegistrationEndpointFilter.DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
request.setContent(convertToByteArray(OIDC_CLIENT_REGISTRATION.build()));
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
OAuth2Error error = readError(response);
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void doFilterWhenClientRegistrationRequestThenClientRegistrationResponse() throws Exception {
|
||||
|
||||
doNothing().when(registeredClientRepository).saveClient(any(RegisteredClient.class));
|
||||
when(authenticationManager.authenticate(any(JwtAuthenticationToken.class))).then(AdditionalAnswers.returnsFirstArg());
|
||||
setSecurityContext("client-registration-token", true, "SCOPE_client.create");
|
||||
|
||||
OidcClientRegistrationEndpointFilter filter =
|
||||
new OidcClientRegistrationEndpointFilter(registeredClientRepository, authenticationManager);
|
||||
|
||||
String requestUri = OidcClientRegistrationEndpointFilter.DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
request.setContent(convertToByteArray(OIDC_CLIENT_REGISTRATION.build()));
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
verify(authenticationManager).authenticate(any());
|
||||
|
||||
ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
|
||||
verify(registeredClientRepository).saveClient(registeredClientCaptor.capture());
|
||||
|
||||
RegisteredClient registeredClient = registeredClientCaptor.getValue();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map<String, Object> clientRegistrationResponse = objectMapper.readerFor(Map.class)
|
||||
.readValue(response.getContentAsString());
|
||||
|
||||
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_ID))
|
||||
.isEqualTo(registeredClient.getClientId());
|
||||
assertThat((String) clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_SECRET))
|
||||
.isEqualTo(registeredClient.getClientSecret());
|
||||
assertThat((List<String>) clientRegistrationResponse.get(OidcClientMetadataClaimNames.REDIRECT_URIS))
|
||||
.containsAll(registeredClient.getRedirectUris());
|
||||
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT))
|
||||
.isNotNull();
|
||||
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT))
|
||||
.isEqualTo(0.0);
|
||||
assertThat((List<String>) clientRegistrationResponse.get(OidcClientMetadataClaimNames.RESPONSE_TYPES))
|
||||
.contains(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat((List<String>) clientRegistrationResponse.get(OidcClientMetadataClaimNames.GRANT_TYPES))
|
||||
.containsAll(grantTypes(registeredClient));
|
||||
|
||||
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.SCOPE))
|
||||
.isEqualTo(String.join(" ", registeredClient.getScopes()));
|
||||
assertThat(clientRegistrationResponse.get(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD))
|
||||
.isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
|
||||
}
|
||||
|
||||
private List<String> grantTypes(RegisteredClient registeredClient) {
|
||||
return registeredClient.getAuthorizationGrantTypes().stream()
|
||||
.map(AuthorizationGrantType::getValue)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static void setSecurityContext(String tokenValue, boolean authenticated, String... authorities) {
|
||||
Jwt jwt = Jwt.withTokenValue(tokenValue)
|
||||
.header("alg", "none")
|
||||
.claim("sub", "client")
|
||||
.build();
|
||||
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(authorities);
|
||||
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, grantedAuthorities);
|
||||
jwtAuthenticationToken.setAuthenticated(authenticated);
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(jwtAuthenticationToken);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
}
|
||||
|
||||
private static byte[] convertToByteArray(OidcClientRegistration clientRegistration) throws JsonProcessingException {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
return objectMapper
|
||||
.writerFor(Map.class)
|
||||
.writeValueAsBytes(clientRegistration.getClaims());
|
||||
}
|
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
|
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
||||
response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
|
||||
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user