1
0
mirror of synced 2026-07-08 20:30:04 +00:00

Compare commits

...

44 Commits

Author SHA1 Message Date
Rob Winch 55dd732443 Release 5.1.2.RELEASE 2018-11-28 08:44:40 -06:00
Rob Winch 45cbdc03ba Remove Thymeleaf dependency management
Fixes: gh-6161
2018-11-28 08:20:09 -06:00
Rob Winch d99c7dbfd1 Update to GAE 1.9.68
Fixes: gh-6160
2018-11-28 08:19:56 -06:00
Rob Winch d8de4d06f7 Update to Mockito 2.23.4
Fixes: gh-6159
2018-11-28 08:19:41 -06:00
Rob Winch be2f0f0fae Update to AspectJ 1.9.2
Fixes: gh-6158
2018-11-28 08:19:33 -06:00
Rob Winch 920d5ca6ad Update to unboundid 4.0.9
Fixes: gh-6157
2018-11-28 08:19:20 -06:00
Rob Winch 6dcf5a27b1 Update to Powermock 2.0.0-RC.4
Fixes: gh-6156
2018-11-28 08:19:13 -06:00
Rob Winch c2166c4207 Update to cglib-nodep:3.2.9
Fixes: gh-6155
2018-11-28 08:18:57 -06:00
Rob Winch 1542835268 Update to Spring Data Lovelace-SR3
Fixes: gh-6154
2018-11-28 08:18:40 -06:00
Rob Winch ae471e1456 Update to Spring 5.1.3
Fixes: gh-6153
2018-11-28 08:18:31 -06:00
Rob Winch 853c54eecb Update to Reactor Californium-SR3
Fixes: gh-6152
2018-11-28 08:18:18 -06:00
Joe Grandja 10f6d10f81 Update com.squareup.okhttp3 deps to 3.12.0
Fixes gh-5570
2018-11-26 12:44:30 -05:00
Rafael Dominguez 02d844d528 Enable Code Coverage Reports in Sonar
This commit ensures that jacocoTestReport task is called when running the Sonar stage.
Additionally, a variable is passed instructing Sonar where to find the test result data.

Fixes: gh-6092
2018-11-26 09:29:21 -07:00
Rafael Dominguez c0849ba891 Update to Gradle 4.10.2
Fixes gh-6106
2018-11-21 09:59:12 -06:00
Josh Cummings db145fa03f Register NullRequestCache When Disabled
Fixes: gh-6102
2018-11-20 07:39:54 -07:00
Josh Cummings 42b8b794a8 RequestCacheConfigurerTests groovy->java
Issue: gh-4939
2018-11-20 07:37:54 -07:00
dperezcabrera 8014114225 Make InMemory*ClientRegistrationRepository Consistent
The previous builders with the list argument were inconsistent with their 
respective builders of var args.
2018-11-19 11:12:37 -06:00
Rafael Dominguez a742c0c3f2 WebClientReactiveClientCredentialsTokenResponseClient.getTokenResponse expects 2xx http status code
This ensures that token response is only extracted when ClientResponse has a successful status

Fixes: gh-6089
2018-11-19 10:51:33 -06:00
Rafael Dominguez 1cec4fe3ac OAuth2AccessTokenResponseBodyExtractor supports Object values
This commit ensures the token response is parsed correctly if the values are not a String.

Fixes: gh-6087
2018-11-15 13:24:48 -06:00
Daniel Bustamante Ospina 8655caa2de Update webflux-form sample to use Built in CSRF Support
Remove the CsrfControllerAdvice class and update dependencies to add
org.thymeleaf.extras:thymeleaf-extras-springsecurity5

Issue: gh-6061
2018-11-14 17:19:06 -06:00
Dongmin Shin dc5f5f348d Fix Typo in Reference Docs
Fixes gh-6076
2018-11-14 11:37:22 -06:00
Rafael Dominguez 35e05780ea Updated Spring Boot version from 2.1.0.M4 to 2.1.0.RELEASE 2018-11-14 10:55:26 -06:00
Gunnar Hillert a461abcf32 Docs: Fix Maven Property example spring-security.version 2018-11-13 15:09:46 -06:00
Josh Cummings ba1f5f3873 Leave Issuer As String - Documentation
Update documentation that indicated the iss claim is proactively
coerced into a URL.

Issue: gh-6073
2018-11-13 12:35:08 -07:00
Josh Cummings 5478b74116 Leave Issuer As String
Since StringOrURI is a valid issuer, MappedJwtClaimSetConverter and
JwtIssuerValidator no longer assume it.

Issue: gh-6073
2018-11-13 10:43:36 -07:00
Josh Cummings dc4aa3d017 Favor URL.toExternalForm
Converts URLs to Strings before comparing them. Uses toString(),
which delegates to toExternalForm().

