Merge remote-tracking branch 'origin/7.0.x' into 7.0.x
This commit is contained in:
+3
-1
@@ -238,7 +238,9 @@ class HttpSecurityConfiguration {
|
|||||||
Map<Class<?>, Object> sharedObjects = new HashMap<>();
|
Map<Class<?>, Object> sharedObjects = new HashMap<>();
|
||||||
sharedObjects.put(ApplicationContext.class, this.context);
|
sharedObjects.put(ApplicationContext.class, this.context);
|
||||||
sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy);
|
sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy);
|
||||||
sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context));
|
sharedObjects.put(PathPatternRequestMatcher.Builder.class,
|
||||||
|
this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class)
|
||||||
|
.getIfUnique(() -> constructRequestMatcherBuilder(this.context)));
|
||||||
return sharedObjects;
|
return sharedObjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ public final class PathPatternRequestMatcherFactoryBean
|
|||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterPropertiesSet() throws Exception {
|
||||||
if (this.basePath != null) {
|
if (this.basePath != null) {
|
||||||
this.builder.basePath(this.basePath);
|
this.builder = this.builder.basePath(this.basePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
@@ -452,6 +452,18 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
|||||||
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
|
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestMatchersWhenBuilderBeanWithBasePathAndRawStringThenHonorsBasePath() throws Exception {
|
||||||
|
this.spring.register(RequestMatchersRawStringServletPathConfig.class, BasicController.class).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
MockHttpServletRequestBuilder matchedByBasePath = get("/spring/path")
|
||||||
|
.servletPath("/spring")
|
||||||
|
.with(user("user").roles("USER"));
|
||||||
|
// @formatter:on
|
||||||
|
this.mvc.perform(matchedByBasePath).andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/path").with(user("user").roles("USER"))).andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception {
|
public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception {
|
||||||
this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire();
|
this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire();
|
||||||
@@ -1359,6 +1371,32 @@ public class AuthorizeHttpRequestsConfigurerTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebMvc
|
||||||
|
@EnableWebSecurity
|
||||||
|
static class RequestMatchersRawStringServletPathConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||||
|
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||||
|
bean.setBasePath("/spring");
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
// @formatter:off
|
||||||
|
return http
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.requestMatchers("/path").hasRole("ADMIN")
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
static class AuthenticatedConfig {
|
static class AuthenticatedConfig {
|
||||||
|
|||||||
+105
@@ -125,6 +125,34 @@ public class HttpSecuritySecurityMatchersTests {
|
|||||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void securityMatcherWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception {
|
||||||
|
loadConfig(SecurityMatcherBuilderBeanConfig.class);
|
||||||
|
this.request.setServletPath("/spring");
|
||||||
|
this.request.setRequestURI("/spring/path");
|
||||||
|
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
setup();
|
||||||
|
this.request.setServletPath("");
|
||||||
|
this.request.setRequestURI("/path");
|
||||||
|
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void securityMatchersWhenBuilderBeanWithBasePathAndRawStringsThenHonorsBasePath() throws Exception {
|
||||||
|
loadConfig(SecurityMatchersBuilderBeanConfig.class);
|
||||||
|
this.request.setServletPath("/spring");
|
||||||
|
this.request.setRequestURI("/spring/path");
|
||||||
|
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
setup();
|
||||||
|
this.request.setServletPath("");
|
||||||
|
this.request.setRequestURI("/path");
|
||||||
|
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void securityMatchersWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception {
|
public void securityMatchersWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception {
|
||||||
loadConfig(MultiMvcMatcherInLambdaConfig.class);
|
loadConfig(MultiMvcMatcherInLambdaConfig.class);
|
||||||
@@ -430,6 +458,83 @@ public class HttpSecuritySecurityMatchersTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Configuration
|
||||||
|
@EnableWebMvc
|
||||||
|
@Import(UsersConfig.class)
|
||||||
|
static class SecurityMatcherBuilderBeanConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||||
|
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||||
|
bean.setBasePath("/spring");
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.securityMatcher("/path")
|
||||||
|
.httpBasic(withDefaults())
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.anyRequest().denyAll());
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
static class PathController {
|
||||||
|
|
||||||
|
@RequestMapping("/path")
|
||||||
|
String path() {
|
||||||
|
return "path";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Configuration
|
||||||
|
@EnableWebMvc
|
||||||
|
@Import(UsersConfig.class)
|
||||||
|
static class SecurityMatchersBuilderBeanConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
|
||||||
|
PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean();
|
||||||
|
bean.setBasePath("/spring");
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.securityMatchers((matchers) -> matchers
|
||||||
|
.requestMatchers("/path")
|
||||||
|
)
|
||||||
|
.httpBasic(withDefaults())
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize
|
||||||
|
.anyRequest().denyAll()
|
||||||
|
);
|
||||||
|
// @formatter:on
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
static class PathController {
|
||||||
|
|
||||||
|
@RequestMapping("/path")
|
||||||
|
String path() {
|
||||||
|
return "path";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
static class UsersConfig {
|
static class UsersConfig {
|
||||||
|
|
||||||
|
|||||||
+140
-4
@@ -79,6 +79,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
|||||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationValidator;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
|
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
@@ -411,6 +412,102 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
|
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenProtocolRelativeRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["//client.example.com/path"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenJavascriptSchemeRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["javascript:alert(document.cookie)"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenDataSchemeRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["data:text/html,<h1>content</h1>"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["https://client.example.com"],
|
||||||
|
"grant_types": ["authorization_code"],
|
||||||
|
"jwks_uri": "http://169.254.169.254/keys",
|
||||||
|
"token_endpoint_auth_method": "private_key_jwt"
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenArbitraryScopeThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["https://client.example.com"],
|
||||||
|
"grant_types": ["client_credentials"],
|
||||||
|
"scope": "read write"
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int requestWhenInvalidClientMetadataThenBadRequest(String json) throws Exception {
|
||||||
|
String clientRegistrationScope = "client.create";
|
||||||
|
// @formatter:off
|
||||||
|
RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-" + System.nanoTime())
|
||||||
|
.clientId("client-registrar-" + System.nanoTime())
|
||||||
|
.clientSecret("{noop}secret")
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||||
|
.scope(clientRegistrationScope)
|
||||||
|
.build();
|
||||||
|
// @formatter:on
|
||||||
|
this.registeredClientRepository.save(clientRegistrar);
|
||||||
|
|
||||||
|
MvcResult tokenResult = this.mvc
|
||||||
|
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
|
||||||
|
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||||
|
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
|
||||||
|
.with(httpBasic(clientRegistrar.getClientId(), "secret")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
OAuth2AccessToken accessToken = readAccessTokenResponse(tokenResult.getResponse()).getAccessToken();
|
||||||
|
|
||||||
|
HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
httpHeaders.setBearerAuth(accessToken.getTokenValue());
|
||||||
|
|
||||||
|
MvcResult registerResult = this.mvc
|
||||||
|
.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(json))
|
||||||
|
.andReturn();
|
||||||
|
return registerResult.getResponse().getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
private OAuth2ClientRegistration registerClient(OAuth2ClientRegistration clientRegistration) throws Exception {
|
private OAuth2ClientRegistration registerClient(OAuth2ClientRegistration clientRegistration) throws Exception {
|
||||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||||
|
|
||||||
@@ -496,6 +593,17 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
return clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
|
return clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Consumer<List<AuthenticationProvider>> scopePermissiveValidatorCustomizer() {
|
||||||
|
return (authenticationProviders) -> authenticationProviders.forEach((authenticationProvider) -> {
|
||||||
|
if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
|
||||||
|
provider.setAuthenticationValidator(
|
||||||
|
OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
||||||
@@ -512,7 +620,7 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
.clientRegistrationRequestConverter(authenticationConverter)
|
.clientRegistrationRequestConverter(authenticationConverter)
|
||||||
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
||||||
.authenticationProvider(authenticationProvider)
|
.authenticationProvider(authenticationProvider)
|
||||||
.authenticationProviders(authenticationProvidersConsumer)
|
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(authenticationProvidersConsumer))
|
||||||
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
||||||
.errorResponseHandler(authenticationFailureHandler)
|
.errorResponseHandler(authenticationFailureHandler)
|
||||||
)
|
)
|
||||||
@@ -539,7 +647,7 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
authorizationServer
|
authorizationServer
|
||||||
.clientRegistrationEndpoint((clientRegistration) ->
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
clientRegistration
|
clientRegistration
|
||||||
.authenticationProviders(configureClientRegistrationConverters())
|
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests((authorize) ->
|
.authorizeHttpRequests((authorize) ->
|
||||||
@@ -577,7 +685,7 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
authorizationServer
|
authorizationServer
|
||||||
.clientRegistrationEndpoint((clientRegistration) ->
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
clientRegistration
|
clientRegistration
|
||||||
.authenticationProviders(configureClientRegistrationConverters())
|
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests((authorize) ->
|
.authorizeHttpRequests((authorize) ->
|
||||||
@@ -614,6 +722,7 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
.clientRegistrationEndpoint((clientRegistration) ->
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
clientRegistration
|
clientRegistration
|
||||||
.openRegistrationAllowed(true)
|
.openRegistrationAllowed(true)
|
||||||
|
.authenticationProviders(scopePermissiveValidatorCustomizer())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests((authorize) ->
|
.authorizeHttpRequests((authorize) ->
|
||||||
@@ -627,6 +736,30 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class DefaultValidatorConfiguration extends AuthorizationServerConfiguration {
|
||||||
|
|
||||||
|
// Override with Customizer.withDefaults() so the default (strict)
|
||||||
|
// OAuth2ClientRegistrationAuthenticationValidator is in effect.
|
||||||
|
// @formatter:off
|
||||||
|
@Bean
|
||||||
|
@Override
|
||||||
|
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.oauth2AuthorizationServer((authorizationServer) ->
|
||||||
|
authorizationServer
|
||||||
|
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests((authorize) ->
|
||||||
|
authorize.anyRequest().authenticated()
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class AuthorizationServerConfiguration {
|
static class AuthorizationServerConfiguration {
|
||||||
@@ -637,7 +770,10 @@ public class OAuth2ClientRegistrationTests {
|
|||||||
http
|
http
|
||||||
.oauth2AuthorizationServer((authorizationServer) ->
|
.oauth2AuthorizationServer((authorizationServer) ->
|
||||||
authorizationServer
|
authorizationServer
|
||||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
|
clientRegistration
|
||||||
|
.authenticationProviders(scopePermissiveValidatorCustomizer())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests((authorize) ->
|
.authorizeHttpRequests((authorize) ->
|
||||||
authorize.anyRequest().authenticated()
|
authorize.anyRequest().authenticated()
|
||||||
|
|||||||
+170
-4
@@ -97,6 +97,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcClientR
|
|||||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
||||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationValidator;
|
||||||
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
|
import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter;
|
||||||
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
|
import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter;
|
||||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||||
@@ -545,6 +546,129 @@ public class OidcClientRegistrationTests {
|
|||||||
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
|
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenProtocolRelativeRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["//client.example.com/path"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenJavascriptSchemeRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["javascript:alert(document.cookie)"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenDataSchemeRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["data:text/html,<h1>content</h1>"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenJavascriptSchemePostLogoutRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["https://client.example.com"],
|
||||||
|
"post_logout_redirect_uris": ["javascript:alert(document.cookie)"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenDataSchemePostLogoutRedirectUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["https://client.example.com"],
|
||||||
|
"post_logout_redirect_uris": ["data:text/html,<h1>content</h1>"],
|
||||||
|
"grant_types": ["authorization_code"]
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["https://client.example.com"],
|
||||||
|
"grant_types": ["authorization_code"],
|
||||||
|
"jwks_uri": "http://169.254.169.254/keys",
|
||||||
|
"token_endpoint_auth_method": "private_key_jwt"
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenArbitraryScopeThenBadRequest() throws Exception {
|
||||||
|
this.spring.register(DefaultValidatorConfiguration.class).autowire();
|
||||||
|
assertThat(requestWhenInvalidClientMetadataThenBadRequest("""
|
||||||
|
{
|
||||||
|
"client_name": "client-name",
|
||||||
|
"redirect_uris": ["https://client.example.com"],
|
||||||
|
"grant_types": ["authorization_code"],
|
||||||
|
"scope": "read write"
|
||||||
|
}
|
||||||
|
""")).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int requestWhenInvalidClientMetadataThenBadRequest(String json) throws Exception {
|
||||||
|
String clientRegistrationScope = "client.create";
|
||||||
|
String clientId = "client-registrar-" + System.nanoTime();
|
||||||
|
// @formatter:off
|
||||||
|
RegisteredClient clientRegistrar = RegisteredClient.withId(clientId)
|
||||||
|
.clientId(clientId)
|
||||||
|
.clientSecret("{noop}secret")
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||||
|
.scope(clientRegistrationScope)
|
||||||
|
.build();
|
||||||
|
// @formatter:on
|
||||||
|
this.registeredClientRepository.save(clientRegistrar);
|
||||||
|
|
||||||
|
MvcResult tokenResult = this.mvc
|
||||||
|
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
|
||||||
|
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||||
|
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
|
||||||
|
.with(httpBasic(clientId, "secret")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
OAuth2AccessToken accessToken = readAccessTokenResponse(tokenResult.getResponse()).getAccessToken();
|
||||||
|
|
||||||
|
HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
httpHeaders.setBearerAuth(accessToken.getTokenValue());
|
||||||
|
|
||||||
|
MvcResult registerResult = this.mvc
|
||||||
|
.perform(post(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(json))
|
||||||
|
.andReturn();
|
||||||
|
return registerResult.getResponse().getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
||||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||||
|
|
||||||
@@ -642,6 +766,18 @@ public class OidcClientRegistrationTests {
|
|||||||
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Consumer<List<AuthenticationProvider>> scopePermissiveValidatorCustomizer() {
|
||||||
|
return (authenticationProviders) -> authenticationProviders.forEach((authenticationProvider) -> {
|
||||||
|
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) {
|
||||||
|
provider.setAuthenticationValidator(
|
||||||
|
OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR.andThen(
|
||||||
|
OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
||||||
@@ -660,7 +796,7 @@ public class OidcClientRegistrationTests {
|
|||||||
.clientRegistrationRequestConverter(authenticationConverter)
|
.clientRegistrationRequestConverter(authenticationConverter)
|
||||||
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
||||||
.authenticationProvider(authenticationProvider)
|
.authenticationProvider(authenticationProvider)
|
||||||
.authenticationProviders(authenticationProvidersConsumer)
|
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(authenticationProvidersConsumer))
|
||||||
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
||||||
.errorResponseHandler(authenticationFailureHandler)
|
.errorResponseHandler(authenticationFailureHandler)
|
||||||
)
|
)
|
||||||
@@ -690,7 +826,7 @@ public class OidcClientRegistrationTests {
|
|||||||
oidc
|
oidc
|
||||||
.clientRegistrationEndpoint((clientRegistration) ->
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
clientRegistration
|
clientRegistration
|
||||||
.authenticationProviders(configureClientRegistrationConverters())
|
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -731,7 +867,7 @@ public class OidcClientRegistrationTests {
|
|||||||
oidc
|
oidc
|
||||||
.clientRegistrationEndpoint((clientRegistration) ->
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
clientRegistration
|
clientRegistration
|
||||||
.authenticationProviders(configureClientRegistrationConverters())
|
.authenticationProviders(scopePermissiveValidatorCustomizer().andThen(configureClientRegistrationConverters()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -755,6 +891,33 @@ public class OidcClientRegistrationTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class DefaultValidatorConfiguration extends AuthorizationServerConfiguration {
|
||||||
|
|
||||||
|
// Override with Customizer.withDefaults() so the default (strict)
|
||||||
|
// OidcClientRegistrationAuthenticationValidator is in effect.
|
||||||
|
// @formatter:off
|
||||||
|
@Bean
|
||||||
|
@Override
|
||||||
|
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.oauth2AuthorizationServer((authorizationServer) ->
|
||||||
|
authorizationServer
|
||||||
|
.oidc((oidc) ->
|
||||||
|
oidc
|
||||||
|
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests((authorize) ->
|
||||||
|
authorize.anyRequest().authenticated()
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class AuthorizationServerConfiguration {
|
static class AuthorizationServerConfiguration {
|
||||||
@@ -767,7 +930,10 @@ public class OidcClientRegistrationTests {
|
|||||||
authorizationServer
|
authorizationServer
|
||||||
.oidc((oidc) ->
|
.oidc((oidc) ->
|
||||||
oidc
|
oidc
|
||||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
.clientRegistrationEndpoint((clientRegistration) ->
|
||||||
|
clientRegistration
|
||||||
|
.authenticationProviders(scopePermissiveValidatorCustomizer())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests((authorize) ->
|
.authorizeHttpRequests((authorize) ->
|
||||||
|
|||||||
+72
@@ -314,6 +314,78 @@ public class InterceptUrlConfigTests {
|
|||||||
.autowire());
|
.autowire());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenUsingDefaultMatcherAndServletPathThenAuthorizesRequestsAccordingly() throws Exception {
|
||||||
|
this.spring.configLocations(this.xml("DefaultMatcherServletPath")).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mvc.perform(get("/spring/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenUsingDefaultMatcherAndServletPathAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||||
|
throws Exception {
|
||||||
|
this.spring.configLocations(this.xml("DefaultMatcherServletPathAuthorizationManager")).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mvc.perform(get("/spring/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
// @formatter:on
|
||||||
|
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenUsingRegexMatcherThenAuthorizesRequestsAccordingly() throws Exception {
|
||||||
|
this.spring.configLocations(this.xml("RegexMatcher")).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mvc.perform(get("/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/other").with(userCredentials()))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenUsingRegexMatcherAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||||
|
throws Exception {
|
||||||
|
this.spring.configLocations(this.xml("RegexMatcherAuthorizationManager")).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mvc.perform(get("/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/other").with(userCredentials()))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
// @formatter:on
|
||||||
|
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenUsingCiRegexMatcherThenAuthorizesRequestsAccordingly() throws Exception {
|
||||||
|
this.spring.configLocations(this.xml("CiRegexMatcher")).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mvc.perform(get("/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/PATH").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestWhenUsingCiRegexMatcherAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||||
|
throws Exception {
|
||||||
|
this.spring.configLocations(this.xml("CiRegexMatcherAuthorizationManager")).autowire();
|
||||||
|
// @formatter:off
|
||||||
|
this.mvc.perform(get("/path").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
this.mvc.perform(get("/PATH").with(userCredentials()))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
// @formatter:on
|
||||||
|
assertThat(this.spring.getContext().getBean(AuthorizationManager.class)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void requestWhenUsingFilterAllDispatcherTypesAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
public void requestWhenUsingFilterAllDispatcherTypesAndAuthorizationManagerThenAuthorizesRequestsAccordingly()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2004-present 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.springframework.org/schema/security"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/security
|
||||||
|
https://www.springframework.org/schema/security/spring-security.xsd
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||||
|
|
||||||
|
<http request-matcher="ciRegex" use-authorization-manager="false">
|
||||||
|
<intercept-url pattern="\A/PATH\Z" access="denyAll"/>
|
||||||
|
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||||
|
<http-basic/>
|
||||||
|
</http>
|
||||||
|
|
||||||
|
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||||
|
|
||||||
|
<b:import resource="userservice.xml"/>
|
||||||
|
</b:beans>
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2004-present 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.springframework.org/schema/security"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/security
|
||||||
|
https://www.springframework.org/schema/security/spring-security.xsd
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||||
|
|
||||||
|
<http request-matcher="ciRegex">
|
||||||
|
<intercept-url pattern="\A/PATH\Z" access="denyAll"/>
|
||||||
|
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||||
|
<http-basic/>
|
||||||
|
</http>
|
||||||
|
|
||||||
|
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||||
|
|
||||||
|
<b:import resource="userservice.xml"/>
|
||||||
|
</b:beans>
|
||||||
+3
@@ -26,8 +26,11 @@
|
|||||||
|
|
||||||
<http use-authorization-manager="false">
|
<http use-authorization-manager="false">
|
||||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||||
|
<intercept-url pattern="/**" access="permitAll"/>
|
||||||
<http-basic/>
|
<http-basic/>
|
||||||
</http>
|
</http>
|
||||||
|
|
||||||
|
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||||
|
|
||||||
<b:import resource="userservice.xml"/>
|
<b:import resource="userservice.xml"/>
|
||||||
</b:beans>
|
</b:beans>
|
||||||
|
|||||||
+3
@@ -26,8 +26,11 @@
|
|||||||
|
|
||||||
<http>
|
<http>
|
||||||
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
<intercept-url pattern="/path" access="denyAll" servlet-path="/spring"/>
|
||||||
|
<intercept-url pattern="/**" access="permitAll"/>
|
||||||
<http-basic/>
|
<http-basic/>
|
||||||
</http>
|
</http>
|
||||||
|
|
||||||
|
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||||
|
|
||||||
<b:import resource="userservice.xml"/>
|
<b:import resource="userservice.xml"/>
|
||||||
</b:beans>
|
</b:beans>
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2004-present 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.springframework.org/schema/security"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/security
|
||||||
|
https://www.springframework.org/schema/security/spring-security.xsd
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||||
|
|
||||||
|
<http request-matcher="regex" use-authorization-manager="false">
|
||||||
|
<intercept-url pattern="\A/path\Z" access="denyAll"/>
|
||||||
|
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||||
|
<http-basic/>
|
||||||
|
</http>
|
||||||
|
|
||||||
|
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||||
|
|
||||||
|
<b:import resource="userservice.xml"/>
|
||||||
|
</b:beans>
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2004-present 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.springframework.org/schema/security"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/security
|
||||||
|
https://www.springframework.org/schema/security/spring-security.xsd
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||||
|
|
||||||
|
<http request-matcher="regex">
|
||||||
|
<intercept-url pattern="\A/path\Z" access="denyAll"/>
|
||||||
|
<intercept-url pattern="\A/.*\Z" access="permitAll"/>
|
||||||
|
<http-basic/>
|
||||||
|
</http>
|
||||||
|
|
||||||
|
<b:bean name="path" class="org.springframework.security.config.http.InterceptUrlConfigTests.PathController"/>
|
||||||
|
|
||||||
|
<b:import resource="userservice.xml"/>
|
||||||
|
</b:beans>
|
||||||
+2
-3
@@ -467,11 +467,10 @@ public class DaoAuthenticationProviderTests {
|
|||||||
public void testDisabledUserTiming() {
|
public void testDisabledUserTiming() {
|
||||||
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
|
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
|
||||||
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
|
||||||
provider.setPasswordEncoder(encoder);
|
|
||||||
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
|
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
|
||||||
users.password = encoder.encode((CharSequence) user.getCredentials());
|
users.password = encoder.encode((CharSequence) user.getCredentials());
|
||||||
provider.setUserDetailsService(users);
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(users);
|
||||||
|
provider.setPasswordEncoder(encoder);
|
||||||
int sampleSize = 100;
|
int sampleSize = 100;
|
||||||
List<Long> enabledTimes = new ArrayList<>(sampleSize);
|
List<Long> enabledTimes = new ArrayList<>(sampleSize);
|
||||||
for (int i = 0; i < sampleSize; i++) {
|
for (int i = 0; i < sampleSize; i++) {
|
||||||
|
|||||||
@@ -14,3 +14,5 @@
|
|||||||
^http://schemas.openid.net/event/backchannel-logout
|
^http://schemas.openid.net/event/backchannel-logout
|
||||||
^http://host.docker.internal:8090/back-channel/logout
|
^http://host.docker.internal:8090/back-channel/logout
|
||||||
^http://host.docker.internal:8090/logout
|
^http://host.docker.internal:8090/logout
|
||||||
|
^http://169.254.169.254/keys
|
||||||
|
|
||||||
|
|||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present 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 java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link OAuth2AuthenticationContext} that holds an
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationToken} and additional information and is
|
||||||
|
* used when validating the OAuth 2.0 Client Registration Request parameters.
|
||||||
|
*
|
||||||
|
* @author addcontent
|
||||||
|
* @since 7.0.5
|
||||||
|
* @see OAuth2AuthenticationContext
|
||||||
|
* @see OAuth2ClientRegistrationAuthenticationToken
|
||||||
|
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||||
|
*/
|
||||||
|
public final class OAuth2ClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
|
||||||
|
|
||||||
|
private final Map<Object, Object> context;
|
||||||
|
|
||||||
|
private OAuth2ClientRegistrationAuthenticationContext(Map<Object, Object> context) {
|
||||||
|
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <V> V get(Object key) {
|
||||||
|
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasKey(Object key) {
|
||||||
|
Assert.notNull(key, "key cannot be null");
|
||||||
|
return this.context.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link Builder} with the provided
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationToken}.
|
||||||
|
* @param authentication the {@link OAuth2ClientRegistrationAuthenticationToken}
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public static Builder with(OAuth2ClientRegistrationAuthenticationToken authentication) {
|
||||||
|
return new Builder(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for {@link OAuth2ClientRegistrationAuthenticationContext}.
|
||||||
|
*/
|
||||||
|
public static final class Builder extends AbstractBuilder<OAuth2ClientRegistrationAuthenticationContext, Builder> {
|
||||||
|
|
||||||
|
private Builder(OAuth2ClientRegistrationAuthenticationToken authentication) {
|
||||||
|
super(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link OAuth2ClientRegistrationAuthenticationContext}.
|
||||||
|
* @return the {@link OAuth2ClientRegistrationAuthenticationContext}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OAuth2ClientRegistrationAuthenticationContext build() {
|
||||||
|
return new OAuth2ClientRegistrationAuthenticationContext(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+29
-37
@@ -16,12 +16,10 @@
|
|||||||
|
|
||||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
@@ -35,12 +33,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
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.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||||
@@ -49,7 +45,6 @@ import org.springframework.security.oauth2.server.authorization.converter.OAuth2
|
|||||||
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
|
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,8 +62,6 @@ import org.springframework.util.StringUtils;
|
|||||||
*/
|
*/
|
||||||
public final class OAuth2ClientRegistrationAuthenticationProvider implements AuthenticationProvider {
|
public final class OAuth2ClientRegistrationAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
|
|
||||||
|
|
||||||
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
|
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
|
||||||
|
|
||||||
private final Log logger = LogFactory.getLog(getClass());
|
private final Log logger = LogFactory.getLog(getClass());
|
||||||
@@ -85,6 +78,8 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
|||||||
|
|
||||||
private boolean openRegistrationAllowed;
|
private boolean openRegistrationAllowed;
|
||||||
|
|
||||||
|
private Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
|
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
|
||||||
* provided parameters.
|
* provided parameters.
|
||||||
@@ -99,6 +94,7 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
|||||||
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
|
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
|
||||||
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
|
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
|
||||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||||
|
this.authenticationValidator = new OAuth2ClientRegistrationAuthenticationValidator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -197,14 +193,35 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
|||||||
this.openRegistrationAllowed = openRegistrationAllowed;
|
this.openRegistrationAllowed = openRegistrationAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code Consumer} providing access to the
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
|
||||||
|
* validating specific OAuth 2.0 Client Registration Request parameters associated in
|
||||||
|
* the {@link OAuth2ClientRegistrationAuthenticationToken}. The default authentication
|
||||||
|
* validator is {@link OAuth2ClientRegistrationAuthenticationValidator}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> The authentication validator MUST throw
|
||||||
|
* {@link OAuth2AuthenticationException} if validation fails.
|
||||||
|
* @param authenticationValidator the {@code Consumer} providing access to the
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
|
||||||
|
* validating specific OAuth 2.0 Client Registration Request parameters
|
||||||
|
* @since 7.0.5
|
||||||
|
*/
|
||||||
|
public void setAuthenticationValidator(
|
||||||
|
Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator) {
|
||||||
|
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||||
|
this.authenticationValidator = authenticationValidator;
|
||||||
|
}
|
||||||
|
|
||||||
private OAuth2ClientRegistrationAuthenticationToken registerClient(
|
private OAuth2ClientRegistrationAuthenticationToken registerClient(
|
||||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||||
OAuth2Authorization authorization) {
|
OAuth2Authorization authorization) {
|
||||||
|
|
||||||
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
|
OAuth2ClientRegistrationAuthenticationContext authenticationContext = OAuth2ClientRegistrationAuthenticationContext
|
||||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
.with(clientRegistrationAuthentication)
|
||||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
.build();
|
||||||
}
|
this.authenticationValidator.accept(authenticationContext);
|
||||||
|
|
||||||
if (this.logger.isTraceEnabled()) {
|
if (this.logger.isTraceEnabled()) {
|
||||||
this.logger.trace("Validated client registration request parameters");
|
this.logger.trace("Validated client registration request parameters");
|
||||||
@@ -277,29 +294,4 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidRedirectUris(List<String> redirectUris) {
|
|
||||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String redirectUri : redirectUris) {
|
|
||||||
try {
|
|
||||||
URI validRedirectUri = new URI(redirectUri);
|
|
||||||
if (validRedirectUri.getFragment() != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (URISyntaxException ex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
|
|
||||||
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
|
|
||||||
throw new OAuth2AuthenticationException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present 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 java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.core.log.LogMessage;
|
||||||
|
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.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code Consumer} providing access to the
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationContext} containing an
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationToken} and is the default
|
||||||
|
* {@link OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||||
|
* authentication validator} used for validating specific OAuth 2.0 Dynamic Client
|
||||||
|
* Registration Request parameters (RFC 7591).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The default implementation validates {@link OAuth2ClientRegistration#getRedirectUris()
|
||||||
|
* redirect_uris}, {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}, and
|
||||||
|
* {@link OAuth2ClientRegistration#getScopes() scope}. If validation fails, an
|
||||||
|
* {@link OAuth2AuthenticationException} is thrown.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Each validated field is backed by two public constants:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code DEFAULT_*_VALIDATOR} — strict validation that rejects unsafe values. This is
|
||||||
|
* the default behavior and may reject input that was previously accepted.</li>
|
||||||
|
* <li>{@code SIMPLE_*_VALIDATOR} — lenient validation preserving the behavior from prior
|
||||||
|
* releases. Use only when strictly required for backward compatibility and with full
|
||||||
|
* understanding that it may accept values that enable attacks against the authorization
|
||||||
|
* server.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author addcontent
|
||||||
|
* @since 7.0.5
|
||||||
|
* @see OAuth2ClientRegistrationAuthenticationContext
|
||||||
|
* @see OAuth2ClientRegistrationAuthenticationToken
|
||||||
|
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||||
|
*/
|
||||||
|
public final class OAuth2ClientRegistrationAuthenticationValidator
|
||||||
|
implements Consumer<OAuth2ClientRegistrationAuthenticationContext> {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
|
||||||
|
|
||||||
|
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientRegistrationAuthenticationValidator.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OAuth2ClientRegistration#getRedirectUris()
|
||||||
|
* redirect_uris}. Rejects URIs that contain a fragment, have no scheme (e.g.
|
||||||
|
* protocol-relative {@code //host/path}), or use an unsafe scheme
|
||||||
|
* ({@code javascript}, {@code data}, {@code vbscript}).
|
||||||
|
*/
|
||||||
|
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUris;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OAuth2ClientRegistration#getRedirectUris()
|
||||||
|
* redirect_uris} that preserves prior behavior (fragment-only check). Use only when
|
||||||
|
* backward compatibility is required; values that enable open redirect and XSS
|
||||||
|
* attacks may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}.
|
||||||
|
* Rejects URIs that do not use the {@code https} scheme.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}
|
||||||
|
* that preserves prior behavior (no validation). Use only when backward compatibility
|
||||||
|
* is required; values that enable SSRF attacks may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OAuth2ClientRegistration#getScopes() scope}.
|
||||||
|
* Rejects any request that includes a non-empty scope value. Deployers that need to
|
||||||
|
* accept scopes during Dynamic Client Registration must configure their own validator
|
||||||
|
* (for example, by chaining on top of {@link #SIMPLE_SCOPE_VALIDATOR}).
|
||||||
|
*/
|
||||||
|
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OAuth2ClientRegistration#getScopes() scope} that
|
||||||
|
* preserves prior behavior (accepts any scope). Use only when backward compatibility
|
||||||
|
* is required; values that enable arbitrary scope injection may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScopeSimple;
|
||||||
|
|
||||||
|
private final Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
this.authenticationValidator.accept(authenticationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateRedirectUris(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||||
|
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String redirectUri : redirectUris) {
|
||||||
|
URI parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URI(redirectUri);
|
||||||
|
}
|
||||||
|
catch (URISyntaxException ex) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER
|
||||||
|
.debug(LogMessage.format("Invalid request: redirect_uri is not parseable ('%s')", redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsed.getFragment() != null) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(
|
||||||
|
LogMessage.format("Invalid request: redirect_uri contains a fragment ('%s')", redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
String scheme = parsed.getScheme();
|
||||||
|
if (scheme == null) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format("Invalid request: redirect_uri has no scheme ('%s')", redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
if (isUnsafeScheme(scheme)) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(
|
||||||
|
LogMessage.format("Invalid request: redirect_uri uses unsafe scheme ('%s')", redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateRedirectUrisSimple(
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||||
|
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String redirectUri : redirectUris) {
|
||||||
|
try {
|
||||||
|
URI parsed = new URI(redirectUri);
|
||||||
|
if (parsed.getFragment() != null) {
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (URISyntaxException ex) {
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateJwkSetUri(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
URL jwkSetUrl = clientRegistrationAuthentication.getClientRegistration().getJwkSetUrl();
|
||||||
|
if (jwkSetUrl == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!"https".equalsIgnoreCase(jwkSetUrl.getProtocol())) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format("Invalid request: jwks_uri does not use https ('%s')", jwkSetUrl));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration("invalid_client_metadata", OAuth2ClientMetadataClaimNames.JWKS_URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateJwkSetUriSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
// No validation. Preserves prior behavior.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateScope(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> scopes = clientRegistrationAuthentication.getClientRegistration().getScopes();
|
||||||
|
if (!CollectionUtils.isEmpty(scopes)) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format(
|
||||||
|
"Invalid request: scope must not be set during Dynamic Client Registration ('%s')", scopes));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ClientMetadataClaimNames.SCOPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateScopeSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
// No validation. Preserves prior behavior.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isUnsafeScheme(String scheme) {
|
||||||
|
return "javascript".equalsIgnoreCase(scheme) || "data".equalsIgnoreCase(scheme)
|
||||||
|
|| "vbscript".equalsIgnoreCase(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
|
||||||
|
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
|
||||||
|
throw new OAuth2AuthenticationException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present 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 java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationContext;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link OAuth2AuthenticationContext} that holds an
|
||||||
|
* {@link OidcClientRegistrationAuthenticationToken} and additional information and is
|
||||||
|
* used when validating the OpenID Connect 1.0 Client Registration Request parameters.
|
||||||
|
*
|
||||||
|
* @author addcontent
|
||||||
|
* @since 7.0.5
|
||||||
|
* @see OAuth2AuthenticationContext
|
||||||
|
* @see OidcClientRegistrationAuthenticationToken
|
||||||
|
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||||
|
*/
|
||||||
|
public final class OidcClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
|
||||||
|
|
||||||
|
private final Map<Object, Object> context;
|
||||||
|
|
||||||
|
private OidcClientRegistrationAuthenticationContext(Map<Object, Object> context) {
|
||||||
|
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <V> V get(Object key) {
|
||||||
|
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasKey(Object key) {
|
||||||
|
Assert.notNull(key, "key cannot be null");
|
||||||
|
return this.context.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link Builder} with the provided
|
||||||
|
* {@link OidcClientRegistrationAuthenticationToken}.
|
||||||
|
* @param authentication the {@link OidcClientRegistrationAuthenticationToken}
|
||||||
|
* @return the {@link Builder}
|
||||||
|
*/
|
||||||
|
public static Builder with(OidcClientRegistrationAuthenticationToken authentication) {
|
||||||
|
return new Builder(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for {@link OidcClientRegistrationAuthenticationContext}.
|
||||||
|
*/
|
||||||
|
public static final class Builder extends AbstractBuilder<OidcClientRegistrationAuthenticationContext, Builder> {
|
||||||
|
|
||||||
|
private Builder(OidcClientRegistrationAuthenticationToken authentication) {
|
||||||
|
super(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link OidcClientRegistrationAuthenticationContext}.
|
||||||
|
* @return the {@link OidcClientRegistrationAuthenticationContext}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OidcClientRegistrationAuthenticationContext build() {
|
||||||
|
return new OidcClientRegistrationAuthenticationContext(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+29
-34
@@ -16,14 +16,12 @@
|
|||||||
|
|
||||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
@@ -60,7 +58,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
|
|||||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,6 +99,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
|||||||
|
|
||||||
private PasswordEncoder passwordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
private Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the
|
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the
|
||||||
* provided parameters.
|
* provided parameters.
|
||||||
@@ -121,6 +120,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
|||||||
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
|
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
|
||||||
this.registeredClientConverter = new OidcClientRegistrationRegisteredClientConverter();
|
this.registeredClientConverter = new OidcClientRegistrationRegisteredClientConverter();
|
||||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||||
|
this.authenticationValidator = new OidcClientRegistrationAuthenticationValidator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -206,20 +206,35 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
|||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code Consumer} providing access to the
|
||||||
|
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
|
||||||
|
* validating specific OpenID Connect 1.0 Client Registration Request parameters
|
||||||
|
* associated in the {@link OidcClientRegistrationAuthenticationToken}. The default
|
||||||
|
* authentication validator is {@link OidcClientRegistrationAuthenticationValidator}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> The authentication validator MUST throw
|
||||||
|
* {@link OAuth2AuthenticationException} if validation fails.
|
||||||
|
* @param authenticationValidator the {@code Consumer} providing access to the
|
||||||
|
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
|
||||||
|
* validating specific OpenID Connect 1.0 Client Registration Request parameters
|
||||||
|
* @since 7.0.5
|
||||||
|
*/
|
||||||
|
public void setAuthenticationValidator(
|
||||||
|
Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator) {
|
||||||
|
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||||
|
this.authenticationValidator = authenticationValidator;
|
||||||
|
}
|
||||||
|
|
||||||
private OidcClientRegistrationAuthenticationToken registerClient(
|
private OidcClientRegistrationAuthenticationToken registerClient(
|
||||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||||
OAuth2Authorization authorization) {
|
OAuth2Authorization authorization) {
|
||||||
|
|
||||||
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
|
OidcClientRegistrationAuthenticationContext authenticationContext = OidcClientRegistrationAuthenticationContext
|
||||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
.with(clientRegistrationAuthentication)
|
||||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
.build();
|
||||||
}
|
this.authenticationValidator.accept(authenticationContext);
|
||||||
|
|
||||||
if (!isValidRedirectUris(
|
|
||||||
clientRegistrationAuthentication.getClientRegistration().getPostLogoutRedirectUris())) {
|
|
||||||
throwInvalidClientRegistration("invalid_client_metadata",
|
|
||||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) {
|
if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) {
|
||||||
throwInvalidClientRegistration("invalid_client_metadata",
|
throwInvalidClientRegistration("invalid_client_metadata",
|
||||||
@@ -351,26 +366,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidRedirectUris(List<String> redirectUris) {
|
|
||||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String redirectUri : redirectUris) {
|
|
||||||
try {
|
|
||||||
URI validRedirectUri = new URI(redirectUri);
|
|
||||||
if (validRedirectUri.getFragment() != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (URISyntaxException ex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) {
|
private static boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) {
|
||||||
String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod();
|
String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod();
|
||||||
String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
|
String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
|
||||||
|
|||||||
+285
@@ -0,0 +1,285 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present 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.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.core.log.LogMessage;
|
||||||
|
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.server.authorization.oidc.OidcClientMetadataClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code Consumer} providing access to the
|
||||||
|
* {@link OidcClientRegistrationAuthenticationContext} containing an
|
||||||
|
* {@link OidcClientRegistrationAuthenticationToken} and is the default
|
||||||
|
* {@link OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||||
|
* authentication validator} used for validating specific OpenID Connect 1.0 Dynamic
|
||||||
|
* Client Registration Request parameters.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The default implementation validates {@link OidcClientRegistration#getRedirectUris()
|
||||||
|
* redirect_uris}, {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||||
|
* post_logout_redirect_uris}, {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}, and
|
||||||
|
* {@link OidcClientRegistration#getScopes() scope}. If validation fails, an
|
||||||
|
* {@link OAuth2AuthenticationException} is thrown.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Each validated field is backed by two public constants:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code DEFAULT_*_VALIDATOR} — strict validation that rejects unsafe values. This is
|
||||||
|
* the default behavior and may reject input that was previously accepted.</li>
|
||||||
|
* <li>{@code SIMPLE_*_VALIDATOR} — lenient validation preserving the behavior from prior
|
||||||
|
* releases. Use only when strictly required for backward compatibility and with full
|
||||||
|
* understanding that it may accept values that enable attacks against the authorization
|
||||||
|
* server.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author addcontent
|
||||||
|
* @since 7.0.5
|
||||||
|
* @see OidcClientRegistrationAuthenticationContext
|
||||||
|
* @see OidcClientRegistrationAuthenticationToken
|
||||||
|
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||||
|
*/
|
||||||
|
public final class OidcClientRegistrationAuthenticationValidator
|
||||||
|
implements Consumer<OidcClientRegistrationAuthenticationContext> {
|
||||||
|
|
||||||
|
private static final String ERROR_URI = "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError";
|
||||||
|
|
||||||
|
private static final Log LOGGER = LogFactory.getLog(OidcClientRegistrationAuthenticationValidator.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OidcClientRegistration#getRedirectUris()
|
||||||
|
* redirect_uris}. Rejects URIs that contain a fragment, have no scheme (e.g.
|
||||||
|
* protocol-relative {@code //host/path}), or use an unsafe scheme
|
||||||
|
* ({@code javascript}, {@code data}, {@code vbscript}).
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUris;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OidcClientRegistration#getRedirectUris()
|
||||||
|
* redirect_uris} that preserves prior behavior (fragment-only check). Use only when
|
||||||
|
* backward compatibility is required; values that enable open redirect and XSS
|
||||||
|
* attacks may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||||
|
* post_logout_redirect_uris}. Applies the same rules as
|
||||||
|
* {@link #DEFAULT_REDIRECT_URI_VALIDATOR}.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUris;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||||
|
* post_logout_redirect_uris} that preserves prior behavior (fragment-only check). Use
|
||||||
|
* only when backward compatibility is required; values that enable XSS attacks on the
|
||||||
|
* authorization server origin may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUrisSimple;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}.
|
||||||
|
* Rejects URIs that do not use the {@code https} scheme.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}
|
||||||
|
* that preserves prior behavior (no validation). Use only when backward compatibility
|
||||||
|
* is required; values that enable SSRF attacks may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default validator for {@link OidcClientRegistration#getScopes() scope}. Rejects
|
||||||
|
* any request that includes a non-empty scope value. Deployers that need to accept
|
||||||
|
* scopes during Dynamic Client Registration must configure their own validator (for
|
||||||
|
* example, by chaining on top of {@link #SIMPLE_SCOPE_VALIDATOR}).
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple validator for {@link OidcClientRegistration#getScopes() scope} that
|
||||||
|
* preserves prior behavior (accepts any scope). Use only when backward compatibility
|
||||||
|
* is required; values that enable arbitrary scope injection may be accepted.
|
||||||
|
*/
|
||||||
|
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScopeSimple;
|
||||||
|
|
||||||
|
private final Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||||
|
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
this.authenticationValidator.accept(authenticationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateRedirectUris(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||||
|
validateRedirectUrisStrict(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validatePostLogoutRedirectUris(
|
||||||
|
OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
|
||||||
|
.getPostLogoutRedirectUris();
|
||||||
|
validateRedirectUrisStrict(postLogoutRedirectUris, "invalid_client_metadata",
|
||||||
|
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateRedirectUrisStrict(List<String> redirectUris, String errorCode, String fieldName) {
|
||||||
|
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String redirectUri : redirectUris) {
|
||||||
|
URI parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URI(redirectUri);
|
||||||
|
}
|
||||||
|
catch (URISyntaxException ex) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(
|
||||||
|
LogMessage.format("Invalid request: %s is not parseable ('%s')", fieldName, redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(errorCode, fieldName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsed.getFragment() != null) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format("Invalid request: %s contains a fragment ('%s')", fieldName,
|
||||||
|
redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(errorCode, fieldName);
|
||||||
|
}
|
||||||
|
String scheme = parsed.getScheme();
|
||||||
|
if (scheme == null) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format("Invalid request: %s has no scheme ('%s')", fieldName, redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(errorCode, fieldName);
|
||||||
|
}
|
||||||
|
if (isUnsafeScheme(scheme)) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(
|
||||||
|
LogMessage.format("Invalid request: %s uses unsafe scheme ('%s')", fieldName, redirectUri));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(errorCode, fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateRedirectUrisSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||||
|
validateRedirectUrisFragmentOnly(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validatePostLogoutRedirectUrisSimple(
|
||||||
|
OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
|
||||||
|
.getPostLogoutRedirectUris();
|
||||||
|
validateRedirectUrisFragmentOnly(postLogoutRedirectUris, "invalid_client_metadata",
|
||||||
|
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateRedirectUrisFragmentOnly(List<String> redirectUris, String errorCode,
|
||||||
|
String fieldName) {
|
||||||
|
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String redirectUri : redirectUris) {
|
||||||
|
try {
|
||||||
|
URI parsed = new URI(redirectUri);
|
||||||
|
if (parsed.getFragment() != null) {
|
||||||
|
throwInvalidClientRegistration(errorCode, fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (URISyntaxException ex) {
|
||||||
|
throwInvalidClientRegistration(errorCode, fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateJwkSetUri(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
URL jwkSetUrl = clientRegistrationAuthentication.getClientRegistration().getJwkSetUrl();
|
||||||
|
if (jwkSetUrl == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!"https".equalsIgnoreCase(jwkSetUrl.getProtocol())) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format("Invalid request: jwks_uri does not use https ('%s')", jwkSetUrl));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.JWKS_URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateJwkSetUriSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
// No validation. Preserves prior behavior.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateScope(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||||
|
.getAuthentication();
|
||||||
|
List<String> scopes = clientRegistrationAuthentication.getClientRegistration().getScopes();
|
||||||
|
if (!CollectionUtils.isEmpty(scopes)) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug(LogMessage.format(
|
||||||
|
"Invalid request: scope must not be set during Dynamic Client Registration ('%s')", scopes));
|
||||||
|
}
|
||||||
|
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, OidcClientMetadataClaimNames.SCOPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateScopeSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||||
|
// No validation. Preserves prior behavior.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isUnsafeScheme(String scheme) {
|
||||||
|
return "javascript".equalsIgnoreCase(scheme) || "data".equalsIgnoreCase(scheme)
|
||||||
|
|| "vbscript".equalsIgnoreCase(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
|
||||||
|
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
|
||||||
|
throw new OAuth2AuthenticationException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+9
@@ -360,6 +360,11 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||||
|
this.authenticationProvider
|
||||||
|
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
|
|
||||||
Jwt jwt = createJwtClientRegistration();
|
Jwt jwt = createJwtClientRegistration();
|
||||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||||
@@ -412,6 +417,10 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
|
|||||||
@Test
|
@Test
|
||||||
public void authenticateWhenOpenRegistrationThenReturnClientRegistration() {
|
public void authenticateWhenOpenRegistrationThenReturnClientRegistration() {
|
||||||
this.authenticationProvider.setOpenRegistrationAllowed(true);
|
this.authenticationProvider.setOpenRegistrationAllowed(true);
|
||||||
|
this.authenticationProvider
|
||||||
|
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
|
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present 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 java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link OAuth2ClientRegistrationAuthenticationValidator}.
|
||||||
|
*
|
||||||
|
* @author addcontent
|
||||||
|
*/
|
||||||
|
public class OAuth2ClientRegistrationAuthenticationValidatorTests {
|
||||||
|
|
||||||
|
private final OAuth2ClientRegistrationAuthenticationValidator validator = new OAuth2ClientRegistrationAuthenticationValidator();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||||
|
assertRejected(context("//client.example.com/path", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||||
|
assertRejected(context("javascript:alert(document.cookie)", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenDataSchemeThenRejected() {
|
||||||
|
assertRejected(context("data:text/html,<h1>content</h1>", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenVbscriptSchemeThenRejected() {
|
||||||
|
assertRejected(context("vbscript:msgbox(\"content\")", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenFragmentThenRejected() {
|
||||||
|
assertRejected(context("https://client.example.com/cb#fragment", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenCustomSchemeForNativeAppThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator.accept(context("myapp://callback", null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenHttpLoopbackThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator.accept(context("http://127.0.0.1:8080", null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
|
||||||
|
assertRejected(context("https://client.example.com", "http://169.254.169.254/keys"), "invalid_client_metadata",
|
||||||
|
OAuth2ClientMetadataClaimNames.JWKS_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(
|
||||||
|
() -> this.validator.accept(context("https://client.example.com", "https://client.example.com/jwks")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultJwkSetUriValidatorWhenAbsentThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||||
|
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||||
|
OAuth2ClientRegistration.builder()
|
||||||
|
.redirectUri("https://client.example.com")
|
||||||
|
.scope("write")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||||
|
.extracting(OAuth2AuthenticationException::getError)
|
||||||
|
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultScopeValidatorWhenEmptyThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleRedirectUriValidatorWhenProtocolRelativeThenAccepted() {
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext context = context("//client.example.com/path", null);
|
||||||
|
assertThatNoException().isThrownBy(
|
||||||
|
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null);
|
||||||
|
assertThatNoException().isThrownBy(
|
||||||
|
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext context = context("https://client.example.com",
|
||||||
|
"http://169.254.169.254/keys");
|
||||||
|
assertThatNoException().isThrownBy(
|
||||||
|
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||||
|
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||||
|
OAuth2ClientRegistration.builder()
|
||||||
|
.redirectUri("https://client.example.com")
|
||||||
|
.scope("write")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
assertThatNoException()
|
||||||
|
.isThrownBy(() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
|
||||||
|
Consumer<OAuth2ClientRegistrationAuthenticationContext> composed = OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
|
||||||
|
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||||
|
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||||
|
OAuth2ClientRegistration.builder()
|
||||||
|
.redirectUri("https://client.example.com")
|
||||||
|
.jwkSetUrl("https://client.example.com/jwks")
|
||||||
|
.scope("openid")
|
||||||
|
.scope("profile")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
assertThatNoException().isThrownBy(() -> composed.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OAuth2ClientRegistrationAuthenticationContext context(String redirectUri, String jwkSetUrl) {
|
||||||
|
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder();
|
||||||
|
if (redirectUri != null) {
|
||||||
|
builder.redirectUri(redirectUri);
|
||||||
|
}
|
||||||
|
if (jwkSetUrl != null) {
|
||||||
|
builder.jwkSetUrl(jwkSetUrl);
|
||||||
|
}
|
||||||
|
return OAuth2ClientRegistrationAuthenticationContext
|
||||||
|
.with(new OAuth2ClientRegistrationAuthenticationToken(null, builder.build()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertRejected(OAuth2ClientRegistrationAuthenticationContext context, String errorCode,
|
||||||
|
String fieldName) {
|
||||||
|
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||||
|
.extracting(OAuth2AuthenticationException::getError)
|
||||||
|
.satisfies((error) -> {
|
||||||
|
assertThat(error.getErrorCode()).isEqualTo(errorCode);
|
||||||
|
assertThat(error.getDescription()).contains(fieldName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+15
@@ -561,6 +561,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
|
public void authenticateWhenTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
|
||||||
|
this.authenticationProvider
|
||||||
|
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
Jwt jwt = createJwtClientRegistration();
|
Jwt jwt = createJwtClientRegistration();
|
||||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||||
@@ -612,6 +617,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenRegistrationAccessTokenNotGeneratedThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenRegistrationAccessTokenNotGeneratedThenThrowOAuth2AuthenticationException() {
|
||||||
|
this.authenticationProvider
|
||||||
|
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
Jwt jwt = createJwtClientRegistration();
|
Jwt jwt = createJwtClientRegistration();
|
||||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||||
@@ -653,6 +663,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||||
|
this.authenticationProvider
|
||||||
|
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||||
Jwt jwt = createJwtClientRegistration();
|
Jwt jwt = createJwtClientRegistration();
|
||||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||||
|
|||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2004-present 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.function.Consumer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
|
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link OidcClientRegistrationAuthenticationValidator}.
|
||||||
|
*
|
||||||
|
* @author addcontent
|
||||||
|
*/
|
||||||
|
public class OidcClientRegistrationAuthenticationValidatorTests {
|
||||||
|
|
||||||
|
private final OidcClientRegistrationAuthenticationValidator validator = new OidcClientRegistrationAuthenticationValidator();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||||
|
assertRejected(context("//client.example.com/path", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||||
|
assertRejected(context("javascript:alert(document.cookie)", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||||
|
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||||
|
assertThatNoException()
|
||||||
|
.isThrownBy(() -> this.validator.accept(context("https://client.example.com", null, null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultPostLogoutRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||||
|
assertRejected(context("https://client.example.com", "javascript:alert(document.cookie)", null),
|
||||||
|
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultPostLogoutRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||||
|
assertRejected(context("https://client.example.com", "//client.example.com/post-logout", null),
|
||||||
|
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultPostLogoutRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator
|
||||||
|
.accept(context("https://client.example.com", "https://client.example.com/post-logout", null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
|
||||||
|
assertRejected(context("https://client.example.com", null, "http://169.254.169.254/keys"),
|
||||||
|
"invalid_client_metadata", OidcClientMetadataClaimNames.JWKS_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
|
||||||
|
assertThatNoException().isThrownBy(() -> this.validator
|
||||||
|
.accept(context("https://client.example.com", null, "https://client.example.com/jwks")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
|
||||||
|
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||||
|
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||||
|
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
|
||||||
|
.build();
|
||||||
|
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||||
|
.extracting(OAuth2AuthenticationException::getError)
|
||||||
|
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||||
|
OidcClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null, null);
|
||||||
|
assertThatNoException().isThrownBy(
|
||||||
|
() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simplePostLogoutRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||||
|
OidcClientRegistrationAuthenticationContext context = context("https://client.example.com",
|
||||||
|
"javascript:alert(document.cookie)", null);
|
||||||
|
assertThatNoException()
|
||||||
|
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR
|
||||||
|
.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
|
||||||
|
OidcClientRegistrationAuthenticationContext ctx = context("https://client.example.com", null,
|
||||||
|
"http://169.254.169.254/keys");
|
||||||
|
assertThatNoException()
|
||||||
|
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
|
||||||
|
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||||
|
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||||
|
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
|
||||||
|
.build();
|
||||||
|
assertThatNoException()
|
||||||
|
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
|
||||||
|
Consumer<OidcClientRegistrationAuthenticationContext> composed = OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||||
|
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
|
||||||
|
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||||
|
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||||
|
OidcClientRegistration.builder()
|
||||||
|
.redirectUri("https://client.example.com")
|
||||||
|
.postLogoutRedirectUri("https://client.example.com/post-logout")
|
||||||
|
.jwkSetUrl("https://client.example.com/jwks")
|
||||||
|
.scope("openid")
|
||||||
|
.scope("profile")
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
assertThatNoException().isThrownBy(() -> composed.accept(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Authentication principal() {
|
||||||
|
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password");
|
||||||
|
principal.setAuthenticated(true);
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OidcClientRegistrationAuthenticationContext context(String redirectUri, String postLogoutRedirectUri,
|
||||||
|
String jwkSetUrl) {
|
||||||
|
OidcClientRegistration.Builder builder = OidcClientRegistration.builder();
|
||||||
|
if (redirectUri != null) {
|
||||||
|
builder.redirectUri(redirectUri);
|
||||||
|
}
|
||||||
|
if (postLogoutRedirectUri != null) {
|
||||||
|
builder.postLogoutRedirectUri(postLogoutRedirectUri);
|
||||||
|
}
|
||||||
|
if (jwkSetUrl != null) {
|
||||||
|
builder.jwkSetUrl(jwkSetUrl);
|
||||||
|
}
|
||||||
|
return OidcClientRegistrationAuthenticationContext
|
||||||
|
.with(new OidcClientRegistrationAuthenticationToken(principal(), builder.build()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertRejected(OidcClientRegistrationAuthenticationContext context, String errorCode,
|
||||||
|
String fieldName) {
|
||||||
|
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||||
|
.extracting(OAuth2AuthenticationException::getError)
|
||||||
|
.satisfies((error) -> {
|
||||||
|
assertThat(error.getErrorCode()).isEqualTo(errorCode);
|
||||||
|
assertThat(error.getDescription()).contains(fieldName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+30
-14
@@ -17,9 +17,11 @@
|
|||||||
package org.springframework.security.web.authentication.preauth.x509;
|
package org.springframework.security.web.authentication.preauth.x509;
|
||||||
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.regex.Matcher;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
|
import javax.naming.InvalidNameException;
|
||||||
|
import javax.naming.ldap.LdapName;
|
||||||
|
import javax.naming.ldap.Rdn;
|
||||||
import javax.security.auth.x500.X500Principal;
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
@@ -47,14 +49,13 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract
|
|||||||
|
|
||||||
private final Log logger = LogFactory.getLog(getClass());
|
private final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
private static final Pattern EMAIL_SUBJECT_DN_PATTERN = Pattern.compile("OID.1.2.840.113549.1.9.1=(.*?)(?:,|$)",
|
private static final String EMAIL_SUBJECT_DN_TYPE = "OID.1.2.840.113549.1.9.1";
|
||||||
Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
private static final Pattern CN_SUBJECT_DN_PATTERN = Pattern.compile("CN=(.*?)(?:,|$)", Pattern.CASE_INSENSITIVE);
|
private static final String CN_SUBJECT_DN_TYPE = "CN";
|
||||||
|
|
||||||
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||||
|
|
||||||
private Pattern subjectDnPattern = CN_SUBJECT_DN_PATTERN;
|
private String subjectDnType = CN_SUBJECT_DN_TYPE;
|
||||||
|
|
||||||
private String x500PrincipalFormat = X500Principal.RFC2253;
|
private String x500PrincipalFormat = X500Principal.RFC2253;
|
||||||
|
|
||||||
@@ -64,16 +65,31 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract
|
|||||||
X500Principal principal = clientCert.getSubjectX500Principal();
|
X500Principal principal = clientCert.getSubjectX500Principal();
|
||||||
String subjectDN = principal.getName(this.x500PrincipalFormat);
|
String subjectDN = principal.getName(this.x500PrincipalFormat);
|
||||||
this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN));
|
this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN));
|
||||||
Matcher matcher = this.subjectDnPattern.matcher(subjectDN);
|
String principalName = getSubject(subjectDN);
|
||||||
if (!matcher.find()) {
|
|
||||||
throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching",
|
|
||||||
new Object[] { subjectDN }, "No matching pattern was found in subject DN: {0}"));
|
|
||||||
}
|
|
||||||
String principalName = matcher.group(1);
|
|
||||||
this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName));
|
this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName));
|
||||||
return principalName;
|
return principalName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Rdn> getDns(String subjectDn) {
|
||||||
|
try {
|
||||||
|
return new LdapName(subjectDn).getRdns();
|
||||||
|
}
|
||||||
|
catch (InvalidNameException ex) {
|
||||||
|
throw new BadCredentialsException("Failed to parse client certificate", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSubject(String subjectDn) {
|
||||||
|
for (Rdn rdn : getDns(subjectDn)) {
|
||||||
|
String type = rdn.getType();
|
||||||
|
if (this.subjectDnType.equals(type)) {
|
||||||
|
return String.valueOf(rdn.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching",
|
||||||
|
new Object[] { subjectDn }, "No matching pattern was found in subject DN: {0}"));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageSource(MessageSource messageSource) {
|
public void setMessageSource(MessageSource messageSource) {
|
||||||
Assert.notNull(messageSource, "messageSource cannot be null");
|
Assert.notNull(messageSource, "messageSource cannot be null");
|
||||||
@@ -104,11 +120,11 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract
|
|||||||
*/
|
*/
|
||||||
public void setExtractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) {
|
public void setExtractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) {
|
||||||
if (extractPrincipalNameFromEmail) {
|
if (extractPrincipalNameFromEmail) {
|
||||||
this.subjectDnPattern = EMAIL_SUBJECT_DN_PATTERN;
|
this.subjectDnType = EMAIL_SUBJECT_DN_TYPE;
|
||||||
this.x500PrincipalFormat = X500Principal.RFC1779;
|
this.x500PrincipalFormat = X500Principal.RFC1779;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.subjectDnPattern = CN_SUBJECT_DN_PATTERN;
|
this.subjectDnType = CN_SUBJECT_DN_TYPE;
|
||||||
this.x500PrincipalFormat = X500Principal.RFC2253;
|
this.x500PrincipalFormat = X500Principal.RFC2253;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -53,6 +53,22 @@ public class SubjectX500PrincipalExtractorTests {
|
|||||||
assertThat(principal).isEqualTo("Duke");
|
assertThat(principal).isEqualTo("Duke");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractWhenDnEmbeddedInCnThenExtractsPrincipalName() throws Exception {
|
||||||
|
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedDn());
|
||||||
|
|
||||||
|
assertThat(principal).isEqualTo("luke");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractWhenEmailDnEmbeddedInCnThenExtractsEmail() throws Exception {
|
||||||
|
this.extractor.setExtractPrincipalNameFromEmail(true);
|
||||||
|
|
||||||
|
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedEmailDn());
|
||||||
|
|
||||||
|
assertThat(principal).isEqualTo("luke@monkeymachine");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void setMessageSourceWhenNullThenThrowsException() {
|
void setMessageSourceWhenNullThenThrowsException() {
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null));
|
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null));
|
||||||
|
|||||||
+50
@@ -135,4 +135,54 @@ public final class X509TestUtils {
|
|||||||
return (X509Certificate) cf.generateCertificate(in);
|
return (X509Certificate) cf.generateCertificate(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static X509Certificate buildTestCertficateWithEmbeddedDn() throws Exception {
|
||||||
|
String cert = "-----BEGIN CERTIFICATE-----\n"
|
||||||
|
+ "MIIDDTCCAfWgAwIBAgIJANSyvk4gJhqPMA0GCSqGSIb3DQEBCwUAMEYxDTALBgNV\n"
|
||||||
|
+ "BAMMBGx1a2UxETAPBgNVBAsMCENOPWR1a2UsMRUwEwYDVQQKDAxFeGFtcGxlIENv\n"
|
||||||
|
+ "cnAxCzAJBgNVBAYTAlVTMB4XDTI2MDEwNDE5MjY0N1oXDTI3MDEwNTE5MjY0N1ow\n"
|
||||||
|
+ "RjENMAsGA1UEAwwEbHVrZTERMA8GA1UECwwIQ049ZHVrZSwxFTATBgNVBAoMDEV4\n"
|
||||||
|
+ "YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n"
|
||||||
|
+ "ggEKAoIBAQDU9fY74nEFbBKfIef7CK02J/BJb42sIF9kD8eHN5OvEwLQBeTh30it\n"
|
||||||
|
+ "E7LLalXyOXeUFkPe1N1ZhGdVak9udsIqULSvQaWqTbN+IrAGklZAxuXYTC1GbhMF\n"
|
||||||
|
+ "AkGWWM55J2SNqVGQaHzZUn6VPxWaDft6nZR0DxuvXMYM5kVG6VErdB3ygGUv8cjQ\n"
|
||||||
|
+ "QBKAYpsZeRldnauRPt2dImmGTagvSuJVyr8X/AioE2Rl0guii456AKw+QSvRiZ+g\n"
|
||||||
|
+ "w08Y8C9nDyzQmurqpdYYkp0X+4yqm1iVowMX+tSPvHnlqJdvVzaW2b0yRzrrT6ao\n"
|
||||||
|
+ "UCgw25slR1P1IcyzqPKWQIoQRnYIaX1bAgMBAAEwDQYJKoZIhvcNAQELBQADggEB\n"
|
||||||
|
+ "AIos+nr8DFM6bAt9AI/79O/12hcN7gVv4F3P4Vz6NRRkkvsb9WMN8fLLDEsEJ/BQ\n"
|
||||||
|
+ "eQkAVnhlmAe++vrqy8OTHoQ7F5C3K0zrr19NLNoyNFTkXkFgnm4ZhYinSbusuIb7\n"
|
||||||
|
+ "LPYoyCnEEiMdl0VMWWSWcOvZpipbvTtH3CiVxTqXLjFFNraEAyUN50kXjo/zuHpK\n"
|
||||||
|
+ "HzTS1BAu0li9GdV3Da2ELdDx90zaUym7dDIejY4YUlXYIJ5UUYS61fqtgOHGLLdb\n"
|
||||||
|
+ "UXGAr5gqEe7OrQ9D4ebg9w5ciTb7g1H2CmirjTf/rkii8AojmsGFKIfGVe3gY6EB\n" + "o9eF5FV9V9leo5yLo25ev08=\n"
|
||||||
|
+ "-----END CERTIFICATE-----";
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
return (X509Certificate) cf.generateCertificate(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static X509Certificate buildTestCertficateWithEmbeddedEmailDn() throws Exception {
|
||||||
|
String cert = "-----BEGIN CERTIFICATE-----\n"
|
||||||
|
+ "MIIDfDCCAmSgAwIBAgIIXHoOUFeZ29MwDQYJKoZIhvcNAQELBQAwfjEhMB8GCSqG\n"
|
||||||
|
+ "SIb3DQEJARYSbHVrZUBtb25rZXltYWNoaW5lMTUwMwYDVQQLDCxPSUQuMS4yLjg0\n"
|
||||||
|
+ "MC4xMTM1NDkuMS45LjE9ZHVrZUBnb3JpbGxhZ2FkZ2V0LDEVMBMGA1UECgwMRXhh\n"
|
||||||
|
+ "bXBsZSBDb3JwMQswCQYDVQQGEwJVUzAeFw0yNjAxMDQxOTMxMDhaFw0yNzAxMDUx\n"
|
||||||
|
+ "OTMxMDhaMH4xITAfBgkqhkiG9w0BCQEWEmx1a2VAbW9ua2V5bWFjaGluZTE1MDMG\n"
|
||||||
|
+ "A1UECwwsT0lELjEuMi44NDAuMTEzNTQ5LjEuOS4xPWR1a2VAZ29yaWxsYWdhZGdl\n"
|
||||||
|
+ "dCwxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqG\n"
|
||||||
|
+ "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBuIQWnj+uvvG+4ZIyFMs4dSbiBavubCmC\n"
|
||||||
|
+ "hudrHr93hP19QbPulbHRTVCUqEi8efvq+J9jmMdPd7tziuDX02PeG9uljp9+c5Ir\n"
|
||||||
|
+ "pw9/oMoTRkF7K4PK1JxLN4tcgxjxVA4QkS+MjKLPeHrYyGCjKspcHbi+zBiQ9Xqp\n"
|
||||||
|
+ "yHWq6N5XPd6mEj2gh0zamnsJCeUCOX4SJbcp3MFtcYzhguHAeVhy9Jv+EAMJejDn\n"
|
||||||
|
+ "YIZmMUdP6Ykf2zTzs/4L3bRZb0oS5WvfeRdJB6SKg8mNO/jdGX87krSio//cRdDy\n"
|
||||||
|
+ "TGQK+YCVDf8GyLLavYZW56AJbZxL3MWgHYilQjj4p+Kw/PWpaBVvAgMBAAEwDQYJ\n"
|
||||||
|
+ "KoZIhvcNAQELBQADggEBAKVTMIo8JO0H0HRrpsEDP17E2pnfMJV4g70BwClUMMek\n"
|
||||||
|
+ "wNIWZn+6XPR8oObzzjnVWXjrovMkmmyFk0vWIpF68MPyiQ++5fwdzOZiQtUP177n\n"
|
||||||
|
+ "9ulAtLoIJld3olGeL9VsCZGp3J2PqiDe613zd+bkSUG1lQYC2awozWqJEdvwJJtf\n"
|
||||||
|
+ "j9nlhyMsARKEEu3tFGJsCHST3XhbhFKOraf/GZ21xW650R7ap0ZNaEiB16M2a5Oe\n"
|
||||||
|
+ "WXasgUukIo82Z8+yK4IITeCcr0aA1fJxwhU8J6qfYWloaoirSYj487HRnPPv3X/b\n"
|
||||||
|
+ "RxZynIjtGKygT6T1dRaWennmoitqfprJnEO2tlhLwP0=\n" + "-----END CERTIFICATE-----";
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
return (X509Certificate) cf.generateCertificate(in);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user