Compare commits
44 Commits
main
...
5.1.2.RELEASE
| Author | SHA1 | Date | |
|---|---|---|---|
| 55dd732443 | |||
| 45cbdc03ba | |||
| d99c7dbfd1 | |||
| d8de4d06f7 | |||
| be2f0f0fae | |||
| 920d5ca6ad | |||
| 6dcf5a27b1 | |||
| c2166c4207 | |||
| 1542835268 | |||
| ae471e1456 | |||
| 853c54eecb | |||
| 10f6d10f81 | |||
| 02d844d528 | |||
| c0849ba891 | |||
| db145fa03f | |||
| 42b8b794a8 | |||
| 8014114225 | |||
| a742c0c3f2 | |||
| 1cec4fe3ac | |||
| 8655caa2de | |||
| dc5f5f348d | |||
| 35e05780ea | |||
| a461abcf32 | |||
| ba1f5f3873 | |||
| 5478b74116 | |||
| dc4aa3d017 | |||
| 2bc156ed19 | |||
| 0270994340 | |||
| a557a324c0 | |||
| 73022059d4 | |||
| 165cbcb723 | |||
| d99ecc2a7b | |||
| ccd2664e6c | |||
| b87ff3c00f | |||
| 8971777908 | |||
| 3d618d78ac | |||
| 697b8cd734 | |||
| cf03faf58e | |||
| 13159e9f88 | |||
| 41b7c74928 | |||
| 0a4ac4dbfc | |||
| 1eb56f46a5 | |||
| cfc3c25304 | |||
| 03a3a3b770 |
+1
-1
@@ -47,7 +47,7 @@ Create your topic branch to be submitted as a pull request from master. The Spri
|
||||
# Use short branch names
|
||||
Branches used when submitting pull requests should preferably be named according to GitHub issues, e.g. 'gh-1234' or 'gh-1234-fix-npe'. Otherwise, use succinct, lower-case, dash (-) delimited names, such as 'fix-warnings', 'fix-typo', etc. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible.
|
||||
|
||||
#Keep commits focused
|
||||
# Keep commits focused
|
||||
|
||||
Remember each ticket should be focused on a single item of interest since the tickets are used to produce the changelog. Since each commit should be tied to a single GitHub issue, ensure that your commits are focused. For example, do not include an update to a transitive library in your commit unless the GitHub is to update the library. Reviewing your commits is essential before sending a pull request.
|
||||
|
||||
|
||||
Vendored
+34
-1
@@ -30,7 +30,7 @@ try {
|
||||
checkout scm
|
||||
withCredentials([string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')]) {
|
||||
try {
|
||||
sh "./gradlew clean sonarqube -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
sh "./gradlew clean jacocoTestReport sonarqube -Dsonar.jacoco.reportPaths='**/build/jacoco/*.exec' -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: sonar'
|
||||
throw e
|
||||
@@ -66,6 +66,36 @@ try {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk10: {
|
||||
stage('JDK 10') {
|
||||
node {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk10' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk10'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk11: {
|
||||
stage('JDK 11') {
|
||||
node {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk11' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk11'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(currentBuild.result == 'SUCCESS') {
|
||||
@@ -106,6 +136,9 @@ try {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: deploys'
|
||||
throw e
|
||||
} finally {
|
||||
def buildStatus = currentBuild.result
|
||||
def buildNotSuccess = !SUCCESS.equals(buildStatus)
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.18.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.22.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
}
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/plugins-snapshot' }
|
||||
maven { url 'https://plugins.gradle.org/m2/' }
|
||||
}
|
||||
}
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
|
||||
+1
@@ -200,6 +200,7 @@ public abstract class WebSecurityConfigurerAdapter implements
|
||||
|
||||
AuthenticationManager authenticationManager = authenticationManager();
|
||||
authenticationBuilder.parentAuthenticationManager(authenticationManager);
|
||||
authenticationBuilder.authenticationEventPublisher(eventPublisher);
|
||||
Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects();
|
||||
|
||||
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
|
||||
|
||||
+7
@@ -25,6 +25,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
@@ -87,6 +88,12 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public H disable() {
|
||||
getBuilder().setSharedObject(RequestCache.class, new NullRequestCache());
|
||||
return super.disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(H http) throws Exception {
|
||||
http.setSharedObject(RequestCache.class, getRequestCache(http));
|
||||
|
||||
+3
-3
@@ -183,9 +183,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
if ( this.jwtConfigurer == null ) {
|
||||
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
|
||||
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
|
||||
"a jwk set uri by doing http.oauth2().resourceServer().jwt().jwkSetUri(uri), or wire a " +
|
||||
"JwtDecoder instance by doing http.oauth2().resourceServer().jwt().decoder(decoder), or " +
|
||||
"expose a JwtDecoder instance as a bean and do http.oauth2().resourceServer().jwt().");
|
||||
"a jwk set uri by doing http.oauth2ResourceServer().jwt().jwkSetUri(uri), or wire a " +
|
||||
"JwtDecoder instance by doing http.oauth2ResourceServer().jwt().decoder(decoder), or " +
|
||||
"expose a JwtDecoder instance as a bean and do http.oauth2ResourceServer().jwt().");
|
||||
}
|
||||
|
||||
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
|
||||
|
||||
+1
-1
@@ -748,7 +748,7 @@ csrf-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
|
||||
attribute request-matcher-ref { xsd:token }?
|
||||
csrf-options.attlist &=
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
|
||||
attribute token-repository-ref { xsd:token }?
|
||||
|
||||
headers =
|
||||
|
||||
+2
-1
@@ -2337,7 +2337,8 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="token-repository-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by
|
||||
LazyCsrfTokenRepository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
|
||||
+1
-1
@@ -738,7 +738,7 @@ csrf-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
|
||||
attribute request-matcher-ref { xsd:token }?
|
||||
csrf-options.attlist &=
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
|
||||
attribute token-repository-ref { xsd:token }?
|
||||
|
||||
headers =
|
||||
|
||||
+2
-1
@@ -2232,7 +2232,8 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="token-repository-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by
|
||||
LazyCsrfTokenRepository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
|
||||
+1
-1
@@ -738,7 +738,7 @@ csrf-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
|
||||
attribute request-matcher-ref { xsd:token }?
|
||||
csrf-options.attlist &=
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
|
||||
attribute token-repository-ref { xsd:token }?
|
||||
|
||||
headers =
|
||||
|
||||
+2
-1
@@ -2232,7 +2232,8 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="token-repository-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by
|
||||
LazyCsrfTokenRepository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.annotation.web.configurers
|
||||
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser
|
||||
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.config.annotation.AnyObjectPostProcessor
|
||||
import org.springframework.security.config.annotation.BaseSpringSpec
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.web.savedrequest.RequestCache
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
|
||||
|
||||
import spock.lang.Unroll;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class RequestCacheConfigurerTests extends BaseSpringSpec {
|
||||
|
||||
def "requestCache ObjectPostProcessor"() {
|
||||
setup:
|
||||
AnyObjectPostProcessor opp = Mock()
|
||||
HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:])
|
||||
when:
|
||||
http
|
||||
.requestCache()
|
||||
.and()
|
||||
.build()
|
||||
|
||||
then: "RequestCacheAwareFilter is registered with LifecycleManager"
|
||||
1 * opp.postProcess(_ as RequestCacheAwareFilter) >> {RequestCacheAwareFilter o -> o}
|
||||
}
|
||||
|
||||
def "invoke requestCache twice does not reset"() {
|
||||
setup:
|
||||
RequestCache RC = Mock()
|
||||
AnyObjectPostProcessor opp = Mock()
|
||||
HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:])
|
||||
when:
|
||||
http
|
||||
.requestCache()
|
||||
.requestCache(RC)
|
||||
.and()
|
||||
.requestCache()
|
||||
|
||||
then:
|
||||
http.getSharedObject(RequestCache) == RC
|
||||
}
|
||||
|
||||
def "RequestCache disables faviocon.ico"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.servletPath = "/favicon.ico"
|
||||
request.requestURI = "/favicon.ico"
|
||||
request.method = "GET"
|
||||
when: "request favicon.ico"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was favicon.ico"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "RequestCache disables faviocon.png"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.servletPath = "/favicon.png"
|
||||
request.requestURI = "/favicon.png"
|
||||
request.method = "GET"
|
||||
when: "request favicon.ico"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was favicon.ico"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "SEC-2321: RequestCache disables application/json"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request application/json"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was application/json. This is desirable since JSON requests are typically not invoked directly from the browser and we don't want the browser to replay them"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "SEC-2321: RequestCache disables X-Requested-With"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request X-Requested-With"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was X-Requested-With"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "RequestCache saves #headerName: #headerValue"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader(headerName, headerValue)
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request content type"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to saved URL"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/messages"
|
||||
where:
|
||||
headerName << ["Accept", "Accept", "Accept", "X-Requested-With"]
|
||||
headerValue << [MediaType.ALL_VALUE, MediaType.TEXT_HTML, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","com.android"]
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDefautlsConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth
|
||||
.inMemoryAuthentication()
|
||||
.withUser(PasswordEncodedUser.user());
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-4
@@ -15,23 +15,31 @@
|
||||
*/
|
||||
package org.springframework.security.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.powermock.api.mockito.PowerMockito.*;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.security.config.util.InMemoryXmlApplicationContext;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.powermock.api.mockito.PowerMockito.doThrow;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyZeroInteractions;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Luke Taylor
|
||||
@@ -40,6 +48,7 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ ClassUtils.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class SecurityNamespaceHandlerTests {
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
+7
-3
@@ -15,16 +15,15 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.powermock.api.mockito.PowerMockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -33,6 +32,10 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
@@ -40,6 +43,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ SpringFactoriesLoader.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class WebSecurityConfigurerAdapterPowermockTests {
|
||||
ConfigurableWebApplicationContext context;
|
||||
|
||||
|
||||
+11
-8
@@ -15,12 +15,22 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
@@ -48,14 +58,6 @@ import org.springframework.web.context.request.async.WebAsyncManager;
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -75,6 +77,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*/
|
||||
@PrepareForTest({WebAsyncManager.class})
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*", "javax.xml.transform.*" })
|
||||
public class WebSecurityConfigurerAdapterTests {
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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.annotation.web.configurers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
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.redirectedUrl;
|
||||
|
||||
/**
|
||||
* Tests for {@link RequestCacheConfigurer}
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class RequestCacheConfigurerTests {
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
public void configureWhenRegisteringObjectPostProcessorThenInvokedOnExceptionTranslationFilter() {
|
||||
this.spring.register(ObjectPostProcessorConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
verify(ObjectPostProcessorConfig.objectPostProcessor)
|
||||
.postProcess(any(RequestCacheAwareFilter.class));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
|
||||
static ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestCache();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
static ObjectPostProcessor<Object> objectPostProcessor() {
|
||||
return objectPostProcessor;
|
||||
}
|
||||
}
|
||||
|
||||
static class ReflectingObjectPostProcessor implements ObjectPostProcessor<Object> {
|
||||
@Override
|
||||
public <O> O postProcess(O object) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenInvokingExceptionHandlingTwiceThenOriginalEntryPointUsed() throws Exception {
|
||||
this.spring.register(InvokeTwiceDoesNotOverrideConfig.class).autowire();
|
||||
|
||||
this.mvc.perform(get("/"));
|
||||
|
||||
verify(InvokeTwiceDoesNotOverrideConfig.requestCache)
|
||||
.getMatchingRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class InvokeTwiceDoesNotOverrideConfig extends WebSecurityConfigurerAdapter {
|
||||
static RequestCache requestCache = mock(RequestCache.class);
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestCache()
|
||||
.requestCache(requestCache)
|
||||
.and()
|
||||
.requestCache();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedUrlIsFaviconIcoThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/favicon.ico"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores favicon.ico
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedUrlIsFaviconPngThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/favicon.png"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores favicon.png
|
||||
}
|
||||
|
||||
// SEC-2321
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores application/json
|
||||
|
||||
// This is desirable since JSON requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
|
||||
// SEC-2321
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsXRequestedWithThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header("X-Requested-With", "XMLHttpRequest"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/"));
|
||||
|
||||
// This is desirable since XHR requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.ALL))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsTextHtmlThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsChromeThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsRequestedWithAndroidThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header("X-Requested-With", "com.android"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDefaultsConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin();
|
||||
}
|
||||
}
|
||||
|
||||
// gh-6102
|
||||
@Test
|
||||
public void getWhenRequestCacheIsDisabledThenExceptionTranslationFilterDoesNotStoreRequest() throws Exception {
|
||||
this.spring.register(RequestCacheDisabledConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/bob"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/"));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
super.configure(http);
|
||||
http.requestCache().disable();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class DefaultSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public InMemoryUserDetailsManager userDetailsManager() {
|
||||
return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static RequestBuilder formLogin(MockHttpSession session) {
|
||||
return post("/login")
|
||||
.param("username", "user")
|
||||
.param("password", "password")
|
||||
.session(session)
|
||||
.with(csrf());
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -15,15 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -32,8 +24,10 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
@@ -52,12 +46,20 @@ import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ ReflectionUtils.class, Method.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class SessionManagementConfigurerServlet31Tests {
|
||||
@Mock
|
||||
Method method;
|
||||
|
||||
+34
-1
@@ -21,6 +21,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.PropertyAccessorFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -28,6 +29,7 @@ import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
@@ -156,6 +158,30 @@ public class OAuth2LoginConfigurerTests {
|
||||
.isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER");
|
||||
}
|
||||
|
||||
// gh-6009
|
||||
@Test
|
||||
public void oauth2LoginWhenSuccessThenAuthenticationSuccessEventPublished() throws Exception {
|
||||
// setup application context
|
||||
loadConfig(OAuth2LoginConfig.class);
|
||||
|
||||
// setup authorization request
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest();
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(
|
||||
authorizationRequest, this.request, this.response);
|
||||
|
||||
// setup authentication parameters
|
||||
this.request.setParameter("code", "code123");
|
||||
this.request.setParameter("state", authorizationRequest.getState());
|
||||
|
||||
// perform test
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
|
||||
|
||||
// assertions
|
||||
assertThat(OAuth2LoginConfig.EVENTS).isNotEmpty();
|
||||
assertThat(OAuth2LoginConfig.EVENTS).hasSize(1);
|
||||
assertThat(OAuth2LoginConfig.EVENTS.get(0)).isInstanceOf(AuthenticationSuccessEvent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oauth2LoginWhenAuthenticatedThenIgnored() throws Exception {
|
||||
// setup application context
|
||||
@@ -467,7 +493,9 @@ public class OAuth2LoginConfigurerTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginConfig extends CommonWebSecurityConfigurerAdapter {
|
||||
static class OAuth2LoginConfig extends CommonWebSecurityConfigurerAdapter implements ApplicationListener<AuthenticationSuccessEvent> {
|
||||
static List<AuthenticationSuccessEvent> EVENTS = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
@@ -476,6 +504,11 @@ public class OAuth2LoginConfigurerTests {
|
||||
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION));
|
||||
super.configure(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(AuthenticationSuccessEvent event) {
|
||||
EVENTS.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
|
||||
+10
-10
@@ -15,15 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.config.http;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -32,14 +24,14 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
|
||||
import org.springframework.security.config.util.InMemoryXmlApplicationContext;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
@@ -47,12 +39,20 @@ import org.springframework.security.web.context.HttpRequestResponseHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ ReflectionUtils.class, Method.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class SessionManagementConfigServlet31Tests {
|
||||
private static final String XML_AUTHENTICATION_MANAGER = "<authentication-manager>"
|
||||
+ " <authentication-provider>" + " <user-service>"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
* 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.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -158,6 +157,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
Class<? extends Authentication> toTest = authentication.getClass();
|
||||
AuthenticationException lastException = null;
|
||||
Authentication result = null;
|
||||
Authentication parentResult = null;
|
||||
boolean debug = logger.isDebugEnabled();
|
||||
|
||||
for (AuthenticationProvider provider : getProviders()) {
|
||||
@@ -196,7 +196,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
if (result == null && parent != null) {
|
||||
// Allow the parent to try.
|
||||
try {
|
||||
result = parent.authenticate(authentication);
|
||||
result = parentResult = parent.authenticate(authentication);
|
||||
}
|
||||
catch (ProviderNotFoundException e) {
|
||||
// ignore as we will throw below if no other exception occurred prior to
|
||||
@@ -217,7 +217,11 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
((CredentialsContainer) result).eraseCredentials();
|
||||
}
|
||||
|
||||
eventPublisher.publishAuthenticationSuccess(result);
|
||||
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
|
||||
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
|
||||
if (parentResult == null) {
|
||||
eventPublisher.publishAuthenticationSuccess(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ public class InMemoryUserDetailsManager implements UserDetailsManager,
|
||||
@Override
|
||||
public UserDetails updatePassword(UserDetails user, String newPassword) {
|
||||
String username = user.getUsername();
|
||||
MutableUserDetails mutableUser = this.users.get(username);
|
||||
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
|
||||
mutableUser.setPassword(newPassword);
|
||||
return mutableUser;
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsMa
|
||||
// GroupManager SQL
|
||||
public static final String DEF_FIND_GROUPS_SQL = "select group_name from groups";
|
||||
public static final String DEF_FIND_USERS_IN_GROUP_SQL = "select username from group_members gm, groups g "
|
||||
+ "where gm.group_id = g.id" + " and g.group_name = ?";
|
||||
+ "where gm.group_id = g.id and g.group_name = ?";
|
||||
public static final String DEF_INSERT_GROUP_SQL = "insert into groups (group_name) values (?)";
|
||||
public static final String DEF_FIND_GROUP_ID_SQL = "select id from groups where group_name = ?";
|
||||
public static final String DEF_INSERT_GROUP_AUTHORITY_SQL = "insert into group_authorities (group_id, authority) values (?,?)";
|
||||
|
||||
+12
@@ -18,6 +18,7 @@ package org.springframework.security.provisioning;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
@@ -37,4 +38,15 @@ public class InMemoryUserDetailsManagerTests {
|
||||
this.manager.updatePassword(this.user, newPassword);
|
||||
assertThat(this.manager.loadUserByUsername(this.user.getUsername()).getPassword()).isEqualTo(newPassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changePasswordWhenUsernameIsNotInLowercase() {
|
||||
UserDetails userNotLowerCase = User.withUserDetails(PasswordEncodedUser.user())
|
||||
.username("User")
|
||||
.build();
|
||||
|
||||
String newPassword = "newPassword";
|
||||
this.manager.updatePassword(userNotLowerCase, newPassword);
|
||||
assertThat(this.manager.loadUserByUsername(userNotLowerCase.getUsername()).getPassword()).isEqualTo(newPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ In order to use Spring Security you must add the necessary dependencies. For the
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity4</artifactId> <1>
|
||||
<artifactId>thymeleaf-extras-springsecurity5</artifactId> <1>
|
||||
<version>2.1.2.RELEASE</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -32,7 +32,7 @@ Now that we have authenticated, let's update the application to display the user
|
||||
[source,html]
|
||||
----
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>Hello Spring Security</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -49,7 +49,7 @@ If you wish to override the Spring Security version, you may do so by providing
|
||||
----
|
||||
<properties>
|
||||
<!-- ... -->
|
||||
<spring-security.version>{spring-security-version}</spring.security.version>
|
||||
<spring-security.version>{spring-security-version}</spring-security.version>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
[[webflux-oauth2-resource-server]]
|
||||
= OAuth2 Resource Server
|
||||
|
||||
Spring Security provides OAuth2 Resource Server support with JWT tokens.
|
||||
Spring Security supports protecting endpoints using https://tools.ietf.org/html/rfc7519[JWT]-encoded OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens].
|
||||
|
||||
This is handy in circumstances where an application has federated its authority management out to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
|
||||
This authorization server can be consulted by Resource Servers to validate authority when serving requests.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver-webflux[*OAuth 2.0 Resource Server WebFlux sample*].
|
||||
====
|
||||
|
||||
The first step is to expose a `ReactiveJwtDecoder` as a `@Bean`.
|
||||
In a Spring Boot application this can be done using:
|
||||
== Dependencies
|
||||
|
||||
Most Resource Server support is collected into `spring-security-oauth2-resource-server`.
|
||||
However, the support for decoding and verifying JWTs is in `spring-security-oauth2-jose`, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens.
|
||||
|
||||
[[webflux-oauth2-resource-server-minimal-configuration]]
|
||||
== Minimal Configuration
|
||||
|
||||
When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server consists of two basic steps.
|
||||
First, include the needed dependencies and second, indicate the location of the authorization server.
|
||||
|
||||
=== Specify the Authorization Server
|
||||
|
||||
In a Spring Boot application, to specify which authorization server to use, simply do:
|
||||
|
||||
[source,yml]
|
||||
----
|
||||
@@ -19,15 +33,107 @@ spring:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: https://idp.example.com/auth/realms/demo
|
||||
issuer-uri: https://idp.example.com
|
||||
----
|
||||
|
||||
The `issuer-uri` instructs Spring Security to leverage the endpoint at `https://idp.example.com/auth/realms/demo/.well-known/openid-configuration` to discover the configuration.
|
||||
The above is all that is necessary to get a minimal Resource Server configured.
|
||||
When new keys are made available, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
Where `https://idp.example.com` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
|
||||
|
||||
By default each scope is mapped to an authority with the prefix `SCOPE_`.
|
||||
For example, the following requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
[NOTE]
|
||||
To use the `issuer-uri` property, it must also be true that `https://idp.example.com/.well-known/openid-configuration` is a supported endpoint for the authorization server.
|
||||
This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint.
|
||||
|
||||
And that's it!
|
||||
|
||||
=== Startup Expectations
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
|
||||
1. Hit the Provider Configuration endpoint, `https://the.issuer.location/.well-known/openid-configuration`, processing the response for the `jwks_url` property
|
||||
2. Configure the validation strategy to query `jwks_url` for valid public keys
|
||||
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
|
||||
|
||||
[NOTE]
|
||||
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
|
||||
|
||||
=== Runtime Expectations
|
||||
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
|
||||
|
||||
[source,html]
|
||||
----
|
||||
GET / HTTP/1.1
|
||||
Authorization: Bearer some-token-value # Resource Server will process this
|
||||
----
|
||||
|
||||
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
|
||||
|
||||
Given a well-formed JWT token, Resource Server will:
|
||||
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
|
||||
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
[NOTE]
|
||||
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
|
||||
|
||||
<<webflux-oauth2-resource-server-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
|
||||
|
||||
<<webflux-oauth2-resource-server-sans-boot,How to Configure without Spring Boot>>
|
||||
|
||||
[[webflux-oauth2-resource-server-jwkseturi]]
|
||||
=== Specifying the Authorization Server JWK Set Uri Directly
|
||||
|
||||
If the authorization server doesn't support the Provider Configuration endpoint, or if Resource Server must be able to start up independently from the authorization server, then `issuer-uri` can be exchanged for `jwk-set-uri`:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
The JWK Set uri is not standardized, but can typically be found in the authorization server's documentation
|
||||
|
||||
Consequently, Resource Server will not ping the authorization server at startup.
|
||||
However, it will also no longer validate the `iss` claim in the JWT (since Resource Server no longer knows what the issuer value should be).
|
||||
|
||||
[NOTE]
|
||||
This property can also be supplied directly on the <<webflux-oauth2-resource-server-jwkseturi-dsl,DSL>>.
|
||||
|
||||
[[webflux-oauth2-resource-server-sans-boot]]
|
||||
=== Overriding or Replacing Boot Auto Configuration
|
||||
|
||||
There are two `@Bean` s that Spring Boot generates on Resource Server's behalf.
|
||||
|
||||
The first is a `SecurityWebFilterChain` that configures the app as a resource server:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
Replacing this is as simple as exposing the bean within the application:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -44,4 +150,247 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
|
||||
|
||||
For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
|
||||
}
|
||||
----
|
||||
|
||||
If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`.
|
||||
|
||||
[[webflux-oauth2-resource-server-jwkseturi-dsl]]
|
||||
==== Using `jwkSetUri()`
|
||||
|
||||
An authorization server's JWK Set Uri can be configured <<webflux-oauth2-resource-server-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
Using `jwkSetUri()` takes precedence over any configuration property.
|
||||
|
||||
[[webflux-oauth2-resource-server-decoder-dsl]]
|
||||
==== Using `decoder()`
|
||||
|
||||
More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.decoder(myCustomDecoder());
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
This is handy when deeper configuration, like <<webflux-oauth2-resource-server-validation,validation>>, is necessary.
|
||||
|
||||
[[webflux-oauth2-resource-server-decoder-bean]]
|
||||
==== Exposing a `ReactiveJwtDecoder` `@Bean`
|
||||
|
||||
Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return new NimbusReactiveJwtDecoder(jwkSetUri);
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-authorization]]
|
||||
=== Configuring Authorization
|
||||
|
||||
A JWT that is issued from an OAuth 2.0 Authorization Server will typically either have a `scope` or `scp` attribute, indicating the scopes (or authorities) it's been granted, for example:
|
||||
|
||||
`{ ..., "scope" : "messages contacts"}`
|
||||
|
||||
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
|
||||
|
||||
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
|
||||
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
Or similarly with method security:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public List<Message> getMessages(...) {}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-authorization-extraction]]
|
||||
==== Extracting Authorities Manually
|
||||
|
||||
However, there are a number of circumstances where this default is insufficient.
|
||||
For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute.
|
||||
Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
|
||||
|
||||
To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
|
||||
GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor();
|
||||
return new ReactiveJwtAuthenticationConverterAdapter(extractor);
|
||||
}
|
||||
----
|
||||
|
||||
which is responsible for converting a `Jwt` into an `Authentication`.
|
||||
|
||||
We can override this quite simply to alter the way granted authorities are derived:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {
|
||||
protected Collection<GrantedAuthorities> extractAuthorities(Jwt jwt) {
|
||||
Collection<String> authorities = (Collection<String>)
|
||||
jwt.getClaims().get("mycustomclaim");
|
||||
|
||||
return authorities.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, Mono<AbstractAuthenticationToken>>`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
||||
public AbstractAuthenticationToken convert(Jwt jwt) {
|
||||
return Mono.just(jwt).map(this::doConversion);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-validation]]
|
||||
=== Configuring Validation
|
||||
|
||||
Using <<webflux-oauth2-resource-server-minimal-configuration,minimal Spring Boot configuration>>, indicating the authorization server's issuer uri, Resource Server will default to verifying the `iss` claim as well as the `exp` and `nbf` timestamp claims.
|
||||
|
||||
In circumstances where validation needs to be customized, Resource Server ships with two standard validators and also accepts custom `OAuth2TokenValidator` instances.
|
||||
|
||||
[[webflux-oauth2-resource-server-validation-clockskew]]
|
||||
==== Customizing Timestamp Validation
|
||||
|
||||
JWT's typically have a window of validity, with the start of the window indicated in the `nbf` claim and the end indicated in the `exp` claim.
|
||||
|
||||
However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another.
|
||||
This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system.
|
||||
|
||||
Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
|
||||
ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri);
|
||||
|
||||
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtTimestampValidator(Duration.ofSeconds(60)),
|
||||
new IssuerValidator(issuerUri));
|
||||
|
||||
jwtDecoder.setJwtValidator(withClockSkew);
|
||||
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 30 seconds.
|
||||
|
||||
[[webflux-oauth2-resource-server-validation-custom]]
|
||||
==== Configuring a Custom Validator
|
||||
|
||||
Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
|
||||
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
if (jwt.getAudience().contains("messaging")) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
} else {
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Then, to add into a resource server, it's a matter of specifying the `ReactiveJwtDecoder` instance:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
|
||||
ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri);
|
||||
|
||||
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
|
||||
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
|
||||
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
|
||||
|
||||
jwtDecoder.setJwtValidator(withAudience);
|
||||
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Spring Security has added Jackson Support for persisting Spring Security related classes.
|
||||
This can improve the performance of serializing Spring Security related classes when working with distributed sessions (i.e. session replication, Spring Session, etc).
|
||||
|
||||
To use it, register the `JacksonJacksonModules.getModules(ClassLoader)` as http://wiki.fasterxml.com/JacksonFeatureModules[Jackson Modules].
|
||||
To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` as http://wiki.fasterxml.com/JacksonFeatureModules[Jackson Modules].
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@@ -397,6 +397,11 @@ Spring Security supports protecting endpoints using https://tools.ietf.org/html/
|
||||
This is handy in circumstances where an application has federated its authority management out to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
|
||||
This authorization server can be consulted by Resource Servers to validate authority when serving requests.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver[*OAuth 2.0 Resource Server Servlet sample*].
|
||||
====
|
||||
|
||||
=== Dependencies
|
||||
|
||||
Most Resource Server support is collected into `spring-security-oauth2-resource-server`.
|
||||
@@ -417,25 +422,27 @@ security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: https://the.issuer.location
|
||||
issuer-uri: https://idp.example.com
|
||||
```
|
||||
|
||||
Where `https://the.issuer.location` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure and subsequently validate incoming JWTs.
|
||||
Where `https://idp.example.com` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
|
||||
|
||||
[NOTE]
|
||||
To use the `issuer-uri` property, it must also be true that `https://the.issuer.location/.well-known/openid-configuration` is a supported endpoint for the authorization server.
|
||||
To use the `issuer-uri` property, it must also be true that `https://idp.example.com/.well-known/openid-configuration` is a supported endpoint for the authorization server.
|
||||
This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint.
|
||||
|
||||
And that's it!
|
||||
|
||||
==== Startup Expectations
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
|
||||
1. Hit the Provider Configuration endpoint, `https://the.issuer.location/.well-known/openid-configuration`, processing the response for the `jwks_url` property
|
||||
1. Hit the Provider Configuration endpoint, `https://idp.example.com/.well-known/openid-configuration`, processing the response for the `jwks_url` property
|
||||
2. Configure the validation strategy to query `jwks_url` for valid public keys
|
||||
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://the.issuer.location`.
|
||||
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
|
||||
|
||||
@@ -444,7 +451,7 @@ If the authorization server is down when Resource Server queries it (given appro
|
||||
|
||||
==== Runtime Expectations
|
||||
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorizatization: Bearer` header:
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
|
||||
|
||||
```http
|
||||
GET / HTTP/1.1
|
||||
@@ -453,10 +460,16 @@ Authorization: Bearer some-token-value # Resource Server will process this
|
||||
|
||||
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
|
||||
|
||||
Given a well-formed JWT token, Resource Server will validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim.
|
||||
It will also validate the signature against a public key obtained from the `jwks_url` endpoint and matched against the JWTs header.
|
||||
Given a well-formed JWT token, Resource Server will
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` map's to the JWT's `sub` property, if one is present.
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
|
||||
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
[NOTE]
|
||||
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
|
||||
|
||||
From here, consider jumping to:
|
||||
|
||||
@@ -474,7 +487,7 @@ security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jwk-set-uri: https://the.issuer.location/.well-known/jwks.json
|
||||
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
|
||||
```
|
||||
|
||||
[NOTE]
|
||||
@@ -514,7 +527,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter
|
||||
protected void configure(HttpSecurity http) {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.mvcMatchers("/admin/**").hasAuthority("SCOPE_admin")
|
||||
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
@@ -524,6 +537,8 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter
|
||||
}
|
||||
```
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
|
||||
|
||||
For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
|
||||
@@ -554,7 +569,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwkSetUri("https://the.issuer.location/.well-known/jwks.json");
|
||||
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -581,7 +596,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
```
|
||||
|
||||
This is handy when deeper configuration, like <<oauth2resourceserver-validator,validation>>, <<oauth2resourceserver-claimsetmapping,mapping>>, or <<oauth2resourceserver-timeouts,request timeouts>>, is necessary.
|
||||
This is handy when deeper configuration, like <<oauth2resourceserver-validation,validation>>, <<oauth2resourceserver-claimsetmapping,mapping>>, or <<oauth2resourceserver-timeouts,request timeouts>>, is necessary.
|
||||
|
||||
[[oauth2resourceserver-decoder-bean]]
|
||||
==== Exposing a `JwtDecoder` `@Bean`
|
||||
@@ -602,7 +617,7 @@ A JWT that is issued from an OAuth 2.0 Authorization Server will typically eithe
|
||||
|
||||
`{ ..., "scope" : "messages contacts"}`
|
||||
|
||||
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the prefix "SCOPE_".
|
||||
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
|
||||
|
||||
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
|
||||
|
||||
@@ -633,7 +648,7 @@ public List<Message> getMessages(...) {}
|
||||
==== Extracting Authorities Manually
|
||||
|
||||
However, there are a number of circumstances where this default is insufficient.
|
||||
For example, some authorization server's don't use the `scope` attribute, but instead have their own custom attribute.
|
||||
For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute.
|
||||
Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
|
||||
|
||||
To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
||||
@@ -776,13 +791,12 @@ For these purposes, Resource Server supports mapping the JWT claim set with `Map
|
||||
|
||||
By default, `MappedJwtClaimSetConverter` will attempt to coerce claims into the following types:
|
||||
|
||||
|
||||
|============
|
||||
| Claim | Java Type
|
||||
| `aud` | `Collection<String>`
|
||||
| `exp` | `Instant`
|
||||
| `iat` | `Instant`
|
||||
| `iss` | `URL`
|
||||
| `iss` | `String`
|
||||
| `jti` | `String`
|
||||
| `nbf` | `Instant`
|
||||
| `sub` | `String`
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
gaeVersion=1.9.66
|
||||
springBootVersion=2.1.0.M4
|
||||
version=5.1.2.BUILD-SNAPSHOT
|
||||
gaeVersion=1.9.68
|
||||
springBootVersion=2.1.0.RELEASE
|
||||
version=5.1.2.RELEASE
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Californium-SR1'
|
||||
ext.reactorVersion = 'Californium-SR3'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.1.1.RELEASE'
|
||||
ext.springVersion = '5.1.3.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Lovelace-SR1'
|
||||
ext.springDataVersion = 'Lovelace-SR3'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
@@ -17,17 +17,17 @@ dependencyManagement {
|
||||
mavenBom "org.springframework.data:spring-data-releasetrain:${springDataVersion}"
|
||||
}
|
||||
dependencies {
|
||||
dependency 'cglib:cglib-nodep:3.2.8'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.10.0'
|
||||
dependency 'cglib:cglib-nodep:3.2.9'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.12.0'
|
||||
dependency 'opensymphony:sitemesh:2.4.2'
|
||||
dependency 'org.gebish:geb-spock:0.10.0'
|
||||
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-core:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.0-RC.4'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.0-RC.4'
|
||||
dependency 'org.powermock:powermock-core:2.0.0-RC.4'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.0-RC.4'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.0-RC.4'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.0-RC.4'
|
||||
dependency 'org.python:jython:2.5.0'
|
||||
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
|
||||
@@ -56,11 +56,11 @@ dependencyManagement {
|
||||
dependency 'com.nimbusds:lang-tag:1.4.3'
|
||||
dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2'
|
||||
dependency 'com.nimbusds:oauth2-oidc-sdk:6.0'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.9.0'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||
dependency 'com.squareup.okio:okio:1.13.0'
|
||||
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
|
||||
dependency 'com.sun.xml.bind:jaxb-impl:2.3.0.1'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.8'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.9'
|
||||
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
|
||||
dependency 'commons-cli:commons-cli:1.4'
|
||||
dependency 'commons-codec:commons-codec:1.11'
|
||||
@@ -81,8 +81,8 @@ dependencyManagement {
|
||||
dependency 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
|
||||
dependency 'junit:junit:4.12'
|
||||
dependency 'ldapsdk:ldapsdk:4.1'
|
||||
dependency 'net.bytebuddy:byte-buddy-agent:1.7.9'
|
||||
dependency 'net.bytebuddy:byte-buddy:1.7.9'
|
||||
dependency 'net.bytebuddy:byte-buddy-agent:1.8.3'
|
||||
dependency 'net.bytebuddy:byte-buddy:1.8.3'
|
||||
dependency 'net.jcip:jcip-annotations:1.0'
|
||||
dependency 'net.minidev:accessors-smart:1.2'
|
||||
dependency 'net.minidev:json-smart:2.3'
|
||||
@@ -140,9 +140,9 @@ dependencyManagement {
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:8.0.44'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:8.5.23'
|
||||
dependency 'org.aspectj:aspectjrt:1.9.1'
|
||||
dependency 'org.aspectj:aspectjtools:1.9.1'
|
||||
dependency 'org.aspectj:aspectjweaver:1.9.1'
|
||||
dependency 'org.aspectj:aspectjrt:1.9.2'
|
||||
dependency 'org.aspectj:aspectjtools:1.9.2'
|
||||
dependency 'org.aspectj:aspectjweaver:1.9.2'
|
||||
dependency 'org.assertj:assertj-core:3.11.1'
|
||||
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
|
||||
dependency 'org.bouncycastle:bcpkix-jdk15on:1.60'
|
||||
@@ -179,10 +179,10 @@ dependencyManagement {
|
||||
dependency 'org.jboss.logging:jboss-logging:3.3.1.Final'
|
||||
dependency 'org.jboss.spec.javax.transaction:jboss-transaction-api_1.2_spec:1.0.1.Final'
|
||||
dependency 'org.jboss:jandex:2.0.3.Final'
|
||||
dependency 'org.mockito:mockito-core:2.22.0'
|
||||
dependency 'org.mockito:mockito-core:2.23.4'
|
||||
dependency 'org.objenesis:objenesis:2.6'
|
||||
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
|
||||
dependency 'org.ow2.asm:asm:6.0'
|
||||
dependency 'org.ow2.asm:asm:6.2.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.1'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.0'
|
||||
dependency 'org.seleniumhq.selenium:selenium-java:3.14.0'
|
||||
@@ -195,8 +195,7 @@ dependencyManagement {
|
||||
dependency 'org.slf4j:slf4j-nop:1.7.25'
|
||||
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
|
||||
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.10.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf:3.0.9.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
|
||||
dependency 'org.unbescape:unbescape:1.1.5.RELEASE'
|
||||
dependency 'org.w3c.css:sac:1.3'
|
||||
dependency 'xalan:serializer:2.7.2'
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
+12
-1
@@ -25,6 +25,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -64,7 +65,17 @@ public class WebClientReactiveClientCredentialsTokenResponseClient implements Re
|
||||
.headers(headers(clientRegistration))
|
||||
.body(body)
|
||||
.exchange()
|
||||
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
|
||||
.flatMap(response ->{
|
||||
if (!response.statusCode().is2xxSuccessful()){
|
||||
// extract the contents of this into a method named oauth2AccessTokenResponse but has an argument for the response
|
||||
throw WebClientResponseException.create(response.rawStatusCode(),
|
||||
"Cannot get token, expected 2xx HTTP Status code",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
return response.body(oauth2AccessTokenResponse()); })
|
||||
.map(response -> {
|
||||
if (response.getAccessToken().getScopes().isEmpty()) {
|
||||
response = OAuth2AccessTokenResponse.withResponse(response)
|
||||
|
||||
+12
-10
@@ -18,16 +18,13 @@ package org.springframework.security.oauth2.client.registration;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
import static java.util.stream.Collectors.collectingAndThen;
|
||||
import static java.util.stream.Collectors.toConcurrentMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link ClientRegistrationRepository} that stores {@link ClientRegistration}(s) in-memory.
|
||||
@@ -39,6 +36,7 @@ import static java.util.stream.Collectors.toConcurrentMap;
|
||||
* @see ClientRegistration
|
||||
*/
|
||||
public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {
|
||||
|
||||
private final Map<String, ClientRegistration> registrations;
|
||||
|
||||
/**
|
||||
@@ -47,7 +45,8 @@ public final class InMemoryClientRegistrationRepository implements ClientRegistr
|
||||
* @param registrations the client registration(s)
|
||||
*/
|
||||
public InMemoryClientRegistrationRepository(ClientRegistration... registrations) {
|
||||
this(Arrays.asList(registrations));
|
||||
Assert.notEmpty(registrations, "registrations cannot be empty");
|
||||
this.registrations = createClientRegistrationIdToClientRegistration(Arrays.asList(registrations));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,10 +56,13 @@ public final class InMemoryClientRegistrationRepository implements ClientRegistr
|
||||
*/
|
||||
public InMemoryClientRegistrationRepository(List<ClientRegistration> registrations) {
|
||||
Assert.notEmpty(registrations, "registrations cannot be empty");
|
||||
Collector<ClientRegistration, ?, ConcurrentMap<String, ClientRegistration>> collector =
|
||||
toConcurrentMap(ClientRegistration::getRegistrationId, Function.identity());
|
||||
this.registrations = registrations.stream()
|
||||
.collect(collectingAndThen(collector, Collections::unmodifiableMap));
|
||||
this.registrations = createClientRegistrationIdToClientRegistration(registrations);
|
||||
}
|
||||
|
||||
private static Map<String, ClientRegistration> createClientRegistrationIdToClientRegistration(Collection<ClientRegistration> registrations) {
|
||||
return Collections.unmodifiableMap(registrations.stream()
|
||||
.peek(registration -> Assert.notNull(registration, "registrations cannot contain null values"))
|
||||
.collect(Collectors.toMap(ClientRegistration::getRegistrationId, Function.identity())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-19
@@ -17,12 +17,6 @@ package org.springframework.security.oauth2.client.registration;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -37,7 +31,7 @@ import reactor.core.publisher.Mono;
|
||||
public final class InMemoryReactiveClientRegistrationRepository
|
||||
implements ReactiveClientRegistrationRepository, Iterable<ClientRegistration> {
|
||||
|
||||
private final Map<String, ClientRegistration> clientIdToClientRegistration;
|
||||
private final InMemoryClientRegistrationRepository delegate;
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryReactiveClientRegistrationRepository} using the provided parameters.
|
||||
@@ -45,12 +39,7 @@ public final class InMemoryReactiveClientRegistrationRepository
|
||||
* @param registrations the client registration(s)
|
||||
*/
|
||||
public InMemoryReactiveClientRegistrationRepository(ClientRegistration... registrations) {
|
||||
Assert.notEmpty(registrations, "registrations cannot be empty");
|
||||
this.clientIdToClientRegistration = new ConcurrentReferenceHashMap<>();
|
||||
for (ClientRegistration registration : registrations) {
|
||||
Assert.notNull(registration, "registrations cannot contain null values");
|
||||
this.clientIdToClientRegistration.put(registration.getRegistrationId(), registration);
|
||||
}
|
||||
this.delegate = new InMemoryClientRegistrationRepository(registrations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,15 +48,12 @@ public final class InMemoryReactiveClientRegistrationRepository
|
||||
* @param registrations the client registration(s)
|
||||
*/
|
||||
public InMemoryReactiveClientRegistrationRepository(List<ClientRegistration> registrations) {
|
||||
Assert.notEmpty(registrations, "registrations cannot be null or empty");
|
||||
this.clientIdToClientRegistration = registrations.stream()
|
||||
.collect(Collectors.toConcurrentMap(ClientRegistration::getRegistrationId, Function.identity()));
|
||||
this.delegate = new InMemoryClientRegistrationRepository(registrations);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
|
||||
return Mono.justOrEmpty(this.clientIdToClientRegistration.get(registrationId));
|
||||
return Mono.justOrEmpty(this.delegate.findByRegistrationId(registrationId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +63,6 @@ public final class InMemoryReactiveClientRegistrationRepository
|
||||
*/
|
||||
@Override
|
||||
public Iterator<ClientRegistration> iterator() {
|
||||
return this.clientIdToClientRegistration.values().iterator();
|
||||
return delegate.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ public class OAuth2UserRequestEntityConverter implements Converter<OAuth2UserReq
|
||||
httpMethod = HttpMethod.POST;
|
||||
}
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
|
||||
.build()
|
||||
.toUri();
|
||||
|
||||
+18
@@ -28,6 +28,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@@ -116,6 +117,23 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests {
|
||||
assertThat(response.getAccessToken().getScopes()).isEqualTo(registration.getScopes());
|
||||
}
|
||||
|
||||
@Test(expected = WebClientResponseException.class)
|
||||
// gh-6089
|
||||
public void getTokenResponseWhenInvalidResponse() throws WebClientResponseException {
|
||||
ClientRegistration registration = this.clientRegistration.build();
|
||||
enqueueUnexpectedResponse();
|
||||
|
||||
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration);
|
||||
|
||||
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
|
||||
}
|
||||
|
||||
private void enqueueUnexpectedResponse(){
|
||||
MockResponse response = new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.setResponseCode(301);
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
private void enqueueJson(String body) {
|
||||
MockResponse response = new MockResponse()
|
||||
|
||||
+3
-3
@@ -316,7 +316,7 @@ public class OidcUserServiceTests {
|
||||
|
||||
this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken));
|
||||
assertThat(this.server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT))
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
// gh-5500
|
||||
@@ -341,7 +341,7 @@ public class OidcUserServiceTests {
|
||||
this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ public class OidcUserServiceTests {
|
||||
this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
+6
@@ -35,6 +35,12 @@ public class InMemoryClientRegistrationRepositoryTests {
|
||||
|
||||
private InMemoryClientRegistrationRepository clients = new InMemoryClientRegistrationRepository(this.registration);
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorVarArgsListClientRegistrationWhenNullThenIllegalArgumentException() {
|
||||
ClientRegistration nullRegistration = null;
|
||||
new InMemoryClientRegistrationRepository(nullRegistration);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorListClientRegistrationWhenNullThenIllegalArgumentException() {
|
||||
List<ClientRegistration> registrations = null;
|
||||
|
||||
+9
-2
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client.registration;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -61,10 +62,16 @@ public class InMemoryReactiveClientRegistrationRepositoryTests {
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientRegistrationListHasNullThenIllegalArgumentException() {
|
||||
List<ClientRegistration> registrations = Arrays.asList(null, registration);
|
||||
assertThatThrownBy(() -> new InMemoryReactiveClientRegistrationRepository(registrations))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientRegistrationIsNullThenIllegalArgumentException() {
|
||||
ClientRegistration registration = null;
|
||||
assertThatThrownBy(() -> new InMemoryReactiveClientRegistrationRepository(registration))
|
||||
assertThatThrownBy(() -> new InMemoryReactiveClientRegistrationRepository(registration, null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -277,7 +277,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||
|
||||
this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken));
|
||||
assertThat(this.server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT))
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
// gh-5500
|
||||
@@ -303,7 +303,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||
this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||
this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ public class OAuth2UserRequestEntityConverterTests {
|
||||
clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri());
|
||||
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON);
|
||||
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo(
|
||||
"Bearer " + userRequest.getAccessToken().getTokenValue());
|
||||
}
|
||||
@@ -77,7 +77,7 @@ public class OAuth2UserRequestEntityConverterTests {
|
||||
clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri());
|
||||
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON);
|
||||
assertThat(headers.getContentType()).isEqualTo(
|
||||
MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
|
||||
|
||||
|
||||
+4
-4
@@ -55,15 +55,15 @@ class OAuth2AccessTokenResponseBodyExtractor
|
||||
@Override
|
||||
public Mono<OAuth2AccessTokenResponse> extract(ReactiveHttpInputMessage inputMessage,
|
||||
Context context) {
|
||||
ParameterizedTypeReference<Map<String, String>> type = new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
BodyExtractor<Mono<Map<String, String>>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type);
|
||||
ParameterizedTypeReference<Map<String, Object>> type = new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
BodyExtractor<Mono<Map<String, Object>>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type);
|
||||
return delegate.extract(inputMessage, context)
|
||||
.map(json -> parse(json))
|
||||
.map(OAuth2AccessTokenResponseBodyExtractor::parse)
|
||||
.flatMap(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse)
|
||||
.map(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse);
|
||||
}
|
||||
|
||||
private static TokenResponse parse(Map<String, String> json) {
|
||||
private static TokenResponse parse(Map<String, Object> json) {
|
||||
try {
|
||||
return TokenResponse.parse(new JSONObject(json));
|
||||
}
|
||||
|
||||
+29
@@ -120,4 +120,33 @@ public class OAuth2BodyExtractorsTests {
|
||||
assertThat(result.getRefreshToken().getTokenValue()).isEqualTo("tGzv3JOkF0XG5Qx2TlKWIA");
|
||||
assertThat(result.getAdditionalParameters()).containsEntry("example_parameter", "example_value");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
// gh-6087
|
||||
public void oauth2AccessTokenResponseWhenMultipleAttributeTypesThenCreated() throws Exception {
|
||||
BodyExtractor<Mono<OAuth2AccessTokenResponse>, ReactiveHttpInputMessage> extractor = OAuth2BodyExtractors
|
||||
.oauth2AccessTokenResponse();
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
|
||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
response.setBody("{\n"
|
||||
+ " \"access_token\":\"2YotnFZFEjr1zCsicMWpAA\",\n"
|
||||
+ " \"token_type\":\"Bearer\",\n"
|
||||
+ " \"expires_in\":3600,\n"
|
||||
+ " \"refresh_token\":\"tGzv3JOkF0XG5Qx2TlKWIA\",\n"
|
||||
+ " \"subjson\":{}, \n"
|
||||
+ " \"list\":[] \n"
|
||||
+ " }");
|
||||
|
||||
Instant now = Instant.now();
|
||||
OAuth2AccessTokenResponse result = extractor.extract(response, this.context).block();
|
||||
|
||||
assertThat(result.getAccessToken().getTokenValue()).isEqualTo("2YotnFZFEjr1zCsicMWpAA");
|
||||
assertThat(result.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
assertThat(result.getAccessToken().getExpiresAt()).isBetween(now.plusSeconds(3600), now.plusSeconds(3600 + 2));
|
||||
assertThat(result.getRefreshToken().getTokenValue()).isEqualTo("tGzv3JOkF0XG5Qx2TlKWIA");
|
||||
assertThat(result.getAdditionalParameters().get("subjson")).isInstanceOfAny(Map.class);
|
||||
assertThat(result.getAdditionalParameters().get("list")).isInstanceOfAny(List.class);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-13
@@ -15,9 +15,6 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
@@ -37,7 +34,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
"This iss claim is not equal to the configured issuer",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
|
||||
private final URL issuer;
|
||||
private final String issuer;
|
||||
|
||||
/**
|
||||
* Constructs a {@link JwtIssuerValidator} using the provided parameters
|
||||
@@ -46,14 +43,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
*/
|
||||
public JwtIssuerValidator(String issuer) {
|
||||
Assert.notNull(issuer, "issuer cannot be null");
|
||||
|
||||
try {
|
||||
this.issuer = new URL(issuer);
|
||||
} catch (MalformedURLException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid Issuer URL " + issuer + " : " + ex.getMessage(),
|
||||
ex);
|
||||
}
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +53,8 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
public OAuth2TokenValidatorResult validate(Jwt token) {
|
||||
Assert.notNull(token, "token cannot be null");
|
||||
|
||||
if (this.issuer.equals(token.getIssuer())) {
|
||||
String tokenIssuer = token.getClaimAsString(JwtClaimNames.ISS);
|
||||
if (this.issuer.equals(tokenIssuer)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
} else {
|
||||
return OAuth2TokenValidatorResult.failure(INVALID_ISSUER);
|
||||
|
||||
+11
-24
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
@@ -42,7 +41,7 @@ public final class MappedJwtClaimSetConverter
|
||||
implements Converter<Map<String, Object>, Map<String, Object>> {
|
||||
|
||||
private static final Converter<Object, Collection<String>> AUDIENCE_CONVERTER = new AudienceConverter();
|
||||
private static final Converter<Object, URL> ISSUER_CONVERTER = new IssuerConverter();
|
||||
private static final Converter<Object, String> ISSUER_CONVERTER = new IssuerConverter();
|
||||
private static final Converter<Object, String> STRING_CONVERTER = new StringConverter();
|
||||
private static final Converter<Object, Instant> TEMPORAL_CONVERTER = new InstantConverter();
|
||||
|
||||
@@ -157,39 +156,27 @@ public final class MappedJwtClaimSetConverter
|
||||
* Coerces an <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4.1.1">Issuer</a> claim
|
||||
* into a {@link URL}, ignoring null values, and throwing an error if its coercion efforts fail.
|
||||
*/
|
||||
private static class IssuerConverter implements Converter<Object, URL> {
|
||||
private static class IssuerConverter implements Converter<Object, String> {
|
||||
|
||||
@Override
|
||||
public URL convert(Object source) {
|
||||
public String convert(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source instanceof URL) {
|
||||
return (URL) source;
|
||||
return ((URL) source).toExternalForm();
|
||||
}
|
||||
|
||||
if (source instanceof URI) {
|
||||
return toUrl((URI) source);
|
||||
if (source instanceof String && ((String) source).contains(":")) {
|
||||
try {
|
||||
return URI.create((String) source).toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URI String", e);
|
||||
}
|
||||
}
|
||||
|
||||
return toUrl(source.toString());
|
||||
}
|
||||
|
||||
private URL toUrl(URI source) {
|
||||
try {
|
||||
return source.toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URL", e);
|
||||
}
|
||||
}
|
||||
|
||||
private URL toUrl(String source) {
|
||||
try {
|
||||
return new URL(source);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URL", e);
|
||||
}
|
||||
return source.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-8
@@ -23,9 +23,6 @@ import org.junit.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
@@ -73,14 +70,36 @@ public class JwtIssuerValidatorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() {
|
||||
assertThatCode(() -> this.validator.validate(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
public void validateWhenJwtHasNoIssuerThenReturnsError() {
|
||||
Jwt jwt = new Jwt(
|
||||
MOCK_TOKEN,
|
||||
MOCK_ISSUED_AT,
|
||||
MOCK_EXPIRES_AT,
|
||||
MOCK_HEADERS,
|
||||
Collections.singletonMap(JwtClaimNames.AUD, "https://aud"));
|
||||
|
||||
OAuth2TokenValidatorResult result = this.validator.validate(jwt);
|
||||
assertThat(result.getErrors()).isNotEmpty();
|
||||
}
|
||||
|
||||
// gh-6073
|
||||
@Test
|
||||
public void validateWhenIssuerMatchesAndIsNotAUriThenReturnsSuccess() {
|
||||
Jwt jwt = new Jwt(
|
||||
MOCK_TOKEN,
|
||||
MOCK_ISSUED_AT,
|
||||
MOCK_EXPIRES_AT,
|
||||
MOCK_HEADERS,
|
||||
Collections.singletonMap(JwtClaimNames.ISS, "issuer"));
|
||||
JwtIssuerValidator validator = new JwtIssuerValidator("issuer");
|
||||
|
||||
assertThat(validator.validate(jwt))
|
||||
.isEqualTo(OAuth2TokenValidatorResult.success());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenMalformedIssuerIsGivenThenThrowsIllegalArgumentException() {
|
||||
assertThatCode(() -> new JwtIssuerValidator("issuer"))
|
||||
public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() {
|
||||
assertThatCode(() -> this.validator.validate(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
|
||||
+25
-3
@@ -108,7 +108,7 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience"));
|
||||
assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(2000000000L));
|
||||
assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo(new URL("https://any.url"));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://any.url");
|
||||
assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234");
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience"));
|
||||
assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(2000000000L));
|
||||
assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo(new URL("https://any.url"));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://any.url");
|
||||
assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234");
|
||||
}
|
||||
@@ -196,7 +196,7 @@ public class MappedJwtClaimSetConverterTests {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.emptyMap());
|
||||
|
||||
Map<String, Object> badIssuer = Collections.singletonMap(JwtClaimNames.ISS, "badly-formed-iss");
|
||||
Map<String, Object> badIssuer = Collections.singletonMap(JwtClaimNames.ISS, "https://badly formed iss");
|
||||
assertThatCode(() -> converter.convert(badIssuer)).isInstanceOf(IllegalStateException.class);
|
||||
|
||||
Map<String, Object> badIssuedAt = Collections.singletonMap(JwtClaimNames.IAT, "badly-formed-iat");
|
||||
@@ -209,6 +209,28 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThatCode(() -> converter.convert(badNotBefore)).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
// gh-6073
|
||||
@Test
|
||||
public void convertWhenIssuerIsNotAUriThenConvertsToString() {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.emptyMap());
|
||||
|
||||
Map<String, Object> nonUriIssuer = Collections.singletonMap(JwtClaimNames.ISS, "issuer");
|
||||
Map<String, Object> target = converter.convert(nonUriIssuer);
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("issuer");
|
||||
}
|
||||
|
||||
// gh-6073
|
||||
@Test
|
||||
public void convertWhenIssuerIsOfTypeURLThenConvertsToString() throws Exception {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.emptyMap());
|
||||
|
||||
Map<String, Object> issuer = Collections.singletonMap(JwtClaimNames.ISS, new URL("https://issuer"));
|
||||
Map<String, Object> target = converter.convert(issuer);
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://issuer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructWhenAnyParameterIsNullThenIllegalArgumentException() {
|
||||
assertThatCode(() -> new MappedJwtClaimSetConverter(null))
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-web')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>Hello Spring Security</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
|
||||
+1
-3
@@ -1,14 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
compile project(':spring-security-oauth2-jose')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'net.sourceforge.htmlunit:htmlunit'
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
compile project(':spring-security-oauth2-jose')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'net.sourceforge.htmlunit:htmlunit'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>Spring Security - OAuth 2.0 Login</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ dependencies {
|
||||
compile project(':spring-security-oauth2-jose')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
compile 'io.projectreactor.netty:reactor-netty'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>OAuth2 WebClient Showcase</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
@@ -9,7 +7,7 @@ dependencies {
|
||||
compile 'org.springframework:spring-webflux'
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
compile 'io.projectreactor.netty:reactor-netty'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>OAuth2 WebClient Showcase</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -22,6 +22,8 @@ dependencies {
|
||||
compile project(':spring-security-web')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
|
||||
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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 sample;
|
||||
|
||||
import org.springframework.security.web.server.csrf.CsrfToken;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class CsrfControllerAdvice {
|
||||
@ModelAttribute
|
||||
public Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
|
||||
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
|
||||
return csrfToken.doOnSuccess(token -> exchange.getAttributes().put(DEFAULT_CSRF_ATTR_NAME, token));
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,10 @@ dependencies {
|
||||
compile 'org.springframework:spring-webmvc'
|
||||
compile slf4jDependencies
|
||||
|
||||
compile 'javax.xml.bind:jaxb-api'
|
||||
compile 'com.sun.xml.bind:jaxb-core'
|
||||
compile 'com.sun.xml.bind:jaxb-impl'
|
||||
|
||||
providedCompile 'javax.servlet.jsp:javax.servlet.jsp-api'
|
||||
providedCompile 'javax.servlet:javax.servlet-api'
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ dependencies {
|
||||
compile 'org.hibernate:hibernate-validator'
|
||||
compile slf4jDependencies
|
||||
|
||||
compile 'javax.xml.bind:jaxb-api'
|
||||
compile 'com.sun.xml.bind:jaxb-core'
|
||||
compile 'com.sun.xml.bind:jaxb-impl'
|
||||
|
||||
providedCompile 'javax.servlet.jsp:javax.servlet.jsp-api'
|
||||
providedCompile 'javax.servlet:javax.servlet-api'
|
||||
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.samples.config;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
public class SecurityConfigTests {
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "org.springframework.security.samples.config")
|
||||
public static class Config {
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private FilterChainProxy springSecurityFilterChain;
|
||||
|
||||
@Test
|
||||
public void securityConfigurationLoads() {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
apply plugin: 'io.spring.convention.spring-sample-war'
|
||||
|
||||
def keystore = "$rootDir/samples/certificates/server.jks"
|
||||
@@ -8,6 +6,9 @@ def password = 'password'
|
||||
dependencies {
|
||||
compile "org.jasig.cas:cas-server-webapp:4.0.0@war"
|
||||
compile slf4jDependencies
|
||||
runtime 'org.aspectj:aspectjrt'
|
||||
runtime 'org.aspectj:aspectjtools'
|
||||
runtime 'org.aspectj:aspectjweaver'
|
||||
}
|
||||
|
||||
project.tasks.withType(org.gradle.api.tasks.bundling.War) { war ->
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ import java.util.regex.Matcher;
|
||||
* expression "CN=(.*?)(?:,|$)" matches the common name field. So
|
||||
* "CN=Jimi Hendrix, OU=..." will give a user name of "Jimi Hendrix".
|
||||
* <p>
|
||||
* The matches are case insensitive. So "emailAddress=(.?)," will match
|
||||
* The matches are case insensitive. So "emailAddress=(.*?)," will match
|
||||
* "EMAILADDRESS=jimi@hendrix.org, CN=..." giving a user name "jimi@hendrix.org"
|
||||
*
|
||||
* @author Luke Taylor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,8 +19,12 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.web.util.OnCommittedResponseWrapper;
|
||||
@@ -33,6 +37,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
* and X-Content-Type-Options.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @author Josh Cummings
|
||||
* @since 3.2
|
||||
*
|
||||
*/
|
||||
@@ -62,8 +67,11 @@ public class HeaderWriterFilter extends OncePerRequestFilter {
|
||||
|
||||
HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
|
||||
response, this.headerWriters);
|
||||
HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request,
|
||||
headerWriterResponse);
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, headerWriterResponse);
|
||||
filterChain.doFilter(headerWriterRequest, headerWriterResponse);
|
||||
}
|
||||
finally {
|
||||
headerWriterResponse.writeHeaders();
|
||||
@@ -106,4 +114,39 @@ public class HeaderWriterFilter extends OncePerRequestFilter {
|
||||
return (HttpServletResponse) getResponse();
|
||||
}
|
||||
}
|
||||
|
||||
static class HeaderWriterRequest extends HttpServletRequestWrapper {
|
||||
private final HeaderWriterResponse response;
|
||||
|
||||
HeaderWriterRequest(HttpServletRequest request, HeaderWriterResponse response) {
|
||||
super(request);
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestDispatcher getRequestDispatcher(String path) {
|
||||
return new HeaderWriterRequestDispatcher(super.getRequestDispatcher(path), this.response);
|
||||
}
|
||||
}
|
||||
|
||||
static class HeaderWriterRequestDispatcher implements RequestDispatcher {
|
||||
private final RequestDispatcher delegate;
|
||||
private final HeaderWriterResponse response;
|
||||
|
||||
HeaderWriterRequestDispatcher(RequestDispatcher delegate, HeaderWriterResponse response) {
|
||||
this.delegate = delegate;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException {
|
||||
this.delegate.forward(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException {
|
||||
this.response.onResponseCommitted();
|
||||
this.delegate.include(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.web.header;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -84,7 +85,9 @@ public class HeaderWriterFilterTests {
|
||||
|
||||
verify(this.writer1).writeHeaders(request, response);
|
||||
verify(this.writer2).writeHeaders(request, response);
|
||||
assertThat(filterChain.getRequest()).isEqualTo(request); // verify the filterChain
|
||||
HeaderWriterFilter.HeaderWriterRequest wrappedRequest = (HeaderWriterFilter.HeaderWriterRequest)
|
||||
filterChain.getRequest();
|
||||
assertThat(wrappedRequest.getRequest()).isEqualTo(request); // verify the filterChain
|
||||
// continued
|
||||
}
|
||||
|
||||
@@ -112,4 +115,25 @@ public class HeaderWriterFilterTests {
|
||||
|
||||
verifyNoMoreInteractions(this.writer1);
|
||||
}
|
||||
|
||||
// gh-5499
|
||||
@Test
|
||||
public void doFilterWhenRequestContainsIncludeThenHeadersStillWritten() throws Exception {
|
||||
HeaderWriterFilter filter = new HeaderWriterFilter(
|
||||
Collections.singletonList(this.writer1));
|
||||
|
||||
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
|
||||
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(mockRequest, mockResponse, (request, response) -> {
|
||||
verifyZeroInteractions(HeaderWriterFilterTests.this.writer1);
|
||||
|
||||
request.getRequestDispatcher("/").include(request, response);
|
||||
|
||||
verify(HeaderWriterFilterTests.this.writer1).writeHeaders(
|
||||
any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
});
|
||||
|
||||
verifyNoMoreInteractions(this.writer1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user