\n"
+ + " \n"
+ + "";
return page.getBytes(Charset.defaultCharset());
}
+ private String formLogin(MultiValueMap queryParams, String csrfTokenHtmlInput) {
+ if (!this.formLoginEnabled) {
+ return "";
+ }
+ boolean isError = queryParams.containsKey("error");
+ boolean isLogoutSuccess = queryParams.containsKey("logout");
+ return " \n";
+ }
+
private static String oauth2LoginLinks(String contextPath, Map oauth2AuthenticationUrlToClientName) {
if (oauth2AuthenticationUrlToClientName.isEmpty()) {
return "";
From 96eaaec041366178808638be72b37968ccd5bb2f Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Mon, 14 May 2018 15:37:02 -0500
Subject: [PATCH 004/226] Single ClientRegistration redirects by default
Fixes: gh-5339
---
.../config/web/server/ServerHttpSecurity.java | 62 ++++++++++++++-----
.../config/web/server/OAuth2LoginTests.java | 56 ++++++++++++-----
.../server/HtmlUnitWebTestClient.java | 8 ++-
3 files changed, 93 insertions(+), 33 deletions(-)
diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
index d54aed8296d..0de6031ee14 100644
--- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
+++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
@@ -183,6 +183,8 @@ public class ServerHttpSecurity {
private LogoutSpec logout = new LogoutSpec();
+ private LoginPageSpec loginPage = new LoginPageSpec();
+
private ReactiveAuthenticationManager authenticationManager;
private ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository();
@@ -387,6 +389,16 @@ public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange,
});
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
+ MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
+ MediaType.TEXT_HTML);
+ htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+ Map urlToText = http.oauth2Login.getLinks();
+ if (urlToText.size() == 1) {
+ http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
+ } else {
+ http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
+ }
+
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
}
@@ -610,31 +622,17 @@ public SecurityWebFilterChain build() {
this.httpBasic.authenticationManager(this.authenticationManager);
this.httpBasic.configure(this);
}
- LoginPageGeneratingWebFilter loginPageFilter = null;
if(this.formLogin != null) {
this.formLogin.authenticationManager(this.authenticationManager);
if(this.securityContextRepository != null) {
this.formLogin.securityContextRepository(this.securityContextRepository);
}
- if (this.authenticationEntryPoint == null) {
- loginPageFilter = new LoginPageGeneratingWebFilter();
- loginPageFilter.setFormLoginEnabled(true);
- this.authenticationEntryPoint = this.formLogin.authenticationEntryPoint;
- }
this.formLogin.configure(this);
}
if (this.oauth2Login != null) {
- if (this.authenticationEntryPoint == null) {
- loginPageFilter = new LoginPageGeneratingWebFilter();
- loginPageFilter.setOauth2AuthenticationUrlToClientName(this.oauth2Login.getLinks());
- }
this.oauth2Login.configure(this);
}
- if (loginPageFilter != null) {
- this.authenticationEntryPoint = new RedirectServerAuthenticationEntryPoint("/login");
- this.webFilters.add(new OrderedWebFilter(loginPageFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder()));
- this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder()));
- }
+ this.loginPage.configure(this);
if(this.logout != null) {
this.logout.configure(this);
}
@@ -1084,6 +1082,8 @@ public class FormLoginSpec {
private ServerAuthenticationEntryPoint authenticationEntryPoint;
+ private boolean isEntryPointExplicit;
+
private ServerWebExchangeMatcher requiresAuthenticationMatcher;
private ServerAuthenticationFailureHandler authenticationFailureHandler;
@@ -1206,7 +1206,10 @@ public ServerHttpSecurity disable() {
protected void configure(ServerHttpSecurity http) {
if(this.authenticationEntryPoint == null) {
+ this.isEntryPointExplicit = false;
loginPage("/login");
+ } else {
+ this.isEntryPointExplicit = true;
}
if(http.requestCache != null) {
ServerRequestCache requestCache = http.requestCache.requestCache;
@@ -1233,6 +1236,35 @@ private FormLoginSpec() {
}
}
+ private class LoginPageSpec {
+ protected void configure(ServerHttpSecurity http) {
+ if (http.authenticationEntryPoint != null) {
+ return;
+ }
+ if (http.formLogin != null && http.formLogin.isEntryPointExplicit) {
+ return;
+ }
+ LoginPageGeneratingWebFilter loginPage = null;
+ if (http.formLogin != null && !http.formLogin.isEntryPointExplicit) {
+ loginPage = new LoginPageGeneratingWebFilter();
+ loginPage.setFormLoginEnabled(true);
+ }
+ if (http.oauth2Login != null) {
+ Map urlToText = http.oauth2Login.getLinks();
+ if (loginPage == null) {
+ loginPage = new LoginPageGeneratingWebFilter();
+ }
+ loginPage.setOauth2AuthenticationUrlToClientName(urlToText);
+ }
+ if (loginPage != null) {
+ http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
+ http.addFilterAt(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
+ }
+ }
+
+ private LoginPageSpec() {}
+ }
+
/**
* Configures HTTP Response Headers.
*
diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java
index 2ff2724f6c5..a2b4e1f73f4 100644
--- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java
+++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java
@@ -21,29 +21,20 @@
import org.junit.Rule;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
-import org.openqa.selenium.WebElement;
-import org.openqa.selenium.support.FindBy;
-import org.openqa.selenium.support.PageFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
-import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
-import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
-import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
-import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
-import org.springframework.security.web.server.csrf.CsrfToken;
-import org.springframework.stereotype.Controller;
import org.springframework.test.web.reactive.server.WebTestClient;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@@ -59,7 +50,7 @@ public class OAuth2LoginTests {
@Autowired
private WebFilterChainProxy springSecurity;
- private ClientRegistration github = CommonOAuth2Provider.GITHUB
+ private static ClientRegistration github = CommonOAuth2Provider.GITHUB
.getBuilder("github")
.clientId("client")
.clientSecret("secret")
@@ -90,11 +81,6 @@ public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
static class OAuth2LoginWithMulitpleClientRegistrations {
@Bean
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
- ClientRegistration github = CommonOAuth2Provider.GITHUB
- .getBuilder("github")
- .clientId("client")
- .clientSecret("secret")
- .build();
ClientRegistration google = CommonOAuth2Provider.GOOGLE
.getBuilder("google")
.clientId("client")
@@ -103,4 +89,40 @@ InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryReactiveClientRegistrationRepository(github, google);
}
}
+
+ @Test
+ public void defaultLoginPageWithSingleClientRegistrationThenRedirect() {
+ this.spring.register(OAuth2LoginWithSingleClientRegistrations.class).autowire();
+
+ WebTestClient webTestClient = WebTestClientBuilder
+ .bindToWebFilters(new GitHubWebFilter(), this.springSecurity)
+ .build();
+
+ WebDriver driver = WebTestClientHtmlUnitDriverBuilder
+ .webTestClientSetup(webTestClient)
+ .build();
+
+ driver.get("http://localhost/");
+
+ assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
+ }
+
+ @EnableWebFluxSecurity
+ static class OAuth2LoginWithSingleClientRegistrations {
+ @Bean
+ InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
+ return new InMemoryReactiveClientRegistrationRepository(github);
+ }
+ }
+
+ static class GitHubWebFilter implements WebFilter {
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
+ if (exchange.getRequest().getURI().getHost().equals("github.com")) {
+ return exchange.getResponse().setComplete();
+ }
+ return chain.filter(exchange);
+ }
+ }
}
diff --git a/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java b/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java
index f9dfab20929..111bd62c31f 100644
--- a/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java
+++ b/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java
@@ -151,8 +151,14 @@ public Mono filter(ClientRequest request, ExchangeFunction next)
private Mono redirectIfNecessary(ClientRequest request, ExchangeFunction next, ClientResponse response) {
URI location = response.headers().asHttpHeaders().getLocation();
+ String host = request.url().getHost();
+ String scheme = request.url().getScheme();
if(location != null) {
- ClientRequest redirect = ClientRequest.method(HttpMethod.GET, URI.create("http://localhost" + location.toASCIIString()))
+ String redirectUrl = location.toASCIIString();
+ if (location.getHost() == null) {
+ redirectUrl = scheme+ "://" + host + location.toASCIIString();
+ }
+ ClientRequest redirect = ClientRequest.method(HttpMethod.GET, URI.create(redirectUrl))
.headers(headers -> headers.addAll(request.headers()))
.cookies(cookies -> cookies.addAll(request.cookies()))
.attributes(attributes -> attributes.putAll(request.attributes()))
From ae6f3244fb77029e47a543b10154f8657517090d Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Mon, 14 May 2018 21:44:09 -0500
Subject: [PATCH 005/226] Release 5.1.0.M1
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index f92573b2f0c..1fa3b1714b6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
gaeVersion=1.9.63
springBootVersion=2.0.1.RELEASE
-version=5.1.0.BUILD-SNAPSHOT
+version=5.1.0.M1
From 41be145b25bae9fbea1bbc82d636273aa6eebd1f Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Mon, 14 May 2018 21:57:08 -0500
Subject: [PATCH 006/226] Disable SNAPSHOT tests for release
---
Jenkinsfile | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index 8703b270ffc..11385ea6dba 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -54,19 +54,6 @@ try {
}
}
},
- snapshots: {
- stage('Snapshot Tests') {
- node {
- checkout scm
- try {
- sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
- } catch(Exception e) {
- currentBuild.result = 'FAILED: snapshots'
- throw e
- }
- }
- }
- },
jdk9: {
stage('JDK 9') {
node {
From 2f9cd49351c41d5a0affa3a9eb93347cf8ef4fc1 Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Mon, 14 May 2018 21:58:27 -0500
Subject: [PATCH 007/226] Next Development Version
---
Jenkinsfile | 13 +++++++++++++
gradle.properties | 2 +-
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index 11385ea6dba..8703b270ffc 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -54,6 +54,19 @@ try {
}
}
},
+ snapshots: {
+ stage('Snapshot Tests') {
+ node {
+ checkout scm
+ try {
+ sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
+ } catch(Exception e) {
+ currentBuild.result = 'FAILED: snapshots'
+ throw e
+ }
+ }
+ }
+ },
jdk9: {
stage('JDK 9') {
node {
diff --git a/gradle.properties b/gradle.properties
index 1fa3b1714b6..f92573b2f0c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
gaeVersion=1.9.63
springBootVersion=2.0.1.RELEASE
-version=5.1.0.M1
+version=5.1.0.BUILD-SNAPSHOT
From d406a9259345c8c41158d2be54fc2927e6b3b154 Mon Sep 17 00:00:00 2001
From: Josh Cummings
Date: Tue, 15 May 2018 08:10:55 -0600
Subject: [PATCH 008/226] HttpConfigTests groovy->java
Issue: gh-4939
---
.../config/http/HttpConfigTests.groovy | 79 ------------
.../security/config/http/HttpConfigTests.java | 114 ++++++++++++++++++
.../config/http/HttpConfigTests-Minimal.xml | 32 +++++
3 files changed, 146 insertions(+), 79 deletions(-)
delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy
create mode 100644 config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java
create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml
diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy
deleted file mode 100644
index 00545fb508b..00000000000
--- a/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2002-2012 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations under the License.
- */
-package org.springframework.security.config.http
-
-import static org.mockito.Matchers.any
-import static org.mockito.Matchers.eq
-import static org.mockito.Mockito.*
-
-import javax.servlet.http.HttpServletResponse
-import javax.servlet.http.HttpServletResponseWrapper
-
-import org.springframework.mock.web.MockFilterChain
-import org.springframework.mock.web.MockHttpServletRequest
-import org.springframework.mock.web.MockHttpServletResponse
-
-/**
- *
- * @author Rob Winch
- */
-class HttpConfigTests extends AbstractHttpConfigTests {
- MockHttpServletRequest request = new MockHttpServletRequest('GET','/secure')
- MockHttpServletResponse response = new MockHttpServletResponse()
- MockFilterChain chain = new MockFilterChain()
-
- def 'http minimal configuration works'() {
- setup:
- xml.http() {}
- createAppContext("""
-
- """)
- when: 'request protected URL'
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'sent to login page'
- response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
- response.redirectedUrl == 'http://localhost/login'
- }
-
- def 'http disable-url-rewriting defaults to true'() {
- setup:
- xml.http() {}
- createAppContext("""
-
- """)
- HttpServletResponse testResponse = new HttpServletResponseWrapper(response) {
- public String encodeURL(String url) {
- throw new RuntimeException("Unexpected invocation of encodeURL")
- }
- public String encodeRedirectURL(String url) {
- throw new RuntimeException("Unexpected invocation of encodeURL")
- }
- public String encodeUrl(String url) {
- throw new RuntimeException("Unexpected invocation of encodeURL")
- }
- public String encodeRedirectUrl(String url) {
- throw new RuntimeException("Unexpected invocation of encodeURL")
- }
- }
- when: 'request protected URL'
- springSecurityFilterChain.doFilter(request,testResponse,{ request,response->
- response.encodeURL("/url")
- response.encodeRedirectURL("/url")
- response.encodeUrl("/url")
- response.encodeRedirectUrl("/url")
- })
- then: 'sent to login page'
- response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
- response.redirectedUrl == 'http://localhost/login'
- }
-}
\ No newline at end of file
diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java
new file mode 100644
index 00000000000..dfc2e7a34d9
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.config.http;
+
+import org.apache.http.HttpStatus;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.web.FilterChainProxy;
+import org.springframework.test.web.servlet.MockMvc;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ *
+ * @author Rob Winch
+ * @author Josh Cummings
+ */
+public class HttpConfigTests {
+
+ private static final String CONFIG_LOCATION_PREFIX =
+ "classpath:org/springframework/security/config/http/HttpConfigTests";
+
+ @Rule
+ public final SpringTestRule spring = new SpringTestRule();
+
+ @Autowired
+ MockMvc mvc;
+
+ @Test
+ public void getWhenUsingMinimalConfigurationThenRedirectsToLogin()
+ throws Exception {
+
+ this.spring.configLocations(this.xml("Minimal")).autowire();
+
+ this.mvc.perform(get("/"))
+ .andExpect(status().isFound())
+ .andExpect(redirectedUrl("http://localhost/login"));
+ }
+
+ @Test
+ public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter()
+ throws Exception {
+
+ this.spring.configLocations(this.xml("Minimal")).autowire();
+
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class);
+
+ proxy.doFilter(
+ request,
+ new EncodeUrlDenyingHttpServletResponseWrapper(response),
+ (req, resp) -> {});
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY);
+ assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login");
+ }
+
+ private static class EncodeUrlDenyingHttpServletResponseWrapper
+ extends HttpServletResponseWrapper {
+
+ public EncodeUrlDenyingHttpServletResponseWrapper(HttpServletResponse response) {
+ super(response);
+ }
+
+ @Override
+ public String encodeURL(String url) {
+ throw new RuntimeException("Unexpected invocation of encodeURL");
+ }
+
+ @Override
+ public String encodeRedirectURL(String url) {
+ throw new RuntimeException("Unexpected invocation of encodeURL");
+ }
+
+ @Override
+ public String encodeUrl(String url) {
+ throw new RuntimeException("Unexpected invocation of encodeURL");
+ }
+
+ @Override
+ public String encodeRedirectUrl(String url) {
+ throw new RuntimeException("Unexpected invocation of encodeURL");
+ }
+ }
+
+ private String xml(String configName) {
+ return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+ }
+}
diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml
new file mode 100644
index 00000000000..1db9eff2276
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
From 1db0782fde2e087f0f0c137675777012c32cafed Mon Sep 17 00:00:00 2001
From: Josh Cummings
Date: Tue, 15 May 2018 08:11:46 -0600
Subject: [PATCH 009/226] HttpCorsConfigTests groovy->java
Issue: gh-4939
---
.../config/http/HttpCorsConfigTests.groovy | 171 ------------------
.../config/http/HttpCorsConfigTests.java | 168 +++++++++++++++++
.../http/HttpCorsConfigTests-RequiresMvc.xml | 31 ++++
.../http/HttpCorsConfigTests-WithCors.xml | 45 +++++
...onfigTests-WithCorsConfigurationSource.xml | 39 ++++
.../HttpCorsConfigTests-WithCorsFilter.xml | 43 +++++
6 files changed, 326 insertions(+), 171 deletions(-)
delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy
create mode 100644 config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java
create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml
diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy
deleted file mode 100644
index f18f142173f..00000000000
--- a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright 2002-2016 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations under the License.
- */
-package org.springframework.security.config.http
-
-import org.springframework.beans.factory.BeanCreationException
-
-import javax.servlet.http.HttpServletResponse
-
-import org.springframework.http.*
-import org.springframework.mock.web.*
-import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint
-import org.springframework.web.bind.annotation.*
-import org.springframework.web.filter.CorsFilter
-import org.springframework.web.cors.CorsConfiguration
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource
-
-/**
- *
- * @author Rob Winch
- * @author Tim Ysewyn
- */
-class HttpCorsConfigTests extends AbstractHttpConfigTests {
- MockHttpServletRequest request
- MockHttpServletResponse response
- MockFilterChain chain
-
- def setup() {
- request = new MockHttpServletRequest(method:"GET")
- response = new MockHttpServletResponse()
- chain = new MockFilterChain()
- }
-
- def "No MVC throws meaningful error"() {
- when:
- xml.http('entry-point-ref' : 'ep') {
- 'cors'()
- 'intercept-url'(pattern:'/**', access: 'authenticated')
- }
- bean('ep', Http403ForbiddenEntryPoint)
- createAppContext()
- then:
- BeanCreationException success = thrown()
- success.message.contains("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext")
- }
-
- def "HandlerMappingIntrospector explicit"() {
- setup:
- xml.http('entry-point-ref' : 'ep') {
- 'cors'()
- 'intercept-url'(pattern:'/**', access: 'authenticated')
- }
- bean('ep', Http403ForbiddenEntryPoint)
- bean('controller', CorsController)
- xml.'mvc:annotation-driven'()
- createAppContext()
- when:
- addCors()
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'Ensure we a CORS response w/ Spring Security headers too'
- responseHeaders['Access-Control-Allow-Origin']
- responseHeaders['X-Content-Type-Options']
- when:
- setup()
- addCors(true)
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'Ensure we a CORS response w/ Spring Security headers too'
- responseHeaders['Access-Control-Allow-Origin']
- responseHeaders['X-Content-Type-Options']
- response.status == HttpServletResponse.SC_OK
- }
-
- def "CorsConfigurationSource"() {
- setup:
- xml.http('entry-point-ref' : 'ep') {
- 'cors'('configuration-source-ref':'ccs')
- 'intercept-url'(pattern:'/**', access: 'authenticated')
- }
- bean('ep', Http403ForbiddenEntryPoint)
- bean('ccs', MyCorsConfigurationSource)
- createAppContext()
- when:
- addCors()
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'Ensure we a CORS response w/ Spring Security headers too'
- responseHeaders['Access-Control-Allow-Origin']
- responseHeaders['X-Content-Type-Options']
- when:
- setup()
- addCors(true)
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'Ensure we a CORS response w/ Spring Security headers too'
- responseHeaders['Access-Control-Allow-Origin']
- responseHeaders['X-Content-Type-Options']
- response.status == HttpServletResponse.SC_OK
- }
-
- def "CorsFilter"() {
- setup:
- xml.http('entry-point-ref' : 'ep') {
- 'cors'('ref' : 'cf')
- 'intercept-url'(pattern:'/**', access: 'authenticated')
- }
- xml.'b:bean'(id: 'cf', 'class': CorsFilter.name) {
- 'b:constructor-arg'(ref: 'ccs')
- }
- bean('ep', Http403ForbiddenEntryPoint)
- bean('ccs', MyCorsConfigurationSource)
- createAppContext()
- when:
- addCors()
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'Ensure we a CORS response w/ Spring Security headers too'
- responseHeaders['Access-Control-Allow-Origin']
- responseHeaders['X-Content-Type-Options']
- when:
- setup()
- addCors(true)
- springSecurityFilterChain.doFilter(request,response,chain)
- then: 'Ensure we a CORS response w/ Spring Security headers too'
- responseHeaders['Access-Control-Allow-Origin']
- responseHeaders['X-Content-Type-Options']
- response.status == HttpServletResponse.SC_OK
- }
-
- def addCors(boolean isPreflight=false) {
- request.addHeader(HttpHeaders.ORIGIN,"https://example.com")
- if(!isPreflight) {
- return
- }
- request.method = HttpMethod.OPTIONS.name()
- request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
- }
-
- def getResponseHeaders() {
- def headers = [:]
- response.headerNames.each { name ->
- headers.put(name, response.getHeaderValues(name).join(','))
- }
- return headers
- }
-
- @RestController
- @CrossOrigin(methods = [
- RequestMethod.GET, RequestMethod.POST
- ])
- static class CorsController {
- @RequestMapping("/")
- String hello() {
- "Hello"
- }
- }
-
- static class MyCorsConfigurationSource extends UrlBasedCorsConfigurationSource {
- MyCorsConfigurationSource() {
- registerCorsConfiguration('/**', new CorsConfiguration(allowedOrigins : ['*'], allowedMethods : [
- RequestMethod.GET.name(),
- RequestMethod.POST.name()
- ]))
- }
- }
-}
diff --git a/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java
new file mode 100644
index 00000000000..52c3245a120
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.config.http;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ *
+ * @author Rob Winch
+ * @author Tim Ysewyn
+ * @author Josh Cummings
+ */
+public class HttpCorsConfigTests {
+
+ private static final String CONFIG_LOCATION_PREFIX =
+ "classpath:org/springframework/security/config/http/HttpCorsConfigTests";
+
+ @Rule
+ public final SpringTestRule spring = new SpringTestRule();
+
+ @Autowired
+ MockMvc mvc;
+
+ @Test
+ public void autowireWhenMissingMvcThenGivesInformativeError() {
+ assertThatThrownBy(() ->
+ this.spring.configLocations(this.xml("RequiresMvc")).autowire())
+ .isInstanceOf(BeanCreationException.class)
+ .hasMessageContaining("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext");
+ }
+
+ @Test
+ public void getWhenUsingCorsThenDoesSpringSecurityCorsHandshake()
+ throws Exception {
+
+ this.spring.configLocations(this.xml("WithCors")).autowire();
+
+ this.mvc.perform(get("/").with(this.approved()))
+ .andExpect(corsResponseHeaders())
+ .andExpect((status().isIAmATeapot()));
+
+ this.mvc.perform(options("/").with(this.preflight()))
+ .andExpect(corsResponseHeaders())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void getWhenUsingCustomCorsConfigurationSourceThenDoesSpringSecurityCorsHandshake()
+ throws Exception {
+
+ this.spring.configLocations(this.xml("WithCorsConfigurationSource")).autowire();
+
+ this.mvc.perform(get("/").with(this.approved()))
+ .andExpect(corsResponseHeaders())
+ .andExpect((status().isIAmATeapot()));
+
+ this.mvc.perform(options("/").with(this.preflight()))
+ .andExpect(corsResponseHeaders())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void getWhenUsingCustomCorsFilterThenDoesSPringSecurityCorsHandshake()
+ throws Exception {
+
+ this.spring.configLocations(this.xml("WithCorsFilter")).autowire();
+
+ this.mvc.perform(get("/").with(this.approved()))
+ .andExpect(corsResponseHeaders())
+ .andExpect((status().isIAmATeapot()));
+
+ this.mvc.perform(options("/").with(this.preflight()))
+ .andExpect(corsResponseHeaders())
+ .andExpect(status().isOk());
+ }
+
+ @RestController
+ @CrossOrigin(methods = {
+ RequestMethod.GET, RequestMethod.POST
+ })
+ static class CorsController {
+ @RequestMapping("/")
+ String hello() {
+ return "Hello";
+ }
+ }
+
+ static class MyCorsConfigurationSource extends UrlBasedCorsConfigurationSource {
+ MyCorsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(Arrays.asList("*"));
+ configuration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
+
+ super.registerCorsConfiguration(
+ "/**",
+ configuration);
+ }
+ }
+
+ private String xml(String configName) {
+ return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+ }
+
+ private RequestPostProcessor preflight() {
+ return cors(true);
+ }
+
+ private RequestPostProcessor approved() {
+ return cors(false);
+ }
+
+ private RequestPostProcessor cors(boolean preflight) {
+ return (request) -> {
+ request.addHeader(HttpHeaders.ORIGIN, "https://example.com");
+
+ if ( preflight ) {
+ request.setMethod(HttpMethod.OPTIONS.name());
+ request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name());
+ }
+
+ return request;
+ };
+ }
+
+ private ResultMatcher corsResponseHeaders() {
+ return result -> {
+ header().exists("Access-Control-Allow-Origin").match(result);
+ header().exists("X-Content-Type-Options").match(result);
+ };
+ }
+
+}
diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml
new file mode 100644
index 00000000000..9a66ab3a5b5
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml
new file mode 100644
index 00000000000..ac57e95b45b
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml
new file mode 100644
index 00000000000..b73f227634d
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml
new file mode 100644
index 00000000000..28daed34b1f
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From a759195e64997ce86f280fbb832e6e0975eba0aa Mon Sep 17 00:00:00 2001
From: Josh Cummings
Date: Tue, 15 May 2018 08:12:25 -0600
Subject: [PATCH 010/226] PlaceHolderAndELConfigTests groovy->java
Issue: gh-4939
---
.../http/PlaceHolderAndELConfigTests.groovy | 156 -------------
.../http/PlaceHolderAndELConfigTests.java | 212 ++++++++++++++++++
...olderAndELConfigTests-AccessDeniedPage.xml | 37 +++
...ELConfigTests-AccessDeniedPageWithSpEL.xml | 37 +++
...ELConfigTests-InterceptUrlAndFormLogin.xml | 42 ++++
...Tests-InterceptUrlAndFormLoginWithSpEL.xml | 45 ++++
...laceHolderAndELConfigTests-PortMapping.xml | 41 ++++
...HolderAndELConfigTests-RequiresChannel.xml | 36 +++
...olderAndELConfigTests-UnsecuredPattern.xml | 38 ++++
9 files changed, 488 insertions(+), 156 deletions(-)
delete mode 100644 config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy
create mode 100644 config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml
create mode 100644 config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml
diff --git a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy
deleted file mode 100644
index 13db6f17244..00000000000
--- a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy
+++ /dev/null
@@ -1,156 +0,0 @@
-package org.springframework.security.config.http
-
-import java.text.MessageFormat
-
-import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer
-import org.springframework.mock.web.MockFilterChain
-import org.springframework.mock.web.MockHttpServletRequest
-import org.springframework.mock.web.MockHttpServletResponse
-import org.springframework.security.access.SecurityConfig
-import org.springframework.security.config.BeanIds
-import org.springframework.security.util.FieldUtils
-import org.springframework.security.web.PortMapperImpl
-import org.springframework.security.web.access.ExceptionTranslationFilter
-import org.springframework.security.web.access.channel.ChannelProcessingFilter
-import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
-
-class PlaceHolderAndELConfigTests extends AbstractHttpConfigTests {
-
- def setup() {
- // Add a PropertyPlaceholderConfigurer to the context for all the tests
- bean(PropertyPlaceholderConfigurer.class.name, PropertyPlaceholderConfigurer.class)
- }
-
- def unsecuredPatternSupportsPlaceholderForPattern() {
- System.setProperty("pattern.nofilters", "/unprotected");
-
- xml.http(pattern: '${pattern.nofilters}', security: 'none')
- httpAutoConfig() {
- interceptUrl('/**', 'ROLE_A')
- }
- createAppContext()
-
- List filters = getFilters("/unprotected");
-
- expect:
- filters.size() == 0
- }
-
- // SEC-1201
- def interceptUrlsAndFormLoginSupportPropertyPlaceholders() {
- System.setProperty("secure.Url", "/secure");
- System.setProperty("secure.role", "ROLE_A");
- System.setProperty("login.page", "/loginPage");
- System.setProperty("default.target", "/defaultTarget");
- System.setProperty("auth.failure", "/authFailure");
-
- xml.http(pattern: '${login.page}', security: 'none')
- xml.http('use-expressions':false) {
- interceptUrl('${secure.Url}', '${secure.role}')
- 'form-login'('login-page':'${login.page}', 'default-target-url': '${default.target}',
- 'authentication-failure-url':'${auth.failure}');
- }
- createAppContext();
-
- expect:
- propertyValuesMatchPlaceholders()
- getFilters("/loginPage").size() == 0
- }
-
- // SEC-1309
- def interceptUrlsAndFormLoginSupportEL() {
- System.setProperty("secure.url", "/secure");
- System.setProperty("secure.role", "ROLE_A");
- System.setProperty("login.page", "/loginPage");
- System.setProperty("default.target", "/defaultTarget");
- System.setProperty("auth.failure", "/authFailure");
-
- xml.http('use-expressions':false) {
- interceptUrl("#{systemProperties['secure.url']}", "#{systemProperties['secure.role']}")
- 'form-login'('login-page':"#{systemProperties['login.page']}", 'default-target-url': "#{systemProperties['default.target']}",
- 'authentication-failure-url':"#{systemProperties['auth.failure']}");
- }
- createAppContext()
-
- expect:
- propertyValuesMatchPlaceholders()
- }
-
- private void propertyValuesMatchPlaceholders() {
- // Check the security attribute
- def fis = getFilter(FilterSecurityInterceptor);
- def fids = fis.getSecurityMetadataSource();
- Collection attrs = fids.getAttributes(createFilterinvocation("/secure", null));
- assert attrs.size() == 1
- assert attrs.contains(new SecurityConfig("ROLE_A"))
-
- // Check the form login properties are set
- def apf = getFilter(UsernamePasswordAuthenticationFilter)
- assert FieldUtils.getFieldValue(apf, "successHandler.defaultTargetUrl") == '/defaultTarget'
- assert "/authFailure" == FieldUtils.getFieldValue(apf, "failureHandler.defaultFailureUrl")
-
- def etf = getFilter(ExceptionTranslationFilter)
- assert "/loginPage"== etf.authenticationEntryPoint.loginFormUrl
- }
-
- def portMappingsWorkWithPlaceholdersAndEL() {
- System.setProperty("http", "9080");
- System.setProperty("https", "9443");
-
- httpAutoConfig {
- 'port-mappings'() {
- 'port-mapping'(http: '#{systemProperties.http}', https: '${https}')
- }
- }
- createAppContext();
-
- def pm = (appContext.getBeansOfType(PortMapperImpl).values() as List)[0];
-
- expect:
- pm.getTranslatedPortMappings().size() == 1
- pm.lookupHttpPort(9443) == 9080
- pm.lookupHttpsPort(9080) == 9443
- }
-
- def requiresChannelSupportsPlaceholder() {
- System.setProperty("secure.url", "/secure");
- System.setProperty("required.channel", "https");
-
- httpAutoConfig {
- 'intercept-url'(pattern: '${secure.url}', 'requires-channel': '${required.channel}')
- }
- createAppContext();
- List filters = getFilters("/secure");
-
- expect:
- filters.size() == AUTO_CONFIG_FILTERS + 1
- filters[0] instanceof ChannelProcessingFilter
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.setServletPath("/secure");
- MockHttpServletResponse response = new MockHttpServletResponse();
- filters[0].doFilter(request, response, new MockFilterChain());
- response.getRedirectedUrl().startsWith("https")
- }
-
- def accessDeniedPageWorksWithPlaceholders() {
- System.setProperty("accessDenied", "/go-away");
- xml.http('auto-config': 'true') {
- 'access-denied-handler'('error-page' : '${accessDenied}') {}
- }
- createAppContext();
-
- expect:
- FieldUtils.getFieldValue(getFilter(ExceptionTranslationFilter.class), "accessDeniedHandler.errorPage") == '/go-away'
- }
-
- def accessDeniedHandlerPageWorksWithEL() {
- httpAutoConfig {
- 'access-denied-handler'('error-page': "#{'/go' + '-away'}")
- }
- createAppContext()
-
- expect:
- getFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == '/go-away'
- }
-}
diff --git a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java
new file mode 100644
index 00000000000..070cf441b7f
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.config.http;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Josh Cummings
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SecurityTestExecutionListeners
+public class PlaceHolderAndELConfigTests {
+
+ private static final String CONFIG_LOCATION_PREFIX =
+ "classpath:org/springframework/security/config/http/PlaceHolderAndELConfigTests";
+
+ @Rule
+ public final SpringTestRule spring = new SpringTestRule();
+
+ @Autowired
+ MockMvc mvc;
+
+ @Test
+ public void getWhenUsingPlaceholderThenUnsecuredPatternCorrectlyConfigured()
+ throws Exception {
+
+ System.setProperty("pattern.nofilters", "/unsecured");
+
+ this.spring.configLocations(this.xml("UnsecuredPattern")).autowire();
+
+ this.mvc.perform(get("/unsecured"))
+ .andExpect(status().isOk());
+ }
+
+ /**
+ * SEC-1201
+ */
+ @Test
+ public void loginWhenUsingPlaceholderThenInterceptUrlsAndFormLoginWorks()
+ throws Exception {
+
+ System.setProperty("secure.Url", "/secured");
+ System.setProperty("secure.role", "ROLE_NUNYA");
+ System.setProperty("login.page", "/loginPage");
+ System.setProperty("default.target", "/defaultTarget");
+ System.setProperty("auth.failure", "/authFailure");
+
+ this.spring.configLocations(this.xml("InterceptUrlAndFormLogin")).autowire();
+
+ // login-page setting
+
+ this.mvc.perform(get("/secured"))
+ .andExpect(redirectedUrl("http://localhost/loginPage"));
+
+ // login-processing-url setting
+ // default-target-url setting
+
+ this.mvc.perform(post("/loginPage")
+ .param("username", "user")
+ .param("password", "password"))
+ .andExpect(redirectedUrl("/defaultTarget"));
+
+ // authentication-failure-url setting
+
+ this.mvc.perform(post("/loginPage")
+ .param("username", "user")
+ .param("password", "wrong"))
+ .andExpect(redirectedUrl("/authFailure"));
+ }
+
+ /**
+ * SEC-1309
+ */
+ @Test
+ public void loginWhenUsingSpELThenInterceptUrlsAndFormLoginWorks()
+ throws Exception {
+
+ System.setProperty("secure.url", "/secured");
+ System.setProperty("secure.role", "ROLE_NUNYA");
+ System.setProperty("login.page", "/loginPage");
+ System.setProperty("default.target", "/defaultTarget");
+ System.setProperty("auth.failure", "/authFailure");
+
+ this.spring.configLocations(
+ this.xml("InterceptUrlAndFormLoginWithSpEL")).autowire();
+
+ // login-page setting
+
+ this.mvc.perform(get("/secured"))
+ .andExpect(redirectedUrl("http://localhost/loginPage"));
+
+ // login-processing-url setting
+ // default-target-url setting
+
+ this.mvc.perform(post("/loginPage")
+ .param("username", "user")
+ .param("password", "password"))
+ .andExpect(redirectedUrl("/defaultTarget"));
+
+ // authentication-failure-url setting
+
+ this.mvc.perform(post("/loginPage")
+ .param("username", "user")
+ .param("password", "wrong"))
+ .andExpect(redirectedUrl("/authFailure"));
+
+ }
+
+ @Test
+ @WithMockUser
+ public void requestWhenUsingPlaceholderOrSpELThenPortMapperWorks()
+ throws Exception {
+
+ System.setProperty("http", "9080");
+ System.setProperty("https", "9443");
+
+ this.spring.configLocations(this.xml("PortMapping")).autowire();
+
+ this.mvc.perform(get("http://localhost:9080/secured"))
+ .andExpect(status().isFound())
+ .andExpect(redirectedUrl("https://localhost:9443/secured"));
+
+ this.mvc.perform(get("https://localhost:9443/unsecured"))
+ .andExpect(status().isFound())
+ .andExpect(redirectedUrl("http://localhost:9080/unsecured"));
+ }
+
+ @Test
+ @WithMockUser
+ public void requestWhenUsingPlaceholderThenRequiresChannelWorks()
+ throws Exception {
+
+ System.setProperty("secure.url", "/secured");
+ System.setProperty("required.channel", "https");
+
+ this.spring.configLocations(this.xml("RequiresChannel")).autowire();
+
+ this.mvc.perform(get("http://localhost/secured"))
+ .andExpect(status().isFound())
+ .andExpect(redirectedUrl("https://localhost/secured"));
+ }
+
+ @Test
+ @WithMockUser
+ public void requestWhenUsingPlaceholderThenAccessDeniedPageWorks()
+ throws Exception {
+
+ System.setProperty("accessDenied", "/go-away");
+
+ this.spring.configLocations(this.xml("AccessDeniedPage")).autowire();
+
+ this.mvc.perform(get("/secured"))
+ .andExpect(forwardedUrl("/go-away"));
+ }
+
+ @Test
+ @WithMockUser
+ public void requestWhenUsingSpELThenAccessDeniedPageWorks()
+ throws Exception {
+
+ this.spring.configLocations(this.xml("AccessDeniedPageWithSpEL")).autowire();
+
+ this.mvc.perform(get("/secured"))
+ .andExpect(forwardedUrl("/go-away"));
+ }
+
+ @RestController
+ static class SimpleController {
+ @GetMapping("/unsecured")
+ String unsecured() {
+ return "unsecured";
+ }
+
+ @GetMapping("/secured")
+ String secured() {
+ return "secured";
+ }
+ }
+
+ private String xml(String configName) {
+ return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+ }
+}
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml
new file mode 100644
index 00000000000..04f10d3d008
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml
new file mode 100644
index 00000000000..d4f67486e0d
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml
new file mode 100644
index 00000000000..be48c5b85d0
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml
new file mode 100644
index 00000000000..93e955f7561
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml
new file mode 100644
index 00000000000..c135a75b08b
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml
new file mode 100644
index 00000000000..1d9c90488d6
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml
new file mode 100644
index 00000000000..139a459c764
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 49f86bce1b4d7e70575a772fbf64b24c86ef05ab Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Tue, 15 May 2018 17:05:31 -0500
Subject: [PATCH 011/226] Update to spring-build-conventions:0.0.17.RELEASE
Fixes: gh-5352
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index 53de5b415a0..40fed3c1e7b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
buildscript {
dependencies {
- classpath 'io.spring.gradle:spring-build-conventions:0.0.15.RELEASE'
+ classpath 'io.spring.gradle:spring-build-conventions:0.0.16.RELEASE'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
}
repositories {
From 9791b8f952b83cd345e33e2c1c135b2cf70fbd96 Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Wed, 16 May 2018 12:28:31 -0500
Subject: [PATCH 012/226] Add ClientRegistration from OpenID Connect Discovery
Fixes: gh-4413
---
config/spring-security-config.gradle | 1 +
.../client/OidcConfigurationProvider.java | 118 ++++++++++
.../OidcConfigurationProviderTests.java | 209 ++++++++++++++++++
.../registration/ClientRegistration.java | 15 ++
4 files changed, 343 insertions(+)
create mode 100644 config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java
create mode 100644 config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java
diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle
index fc56abf254f..e59f23fb099 100644
--- a/config/spring-security-config.gradle
+++ b/config/spring-security-config.gradle
@@ -34,6 +34,7 @@ dependencies {
testCompile apachedsDependencies
testCompile powerMock2Dependencies
testCompile spockDependencies
+ testCompile 'com.squareup.okhttp3:mockwebserver'
testCompile 'ch.qos.logback:logback-classic'
testCompile 'io.projectreactor.ipc:reactor-netty'
testCompile 'javax.annotation:jsr250-api:1.0'
diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java
new file mode 100644
index 00000000000..77984669c56
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/oauth2/client/OidcConfigurationProvider.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.oauth2.client;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
+import org.springframework.web.client.RestTemplate;
+
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+/**
+ * Allows creating a {@link ClientRegistration.Builder} from an
+ * OpenID Provider Configuration.
+ *
+ * @author Rob Winch
+ * @since 5.1
+ */
+public final class OidcConfigurationProvider {
+
+ /**
+ * Given the Issuer creates a
+ * {@link ClientRegistration.Builder} by making an
+ * OpenID Provider
+ * Configuration Request and using the values in the
+ * OpenID
+ * Provider Configuration Response to initialize the {@link ClientRegistration.Builder}.
+ *
+ *
+ * For example if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will
+ * be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID
+ * Provider Configuration Response".
+ *
+ * @param issuer the Issuer
+ * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration.
+ */
+ public static ClientRegistration.Builder issuer(String issuer) {
+ RestTemplate rest = new RestTemplate();
+ String openidConfiguration = rest.getForObject(issuer + "/.well-known/openid-configuration", String.class);
+ OIDCProviderMetadata metadata = parse(openidConfiguration);
+ String name = URI.create(issuer).getHost();
+ List metadataAuthMethods = metadata.getTokenEndpointAuthMethods();
+ // if null, the default includes client_secret_basic
+ if (metadataAuthMethods != null && !metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+ throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC is supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods);
+ }
+ List grantTypes = metadata.getGrantTypes();
+ // If null, the default includes authorization_code
+ if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) {
+ throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes);
+ }
+ List scopes = getScopes(metadata);
+ return ClientRegistration.withRegistrationId(name)
+ .userNameAttributeName(IdTokenClaimNames.SUB)
+ .scope(scopes)
+ .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+ .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+ .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
+ .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
+ .jwkSetUri(metadata.getJWKSetURI().toASCIIString())
+ .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
+ .tokenUri(metadata.getTokenEndpointURI().toASCIIString())
+ .clientName(issuer);
+ }
+
+ private static List getScopes(OIDCProviderMetadata metadata) {
+ Scope scope = metadata.getScopes();
+ if (scope == null) {
+ // If null, default to "openid" which must be supported
+ return Arrays.asList("openid");
+ } else {
+ return scope.toStringList();
+ }
+ }
+
+ private static OIDCProviderMetadata parse(String body) {
+ try {
+ return OIDCProviderMetadata.parse(body);
+ }
+ catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private OidcConfigurationProvider() {}
+}
diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java
new file mode 100644
index 00000000000..bd967c58ed5
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/oauth2/client/OidcConfigurationProviderTests.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.oauth2.client;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * @author Rob Winch
+ * @since 5.1
+ */
+public class OidcConfigurationProviderTests {
+
+ /**
+ * Contains all optional parameters that are found in ClientRegistration
+ */
+ private static final String DEFAULT_RESPONSE =
+ "{\n"
+ + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n"
+ + " \"claims_supported\": [\n"
+ + " \"aud\", \n"
+ + " \"email\", \n"
+ + " \"email_verified\", \n"
+ + " \"exp\", \n"
+ + " \"family_name\", \n"
+ + " \"given_name\", \n"
+ + " \"iat\", \n"
+ + " \"iss\", \n"
+ + " \"locale\", \n"
+ + " \"name\", \n"
+ + " \"picture\", \n"
+ + " \"sub\"\n"
+ + " ], \n"
+ + " \"code_challenge_methods_supported\": [\n"
+ + " \"plain\", \n"
+ + " \"S256\"\n"
+ + " ], \n"
+ + " \"id_token_signing_alg_values_supported\": [\n"
+ + " \"RS256\"\n"
+ + " ], \n"
+ + " \"issuer\": \"https://example.com\", \n"
+ + " \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \n"
+ + " \"response_types_supported\": [\n"
+ + " \"code\", \n"
+ + " \"token\", \n"
+ + " \"id_token\", \n"
+ + " \"code token\", \n"
+ + " \"code id_token\", \n"
+ + " \"token id_token\", \n"
+ + " \"code token id_token\", \n"
+ + " \"none\"\n"
+ + " ], \n"
+ + " \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n"
+ + " \"scopes_supported\": [\n"
+ + " \"openid\", \n"
+ + " \"email\", \n"
+ + " \"profile\"\n"
+ + " ], \n"
+ + " \"subject_types_supported\": [\n"
+ + " \"public\"\n"
+ + " ], \n"
+ + " \"grant_types_supported\" : [\"authorization_code\"], \n"
+ + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n"
+ + " \"token_endpoint_auth_methods_supported\": [\n"
+ + " \"client_secret_post\", \n"
+ + " \"client_secret_basic\"\n"
+ + " ], \n"
+ + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n"
+ + "}";
+
+ private MockWebServer server;
+
+ private ObjectMapper mapper = new ObjectMapper();
+
+ private Map response;
+
+ private String issuer;
+
+ @Before
+ public void setup() throws Exception {
+ this.server = new MockWebServer();
+ this.server.start();
+ this.response = this.mapper.readValue(DEFAULT_RESPONSE, new TypeReference
*
*
+ * Rejects HTTP methods that are not allowed. This specified to block
+ * HTTP Verb tampering and XST attacks.
+ * See {@link #setAllowedHttpMethods(Collection)}
+ *
+ *
* Rejects URLs that are not normalized to avoid bypassing security constraints. There is
* no way to disable this as it is considered extremely risky to disable this constraint.
* A few options to allow this behavior is to normalize the request prior to the firewall
@@ -66,6 +73,11 @@
* @since 4.2.4
*/
public class StrictHttpFirewall implements HttpFirewall {
+ /**
+ * Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method should be allowed.
+ */
+ private static final Set ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());
+
private static final String ENCODED_PERCENT = "%25";
private static final String PERCENT = "%";
@@ -82,6 +94,8 @@ public class StrictHttpFirewall implements HttpFirewall {
private Set decodedUrlBlacklist = new HashSet();
+ private Set allowedHttpMethods = createDefaultAllowedHttpMethods();
+
public StrictHttpFirewall() {
urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
@@ -92,6 +106,39 @@ public StrictHttpFirewall() {
this.decodedUrlBlacklist.add(PERCENT);
}
+ /**
+ * Sets if any HTTP method is allowed. If this set to true, then no validation on the HTTP method will be performed.
+ * This can open the application up to
+ * HTTP Verb tampering and XST attacks
+ * @param unsafeAllowAnyHttpMethod if true, disables HTTP method validation, else resets back to the defaults. Default is false.
+ * @see #setAllowedHttpMethods(Collection)
+ * @since 5.1
+ */
+ public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
+ this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
+ }
+
+ /**
+ *
+ * Determines which HTTP methods should be allowed. The default is to allow "DELETE", "GET", "HEAD", "OPTIONS",
+ * "PATCH", "POST", and "PUT".
+ *
+ *
+ * @param allowedHttpMethods the case-sensitive collection of HTTP methods that are allowed.
+ * @see #setUnsafeAllowAnyHttpMethod(boolean)
+ * @since 5.1
+ */
+ public void setAllowedHttpMethods(Collection allowedHttpMethods) {
+ if (allowedHttpMethods == null) {
+ throw new IllegalArgumentException("allowedHttpMethods cannot be null");
+ }
+ if (allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
+ this.allowedHttpMethods = ALLOW_ANY_HTTP_METHOD;
+ } else {
+ this.allowedHttpMethods = new HashSet<>(allowedHttpMethods);
+ }
+ }
+
/**
*
* Determines if semicolon is allowed in the URL (i.e. matrix variables). The default
@@ -242,6 +289,7 @@ private void urlBlacklistsRemoveAll(Collection values) {
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
+ rejectForbiddenHttpMethod(request);
rejectedBlacklistedUrls(request);
if (!isNormalized(request)) {
@@ -259,6 +307,18 @@ public void reset() {
};
}
+ private void rejectForbiddenHttpMethod(HttpServletRequest request) {
+ if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
+ return;
+ }
+ if (!this.allowedHttpMethods.contains(request.getMethod())) {
+ throw new RequestRejectedException("The request was rejected because the HTTP method \"" +
+ request.getMethod() +
+ "\" was not included within the whitelist " +
+ this.allowedHttpMethods);
+ }
+ }
+
private void rejectedBlacklistedUrls(HttpServletRequest request) {
for (String forbidden : this.encodedUrlBlacklist) {
if (encodedUrlContains(request, forbidden)) {
@@ -277,6 +337,18 @@ public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new FirewalledResponse(response);
}
+ private static Set createDefaultAllowedHttpMethods() {
+ Set result = new HashSet<>();
+ result.add(HttpMethod.DELETE.name());
+ result.add(HttpMethod.GET.name());
+ result.add(HttpMethod.HEAD.name());
+ result.add(HttpMethod.OPTIONS.name());
+ result.add(HttpMethod.PATCH.name());
+ result.add(HttpMethod.POST.name());
+ result.add(HttpMethod.PUT.name());
+ return result;
+ }
+
private static boolean isNormalized(HttpServletRequest request) {
if (!isNormalized(request.getRequestURI())) {
return false;
diff --git a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java
index 576fda3f75f..62738494247 100644
--- a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java
+++ b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java
@@ -69,7 +69,7 @@ public Object answer(InvocationOnMock inv) throws Throwable {
fcp = new FilterChainProxy(new DefaultSecurityFilterChain(matcher,
Arrays.asList(filter)));
fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class));
- request = new MockHttpServletRequest();
+ request = new MockHttpServletRequest("GET", "");
request.setServletPath("/path");
response = new MockHttpServletResponse();
chain = mock(FilterChain.class);
diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java
index 5d2e57d3e78..27e2c0c3808 100644
--- a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java
+++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java
@@ -16,11 +16,17 @@
package org.springframework.security.web.firewall;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+
+import java.util.Arrays;
+import java.util.List;
+
import org.junit.Test;
+import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
-import static org.assertj.core.api.Assertions.fail;
-
/**
* @author Rob Winch
*/
@@ -31,12 +37,61 @@ public class StrictHttpFirewallTests {
private StrictHttpFirewall firewall = new StrictHttpFirewall();
- private MockHttpServletRequest request = new MockHttpServletRequest();
+ private MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
+
+ @Test
+ public void getFirewalledRequestWhenInvalidMethodThenThrowsRequestRejectedException() {
+ this.request.setMethod("INVALID");
+ assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
+ .isInstanceOf(RequestRejectedException.class);
+ }
+
+ // blocks XST attacks
+ @Test
+ public void getFirewalledRequestWhenTraceMethodThenThrowsRequestRejectedException() {
+ this.request.setMethod(HttpMethod.TRACE.name());
+ assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
+ .isInstanceOf(RequestRejectedException.class);
+ }
+
+ @Test
+ // blocks XST attack if request is forwarded to a Microsoft IIS web server
+ public void getFirewalledRequestWhenTrackMethodThenThrowsRequestRejectedException() {
+ this.request.setMethod("TRACK");
+ assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
+ .isInstanceOf(RequestRejectedException.class);
+ }
+
+ @Test
+ // HTTP methods are case sensitive
+ public void getFirewalledRequestWhenLowercaseGetThenThrowsRequestRejectedException() {
+ this.request.setMethod("get");
+ assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
+ .isInstanceOf(RequestRejectedException.class);
+ }
+
+ @Test
+ public void getFirewalledRequestWhenAllowedThenNoException() {
+ List allowedMethods = Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT");
+ for (String allowedMethod : allowedMethods) {
+ this.request = new MockHttpServletRequest(allowedMethod, "");
+ assertThatCode(() -> this.firewall.getFirewalledRequest(this.request))
+ .doesNotThrowAnyException();
+ }
+ }
+
+ @Test
+ public void getFirewalledRequestWhenInvalidMethodAndAnyMethodThenNoException() {
+ this.firewall.setUnsafeAllowAnyHttpMethod(true);
+ this.request.setMethod("INVALID");
+ assertThatCode(() -> this.firewall.getFirewalledRequest(this.request))
+ .doesNotThrowAnyException();
+ }
@Test
public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestRejectedException() throws Exception {
for (String path : this.unnormalizedPaths) {
- this.request = new MockHttpServletRequest();
+ this.request = new MockHttpServletRequest("GET", "");
this.request.setRequestURI(path);
try {
this.firewall.getFirewalledRequest(this.request);
@@ -49,7 +104,7 @@ public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestReje
@Test
public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRejectedException() throws Exception {
for (String path : this.unnormalizedPaths) {
- this.request = new MockHttpServletRequest();
+ this.request = new MockHttpServletRequest("GET", "");
this.request.setContextPath(path);
try {
this.firewall.getFirewalledRequest(this.request);
@@ -62,7 +117,7 @@ public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRej
@Test
public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRejectedException() throws Exception {
for (String path : this.unnormalizedPaths) {
- this.request = new MockHttpServletRequest();
+ this.request = new MockHttpServletRequest("GET", "");
this.request.setServletPath(path);
try {
this.firewall.getFirewalledRequest(this.request);
@@ -75,7 +130,7 @@ public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRej
@Test
public void getFirewalledRequestWhenPathInfoNotNormalizedThenThrowsRequestRejectedException() throws Exception {
for (String path : this.unnormalizedPaths) {
- this.request = new MockHttpServletRequest();
+ this.request = new MockHttpServletRequest("GET", "");
this.request.setPathInfo(path);
try {
this.firewall.getFirewalledRequest(this.request);
@@ -352,7 +407,7 @@ public void getFirewalledRequestWhenUppercaseEncodedPathThenException() {
public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowSemicolon(true);
- MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
request.setRequestURI("/context-root/a/b;%2f1/c");
request.setContextPath("/context-root");
request.setServletPath("");
@@ -365,7 +420,7 @@ public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathT
public void getFirewalledRequestWhenAllowUrlEncodedSlashAndUppercaseEncodedPathThenNoException() {
this.firewall.setAllowUrlEncodedSlash(true);
this.firewall.setAllowSemicolon(true);
- MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
request.setRequestURI("/context-root/a/b;%2F1/c");
request.setContextPath("/context-root");
request.setServletPath("");
From 2662f768a9dd85e7695da699f8a27b9b87b2009a Mon Sep 17 00:00:00 2001
From: Eric Deandrea
Date: Thu, 24 May 2018 09:31:36 -0400
Subject: [PATCH 026/226] DelegatingServerLogoutHandler
Create a ServerLogoutHandler which delegates to a group of
ServerLogoutHandler implementations.
Fixes gh-4839
---
.../logout/DelegatingServerLogoutHandler.java | 49 ++++++++++
.../DelegatingServerLogoutHandlerTests.java | 91 +++++++++++++++++++
2 files changed, 140 insertions(+)
create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java
create mode 100644 web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java
diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java
new file mode 100644
index 00000000000..f5813f4bd3c
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication.logout;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+import org.springframework.util.Assert;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Delegates to a collection of {@link ServerLogoutHandler} implementations.
+ *
+ * @author Eric Deandrea
+ * @since 5.1
+ */
+public class DelegatingServerLogoutHandler implements ServerLogoutHandler {
+ private final List delegates;
+
+ public DelegatingServerLogoutHandler(ServerLogoutHandler... delegates) {
+ Assert.notEmpty(delegates, "delegates cannot be null or empty");
+ this.delegates = Arrays.asList(delegates);
+ }
+
+ @Override
+ public Mono logout(WebFilterExchange exchange, Authentication authentication) {
+ Stream> results = this.delegates.stream().map(delegate -> delegate.logout(exchange, authentication));
+ return Mono.when(results.collect(Collectors.toList()));
+ }
+}
diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java
new file mode 100644
index 00000000000..ef1a0258ae4
--- /dev/null
+++ b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication.logout;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+
+import reactor.test.publisher.PublisherProbe;
+
+/**
+ * @author Eric Deandrea
+ * @since 5.1
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DelegatingServerLogoutHandlerTests {
+ @Mock
+ private ServerLogoutHandler delegate1;
+
+ @Mock
+ private ServerLogoutHandler delegate2;
+ private PublisherProbe delegate1Result = PublisherProbe.empty();
+ private PublisherProbe delegate2Result = PublisherProbe.empty();
+
+ @Mock
+ private WebFilterExchange exchange;
+
+ @Mock
+ private Authentication authentication;
+
+ @Before
+ public void setup() {
+ when(this.delegate1.logout(any(WebFilterExchange.class), any(Authentication.class))).thenReturn(this.delegate1Result.mono());
+ when(this.delegate2.logout(any(WebFilterExchange.class), any(Authentication.class))).thenReturn(this.delegate2Result.mono());
+ }
+
+ @Test
+ public void constructorWhenNullThenIllegalArgumentException() {
+ assertThatThrownBy(() -> new DelegatingServerLogoutHandler((ServerLogoutHandler[]) null))
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessage("delegates cannot be null or empty")
+ .hasNoCause();
+ }
+
+ @Test
+ public void constructorWhenEmptyThenIllegalArgumentException() {
+ assertThatThrownBy(() -> new DelegatingServerLogoutHandler(new ServerLogoutHandler[0]))
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessage("delegates cannot be null or empty")
+ .hasNoCause();
+ }
+
+ @Test
+ public void logoutWhenSingleThenExecuted() {
+ DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(this.delegate1);
+ handler.logout(this.exchange, this.authentication).block();
+
+ this.delegate1Result.assertWasSubscribed();
+ }
+
+ @Test
+ public void logoutWhenMultipleThenExecuted() {
+ DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(this.delegate1, this.delegate2);
+ handler.logout(this.exchange, this.authentication).block();
+
+ this.delegate1Result.assertWasSubscribed();
+ this.delegate2Result.assertWasSubscribed();
+ }
+}
From 6d98e3c59e278872287d04f5d474c38c5bf575a4 Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Thu, 24 May 2018 09:44:29 -0500
Subject: [PATCH 027/226] Add
DelegatingServerLogoutHandler(List delegates)
Issue: gh-4839
---
.../logout/DelegatingServerLogoutHandler.java | 6 ++++++
.../logout/DelegatingServerLogoutHandlerTests.java | 12 +++++++++++-
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java
index f5813f4bd3c..f809f43f17e 100644
--- a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java
+++ b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java
@@ -16,6 +16,7 @@
package org.springframework.security.web.server.authentication.logout;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -41,6 +42,11 @@ public DelegatingServerLogoutHandler(ServerLogoutHandler... delegates) {
this.delegates = Arrays.asList(delegates);
}
+ public DelegatingServerLogoutHandler(List delegates) {
+ Assert.notEmpty(delegates, "delegates cannot be null or empty");
+ this.delegates = new ArrayList<>(delegates);
+ }
+
@Override
public Mono logout(WebFilterExchange exchange, Authentication authentication) {
Stream> results = this.delegates.stream().map(delegate -> delegate.logout(exchange, authentication));
diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java
index ef1a0258ae4..4073c636ee7 100644
--- a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java
@@ -30,6 +30,8 @@
import reactor.test.publisher.PublisherProbe;
+import java.util.List;
+
/**
* @author Eric Deandrea
* @since 5.1
@@ -57,13 +59,21 @@ public void setup() {
}
@Test
- public void constructorWhenNullThenIllegalArgumentException() {
+ public void constructorWhenNullVargsThenIllegalArgumentException() {
assertThatThrownBy(() -> new DelegatingServerLogoutHandler((ServerLogoutHandler[]) null))
.isExactlyInstanceOf(IllegalArgumentException.class)
.hasMessage("delegates cannot be null or empty")
.hasNoCause();
}
+ @Test
+ public void constructorWhenNullListThenIllegalArgumentException() {
+ assertThatThrownBy(() -> new DelegatingServerLogoutHandler((List) null))
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessage("delegates cannot be null or empty")
+ .hasNoCause();
+ }
+
@Test
public void constructorWhenEmptyThenIllegalArgumentException() {
assertThatThrownBy(() -> new DelegatingServerLogoutHandler(new ServerLogoutHandler[0]))
From f2e9a5f3584b6e34e7c14a8019f421ed73f804be Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Thu, 24 May 2018 15:03:12 -0500
Subject: [PATCH 028/226] OAuth2AuthorizationRequestRedirectWebFilter handles
ClientAuthorizationRequiredException
Fixes: gh-5383
---
.../OAuth2AuthorizationRequestRedirectWebFilter.java | 1 +
...h2AuthorizationRequestRedirectWebFilterTests.java | 12 ++++++++++++
2 files changed, 13 insertions(+)
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java
index f49eb4d6df0..d6fcad1fea7 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java
@@ -136,6 +136,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
.map(ServerWebExchangeMatcher.MatchResult::getVariables)
.map(variables -> variables.get(REGISTRATION_ID_URI_VARIABLE_NAME))
.cast(String.class)
+ .onErrorResume(ClientAuthorizationRequiredException.class, e -> Mono.just(e.getClientRegistrationId()))
.flatMap(clientRegistrationId -> this.findByRegistrationId(exchange, clientRegistrationId))
.flatMap(clientRegistration -> sendRedirectForAuthorization(exchange, clientRegistration));
}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java
index 82fc51de2ae..ce839c68107 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java
@@ -21,6 +21,7 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -133,4 +134,15 @@ public void filterWhenDoesMatchThenClientRegistrationRepositoryNotSubscribed() {
});
verify(this.authzRequestRepository).saveAuthorizationRequest(any(), any());
}
+
+ @Test
+ public void filterWhenExceptionThenRedirected() {
+ FilteringWebHandler webHandler = new FilteringWebHandler(e -> Mono.error(new ClientAuthorizationRequiredException(this.github.getRegistrationId())), Arrays.asList(this.filter));
+ this.client = WebTestClient.bindToWebHandler(webHandler).build();
+ FluxExchangeResult result = this.client.get()
+ .uri("https://example.com/foo").exchange()
+ .expectStatus()
+ .is3xxRedirection()
+ .returnResult(String.class);
+ }
}
From 74d42802358d86875c817664c529979d01c5aa32 Mon Sep 17 00:00:00 2001
From: Rob Winch
Date: Fri, 25 May 2018 09:25:26 -0500
Subject: [PATCH 029/226] Add OAuth2AuthorizedClientExchangeFilterFunction
Fixes: gh-5386
---
...uthorizedClientExchangeFilterFunction.java | 84 +++++++++++++++
.../function/client/MockExchangeFunction.java | 47 ++++++++
...izedClientExchangeFilterFunctionTests.java | 101 ++++++++++++++++++
3 files changed, 232 insertions(+)
create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java
create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java
create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunctionTests.java
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java
new file mode 100644
index 00000000000..178868c39c1
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2AuthorizedClientExchangeFilterFunction.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.client.web.reactive.function.client;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ClientResponse;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the
+ * token as a Bearer Token.
+ *
+ * @author Rob Winch
+ * @since 5.1
+ */
+public final class OAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
+ /**
+ * The request attribute name used to locate the {@link OAuth2AuthorizedClient}.
+ */
+ private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName();
+
+ /**
+ * Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for
+ * providing the Bearer Token. Example usage:
+ *
+ *