1
0
mirror of synced 2026-08-06 02:08:01 +00:00

Compare commits

...

34 Commits

Author SHA1 Message Date
Eleftheria Stein 0ae7ceef70 Release 5.1.9.RELEASE 2020-04-01 14:37:37 -04:00
Eleftheria Stein b3c7f0a79f Update to httpclient 4.5.12
Fixes gh-8294
2020-04-01 11:19:49 -04:00
Eleftheria Stein 4df4f9a63d Update to hibernate-validator 6.0.19.Final
Fixes gh-8293
2020-04-01 11:19:09 -04:00
Eleftheria Stein 9d25c2d2f8 Update to reactive-streams 1.0.3
Fixes gh-8292
2020-04-01 11:18:35 -04:00
Eleftheria Stein 94b116c8c8 Update to hibernate-core 5.2.18.Final
Fixes gh-8291
2020-04-01 11:18:05 -04:00
Eleftheria Stein 3c6f08ede0 Update to groovy 2.4.19
Fixes gh-8290
2020-04-01 11:04:53 -04:00
Eleftheria Stein 9a94234dae Update to unboundid-ldapsdk 4.0.14
Fixes gh-8289
2020-04-01 11:03:25 -04:00
Eleftheria Stein 017044bf93 Update to okhttp 3.12.10
Fixes gh-8288
2020-04-01 11:02:52 -04:00
Eleftheria Stein ae99097723 Update to mockwebserver 3.12.10
Fixes gh-8287
2020-04-01 11:01:53 -04:00
Eleftheria Stein acb7e63cf7 Update to org.powermock 2.0.6
Fixes gh-8286
2020-04-01 11:01:12 -04:00
Eleftheria Stein 0256439a50 Update to Spring Boot 2.1.13.RELEASE
Fixes gh-8285
2020-04-01 11:00:28 -04:00
Eleftheria Stein c8f72a1b87 Update to GAE 1.9.79
Fixes gh-8284
2020-04-01 11:00:00 -04:00
Eleftheria Stein 332e8ce07a Update to Spring Data Lovelace-SR16
Fixes gh-8282
2020-04-01 10:59:11 -04:00
Eleftheria Stein 7aea459535 Update to Reactor Californium-SR17
Fixes gh-8283
2020-04-01 10:58:44 -04:00
Eleftheria Stein c91389ff8b Update to Spring Framework 5.1.14.RELEASE
Fixes gh-8281
2020-04-01 10:58:04 -04:00
Rob Winch 6de345b972 Fix HttpServlet3RequestFactory Logout Handlers
Previously there was a problem with Servlet API logout integration
when Servlet API was configured before log out.

This ensures that logout handlers is a reference to the logout handlers
vs copying the logout handlers. This ensures that the ordering does not
matter.

Closes gh-4760
2020-03-30 21:55:27 -05:00
Ruby Hartono 19f08cbedb Improve OAuth2LoginAuthenticationProvider
1. update OAuth2LoginAuthenticationProvider to use
OAuth2AuthorizationCodeAuthenticationProvider
2. apply fix gh-5368 for OAuth2AuthorizationCodeAuthenticationProvider
to return additionalParameters value from accessTokenResponse

Fixes gh-5633
2020-03-30 21:15:08 -04:00
Josh Cummings f82e435aaf Add Missing Import
Restored an import that was lost during a backport

