Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16c350a7bc | |||
| 6d6a22eda1 | |||
| 1e957e96b9 | |||
| 9986a33177 | |||
| ecf58c9549 | |||
| 497ee84537 | |||
| 30a68b6c10 | |||
| aa118466e5 | |||
| f98db2d866 | |||
| 52af3998c8 | |||
| 1b91259412 | |||
| 9d3ea58bb1 | |||
| c2296b0376 | |||
| 24d251f232 | |||
| 18610eab6f | |||
| 1964cfdfaa | |||
| 0ae7ceef70 | |||
| b3c7f0a79f | |||
| 4df4f9a63d | |||
| 9d25c2d2f8 | |||
| 94b116c8c8 | |||
| 3c6f08ede0 | |||
| 9a94234dae | |||
| 017044bf93 | |||
| ae99097723 | |||
| acb7e63cf7 | |||
| 0256439a50 | |||
| c8f72a1b87 | |||
| 332e8ce07a | |||
| 7aea459535 | |||
| c91389ff8b | |||
| 6de345b972 | |||
| 19f08cbedb | |||
| f82e435aaf | |||
| a44225d334 | |||
| 5decfb1ece | |||
| a24d67375b | |||
| 3a46ba8a85 | |||
| a4851095df | |||
| 633e5b85b4 | |||
| bfa5830e3d | |||
| e43fab518c | |||
| c9f676739b | |||
| b7ce65b284 | |||
| 67d561b5f7 | |||
| 4dd2b1dfe1 | |||
| aa28a0b453 | |||
| 8145cb557b | |||
| ef4109358a | |||
| 6d6fd09665 |
+1
-1
@@ -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.
|
||||
*
|
||||
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
|
||||
|
||||
+4
@@ -16,8 +16,10 @@
|
||||
package org.springframework.security.config.annotation.configuration;
|
||||
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -34,9 +36,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
* @since 3.2
|
||||
*/
|
||||
@Configuration
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class ObjectPostProcessorConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public ObjectPostProcessor<Object> objectPostProcessor(
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
return new AutowireBeanFactoryObjectPostProcessor(beanFactory);
|
||||
|
||||
+3
@@ -31,8 +31,10 @@ import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.AdviceMode;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
@@ -83,6 +85,7 @@ import org.springframework.util.Assert;
|
||||
* @see EnableGlobalMethodSecurity
|
||||
*/
|
||||
@Configuration
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class GlobalMethodSecurityConfiguration
|
||||
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware {
|
||||
private static final Log logger = LogFactory
|
||||
|
||||
+4
@@ -15,14 +15,18 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.method.configuration;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
|
||||
|
||||
@Configuration
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
class Jsr250MetadataSourceConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() {
|
||||
return new Jsr250MethodSecurityMetadataSource();
|
||||
}
|
||||
|
||||
+2
@@ -49,6 +49,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public DelegatingMethodSecurityMetadataSource methodMetadataSource() {
|
||||
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
|
||||
new DefaultMethodSecurityExpressionHandler());
|
||||
@@ -69,6 +70,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
|
||||
return new DefaultMethodSecurityExpressionHandler();
|
||||
}
|
||||
|
||||
+2
-1
@@ -586,6 +586,7 @@ public final class HttpSecurity extends
|
||||
|
||||
/**
|
||||
* Allows restricting access based upon the {@link HttpServletRequest} using
|
||||
* {@link RequestMatcher} implementations (i.e. via URL patterns).
|
||||
*
|
||||
* <h2>Example Configurations</h2>
|
||||
*
|
||||
@@ -723,7 +724,7 @@ public final class HttpSecurity extends
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return the {@link ServletApiConfigurer} for further customizations
|
||||
* @return the {@link CsrfConfigurer} for further customizations
|
||||
* @throws Exception
|
||||
*/
|
||||
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
|
||||
|
||||
/**
|
||||
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
|
||||
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites
|
||||
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
|
||||
* set.
|
||||
*
|
||||
* @param successHandler the {@link AuthenticationSuccessHandler}.
|
||||
|
||||
+19
-19
@@ -160,6 +160,25 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
|
||||
@Override
|
||||
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);
|
||||
registerDefaultEntryPoint(http);
|
||||
registerDefaultCsrfOverride(http);
|
||||
@@ -179,25 +198,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
filter = postProcess(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 {
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ public class GlobalMethodSecurityBeanDefinitionParser implements BeanDefinitionP
|
||||
pc.registerBeanComponent(new BeanComponentDefinition(
|
||||
expressionHandler, expressionHandlerRef));
|
||||
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
|
||||
|
||||
+54
-12
@@ -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");
|
||||
* 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.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
|
||||
|
||||
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||
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")));
|
||||
}
|
||||
setDefaultEntryPoints(http);
|
||||
|
||||
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
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() {
|
||||
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
|
||||
}
|
||||
@@ -1467,7 +1503,7 @@ public class ServerHttpSecurity {
|
||||
|
||||
/**
|
||||
* Require a specific authority.
|
||||
* @param authority the authority to require (i.e. "USER" woudl require authority of "USER").
|
||||
* @param authority the authority to require (i.e. "USER" would require authority of "USER").
|
||||
* @return the {@link AuthorizeExchangeSpec} to configure
|
||||
*/
|
||||
public AuthorizeExchangeSpec hasAuthority(String authority) {
|
||||
@@ -2376,7 +2412,9 @@ public class ServerHttpSecurity {
|
||||
*/
|
||||
public final class LogoutSpec {
|
||||
private LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
|
||||
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(new SecurityContextServerLogoutHandler()));
|
||||
private final SecurityContextServerLogoutHandler DEFAULT_LOGOUT_HANDLER = new SecurityContextServerLogoutHandler();
|
||||
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(this.DEFAULT_LOGOUT_HANDLER));
|
||||
|
||||
|
||||
/**
|
||||
* Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler}
|
||||
@@ -2440,6 +2478,10 @@ public class ServerHttpSecurity {
|
||||
}
|
||||
|
||||
private Optional<ServerLogoutHandler> createLogoutHandler() {
|
||||
ServerSecurityContextRepository securityContextRepository = ServerHttpSecurity.this.securityContextRepository;
|
||||
if (securityContextRepository != null) {
|
||||
this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository);
|
||||
}
|
||||
if (this.logoutHandlers.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
+30
-1
@@ -298,6 +298,18 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.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
|
||||
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
|
||||
throws Exception {
|
||||
@@ -652,7 +664,8 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
|
||||
@Test
|
||||
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
|
||||
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire())
|
||||
assertThatCode(() -> this.spring
|
||||
.register(JwtDecoderConfig.class, MultipleBearerTokenResolverBeansConfig.class).autowire())
|
||||
.isInstanceOf(BeanCreationException.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
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
+42
@@ -21,6 +21,7 @@ import org.openqa.selenium.WebDriver;
|
||||
import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
|
||||
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
@@ -117,4 +118,45 @@ public class LogoutSpecTests {
|
||||
.assertAt()
|
||||
.assertLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutWhenCustomSecurityContextRepositoryThenLogsOut() {
|
||||
WebSessionServerSecurityContextRepository repository = new WebSessionServerSecurityContextRepository();
|
||||
repository.setSpringSecurityContextAttrName("CUSTOM_CONTEXT_ATTR");
|
||||
SecurityWebFilterChain securityWebFilter = this.http
|
||||
.securityContextRepository(repository)
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.and()
|
||||
.logout()
|
||||
.and()
|
||||
.build();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(securityWebFilter)
|
||||
.build();
|
||||
|
||||
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
|
||||
.webTestClientSetup(webTestClient)
|
||||
.build();
|
||||
|
||||
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
|
||||
.assertAt();
|
||||
|
||||
FormLoginTests.HomePage homePage = loginPage.loginForm()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.submit(FormLoginTests.HomePage.class);
|
||||
|
||||
homePage.assertAt();
|
||||
|
||||
FormLoginTests.DefaultLogoutPage.to(driver)
|
||||
.assertAt()
|
||||
.logout();
|
||||
|
||||
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
|
||||
.assertAt();
|
||||
}
|
||||
}
|
||||
|
||||
+31
-1
@@ -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");
|
||||
* 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.openqa.selenium.WebDriver;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
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.authentication.ServerAuthenticationConverter;
|
||||
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.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -79,6 +83,8 @@ public class OAuth2LoginTests {
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
private WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
private WebFilterChainProxy springSecurity;
|
||||
|
||||
@@ -94,6 +100,14 @@ public class OAuth2LoginTests {
|
||||
.clientSecret("secret")
|
||||
.build();
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
if (context.getBeanNamesForType(WebHandler.class).length > 0) {
|
||||
this.client = WebTestClient.bindToApplicationContext(context)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
|
||||
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
|
||||
@@ -140,6 +154,22 @@ public class OAuth2LoginTests {
|
||||
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
|
||||
static class OAuth2LoginWithSingleClientRegistrations {
|
||||
@Bean
|
||||
|
||||
-2
@@ -17,7 +17,6 @@ package org.springframework.security.access.expression;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
@@ -158,7 +157,6 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
|
||||
|
||||
private Set<String> getAuthoritySet() {
|
||||
if (roles == null) {
|
||||
roles = new HashSet<>();
|
||||
Collection<? extends GrantedAuthority> userAuthorities = authentication
|
||||
.getAuthorities();
|
||||
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
|
||||
* </property>
|
||||
* </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
|
||||
* gets configured, create a subclass of this and override the
|
||||
* {@link #configureJaas(Resource)} method.
|
||||
|
||||
@@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found
|
||||
JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority
|
||||
JdbcDaoImpl.notFound=User {0} not found
|
||||
LdapAuthenticationProvider.badCredentials=Bad credentials
|
||||
LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
|
||||
LdapAuthenticationProvider.credentialsExpired=User credentials have expired
|
||||
LdapAuthenticationProvider.disabled=User is disabled
|
||||
LdapAuthenticationProvider.expired=User account has expired
|
||||
|
||||
+8
@@ -65,6 +65,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
}
|
||||
|
||||
public String encode(CharSequence rawPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
|
||||
String salt;
|
||||
if (strength > 0) {
|
||||
if (random != null) {
|
||||
@@ -81,6 +85,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
}
|
||||
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
|
||||
if (encodedPassword == null || encodedPassword.length() == 0) {
|
||||
logger.warn("Empty encoded password");
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2016 the original author or authors.
|
||||
* Copyright 2011-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,16 +32,13 @@ public class Encryptors {
|
||||
* (Password-Based Key Derivation Function #2). Salts the password to prevent
|
||||
* dictionary attacks against the key. The 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 encrypted message will be
|
||||
* random 16-byte initialization vector to ensure each encrypted message will be
|
||||
* unique. Requires Java 6.
|
||||
*
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
|
||||
* (instead of GCM)
|
||||
*/
|
||||
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt,
|
||||
@@ -53,13 +50,21 @@ public class Encryptors {
|
||||
* Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation
|
||||
* Function #2). Salts the password to prevent dictionary attacks against the key. The
|
||||
* 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.
|
||||
* 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
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #stronger(CharSequence, CharSequence), which uses the significatly more secure
|
||||
* GCM (instead of CBC)
|
||||
*/
|
||||
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt,
|
||||
@@ -100,7 +105,10 @@ public class Encryptors {
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* secret key
|
||||
* @deprecated This encryptor is not secure. Instead, look to your data store for a
|
||||
* mechanism to query encrypted data.
|
||||
*/
|
||||
@Deprecated
|
||||
public static TextEncryptor queryableText(CharSequence password, CharSequence salt) {
|
||||
return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
|
||||
salt));
|
||||
|
||||
+11
@@ -92,4 +92,15 @@ public class BCryptPasswordEncoderTests {
|
||||
assertThat(encoder.matches("password", "012345678901234567890123456789")).isFalse();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void encodeNullRawPassword() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
encoder.encode(null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void matchNullRawPassword() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
encoder.matches(null, "does-not-matter");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
|
||||
|
||||
[[spring-security-crypto-encryption-bytes]]
|
||||
==== BytesEncryptor
|
||||
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor:
|
||||
Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
|
||||
|
||||
[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.
|
||||
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.
|
||||
@@ -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
|
||||
----
|
||||
|
||||
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]]
|
||||
==== TextEncryptor
|
||||
Use the Encryptors.text factory method to construct a standard TextEncryptor:
|
||||
|
||||
@@ -41,7 +41,7 @@ A pre-invocation decision on whether the invocation is allowed to proceed is mad
|
||||
[[authz-access-decision-manager]]
|
||||
==== The AccessDecisionManager
|
||||
The `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` and is responsible for making final access control decisions.
|
||||
the `AccessDecisionManager` interface contains three methods:
|
||||
The `AccessDecisionManager` interface contains three methods:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
gaeVersion=1.9.71
|
||||
springBootVersion=2.1.12.RELEASE
|
||||
version=5.1.8.RELEASE
|
||||
gaeVersion=1.9.80
|
||||
springBootVersion=2.1.13.RELEASE
|
||||
version=5.1.10.RELEASE
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Californium-SR15'
|
||||
ext.reactorVersion = 'Californium-SR18'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.1.13.RELEASE'
|
||||
ext.springVersion = '5.1.15.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Lovelace-SR15'
|
||||
ext.springDataVersion = 'Lovelace-SR17'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
@@ -18,16 +18,16 @@ dependencyManagement {
|
||||
}
|
||||
dependencies {
|
||||
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 'org.gebish:geb-spock:0.10.0'
|
||||
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.5'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.5'
|
||||
dependency 'org.powermock:powermock-core:2.0.5'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.5'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.5'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.5'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.7'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.7'
|
||||
dependency 'org.powermock:powermock-core:2.0.7'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.7'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.7'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.7'
|
||||
dependency 'org.python:jython:2.5.3'
|
||||
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
|
||||
@@ -45,22 +45,22 @@ dependencyManagement {
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.10.2'
|
||||
dependency 'com.fasterxml:classmate:1.3.4'
|
||||
dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1'
|
||||
dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.76'
|
||||
dependency 'com.google.appengine:appengine-api-labs:1.9.76'
|
||||
dependency 'com.google.appengine:appengine-api-stubs:1.9.76'
|
||||
dependency 'com.google.appengine:appengine-testing:1.9.76'
|
||||
dependency 'com.google.appengine:appengine:1.9.76'
|
||||
dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.80'
|
||||
dependency 'com.google.appengine:appengine-api-labs:1.9.80'
|
||||
dependency 'com.google.appengine:appengine-api-stubs:1.9.80'
|
||||
dependency 'com.google.appengine:appengine-testing:1.9.80'
|
||||
dependency 'com.google.appengine:appengine:1.9.80'
|
||||
dependency 'com.google.code.gson:gson:2.8.2'
|
||||
dependency 'com.google.guava:guava:20.0'
|
||||
dependency 'com.google.inject:guice:3.0'
|
||||
dependency 'com.nimbusds:lang-tag:1.4.3'
|
||||
dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2'
|
||||
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.sun.xml.bind:jaxb-core:2.3.0.1'
|
||||
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 'commons-cli:commons-cli:1.4'
|
||||
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-ldap-constants: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:httpmime:4.5.3'
|
||||
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.bouncycastle:bcpkix-jdk15on:1.64'
|
||||
dependency 'org.bouncycastle:bcprov-jdk15on:1.58'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.14'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.19'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.19'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.19'
|
||||
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-client:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
|
||||
dependency 'org.gebish:geb-ast: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.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: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-validator:6.0.18.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.0.19.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.4.1'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
|
||||
dependency 'org.javassist:javassist:3.22.0-CR2'
|
||||
@@ -183,7 +183,7 @@ dependencyManagement {
|
||||
dependency 'org.objenesis:objenesis:2.6'
|
||||
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
|
||||
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:selenium-api:3.141.59'
|
||||
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
|
||||
|
||||
+17
-4
@@ -16,6 +16,7 @@
|
||||
package org.springframework.security.ldap.authentication.ad;
|
||||
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.ldap.CommunicationException;
|
||||
import org.springframework.ldap.core.DirContextOperations;
|
||||
import org.springframework.ldap.core.DistinguishedName;
|
||||
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
|
||||
@@ -24,6 +25,7 @@ import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -142,12 +144,15 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
UsernamePasswordAuthenticationToken auth) {
|
||||
String username = auth.getName();
|
||||
String password = (String) auth.getCredentials();
|
||||
|
||||
DirContext ctx = bindAsUser(username, password);
|
||||
DirContext ctx = null;
|
||||
|
||||
try {
|
||||
ctx = bindAsUser(username, password);
|
||||
return searchForUser(ctx, username);
|
||||
}
|
||||
catch (CommunicationException e) {
|
||||
throw badLdapConnection(e);
|
||||
}
|
||||
catch (NamingException e) {
|
||||
logger.error("Failed to locate directory entry for authenticated user: "
|
||||
+ username, e);
|
||||
@@ -210,8 +215,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
|| (e instanceof OperationNotSupportedException)) {
|
||||
handleBindException(bindPrincipal, e);
|
||||
throw badCredentials(e);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw LdapUtils.convertLdapException(e);
|
||||
}
|
||||
}
|
||||
@@ -313,6 +317,12 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
return (BadCredentialsException) badCredentials().initCause(cause);
|
||||
}
|
||||
|
||||
private InternalAuthenticationServiceException badLdapConnection(Throwable cause) {
|
||||
return new InternalAuthenticationServiceException(messages.getMessage(
|
||||
"LdapAuthenticationProvider.badLdapConnection",
|
||||
"Connection to LDAP server failed."), cause);
|
||||
}
|
||||
|
||||
private DirContextOperations searchForUser(DirContext context, String username)
|
||||
throws NamingException {
|
||||
SearchControls searchControls = new SearchControls();
|
||||
@@ -327,6 +337,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|
||||
searchControls, searchRoot, searchFilter,
|
||||
new Object[] { bindPrincipal, username });
|
||||
}
|
||||
catch (CommunicationException ldapCommunicationException) {
|
||||
throw badLdapConnection(ldapCommunicationException);
|
||||
}
|
||||
catch (IncorrectResultSizeDataAccessException incorrectResults) {
|
||||
// Search should never return multiple results if properly configured - just
|
||||
// rethrow
|
||||
|
||||
+27
-8
@@ -32,6 +32,7 @@ import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -58,6 +59,9 @@ import static org.springframework.security.ldap.authentication.ad.ActiveDirector
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
public static final String EXISTING_LDAP_PROVIDER = "ldap://192.168.1.200/";
|
||||
public static final String NON_EXISTING_LDAP_PROVIDER = "ldap://192.168.1.201/";
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@@ -378,17 +382,29 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test(expected = org.springframework.ldap.CommunicationException.class)
|
||||
public void nonAuthenticationExceptionIsConvertedToSpringLdapException()
|
||||
throws Exception {
|
||||
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
|
||||
msg));
|
||||
provider.authenticate(joe);
|
||||
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Throwable {
|
||||
try {
|
||||
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
|
||||
msg));
|
||||
provider.authenticate(joe);
|
||||
} catch (InternalAuthenticationServiceException e) {
|
||||
// Since GH-8418 ldap communication exception is wrapped into InternalAuthenticationServiceException.
|
||||
// This test is about the wrapped exception, so we throw it.
|
||||
throw e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = org.springframework.security.authentication.InternalAuthenticationServiceException.class )
|
||||
public void connectionExceptionIsWrappedInInternalException() throws Exception {
|
||||
ActiveDirectoryLdapAuthenticationProvider noneReachableProvider = new ActiveDirectoryLdapAuthenticationProvider(
|
||||
"mydomain.eu", NON_EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
|
||||
noneReachableProvider.doAuthentication(joe);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception {
|
||||
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
|
||||
"mydomain.eu", "ldap://192.168.1.200/", "dc=ad,dc=eu,dc=mydomain");
|
||||
"mydomain.eu", EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
|
||||
checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider);
|
||||
|
||||
}
|
||||
@@ -414,8 +430,11 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
|
||||
provider.authenticate(joe);
|
||||
fail("CommunicationException was expected with a root cause of ClassNotFoundException");
|
||||
}
|
||||
catch (org.springframework.ldap.CommunicationException expected) {
|
||||
assertThat(expected.getRootCause()).isInstanceOf(ClassNotFoundException.class);
|
||||
catch (InternalAuthenticationServiceException expected) {
|
||||
assertThat(expected.getCause()).isInstanceOf(org.springframework.ldap.CommunicationException.class);
|
||||
org.springframework.ldap.CommunicationException cause =
|
||||
(org.springframework.ldap.CommunicationException) expected.getCause();
|
||||
assertThat(cause.getRootCause()).isInstanceOf(ClassNotFoundException.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -73,7 +73,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange(),
|
||||
accessTokenResponse.getAccessToken(),
|
||||
accessTokenResponse.getRefreshToken());
|
||||
accessTokenResponse.getRefreshToken(),
|
||||
accessTokenResponse.getAdditionalParameters());
|
||||
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
|
||||
+17
-23
@@ -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");
|
||||
* 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.OAuth2AuthorizationException;
|
||||
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.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>
|
||||
*/
|
||||
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
|
||||
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
|
||||
|
||||
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
|
||||
|
||||
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
|
||||
Assert.notNull(userService, "userService cannot be null");
|
||||
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
|
||||
OAuth2LoginAuthenticationToken loginAuthenticationToken =
|
||||
(OAuth2LoginAuthenticationToken) authentication;
|
||||
|
||||
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
// scope
|
||||
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
|
||||
if (authorizationCodeAuthentication.getAuthorizationExchange()
|
||||
if (loginAuthenticationToken.getAuthorizationExchange()
|
||||
.getAuthorizationRequest().getScopes().contains("openid")) {
|
||||
// This is an OpenID Connect Authentication Request so return null
|
||||
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
|
||||
return null;
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse;
|
||||
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
|
||||
try {
|
||||
OAuth2AuthorizationExchangeValidator.validate(
|
||||
authorizationCodeAuthentication.getAuthorizationExchange());
|
||||
|
||||
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
||||
new OAuth2AuthorizationCodeGrantRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange()));
|
||||
|
||||
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
|
||||
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
loginAuthenticationToken.getClientRegistration(),
|
||||
loginAuthenticationToken.getAuthorizationExchange()));
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
OAuth2Error oauth2Error = ex.getError();
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
|
||||
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
|
||||
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
|
||||
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
|
||||
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
|
||||
|
||||
Collection<? extends GrantedAuthority> mappedAuthorities =
|
||||
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
|
||||
|
||||
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange(),
|
||||
loginAuthenticationToken.getClientRegistration(),
|
||||
loginAuthenticationToken.getAuthorizationExchange(),
|
||||
oauth2User,
|
||||
mappedAuthorities,
|
||||
accessToken,
|
||||
accessTokenResponse.getRefreshToken());
|
||||
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
authorizationCodeAuthenticationToken.getRefreshToken());
|
||||
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
}
|
||||
|
||||
+33
-15
@@ -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");
|
||||
* 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.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
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,
|
||||
@@ -132,24 +138,39 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (this.shouldProcessAuthorizationResponse(request)) {
|
||||
this.processAuthorizationResponse(request, response);
|
||||
if (matchesAuthorizationResponse(request)) {
|
||||
processAuthorizationResponse(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
if (authorizationRequest == null) {
|
||||
return false;
|
||||
}
|
||||
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
|
||||
request.getServerPort(), request.getRequestURI(), null);
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
|
||||
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
|
||||
|
||||
// Compare redirect_uri
|
||||
UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
|
||||
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).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 true;
|
||||
}
|
||||
return false;
|
||||
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
|
||||
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
String redirectUri = UrlUtils.buildFullRequestUrl(request);
|
||||
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
OAuth2Error error = ex.getError();
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationResponse.getRedirectUri())
|
||||
.fromUriString(authorizationRequest.getRedirectUri())
|
||||
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
|
||||
if (!StringUtils.isEmpty(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);
|
||||
|
||||
String redirectUrl = authorizationResponse.getRedirectUri();
|
||||
String redirectUrl = authorizationRequest.getRedirectUri();
|
||||
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||
if (savedRequest != null) {
|
||||
redirectUrl = savedRequest.getRedirectUrl();
|
||||
|
||||
+40
-19
@@ -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");
|
||||
* 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.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
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,
|
||||
* which handles the processing of the OAuth 2.0 Authorization Response.
|
||||
@@ -138,10 +145,10 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.requiresAuthenticationMatcher.matches(exchange)
|
||||
.filter( matchResult -> matchResult.isMatch())
|
||||
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.flatMap(matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.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,
|
||||
@@ -171,20 +178,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
}
|
||||
|
||||
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
|
||||
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||
.flatMap(authorizationRequest -> {
|
||||
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
|
||||
.query(null)
|
||||
.build()
|
||||
.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)
|
||||
return Mono.just(exchange)
|
||||
.filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
|
||||
.flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||
.flatMap(authorizationRequest ->
|
||||
matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -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");
|
||||
* 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.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -103,14 +102,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
|
||||
MultiValueMap<String, String> queryParams = exchange.getRequest()
|
||||
.getQueryParams();
|
||||
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
|
||||
.query(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
return OAuth2AuthorizationResponseUtils
|
||||
.convert(queryParams, redirectUri);
|
||||
.convert(exchange.getRequest().getQueryParams(), redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,10 @@
|
||||
*/
|
||||
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.Test;
|
||||
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.OAuth2AuthorizationResponse;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}.
|
||||
@@ -130,4 +133,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
+191
-127
@@ -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");
|
||||
* 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.Before;
|
||||
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.MockHttpServletResponse;
|
||||
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.InMemoryClientRegistrationRepository;
|
||||
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.OAuth2Error;
|
||||
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.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
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.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.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}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@PowerMockIgnore("javax.security.*")
|
||||
@PrepareForTest({OAuth2AuthorizationRequest.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationCodeGrantFilter.class})
|
||||
@RunWith(PowerMockRunner.class)
|
||||
public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
private ClientRegistration registration1;
|
||||
private String principalName1 = "principal-1";
|
||||
@@ -132,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
@@ -143,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
|
||||
String requestUri = "/path";
|
||||
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.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");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
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.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
|
||||
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(authorizationResponse)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() 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");
|
||||
|
||||
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.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
when(this.authenticationManager.authenticate(any(Authentication.class)))
|
||||
.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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() 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");
|
||||
|
||||
public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), this.principalName1);
|
||||
@@ -242,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() 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");
|
||||
|
||||
public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, 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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
|
||||
String requestUri = "/saved-request";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RequestCache requestCache = new HttpSessionRequestCache();
|
||||
requestCache.saveRequest(request, response);
|
||||
|
||||
requestUri = "/callback/client-1";
|
||||
request.setRequestURI(requestUri);
|
||||
request.setRequestURI("/callback/client-1");
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
@@ -285,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
AnonymousAuthenticationToken anonymousPrincipal =
|
||||
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(anonymousPrincipal);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), anonymousPrincipal, request);
|
||||
this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
|
||||
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
|
||||
assertThat(authorizedClient.getAccessToken()).isNotNull();
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
HttpSession session = authorizationResponse.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -326,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
SecurityContextHolder.setContext(securityContext); // null Authentication
|
||||
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), null, request);
|
||||
this.registration1.getRegistrationId(), null, authorizationResponse);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
|
||||
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
|
||||
assertThat(authorizedClient.getAccessToken()).isNotNull();
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
HttpSession session = authorizationResponse.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -363,23 +393,57 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
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,
|
||||
ClientRegistration registration) {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
|
||||
OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class);
|
||||
when(authorizationRequest.getAdditionalParameters()).thenReturn(additionalParameters);
|
||||
when(authorizationRequest.getRedirectUri()).thenReturn(request.getRequestURL().toString());
|
||||
when(authorizationRequest.getState()).thenReturn("state");
|
||||
OAuth2AuthorizationRequest authorizationRequest = request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
|
||||
}
|
||||
|
||||
private void setUpAuthenticationResult(ClientRegistration registration) {
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication = mock(OAuth2AuthorizationCodeAuthenticationToken.class);
|
||||
when(authentication.getClientRegistration()).thenReturn(registration);
|
||||
when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class));
|
||||
when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
|
||||
when(authentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class));
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(registration, success(), noScopes(), refreshToken());
|
||||
when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
+154
-39
@@ -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");
|
||||
* 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.security.authentication.AnonymousAuthenticationToken;
|
||||
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.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
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.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.handler.DefaultWebFilterChain;
|
||||
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.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
|
||||
@@ -101,7 +104,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
MockServerWebExchange exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/"));
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete());
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
|
||||
@@ -110,42 +113,154 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
|
||||
@Test
|
||||
public void filterWhenMatchThenAuthorizedClientSaved() {
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||
.redirectUri("/authorize/registration-id")
|
||||
.build();
|
||||
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));
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||
.thenReturn(Mono.just(clientRegistration));
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
||||
.thenReturn(Mono.empty());
|
||||
ServerAuthenticationConverter converter = e -> authentication;
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
|
||||
|
||||
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
|
||||
this.authenticationManager, converter, this.authorizedClientRepository);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/authorize/registration-id")
|
||||
.queryParam(OAuth2ParameterNames.CODE, "code")
|
||||
.queryParam(OAuth2ParameterNames.STATE, "state")
|
||||
.build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback");
|
||||
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());
|
||||
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange).block();
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -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");
|
||||
* 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> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@@ -82,10 +82,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
try {
|
||||
// gh-6463
|
||||
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
|
||||
@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);
|
||||
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) {
|
||||
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
|
||||
ex.getMessage(), ex, inputMessage);
|
||||
|
||||
+12
-5
@@ -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");
|
||||
* 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.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@@ -70,10 +71,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
try {
|
||||
// gh-8157
|
||||
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
|
||||
@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);
|
||||
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) {
|
||||
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
|
||||
ex.getMessage(), ex, inputMessage);
|
||||
|
||||
+34
-1
@@ -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");
|
||||
* 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
|
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
|
||||
Converter tokenResponseConverter = mock(Converter.class);
|
||||
|
||||
+20
-1
@@ -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");
|
||||
* 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");
|
||||
}
|
||||
|
||||
// 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
|
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
|
||||
Converter errorConverter = mock(Converter.class);
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class JettyCasService extends Server {
|
||||
String password = System.getProperty('javax.net.ssl.trustStorePassword','password')
|
||||
|
||||
|
||||
SslContextFactory sslContextFactory = new SslContextFactory();
|
||||
SslContextFactory sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setKeyStorePath(getTrustStore());
|
||||
sslContextFactory.setKeyStorePassword(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"]
|
||||
+1
-1
@@ -43,7 +43,7 @@ final class WithMockUserSecurityContextFactory implements
|
||||
.username() : withUser.value();
|
||||
if (username == null) {
|
||||
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<>();
|
||||
|
||||
@@ -60,7 +60,7 @@ import java.util.*;
|
||||
* requests which match the pattern. An example configuration might look like this:
|
||||
*
|
||||
* <pre>
|
||||
* <bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
|
||||
* <bean id="myfilterChainProxy" class="org.springframework.security.web.FilterChainProxy">
|
||||
* <constructor-arg>
|
||||
* <util:list>
|
||||
* <security:filter-chain pattern="/do/not/filter*" filters="none"/>
|
||||
|
||||
+1
-1
@@ -563,6 +563,6 @@ public class SwitchUserFilter extends GenericFilterBean
|
||||
}
|
||||
|
||||
private static RequestMatcher createMatcher(String pattern) {
|
||||
return new AntPathRequestMatcher(pattern, null, true, new UrlPathHelper());
|
||||
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -42,7 +42,6 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -82,7 +81,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||
private AuthenticationManager authenticationManager;
|
||||
private LogoutHandler logoutHandler;
|
||||
private List<LogoutHandler> logoutHandlers;
|
||||
|
||||
HttpServlet3RequestFactory(String rolePrefix) {
|
||||
this.rolePrefix = rolePrefix;
|
||||
@@ -146,7 +145,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
* {@link HttpServletRequest#logout()}.
|
||||
*/
|
||||
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
|
||||
public void logout() throws ServletException {
|
||||
LogoutHandler handler = HttpServlet3RequestFactory.this.logoutHandler;
|
||||
if (handler == null) {
|
||||
List<LogoutHandler> handlers = HttpServlet3RequestFactory.this.logoutHandlers;
|
||||
if (CollectionUtils.isEmpty(handlers)) {
|
||||
HttpServlet3RequestFactory.this.logger.debug(
|
||||
"logoutHandlers is null, so allowing original HttpServletRequest to handle logout");
|
||||
super.logout();
|
||||
@@ -255,7 +254,9 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
}
|
||||
Authentication authentication = SecurityContextHolder.getContext()
|
||||
.getAuthentication();
|
||||
handler.logout(this, this.response, authentication);
|
||||
for (LogoutHandler handler : handlers) {
|
||||
handler.logout(this, this.response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthenticated() {
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ public final class AntPathRequestMatcher
|
||||
*
|
||||
* @param pattern the ant pattern to use for matching
|
||||
* @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 urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest
|
||||
*/
|
||||
|
||||
+42
-5
@@ -16,11 +16,17 @@
|
||||
|
||||
package org.springframework.security.web.authentication.switchuser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import java.util.ArrayList;
|
||||
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.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
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.util.matcher.AnyRequestMatcher;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import java.util.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests
|
||||
@@ -75,6 +83,7 @@ public class SwitchUserFilterTests {
|
||||
request.setScheme("http");
|
||||
request.setServerName("localhost");
|
||||
request.setRequestURI("/login/impersonate");
|
||||
request.setMethod("POST");
|
||||
|
||||
return request;
|
||||
}
|
||||
@@ -125,6 +134,20 @@ public class SwitchUserFilterTests {
|
||||
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
|
||||
public void requiresExitUserWhenMatcherThenWorks() {
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
@@ -159,6 +182,20 @@ public class SwitchUserFilterTests {
|
||||
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
|
||||
public void requiresSwitchUserWhenMatcherThenWorks() {
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
|
||||
Reference in New Issue
Block a user