Fixes: gh-6073
2018-11-13 07:39:24 -07:00
Josh Cummings 2bc156ed19 Update to spring-build-conventions:0.0.22.RELEASE
Fixes: gh-6064
2018-11-12 09:31:51 -07:00
dperezcabrera 0270994340 InMemoryUserDetailsManager.updatePassword case-insenstive
Previously updatePassword was case sensitive which was
inconsistent with the rest of the class.

This commit updates updatePassword to be case insensitive.

Fixes: gh-6039
2018-11-09 11:41:44 -06:00
Erik van Paassen a557a324c0 Fix csrf:token-repository-ref XSD documentation
The documentation of the token-repository-ref attribute of the csrf
element in the schema has been updated to make clear the default
repository is lazy. Targets versions 4.2, 5.0 and 5.1.

Fixes gh-6037
2018-11-08 10:22:07 -06:00
Josh Cummings 73022059d4 Update Reactive Resource Server Docs
Resource Server documentation for both Servlet and Reactive now have a
similar feel and offer deeper exposure to common use cases.

Fixes: gh-6054
2018-11-07 12:10:33 -07:00
Josh Cummings 165cbcb723 Write Security Headers Before Servlet Include
HeaderWriterFilter wraps request dispatcher so it can write security
headers before the include occurs.

Fixes: gh-5499
2018-10-31 09:32:40 -06:00
Josh Cummings d99ecc2a7b Add JDK 11 to Jenkins
Fixes: gh-5860
2018-10-26 07:38:55 -06:00
Josh Cummings ccd2664e6c JDK 11 Compatibility
Upgraded dependencies and removed a test in the Java Config LDAP
sample which is arguably an integration test since it starts up an
LDAP container. This test also isn't JDK 11 compatible and the
remaining integration tests in the sample cover the same material.

Issue: gh-5860
2018-10-26 07:38:31 -06:00
Joe Grandja b87ff3c00f Remove charset from Accept header in UserInfo request
Fixes gh-6017
2018-10-25 14:10:41 -04:00
Joe Grandja 8971777908 Set AuthenticationEventPublisher on each AuthenticationManagerBuilder
Fixes gh-6009
2018-10-23 14:41:26 -04:00
Drummond Dawson 3d618d78ac Remove unnecessary concatenation of sql in JdbcUserDetailsManager 2018-10-19 15:30:24 -05:00
Drummond Dawson 697b8cd734 Add missing space to heading for it to render properly 2018-10-19 15:27:35 -05:00
Rob Winch cf03faf58e Update to spring-build-conventions:0.0.20.RELEASE
Fixes: gh-5998
2018-10-18 16:14:59 -05:00
Rob Winch 13159e9f88 Fail the build if deploy fails
Fixes: gh-5997
2018-10-18 16:14:59 -05:00
Brian Demers 41b7c74928 Fixing IllegalStateException message in OAuth2ResourceServerConfigurer
Updated message to include `http.oauth2ResourceServer()`
2018-10-17 15:14:49 -05:00
Josh Cummings 0a4ac4dbfc Add JDK 10 to Jenkins
Fixes: gh-5860
2018-10-17 15:06:14 -05:00
Josh Cummings 1eb56f46a5 JDK 10 Compatibility
Upgrading dependencies and reconfiguring PowerMock

Issue: gh-5860
2018-10-17 15:06:14 -05:00
Josh Cummings cfc3c25304 Update to Spring Build Conventions 0.0.19
Fixes: gh-5981
2018-10-17 15:06:14 -05:00
sunflower-seed 03a3a3b770 Update SubjectDnX509PrincipalExtractor.java
Added missing asterisk
2018-10-17 14:58:31 -05:00
69 changed files with 1143 additions and 522 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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'
@@ -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,
@@ -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));
@@ -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();
@@ -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 =
@@ -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>
@@ -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 =
@@ -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>
@@ -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 =
@@ -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,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());
}
}
}
@@ -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();
@@ -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;
@@ -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();
@@ -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());
}
}
@@ -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;
@@ -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
@@ -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;
}
@@ -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;
}
@@ -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 (?,?)";
@@ -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
View File
@@ -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
+21 -22
View File
@@ -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'
Binary file not shown.
+1 -1
View File
@@ -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
@@ -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)
@@ -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
@@ -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();
}
}
@@ -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();
@@ -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()
@@ -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());
}
@@ -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;
@@ -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);
}
@@ -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());
}
@@ -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"));
@@ -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));
}
@@ -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);
}
}
@@ -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);
@@ -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();
}
}
@@ -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);
}
@@ -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,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" />
@@ -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'
@@ -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 ->
@@ -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);
}
}
}
@@ -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);
}
}