Issue gh-4183
2020-03-27 15:07:57 -06:00
Josh Cummings a44225d334 SwitchUserFilter Defaults to POST
Fixes gh-4183
2020-03-27 14:42:21 -06:00
Eleftheria Stein 5decfb1ece Update Encryptors documentation
Fixes gh-8208
2020-03-27 10:48:45 -04:00
Joe Grandja a24d67375b oauth2Login WebFlux does not auto-redirect for XHR request
Fixes gh-8118
2020-03-26 05:27:43 -04:00
Joe Grandja 3a46ba8a85 OAuth2ErrorHttpMessageConverter handles JSON object parameters
Fixes gh-8157
2020-03-24 14:57:52 -04:00
Erik van Paassen a4851095df Fix typo in Javadoc of HttpSecurity#csrf()
`HttpSecurity#csrf()` obviously returns a `CsrfConfigurer`, while the Javadoc states that it returns the `ServletApiConfigurer`.
2020-03-17 13:37:01 -06:00
Markus Engelbrecht 633e5b85b4 Fix typo 'properites' in documentation
Fixes gh-8095
2020-03-11 11:02:05 -06:00
Josh Cummings bfa5830e3d Update to Jetty 9.4.27
Fixes gh-7507
2020-03-09 10:10:04 -06:00
Pim Moerenhout e43fab518c Fix typo in log message. 2020-03-04 10:33:36 -07:00
AmitB c9f676739b Fix typo in AntPathRequestMatcher contructor comment
Closes gh-8042
2020-03-02 07:20:50 -06:00
Josh Cummings b7ce65b284 Register Authentication Provider in Init Phase
Fixes gh-8031
2020-02-29 13:07:39 -07:00
Joe Grandja 67d561b5f7 OAuth2AccessTokenResponseHttpMessageConverter handles JSON object parameters
Fixes gh-6463
2020-02-24 16:10:18 -05:00
LeeHainie 4dd2b1dfe1 Remove unwanted code
Remove unwanted code
2020-02-20 12:33:55 -07:00
Joe Grandja aa28a0b453 OAuth2AuthorizationCodeGrantWebFilter matches on query parameters
Fixes gh-7966
2020-02-10 16:09:57 -05:00
Joe Grandja 8145cb557b OAuth2AuthorizationCodeGrantFilter matches on query parameters
Fixes gh-7963
2020-02-10 05:41:34 -05:00
Joe Grandja ef4109358a Add release-notes-sections.yml 2020-02-05 15:19:57 -05:00
Joe Grandja 6d6fd09665 Next Development Version 2020-02-05 10:04:48 -05:00
33 changed files with 794 additions and 339 deletions
@@ -47,7 +47,7 @@ public class ServiceAuthenticationDetailsSource implements
// =================================================================================================== // ===================================================================================================
/** /**
* Creates an implementation that uses the specified ServiceProperites and the default * Creates an implementation that uses the specified ServiceProperties and the default
* CAS artifactParameterName. * CAS artifactParameterName.
* *
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl. * @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
@@ -723,7 +723,7 @@ public final class HttpSecurity extends
* } * }
* </pre> * </pre>
* *
* @return the {@link ServletApiConfigurer} for further customizations * @return the {@link CsrfConfigurer} for further customizations
* @throws Exception * @throws Exception
*/ */
public CsrfConfigurer<HttpSecurity> csrf() throws Exception { public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
@@ -171,7 +171,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
/** /**
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is * Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites * {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
* set. * set.
* *
* @param successHandler the {@link AuthenticationSuccessHandler}. * @param successHandler the {@link AuthenticationSuccessHandler}.
@@ -160,6 +160,25 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
@Override @Override
public void init(H http) throws Exception { public void init(H http) throws Exception {
if ( this.jwtConfigurer == null ) {
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
"a jwk set uri by doing http.oauth2ResourceServer().jwt().jwkSetUri(uri), or wire a " +
"JwtDecoder instance by doing http.oauth2ResourceServer().jwt().decoder(decoder), or " +
"expose a JwtDecoder instance as a bean and do http.oauth2ResourceServer().jwt().");
}
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
this.jwtConfigurer.getJwtAuthenticationConverter();
JwtAuthenticationProvider provider =
new JwtAuthenticationProvider(decoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
provider = postProcess(provider);
http.authenticationProvider(provider);
registerDefaultAccessDeniedHandler(http); registerDefaultAccessDeniedHandler(http);
registerDefaultEntryPoint(http); registerDefaultEntryPoint(http);
registerDefaultCsrfOverride(http); registerDefaultCsrfOverride(http);
@@ -179,25 +198,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
filter = postProcess(filter); filter = postProcess(filter);
http.addFilter(filter); http.addFilter(filter);
if ( this.jwtConfigurer == null ) {
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
"a jwk set uri by doing http.oauth2ResourceServer().jwt().jwkSetUri(uri), or wire a " +
"JwtDecoder instance by doing http.oauth2ResourceServer().jwt().decoder(decoder), or " +
"expose a JwtDecoder instance as a bean and do http.oauth2ResourceServer().jwt().");
}
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
this.jwtConfigurer.getJwtAuthenticationConverter();
JwtAuthenticationProvider provider =
new JwtAuthenticationProvider(decoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
provider = postProcess(provider);
http.authenticationProvider(provider);
} }
public class JwtConfigurer { public class JwtConfigurer {
@@ -208,7 +208,7 @@ public class GlobalMethodSecurityBeanDefinitionParser implements BeanDefinitionP
pc.registerBeanComponent(new BeanComponentDefinition( pc.registerBeanComponent(new BeanComponentDefinition(
expressionHandler, expressionHandlerRef)); expressionHandler, expressionHandlerRef));
logger.info("Expressions were enabled for method security but no SecurityExpressionHandler was configured. " logger.info("Expressions were enabled for method security but no SecurityExpressionHandler was configured. "
+ "All hasPermision() expressions will evaluate to false."); + "All hasPermission() expressions will evaluate to false.");
} }
BeanDefinitionBuilder expressionPreAdviceBldr = BeanDefinitionBuilder BeanDefinitionBuilder expressionPreAdviceBldr = BeanDefinitionBuilder
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -621,20 +621,56 @@ public class ServerHttpSecurity {
authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error")); authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error"));
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( setDefaultEntryPoints(http);
MediaType.TEXT_HTML);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
Map<String, String> urlToText = http.oauth2Login.getLinks();
if (urlToText.size() == 1) {
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
} else {
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
}
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
} }
private void setDefaultEntryPoints(ServerHttpSecurity http) {
String defaultLoginPage = "/login";
Map<String, String> urlToText = http.oauth2Login.getLinks();
String providerLoginPage = null;
if (urlToText.size() == 1) {
providerLoginPage = urlToText.keySet().iterator().next();
}
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"),
MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
ServerWebExchangeMatcher xhrMatcher = exchange -> {
if (exchange.getRequest().getHeaders().getOrDefault("X-Requested-With", Collections.emptyList()).contains("XMLHttpRequest")) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
};
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(
notXhrMatcher, htmlMatcher);
if (providerLoginPage != null) {
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage);
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
RedirectServerAuthenticationEntryPoint entryPoint =
new RedirectServerAuthenticationEntryPoint(providerLoginPage);
entryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
}
RedirectServerAuthenticationEntryPoint defaultEntryPoint =
new RedirectServerAuthenticationEntryPoint(defaultLoginPage);
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
}
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() { private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}"); return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
} }
@@ -298,6 +298,18 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
} }
// gh-8031
@Test
public void getWhenAnonymousDisabledThenAllows() throws Exception {
this.spring.register(JwtDecoderConfig.class, AnonymousDisabledConfig.class).autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
when(decoder.decode(anyString())).thenReturn(JWT);
this.mvc.perform(get("/authenticated")
.with(bearerToken("token")))
.andExpect(status().isNotFound());
}
@Test @Test
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized() public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
throws Exception { throws Exception {
@@ -652,7 +664,8 @@ public class OAuth2ResourceServerConfigurerTests {
@Test @Test
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() { public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire()) assertThatCode(() -> this.spring
.register(JwtDecoderConfig.class, MultipleBearerTokenResolverBeansConfig.class).autowire())
.isInstanceOf(BeanCreationException.class) .isInstanceOf(BeanCreationException.class)
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class); .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
} }
@@ -1097,6 +1110,22 @@ public class OAuth2ResourceServerConfigurerTests {
} }
} }
@EnableWebSecurity
static class AnonymousDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.anonymous().disable()
.oauth2ResourceServer()
.jwt();
// @formatter:on
}
}
@EnableWebSecurity @EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter { static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -26,8 +26,10 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
@@ -61,10 +63,12 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.time.Duration; import java.time.Duration;
@@ -79,6 +83,8 @@ public class OAuth2LoginTests {
@Rule @Rule
public final SpringTestRule spring = new SpringTestRule(); public final SpringTestRule spring = new SpringTestRule();
private WebTestClient client;
@Autowired @Autowired
private WebFilterChainProxy springSecurity; private WebFilterChainProxy springSecurity;
@@ -94,6 +100,14 @@ public class OAuth2LoginTests {
.clientSecret("secret") .clientSecret("secret")
.build(); .build();
@Autowired
public void setApplicationContext(ApplicationContext context) {
if (context.getBeanNamesForType(WebHandler.class).length > 0) {
this.client = WebTestClient.bindToApplicationContext(context)
.build();
}
}
@Test @Test
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() { public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire(); this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
@@ -140,6 +154,22 @@ public class OAuth2LoginTests {
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize"); assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
} }
// gh-8118
@Test
public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() {
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire();
this.client.get()
.uri("/")
.header("X-Requested-With", "XMLHttpRequest")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
}
@EnableWebFlux
static class WebFluxConfig { }
@EnableWebFluxSecurity @EnableWebFluxSecurity
static class OAuth2LoginWithSingleClientRegistrations { static class OAuth2LoginWithSingleClientRegistrations {
@Bean @Bean
@@ -17,7 +17,6 @@ package org.springframework.security.access.expression;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.PermissionEvaluator;
@@ -158,7 +157,6 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
private Set<String> getAuthoritySet() { private Set<String> getAuthoritySet() {
if (roles == null) { if (roles == null) {
roles = new HashSet<>();
Collection<? extends GrantedAuthority> userAuthorities = authentication Collection<? extends GrantedAuthority> userAuthorities = authentication
.getAuthorities(); .getAuthorities();
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
* &lt;/property&gt; * &lt;/property&gt;
* </pre> * </pre>
* *
* A configuration note: The JaasAuthenticationProvider uses the security properites * A configuration note: The JaasAuthenticationProvider uses the security properties
* "login.config.url.X" to configure jaas. If you would like to customize the way Jaas * "login.config.url.X" to configure jaas. If you would like to customize the way Jaas
* gets configured, create a subclass of this and override the * gets configured, create a subclass of this and override the
* {@link #configureJaas(Resource)} method. * {@link #configureJaas(Resource)} method.
@@ -39,9 +39,6 @@ public class Encryptors {
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* key * key
*
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
* (instead of GCM)
*/ */
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) { public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt, return new AesBytesEncryptor(password.toString(), salt,
@@ -55,11 +52,19 @@ public class Encryptors {
* provided salt is expected to be hex-encoded; it should be random and at least 8 * provided salt is expected to be hex-encoded; it should be random and at least 8
* bytes in length. Also applies a random 16 byte initialization vector to ensure each * bytes in length. Also applies a random 16 byte initialization vector to ensure each
* encrypted message will be unique. Requires Java 6. * encrypted message will be unique. Requires Java 6.
* NOTE: This mode is not
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
* and does not provide any guarantees about the authenticity of the data.
* For a more secure alternative, users should prefer
* {@link #stronger(CharSequence, CharSequence)}.
* *
* @param password the password used to generate the encryptor's secret key; should * @param password the password used to generate the encryptor's secret key; should
* not be shared * not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the * @param salt a hex-encoded, random, site-global salt value to use to generate the
* key * key
*
* @see #stronger(CharSequence, CharSequence) which uses the significatly more secure
* GCM (instead of CBC)
*/ */
public static BytesEncryptor standard(CharSequence password, CharSequence salt) { public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt, return new AesBytesEncryptor(password.toString(), salt,
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
[[spring-security-crypto-encryption-bytes]] [[spring-security-crypto-encryption-bytes]]
==== BytesEncryptor ==== BytesEncryptor
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor: Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
[source,java] [source,java]
---- ----
Encryptors.standard("password", "salt"); Encryptors.stronger("password", "salt");
---- ----
The "standard" encryption method is 256-bit AES using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2). The "stronger" encryption method creates an encryptor using 256 bit AES encryption with
Galois Counter Mode (GCM).
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
This method requires Java 6. This method requires Java 6.
The password used to generate the SecretKey should be kept in a secure place and not be shared. The password used to generate the SecretKey should be kept in a secure place and not be shared.
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised. The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
@@ -38,6 +40,11 @@ Such a salt may be generated using a KeyGenerator:
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
---- ----
Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
guarantees about the authenticity of the data.
For a more secure alternative, users should prefer `Encryptors.stronger`.
[[spring-security-crypto-encryption-text]] [[spring-security-crypto-encryption-text]]
==== TextEncryptor ==== TextEncryptor
Use the Encryptors.text factory method to construct a standard TextEncryptor: Use the Encryptors.text factory method to construct a standard TextEncryptor:
+3 -3
View File
@@ -1,3 +1,3 @@
gaeVersion=1.9.71 gaeVersion=1.9.79
springBootVersion=2.1.12.RELEASE springBootVersion=2.1.13.RELEASE
version=5.1.8.RELEASE version=5.1.9.RELEASE
+30 -30
View File
@@ -1,13 +1,13 @@
if (!project.hasProperty('reactorVersion')) { if (!project.hasProperty('reactorVersion')) {
ext.reactorVersion = 'Californium-SR15' ext.reactorVersion = 'Californium-SR17'
} }
if (!project.hasProperty('springVersion')) { if (!project.hasProperty('springVersion')) {
ext.springVersion = '5.1.13.RELEASE' ext.springVersion = '5.1.14.RELEASE'
} }
if (!project.hasProperty('springDataVersion')) { if (!project.hasProperty('springDataVersion')) {
ext.springDataVersion = 'Lovelace-SR15' ext.springDataVersion = 'Lovelace-SR16'
} }
dependencyManagement { dependencyManagement {
@@ -18,16 +18,16 @@ dependencyManagement {
} }
dependencies { dependencies {
dependency 'cglib:cglib-nodep:3.2.12' dependency 'cglib:cglib-nodep:3.2.12'
dependency 'com.squareup.okhttp3:mockwebserver:3.12.8' dependency 'com.squareup.okhttp3:mockwebserver:3.12.10'
dependency 'opensymphony:sitemesh:2.4.2' dependency 'opensymphony:sitemesh:2.4.2'
dependency 'org.gebish:geb-spock:0.10.0' dependency 'org.gebish:geb-spock:0.10.0'
dependency 'org.jasig.cas:cas-server-webapp:4.2.7' dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
dependency 'org.powermock:powermock-api-mockito2:2.0.5' dependency 'org.powermock:powermock-api-mockito2:2.0.6'
dependency 'org.powermock:powermock-api-support:2.0.5' dependency 'org.powermock:powermock-api-support:2.0.6'
dependency 'org.powermock:powermock-core:2.0.5' dependency 'org.powermock:powermock-core:2.0.6'
dependency 'org.powermock:powermock-module-junit4-common:2.0.5' dependency 'org.powermock:powermock-module-junit4-common:2.0.6'
dependency 'org.powermock:powermock-module-junit4:2.0.5' dependency 'org.powermock:powermock-module-junit4:2.0.6'
dependency 'org.powermock:powermock-reflect:2.0.5' dependency 'org.powermock:powermock-reflect:2.0.6'
dependency 'org.python:jython:2.5.3' dependency 'org.python:jython:2.5.3'
dependency 'org.spockframework:spock-core:1.0-groovy-2.4' dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4' dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
@@ -56,11 +56,11 @@ dependencyManagement {
dependency 'com.nimbusds:lang-tag:1.4.3' dependency 'com.nimbusds:lang-tag:1.4.3'
dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2' dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2'
dependency 'com.nimbusds:oauth2-oidc-sdk:6.0' dependency 'com.nimbusds:oauth2-oidc-sdk:6.0'
dependency 'com.squareup.okhttp3:okhttp:3.12.8' dependency 'com.squareup.okhttp3:okhttp:3.12.10'
dependency 'com.squareup.okio:okio:1.13.0' dependency 'com.squareup.okio:okio:1.13.0'
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1' dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2' dependency 'com.sun.xml.bind:jaxb-impl:2.3.2'
dependency 'com.unboundid:unboundid-ldapsdk:4.0.11' dependency 'com.unboundid:unboundid-ldapsdk:4.0.14'
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1' dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
dependency 'commons-cli:commons-cli:1.4' dependency 'commons-cli:commons-cli:1.4'
dependency 'commons-codec:commons-codec:1.11' dependency 'commons-codec:commons-codec:1.11'
@@ -127,7 +127,7 @@ dependencyManagement {
dependency 'org.apache.directory.shared:shared-cursor:0.9.15' dependency 'org.apache.directory.shared:shared-cursor:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15' dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap:0.9.15' dependency 'org.apache.directory.shared:shared-ldap:0.9.15'
dependency 'org.apache.httpcomponents:httpclient:4.5.11' dependency 'org.apache.httpcomponents:httpclient:4.5.12'
dependency 'org.apache.httpcomponents:httpcore:4.4.8' dependency 'org.apache.httpcomponents:httpcore:4.4.8'
dependency 'org.apache.httpcomponents:httpmime:4.5.3' dependency 'org.apache.httpcomponents:httpmime:4.5.3'
dependency 'org.apache.mina:mina-core:2.0.0-M6' dependency 'org.apache.mina:mina-core:2.0.0-M6'
@@ -147,21 +147,21 @@ dependencyManagement {
dependency 'org.attoparser:attoparser:2.0.4.RELEASE' dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64' dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
dependency 'org.bouncycastle:bcprov-jdk15on:1.58' dependency 'org.bouncycastle:bcprov-jdk15on:1.58'
dependency 'org.codehaus.groovy:groovy-all:2.4.17' dependency 'org.codehaus.groovy:groovy-all:2.4.19'
dependency 'org.codehaus.groovy:groovy-json:2.4.17' dependency 'org.codehaus.groovy:groovy-json:2.4.19'
dependency 'org.codehaus.groovy:groovy:2.4.14' dependency 'org.codehaus.groovy:groovy:2.4.19'
dependency 'org.eclipse.jdt:ecj:3.12.3' dependency 'org.eclipse.jdt:ecj:3.12.3'
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.19.v20190610' dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.19.v20190610' dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610' dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610' dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
dependency 'org.eclipse.persistence:javax.persistence:2.2.1' dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
dependency 'org.gebish:geb-ast:0.10.0' dependency 'org.gebish:geb-ast:0.10.0'
dependency 'org.gebish:geb-core:0.10.0' dependency 'org.gebish:geb-core:0.10.0'
@@ -170,9 +170,9 @@ dependencyManagement {
dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hamcrest:hamcrest-core:1.3'
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final' dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
dependency 'org.hibernate:hibernate-core:5.2.17.Final' dependency 'org.hibernate:hibernate-core:5.2.18.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final' dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final'
dependency 'org.hibernate:hibernate-validator:6.0.18.Final' dependency 'org.hibernate:hibernate-validator:6.0.19.Final'
dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.hsqldb:hsqldb:2.4.1'
dependency 'org.jasig.cas.client:cas-client-core:3.5.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
dependency 'org.javassist:javassist:3.22.0-CR2' dependency 'org.javassist:javassist:3.22.0-CR2'
@@ -183,7 +183,7 @@ dependencyManagement {
dependency 'org.objenesis:objenesis:2.6' dependency 'org.objenesis:objenesis:2.6'
dependency 'org.openid4java:openid4java-nodeps:0.9.6' dependency 'org.openid4java:openid4java-nodeps:0.9.6'
dependency 'org.ow2.asm:asm:6.2.1' dependency 'org.ow2.asm:asm:6.2.1'
dependency 'org.reactivestreams:reactive-streams:1.0.1' dependency 'org.reactivestreams:reactive-streams:1.0.3'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.3' dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.3'
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59' dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59' dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -73,7 +73,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), authorizationCodeAuthentication.getAuthorizationExchange(),
accessTokenResponse.getAccessToken(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken()); accessTokenResponse.getRefreshToken(),
accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult; return authenticationResult;
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -28,7 +28,6 @@ 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.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -60,7 +59,7 @@ import java.util.Map;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a> * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
*/ */
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient; private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService; private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient, OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) { OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
Assert.notNull(userService, "userService cannot be null"); Assert.notNull(userService, "userService cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient; this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
this.userService = userService; this.userService = userService;
} }
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication = OAuth2LoginAuthenticationToken loginAuthenticationToken =
(OAuth2LoginAuthenticationToken) authentication; (OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope // scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (authorizationCodeAuthentication.getAuthorizationExchange() if (loginAuthenticationToken.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) { .getAuthorizationRequest().getScopes().contains("openid")) {
// This is an OpenID Connect Authentication Request so return null // This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead // and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null; return null;
} }
OAuth2AccessTokenResponse accessTokenResponse; OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try { try {
OAuth2AuthorizationExchangeValidator.validate( authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
authorizationCodeAuthentication.getAuthorizationExchange()); .authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( loginAuthenticationToken.getAuthorizationExchange()));
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) { } catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError(); OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} }
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters(); Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters)); loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(), loginAuthenticationToken.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, oauth2User,
mappedAuthorities, mappedAuthorities,
accessToken, accessToken,
accessTokenResponse.getRefreshToken()); authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult; return authenticationResult;
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/** /**
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant, * A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
@@ -132,24 +138,39 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
if (this.shouldProcessAuthorizationResponse(request)) { if (matchesAuthorizationResponse(request)) {
this.processAuthorizationResponse(request, response); processAuthorizationResponse(request, response);
return; return;
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) { private boolean matchesAuthorizationResponse(HttpServletRequest request) {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
return false;
}
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request); OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest == null) { if (authorizationRequest == null) {
return false; return false;
} }
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
request.getServerPort(), request.getRequestURI(), null); // Compare redirect_uri
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
if (requestUrl.equals(authorizationRequest.getRedirectUri()) && UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build();
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { Set<Map.Entry<String, List<String>>> requestUriParameters = new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
Set<Map.Entry<String, List<String>>> redirectUriParameters = new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
// Remove the additional request parameters (if any) from the authorization response (request)
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
requestUriParameters.retainAll(redirectUriParameters);
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
return true; return true;
} }
return false; return false;
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) String redirectUri = UrlUtils.buildFullRequestUrl(request);
.replaceQuery(null)
.build()
.toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri); OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken( OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
} catch (OAuth2AuthorizationException ex) { } catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError(); OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authorizationResponse.getRedirectUri()) .fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode()); .queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.isEmpty(error.getDescription())) { if (!StringUtils.isEmpty(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription()); uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
@@ -206,7 +224,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response); this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
String redirectUrl = authorizationResponse.getRedirectUri(); String redirectUrl = authorizationRequest.getRedirectUri();
SavedRequest savedRequest = this.requestCache.getRequest(request, response); SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) { if (savedRequest != null) {
redirectUrl = savedRequest.getRedirectUrl(); redirectUrl = savedRequest.getRedirectUrl();
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -37,13 +37,20 @@ import org.springframework.security.web.server.authentication.ServerAuthenticati
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/** /**
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant, * A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
* which handles the processing of the OAuth 2.0 Authorization Response. * which handles the processing of the OAuth 2.0 Authorization Response.
@@ -138,10 +145,10 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.requiresAuthenticationMatcher.matches(exchange) return this.requiresAuthenticationMatcher.matches(exchange)
.filter( matchResult -> matchResult.isMatch()) .filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap( matchResult -> this.authenticationConverter.convert(exchange)) .flatMap(matchResult -> this.authenticationConverter.convert(exchange))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty())) .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap( token -> authenticate(exchange, chain, token)); .flatMap(token -> authenticate(exchange, chain, token));
} }
private Mono<Void> authenticate(ServerWebExchange exchange, private Mono<Void> authenticate(ServerWebExchange exchange,
@@ -171,20 +178,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
} }
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) { private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange) return Mono.just(exchange)
.flatMap(authorizationRequest -> { .filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) .flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
.query(null) .flatMap(authorizationRequest ->
.build() matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
.toUriString();
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(queryParams)) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
})
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); .switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
} }
private static Mono<ServerWebExchangeMatcher.MatchResult> matchesRedirectUri(
URI authorizationResponseUri, String authorizationRequestRedirectUri) {
UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build();
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build();
Set<Map.Entry<String, List<String>>> requestUriParameters =
new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
Set<Map.Entry<String, List<String>>> redirectUriParameters =
new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
// Remove the additional request parameters (if any) from the authorization response (request)
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
requestUriParameters.retainAll(redirectUriParameters);
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
}
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -103,14 +102,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
} }
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) { private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
MultiValueMap<String, String> queryParams = exchange.getRequest()
.getQueryParams();
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.query(null)
.build() .build()
.toUriString(); .toUriString();
return OAuth2AuthorizationResponseUtils return OAuth2AuthorizationResponseUtils
.convert(queryParams, redirectUri); .convert(exchange.getRequest().getQueryParams(), redirectUri);
} }
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,6 +15,10 @@
*/ */
package org.springframework.security.oauth2.client.authentication; package org.springframework.security.oauth2.client.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -33,13 +37,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse;
/** /**
* Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}. * Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}.
@@ -130,4 +133,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken); assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken);
assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken); assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken);
} }
// gh-5368
@Test
public void authenticateWhenAuthorizationSuccessResponseThenAdditionalParametersIncluded() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("param1", "value1");
additionalParameters.put("param2", "value2");
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
.build();
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest,
this.authorizationResponse);
OAuth2AuthorizationCodeAuthenticationToken authentication = (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider
.authenticate(
new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, authorizationExchange));
assertThat(authentication.getAdditionalParameters())
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
}
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -18,10 +18,6 @@ package org.springframework.security.oauth2.client.web;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -39,36 +35,44 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error; 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.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.CollectionUtils;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
import static org.springframework.security.oauth2.core.TestOAuth2RefreshTokens.refreshToken;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges.success;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
/** /**
* Tests for {@link OAuth2AuthorizationCodeGrantFilter}. * Tests for {@link OAuth2AuthorizationCodeGrantFilter}.
* *
* @author Joe Grandja * @author Joe Grandja
*/ */
@PowerMockIgnore("javax.security.*")
@PrepareForTest({OAuth2AuthorizationRequest.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationCodeGrantFilter.class})
@RunWith(PowerMockRunner.class)
public class OAuth2AuthorizationCodeGrantFilterTests { public class OAuth2AuthorizationCodeGrantFilterTests {
private ClientRegistration registration1; private ClientRegistration registration1;
private String principalName1 = "principal-1"; private String principalName1 = "principal-1";
@@ -132,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri); request.setServletPath(requestUri);
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter. // NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
MockHttpServletResponse response = new MockHttpServletResponse();
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(request, response, filterChain);
@@ -143,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
@Test @Test
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception { public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
String requestUri = "/path"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
request.setRequestURI(requestUri + "-no-match");
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1); this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestRedirectUriDoesNotMatchThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri);
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
authorizationResponse.setRequestURI(requestUri + "-no-match");
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
// gh-7963
@Test
public void doFilterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() throws Exception {
// 1) redirect_uri with query parameters
String requestUri = "/callback/client-1";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
FilterChain filterChain = mock(FilterChain.class);
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
this.filter.doFilter(authorizationResponse, response, filterChain);
verifyZeroInteractions(filterChain);
// 2) redirect_uri with query parameters AND authorization response additional parameters
Map<String, String> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("auth-param1", "value1");
additionalParameters.put("auth-param2", "value2");
response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
this.filter.doFilter(authorizationResponse, response, filterChain);
verifyZeroInteractions(filterChain);
}
// gh-7963
@Test
public void doFilterWhenAuthorizationRequestRedirectUriParametersDoesNotMatchThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
FilterChain filterChain = mock(FilterChain.class);
// 1) Parameter value
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.put("param2", "value8");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
// 2) Parameter order
parametersNotMatch = new LinkedHashMap<>();
parametersNotMatch.put("param2", "value2");
parametersNotMatch.put("param1", "value1");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(2)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
// 3) Parameter missing
parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.remove("param2");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(3)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestMatchThenAuthorizationRequestRemoved() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull(); assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(authorizationResponse)).isNull();
} }
@Test @Test
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception { public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception {
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT); OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
when(this.authenticationManager.authenticate(any(Authentication.class))) when(this.authenticationManager.authenticate(any(Authentication.class)))
.thenThrow(new OAuth2AuthorizationException(error)); .thenThrow(new OAuth2AuthorizationException(error));
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant"); assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception { public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
this.registration1.getRegistrationId(), this.principalName1); this.registration1.getRegistrationId(), this.principalName1);
@@ -242,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception { public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1"); assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception { public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
String requestUri = "/saved-request"; String requestUri = "/saved-request";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri); request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
RequestCache requestCache = new HttpSessionRequestCache(); RequestCache requestCache = new HttpSessionRequestCache();
requestCache.saveRequest(request, response); requestCache.saveRequest(request, response);
request.setRequestURI("/callback/client-1");
requestUri = "/callback/client-1";
request.setRequestURI(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code"); request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state"); request.addParameter(OAuth2ParameterNames.STATE, "state");
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1); this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
@@ -285,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception { public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
AnonymousAuthenticationToken anonymousPrincipal = AnonymousAuthenticationToken anonymousPrincipal =
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(anonymousPrincipal); securityContext.setAuthentication(anonymousPrincipal);
SecurityContextHolder.setContext(securityContext); SecurityContextHolder.setContext(securityContext);
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), anonymousPrincipal, request); this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
assertThat(authorizedClient).isNotNull(); assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName()); assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
assertThat(authorizedClient.getAccessToken()).isNotNull(); assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false); HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull(); assertThat(session).isNotNull();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -326,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
} }
@Test @Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception { public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(securityContext); // null Authentication SecurityContextHolder.setContext(securityContext); // null Authentication
String requestUri = "/callback/client-1"; MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class); FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1); this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain); this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), null, request); this.registration1.getRegistrationId(), null, authorizationResponse);
assertThat(authorizedClient).isNotNull(); assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser"); assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
assertThat(authorizedClient.getAccessToken()).isNotNull(); assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false); HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull(); assertThat(session).isNotNull();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -363,23 +393,57 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient);
} }
private static MockHttpServletRequest createAuthorizationRequest(String requestUri) {
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
}
private static MockHttpServletRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
if (!CollectionUtils.isEmpty(parameters)) {
parameters.forEach(request::addParameter);
request.setQueryString(
parameters.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&")));
}
return request;
}
private static MockHttpServletRequest createAuthorizationResponse(MockHttpServletRequest authorizationRequest) {
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
}
private static MockHttpServletRequest createAuthorizationResponse(
MockHttpServletRequest authorizationRequest, Map<String, String> additionalParameters) {
MockHttpServletRequest authorizationResponse = new MockHttpServletRequest(
authorizationRequest.getMethod(), authorizationRequest.getRequestURI());
authorizationResponse.setServletPath(authorizationRequest.getRequestURI());
authorizationRequest.getParameterMap().forEach(authorizationResponse::addParameter);
authorizationResponse.addParameter(OAuth2ParameterNames.CODE, "code");
authorizationResponse.addParameter(OAuth2ParameterNames.STATE, "state");
additionalParameters.forEach(authorizationResponse::addParameter);
authorizationResponse.setQueryString(
authorizationResponse.getParameterMap().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()[0])
.collect(Collectors.joining("&")));
authorizationResponse.setSession(authorizationRequest.getSession());
return authorizationResponse;
}
private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
ClientRegistration registration) { ClientRegistration registration) {
Map<String, Object> additionalParameters = new HashMap<>(); Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId()); additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class); OAuth2AuthorizationRequest authorizationRequest = request()
when(authorizationRequest.getAdditionalParameters()).thenReturn(additionalParameters); .additionalParameters(additionalParameters)
when(authorizationRequest.getRedirectUri()).thenReturn(request.getRequestURL().toString()); .redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
when(authorizationRequest.getState()).thenReturn("state");
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
} }
private void setUpAuthenticationResult(ClientRegistration registration) { private void setUpAuthenticationResult(ClientRegistration registration) {
OAuth2AuthorizationCodeAuthenticationToken authentication = mock(OAuth2AuthorizationCodeAuthenticationToken.class); OAuth2AuthorizationCodeAuthenticationToken authentication =
when(authentication.getClientRegistration()).thenReturn(registration); new OAuth2AuthorizationCodeAuthenticationToken(registration, success(), noScopes(), refreshToken());
when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class));
when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
when(authentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class));
when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
} }
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -25,25 +25,28 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens; import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; import org.springframework.util.CollectionUtils;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.handler.DefaultWebFilterChain; import org.springframework.web.server.handler.DefaultWebFilterChain;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
/** /**
* @author Rob Winch * @author Rob Winch
@@ -101,7 +104,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
MockServerWebExchange exchange = MockServerWebExchange MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/")); .from(MockServerHttpRequest.get("/"));
DefaultWebFilterChain chain = new DefaultWebFilterChain( DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete()); e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block(); this.filter.filter(exchange, chain).block();
@@ -110,42 +113,154 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
@Test @Test
public void filterWhenMatchThenAuthorizedClientSaved() { public void filterWhenMatchThenAuthorizedClientSaved() {
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
.redirectUri("/authorize/registration-id") when(this.clientRegistrationRepository.findByRegistrationId(any()))
.build(); .thenReturn(Mono.just(clientRegistration));
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
.redirectUri("/authorize/registration-id")
.build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
Mono<Authentication> authentication = Mono.just(
new OAuth2AuthorizationCodeAuthenticationToken(registration, authorizationExchange));
OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens
.authenticated();
when(this.authenticationManager.authenticate(any())).thenReturn(
Mono.just(authenticated));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())) when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
ServerAuthenticationConverter converter = e -> authentication; when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
this.filter = new OAuth2AuthorizationCodeGrantWebFilter( MockServerHttpRequest authorizationRequest =
this.authenticationManager, converter, this.authorizedClientRepository); createAuthorizationRequest("/authorization/callback");
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
MockServerHttpRequest request = MockServerHttpRequest createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
.get("/authorize/registration-id") MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
.queryParam(OAuth2ParameterNames.CODE, "code") MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
.queryParam(OAuth2ParameterNames.STATE, "state") this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
DefaultWebFilterChain chain = new DefaultWebFilterChain( DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete()); e -> e.getResponse().setComplete(), Collections.emptyList());
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block(); this.filter.filter(exchange, chain).block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any()); verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any());
} }
// gh-7966
@Test
public void filterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any()))
.thenReturn(Mono.just(clientRegistration));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty());
when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
// 1) redirect_uri with query parameters
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback", parameters);
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verify(this.authenticationManager, times(1)).authenticate(any());
// 2) redirect_uri with query parameters AND authorization response additional parameters
Map<String, String> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("auth-param1", "value1");
additionalParameters.put("auth-param2", "value2");
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
exchange = MockServerWebExchange.from(authorizationResponse);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block();
verify(this.authenticationManager, times(2)).authenticate(any());
}
// gh-7966
@Test
public void filterWhenAuthorizationRequestRedirectUriParametersNotMatchThenNotProcessed() {
String requestUri = "/authorization/callback";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest(requestUri, parameters);
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
// 1) Parameter value
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.put("param2", "value8");
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
// 2) Parameter order
parametersNotMatch = new LinkedHashMap<>();
parametersNotMatch.put("param2", "value2");
parametersNotMatch.put("param1", "value1");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
exchange = MockServerWebExchange.from(authorizationResponse);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
// 3) Parameter missing
parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.remove("param2");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
exchange = MockServerWebExchange.from(authorizationResponse);
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
}
private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(
MockServerHttpRequest authorizationRequest, ClientRegistration registration) {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
return request()
.additionalParameters(additionalParameters)
.redirectUri(authorizationRequest.getURI().toString())
.build();
}
private static MockServerHttpRequest createAuthorizationRequest(String requestUri) {
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
}
private static MockServerHttpRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
.get(requestUri);
if (!CollectionUtils.isEmpty(parameters)) {
parameters.forEach(builder::queryParam);
}
return builder.build();
}
private static MockServerHttpRequest createAuthorizationResponse(MockServerHttpRequest authorizationRequest) {
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
}
private static MockServerHttpRequest createAuthorizationResponse(
MockServerHttpRequest authorizationRequest, Map<String, String> additionalParameters) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
.get(authorizationRequest.getURI().toString());
builder.queryParam(OAuth2ParameterNames.CODE, "code");
builder.queryParam(OAuth2ParameterNames.STATE, "state");
additionalParameters.forEach(builder::queryParam);
builder.cookies(authorizationRequest.getCookies());
return builder.build();
}
} }
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -57,8 +57,8 @@ import java.util.stream.Stream;
public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> { public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE = private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {}; new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -82,10 +82,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
try { try {
// gh-6463
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<String, String> tokenResponseParameters = (Map<String, String>) this.jsonMessageConverter.read( Map<String, Object> tokenResponseParameters = (Map<String, Object>) this.jsonMessageConverter.read(
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage); PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
return this.tokenResponseConverter.convert(tokenResponseParameters); return this.tokenResponseConverter.convert(
tokenResponseParameters.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().toString())));
} catch (Exception ex) { } catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " + throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
ex.getMessage(), ex, inputMessage); ex.getMessage(), ex, inputMessage);
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
/** /**
* A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}. * A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}.
@@ -47,8 +48,8 @@ import java.util.Map;
public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> { public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE = private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {}; new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -70,10 +71,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
try { try {
// gh-8157
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<String, String> errorParameters = (Map<String, String>) this.jsonMessageConverter.read( Map<String, Object> errorParameters = (Map<String, Object>) this.jsonMessageConverter.read(
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage); PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
return this.errorConverter.convert(errorParameters); return this.errorConverter.convert(
errorParameters.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> String.valueOf(entry.getValue()))));
} catch (Exception ex) { } catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " + throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
ex.getMessage(), ex, inputMessage); ex.getMessage(), ex, inputMessage);
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -96,6 +96,39 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests {
} }
// gh-6463
@Test
public void readInternalWhenSuccessfulTokenResponseWithObjectThenReadOAuth2AccessTokenResponse() throws Exception {
String tokenResponse = "{\n" +
" \"access_token\": \"access-token-1234\",\n" +
" \"token_type\": \"bearer\",\n" +
" \"expires_in\": 3600,\n" +
" \"scope\": \"read write\",\n" +
" \"refresh_token\": \"refresh-token-1234\",\n" +
" \"custom_object_1\": {\"name1\": \"value1\"},\n" +
" \"custom_object_2\": [\"value1\", \"value2\"],\n" +
" \"custom_parameter_1\": \"custom-value-1\",\n" +
" \"custom_parameter_2\": \"custom-value-2\"\n" +
"}\n";
MockClientHttpResponse response = new MockClientHttpResponse(
tokenResponse.getBytes(), HttpStatus.OK);
OAuth2AccessTokenResponse accessTokenResponse = this.messageConverter.readInternal(
OAuth2AccessTokenResponse.class, response);
assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBeforeOrEqualTo(Instant.now().plusSeconds(3600));
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write");
assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234");
assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(
entry("custom_object_1", "{name1=value1}"),
entry("custom_object_2", "[value1, value2]"),
entry("custom_parameter_1", "custom-value-1"),
entry("custom_parameter_2", "custom-value-2"));
}
@Test @Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter tokenResponseConverter = mock(Converter.class); Converter tokenResponseConverter = mock(Converter.class);
@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -78,6 +78,25 @@ public class OAuth2ErrorHttpMessageConverterTests {
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2"); assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
} }
// gh-8157
@Test
public void readInternalWhenErrorResponseWithObjectThenReadOAuth2Error() throws Exception {
String errorResponse = "{\n" +
" \"error\": \"unauthorized_client\",\n" +
" \"error_description\": \"The client is not authorized\",\n" +
" \"error_codes\": [65001],\n" +
" \"error_uri\": \"https://tools.ietf.org/html/rfc6749#section-5.2\"\n" +
"}\n";
MockClientHttpResponse response = new MockClientHttpResponse(
errorResponse.getBytes(), HttpStatus.BAD_REQUEST);
OAuth2Error oauth2Error = this.messageConverter.readInternal(OAuth2Error.class, response);
assertThat(oauth2Error.getErrorCode()).isEqualTo("unauthorized_client");
assertThat(oauth2Error.getDescription()).isEqualTo("The client is not authorized");
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
}
@Test @Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter errorConverter = mock(Converter.class); Converter errorConverter = mock(Converter.class);
@@ -66,7 +66,7 @@ class JettyCasService extends Server {
String password = System.getProperty('javax.net.ssl.trustStorePassword','password') String password = System.getProperty('javax.net.ssl.trustStorePassword','password')
SslContextFactory sslContextFactory = new SslContextFactory(); SslContextFactory sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(getTrustStore()); sslContextFactory.setKeyStorePath(getTrustStore());
sslContextFactory.setKeyStorePassword(password); sslContextFactory.setKeyStorePassword(password);
sslContextFactory.setKeyManagerPassword(password); sslContextFactory.setKeyManagerPassword(password);
@@ -0,0 +1,14 @@
releasenotes:
sections:
- title: "New Features"
emoji: ":star:"
labels: ["enhancement"]
- title: "Bug Fixes"
emoji: ":beetle:"
labels: ["bug", "regression"]
- title: "Dependency Upgrades"
emoji: ":hammer:"
labels: ["dependency-upgrade"]
- title: "Non-passive"
emoji: ":rewind:"
labels: ["breaks-passivity"]
@@ -43,7 +43,7 @@ final class WithMockUserSecurityContextFactory implements
.username() : withUser.value(); .username() : withUser.value();
if (username == null) { if (username == null) {
throw new IllegalArgumentException(withUser throw new IllegalArgumentException(withUser
+ " cannot have null username on both username and value properites"); + " cannot have null username on both username and value properties");
} }
List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
@@ -563,6 +563,6 @@ public class SwitchUserFilter extends GenericFilterBean
} }
private static RequestMatcher createMatcher(String pattern) { private static RequestMatcher createMatcher(String pattern) {
return new AntPathRequestMatcher(pattern, null, true, new UrlPathHelper()); return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
} }
} }
@@ -42,7 +42,6 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@@ -82,7 +81,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
private LogoutHandler logoutHandler; private List<LogoutHandler> logoutHandlers;
HttpServlet3RequestFactory(String rolePrefix) { HttpServlet3RequestFactory(String rolePrefix) {
this.rolePrefix = rolePrefix; this.rolePrefix = rolePrefix;
@@ -146,7 +145,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
* {@link HttpServletRequest#logout()}. * {@link HttpServletRequest#logout()}.
*/ */
public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) { public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) {
this.logoutHandler = CollectionUtils.isEmpty(logoutHandlers) ? null : new CompositeLogoutHandler(logoutHandlers); this.logoutHandlers = logoutHandlers;
} }
/** /**
@@ -246,8 +245,8 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
@Override @Override
public void logout() throws ServletException { public void logout() throws ServletException {
LogoutHandler handler = HttpServlet3RequestFactory.this.logoutHandler; List<LogoutHandler> handlers = HttpServlet3RequestFactory.this.logoutHandlers;
if (handler == null) { if (CollectionUtils.isEmpty(handlers)) {
HttpServlet3RequestFactory.this.logger.debug( HttpServlet3RequestFactory.this.logger.debug(
"logoutHandlers is null, so allowing original HttpServletRequest to handle logout"); "logoutHandlers is null, so allowing original HttpServletRequest to handle logout");
super.logout(); super.logout();
@@ -255,7 +254,9 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
} }
Authentication authentication = SecurityContextHolder.getContext() Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication(); .getAuthentication();
handler.logout(this, this.response, authentication); for (LogoutHandler handler : handlers) {
handler.logout(this, this.response, authentication);
}
} }
private boolean isAuthenticated() { private boolean isAuthenticated() {
@@ -107,7 +107,7 @@ public final class AntPathRequestMatcher
* *
* @param pattern the ant pattern to use for matching * @param pattern the ant pattern to use for matching
* @param httpMethod the HTTP method. The {@code matches} method will return false if * @param httpMethod the HTTP method. The {@code matches} method will return false if
* the incoming request doesn't doesn't have the same method. * the incoming request doesn't have the same method.
* @param caseSensitive true if the matcher should consider case, else false * @param caseSensitive true if the matcher should consider case, else false
* @param urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest * @param urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest
*/ */
@@ -16,11 +16,17 @@
package org.springframework.security.web.authentication.switchuser; package org.springframework.security.web.authentication.switchuser;
import static org.assertj.core.api.Assertions.*; import java.util.ArrayList;
import static org.mockito.Mockito.*; import java.util.Collection;
import java.util.List;
import javax.servlet.FilterChain;
import org.junit.*; import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.AccountExpiredException;
@@ -42,8 +48,10 @@ import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import javax.servlet.FilterChain; import static org.assertj.core.api.Assertions.assertThat;
import java.util.*; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/** /**
* Tests * Tests
@@ -75,6 +83,7 @@ public class SwitchUserFilterTests {
request.setScheme("http"); request.setScheme("http");
request.setServerName("localhost"); request.setServerName("localhost");
request.setRequestURI("/login/impersonate"); request.setRequestURI("/login/impersonate");
request.setMethod("POST");
return request; return request;
} }
@@ -125,6 +134,20 @@ public class SwitchUserFilterTests {
assertThat(filter.requiresExitUser(request)).isFalse(); assertThat(filter.requiresExitUser(request)).isFalse();
} }
@Test
// gh-4183
public void requiresExitUserWhenGetThenDoesNotMatch() {
SwitchUserFilter filter = new SwitchUserFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("localhost");
request.setRequestURI("/login/impersonate");
request.setMethod("GET");
assertThat(filter.requiresExitUser(request)).isFalse();
}
@Test @Test
public void requiresExitUserWhenMatcherThenWorks() { public void requiresExitUserWhenMatcherThenWorks() {
SwitchUserFilter filter = new SwitchUserFilter(); SwitchUserFilter filter = new SwitchUserFilter();
@@ -159,6 +182,20 @@ public class SwitchUserFilterTests {
assertThat(filter.requiresSwitchUser(request)).isFalse(); assertThat(filter.requiresSwitchUser(request)).isFalse();
} }
@Test
// gh-4183
public void requiresSwitchUserWhenGetThenDoesNotMatch() {
SwitchUserFilter filter = new SwitchUserFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme("http");
request.setServerName("localhost");
request.setRequestURI("/login/impersonate");
request.setMethod("GET");
assertThat(filter.requiresSwitchUser(request)).isFalse();
}
@Test @Test
public void requiresSwitchUserWhenMatcherThenWorks() { public void requiresSwitchUserWhenMatcherThenWorks() {
SwitchUserFilter filter = new SwitchUserFilter(); SwitchUserFilter filter = new SwitchUserFilter();