Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce46f06d81 | |||
| a93cc3f0a0 | |||
| 2fcf48a11b | |||
| 0398d86f39 | |||
| c9112dfaab | |||
| 4156766fe0 | |||
| f125785328 | |||
| 4643c0d999 | |||
| d2d82b5e94 | |||
| d4d03ea146 | |||
| 6ba88e6545 | |||
| a3855ea092 | |||
| f57b433fb1 | |||
| 30566eba9e | |||
| c373f6afd9 | |||
| d245a54951 | |||
| 5f599d3d77 | |||
| f49a933490 | |||
| 8961264ec5 | |||
| aaa6983fda | |||
| a1b282ff03 | |||
| 2db4430dcd | |||
| 0baf650f38 | |||
| 000b4bc495 | |||
| 82c86b822f | |||
| 4f3faa78f7 | |||
| ca1961d35e | |||
| 821db0a1ea | |||
| 45b81b194b | |||
| 0d4c619648 | |||
| 4a8961bc80 | |||
| aad3e61f4d | |||
| eaa9692c0c | |||
| 51d5fb9c03 | |||
| 33e72b35f9 | |||
| c306df9b46 | |||
| d42405de42 | |||
| 7db357d36a | |||
| 5beabbe357 | |||
| f5bc6ce665 | |||
| 108b03da3a | |||
| 4cbb057b97 | |||
| 8758a00e90 | |||
| c0f7cecc6d | |||
| 22ffa833ca | |||
| 090c5f96ce | |||
| f41b77a4db | |||
| 6f43104eb3 | |||
| 2028507bf8 | |||
| ae46032ced | |||
| ffdb397830 | |||
| 88a8ef647b | |||
| 5e1db6a771 | |||
| 225dc593a8 | |||
| fda0e9a2b6 | |||
| 3cfaf0d11d | |||
| 892bbcfe0f | |||
| 5406fed5dc | |||
| fbfa13bd47 | |||
| 992bda32f9 | |||
| 0c6b427132 | |||
| 0f7d17bae6 | |||
| 44d5cb0a63 | |||
| c11248b589 | |||
| 972075677f | |||
| 11614edcfe |
@@ -55,6 +55,7 @@ jobs:
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: 'gradle'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Set up gradle user name
|
||||
|
||||
+7
-1
@@ -3,7 +3,7 @@ import io.spring.gradle.IncludeRepoTask
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion"
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
|
||||
classpath 'io.spring.nohttp:nohttp-gradle:0.0.11'
|
||||
classpath "io.freefair.gradle:aspectj-plugin:6.5.1"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
@@ -119,6 +119,12 @@ updateDependenciesSettings {
|
||||
selection.reject("nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency");
|
||||
}
|
||||
}
|
||||
components.withModule("io.mockk:mockk") { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("mockk updates break tests");
|
||||
}
|
||||
}
|
||||
components.all { selection ->
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
// Do not compare version due to multiple versions existing
|
||||
|
||||
@@ -93,13 +93,13 @@ dependencies {
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
implementation 'com.github.spullara.mustache.java:compiler:0.9.4'
|
||||
implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.10'
|
||||
implementation 'io.spring.nohttp:nohttp-gradle:0.0.11'
|
||||
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0'
|
||||
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
|
||||
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0'
|
||||
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.9.1')
|
||||
testImplementation platform('org.junit:junit-bom:5.9.2')
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine"
|
||||
|
||||
@@ -36,7 +36,7 @@ class CheckstylePlugin implements Plugin<Project> {
|
||||
if (checkstyleDir.exists() && checkstyleDir.directory) {
|
||||
project.getPluginManager().apply('checkstyle')
|
||||
project.dependencies.add('checkstyle', 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.15')
|
||||
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.10')
|
||||
project.dependencies.add('checkstyle', 'io.spring.nohttp:nohttp-checkstyle:0.0.11')
|
||||
|
||||
project.checkstyle {
|
||||
configDirectory = checkstyleDir
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -3270,7 +3270,12 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
}
|
||||
|
||||
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
|
||||
int order = this.filterOrders.getOrder(registeredFilter) + offset;
|
||||
Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter);
|
||||
if (registeredFilterOrder == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"The Filter class " + registeredFilter.getName() + " does not have a registered order");
|
||||
}
|
||||
int order = registeredFilterOrder + offset;
|
||||
this.filters.add(new OrderedFilter(filter, order));
|
||||
this.filterOrders.put(filter.getClass(), order);
|
||||
return this;
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -56,6 +56,8 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping";
|
||||
|
||||
private static final String CSRF_CHANNEL_INTERCEPTOR_BEAN_NAME = "csrfChannelInterceptor";
|
||||
|
||||
private MessageMatcherDelegatingAuthorizationManager b;
|
||||
|
||||
private static final AuthorizationManager<Message<?>> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager
|
||||
@@ -66,7 +68,7 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
private final SecurityContextChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor();
|
||||
|
||||
private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
|
||||
private ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
|
||||
|
||||
private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor(
|
||||
ANY_MESSAGE_AUTHENTICATED);
|
||||
@@ -86,6 +88,12 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
ChannelInterceptor csrfChannelInterceptor = getBeanOrNull(CSRF_CHANNEL_INTERCEPTOR_BEAN_NAME,
|
||||
ChannelInterceptor.class);
|
||||
if (csrfChannelInterceptor != null) {
|
||||
this.csrfChannelInterceptor = csrfChannelInterceptor;
|
||||
}
|
||||
|
||||
this.authorizationChannelInterceptor
|
||||
.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context));
|
||||
this.authorizationChannelInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
|
||||
|
||||
+47
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -30,6 +30,7 @@ import org.assertj.core.api.ListAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.UnsatisfiedDependencyException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
@@ -45,12 +46,29 @@ import org.springframework.security.web.context.request.async.WebAsyncManagerInt
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class HttpSecurityAddFilterTest {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Test
|
||||
public void addFilterAfterFilterNotRegisteredYetThenThrowIllegalArgument() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MyOtherFilterAfterMyFilterNotRegisteredYetConfig.class).autowire())
|
||||
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterBeforeFilterNotRegisteredYetThenThrowIllegalArgument() {
|
||||
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
|
||||
.isThrownBy(
|
||||
() -> this.spring.register(MyOtherFilterBeforeMyFilterNotRegisteredYetConfig.class).autowire())
|
||||
.havingRootCause().isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addFilterAfterWhenSameFilterDifferentPlacesThenOrderCorrect() {
|
||||
this.spring.register(MyFilterMultipleAfterConfig.class).autowire();
|
||||
@@ -209,6 +227,34 @@ public class HttpSecurityAddFilterTest {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterAfterMyFilterNotRegisteredYetConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterAfter(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterBeforeMyFilterNotRegisteredYetConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.addFilterBefore(new MyOtherFilter(), MyFilter.class);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MyOtherFilterRelativeToMyFilterBeforeConfig {
|
||||
|
||||
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -61,6 +61,7 @@ import org.springframework.security.messaging.context.SecurityContextChannelInte
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -79,6 +80,7 @@ import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSo
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
|
||||
public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
|
||||
@@ -284,7 +286,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
|
||||
private void assertHandshake(HttpServletRequest request) {
|
||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
|
||||
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
|
||||
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
|
||||
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
|
||||
}
|
||||
@@ -306,7 +308,7 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
|
||||
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
|
||||
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
|
||||
request.setAttribute(CsrfToken.class.getName(), this.token);
|
||||
request.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token));
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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.socket;
|
||||
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
final class TestDeferredCsrfToken implements DeferredCsrfToken {
|
||||
|
||||
private final CsrfToken csrfToken;
|
||||
|
||||
TestDeferredCsrfToken(CsrfToken csrfToken) {
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken get() {
|
||||
return this.csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -70,6 +70,7 @@ import org.springframework.security.messaging.context.SecurityContextChannelInte
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -92,6 +93,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
|
||||
public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
|
||||
@@ -367,7 +369,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
|
||||
private void assertHandshake(HttpServletRequest request) {
|
||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
|
||||
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
|
||||
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
|
||||
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
|
||||
}
|
||||
@@ -389,7 +391,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
|
||||
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
|
||||
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
|
||||
request.setAttribute(CsrfToken.class.getName(), this.token);
|
||||
request.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token));
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
+31
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -61,6 +61,7 @@ import org.springframework.security.test.context.annotation.SecurityTestExecutio
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -77,6 +78,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
@@ -381,12 +383,14 @@ public class WebSocketMessageBrokerConfigTests {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
String csrfAttributeName = CsrfToken.class.getName();
|
||||
String customAttributeName = this.getClass().getName();
|
||||
MvcResult result = mvc.perform(get("/app").requestAttr(csrfAttributeName, this.token)
|
||||
.sessionAttr(customAttributeName, "attributeValue")).andReturn();
|
||||
MvcResult result = mvc.perform(
|
||||
get("/app").requestAttr(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token))
|
||||
.sessionAttr(customAttributeName, "attributeValue"))
|
||||
.andReturn();
|
||||
CsrfToken handshakeToken = (CsrfToken) this.testHandshakeHandler.attributes.get(csrfAttributeName);
|
||||
String handshakeValue = (String) this.testHandshakeHandler.attributes.get(customAttributeName);
|
||||
String sessionValue = (String) result.getRequest().getSession().getAttribute(customAttributeName);
|
||||
assertThat(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThatCsrfToken(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThat(handshakeValue).isEqualTo(sessionValue)
|
||||
.withFailMessage("Explicitly listed session variables are not overridden");
|
||||
}
|
||||
@@ -398,12 +402,13 @@ public class WebSocketMessageBrokerConfigTests {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
String csrfAttributeName = CsrfToken.class.getName();
|
||||
String customAttributeName = this.getClass().getName();
|
||||
MvcResult result = mvc.perform(get("/app/289/tpyx6mde/websocket").requestAttr(csrfAttributeName, this.token)
|
||||
MvcResult result = mvc.perform(get("/app/289/tpyx6mde/websocket")
|
||||
.requestAttr(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(this.token))
|
||||
.sessionAttr(customAttributeName, "attributeValue")).andReturn();
|
||||
CsrfToken handshakeToken = (CsrfToken) this.testHandshakeHandler.attributes.get(csrfAttributeName);
|
||||
String handshakeValue = (String) this.testHandshakeHandler.attributes.get(customAttributeName);
|
||||
String sessionValue = (String) result.getRequest().getSession().getAttribute(customAttributeName);
|
||||
assertThat(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThatCsrfToken(handshakeToken).isEqualTo(this.token).withFailMessage("CsrfToken is populated");
|
||||
assertThat(handshakeValue).isEqualTo(sessionValue)
|
||||
.withFailMessage("Explicitly listed session variables are not overridden");
|
||||
}
|
||||
@@ -526,6 +531,26 @@ public class WebSocketMessageBrokerConfigTests {
|
||||
return SecurityContextHolder.getContextHolderStrategy();
|
||||
}
|
||||
|
||||
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
|
||||
|
||||
private final CsrfToken csrfToken;
|
||||
|
||||
TestDeferredCsrfToken(CsrfToken csrfToken) {
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken get() {
|
||||
return this.csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MessageController {
|
||||
|
||||
|
||||
+6
-6
@@ -8,14 +8,14 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.26")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.28")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.3")
|
||||
api platform("org.junit:junit-bom:5.9.1")
|
||||
api platform("org.junit:junit-bom:5.9.2")
|
||||
api platform("org.mockito:mockito-bom:4.8.1")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.6")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.2.8")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.4.20221013")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.5")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.2.11"
|
||||
api "com.google.inject:guice:3.0"
|
||||
@@ -26,7 +26,7 @@ dependencies {
|
||||
api "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.13.3"
|
||||
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
|
||||
api "io.projectreactor.tools:blockhound:1.0.7.RELEASE"
|
||||
api "jakarta.inject:jakarta.inject-api:1.0.5"
|
||||
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7"
|
||||
@@ -54,7 +54,7 @@ dependencies {
|
||||
api "org.eclipse.jetty:jetty-servlet:9.4.50.v20221201"
|
||||
api "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.14.Final"
|
||||
api "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
api "org.hsqldb:hsqldb:2.7.1"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
api "org.openid4java:openid4java-nodeps:0.9.6"
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
/package-lock.json
|
||||
/node_modules/
|
||||
/*-antora-playbook.yml
|
||||
|
||||
+8
-2
@@ -6,7 +6,13 @@ nav:
|
||||
ext:
|
||||
collector:
|
||||
run:
|
||||
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntora
|
||||
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntoraYml
|
||||
local: true
|
||||
scan:
|
||||
dir: ./build/generateAntora
|
||||
dir: ./build/generated-antora-resources
|
||||
asciidoc:
|
||||
attributes:
|
||||
icondir: icons
|
||||
gh-old-samples-url: 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
gh-samples-url: "https://github.com/spring-projects/spring-security-samples/tree/{gh-tag}"
|
||||
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# The purpose of this Antora playbook is to generate a preview of the docs in the current branch.
|
||||
antora:
|
||||
extensions:
|
||||
- '@antora/collector-extension'
|
||||
site:
|
||||
title: Spring Security
|
||||
url: https://docs.spring.io/spring-security/reference
|
||||
content:
|
||||
sources:
|
||||
- url: ./..
|
||||
branches: HEAD
|
||||
start_path: docs
|
||||
worktrees: true
|
||||
asciidoc:
|
||||
attributes:
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
urls:
|
||||
latest_version_segment: ''
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
|
||||
snapshot: true
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 89 KiB |
@@ -4,12 +4,11 @@
|
||||
The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0.
|
||||
Use 5.8 and the steps below to minimize changes when
|
||||
ifdef::spring-security-version[]
|
||||
xref:6.0.0@migration/index.adoc[updating to 6.0]
|
||||
xref:6.0.2@migration/index.adoc[updating to 6.0].
|
||||
endif::[]
|
||||
ifndef::spring-security-version[]
|
||||
updating to 6.0
|
||||
updating to 6.0.
|
||||
endif::[]
|
||||
.
|
||||
|
||||
== Update to Spring Security 5.8
|
||||
|
||||
|
||||
@@ -400,6 +400,58 @@ http {
|
||||
|
||||
To opt-out of the 6.0 defaults and instead continue to pass `AuthenticationServiceException` on to ``ServerAuthenticationEntryPoint``s, you can follow the same steps as above, except set `rethrowAuthenticationServiceException` to false.
|
||||
|
||||
[[add-configuration-annotation]]
|
||||
== Add `@Configuration` annotation
|
||||
|
||||
In 6.0, `@Configuration` is removed from `@EnableWebFluxSecurity` and `@EnableReactiveMethodSecurity`.
|
||||
|
||||
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
|
||||
For example, `@EnableReactiveMethodSecurity` changes from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableReactiveMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
== Address OAuth2 Client Deprecations
|
||||
|
||||
=== `ServerOAuth2AuthorizedClientExchangeFilterFunction`
|
||||
@@ -424,3 +476,47 @@ The method `setAllowMultipleAuthorizationRequests(...)` has no direct replacemen
|
||||
=== `UnAuthenticatedServerOAuth2AuthorizedClientRepository`
|
||||
|
||||
The class `UnAuthenticatedServerOAuth2AuthorizedClientRepository` has no direct replacement. Usage of the class can be replaced with `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`.
|
||||
|
||||
== Add `@Configuration` to `@Enable*` annotations
|
||||
|
||||
In 6.0, all Spring Security's `@Enable*` annotations had their `@Configuration` removed.
|
||||
While convenient, it was not consistent with the rest of the Spring projects and most notably Spring Framework's `@Enable*` annotations.
|
||||
Additionally, the introduction of support for `@Configuration(proxyBeanMethods=false)` in Spring Framework provides another reason to remove `@Configuration` meta-annotation from Spring Security's `@Enable*` annotations and allow users to opt into their preferred configuration mode.
|
||||
|
||||
The following annotations had their `@Configuration` removed:
|
||||
|
||||
- `@EnableGlobalAuthentication`
|
||||
- `@EnableGlobalMethodSecurity`
|
||||
- `@EnableMethodSecurity`
|
||||
- `@EnableReactiveMethodSecurity`
|
||||
- `@EnableWebSecurity`
|
||||
- `@EnableWebFluxSecurity`
|
||||
|
||||
For example, if you are using `@EnableWebFluxSecurity`, you will need to change:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
And the same applies to every other annotation listed above.
|
||||
|
||||
@@ -2,6 +2,58 @@
|
||||
|
||||
The following steps relate to changes around how to configure `HttpSecurity`, `WebSecurity`, and `AuthenticationManager`.
|
||||
|
||||
[[add-configuration-annotation]]
|
||||
== Add `@Configuration` annotation
|
||||
|
||||
In 6.0, `@Configuration` is removed from `@EnableWebSecurity`, `@EnableMethodSecurity`, `@EnableGlobalMethodSecurity`, and `@EnableGlobalAuthentication`.
|
||||
|
||||
To prepare for this, wherever you are using one of these annotations, you may need to add `@Configuration`.
|
||||
For example, `@EnableMethodSecurity` changes from:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
to:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
open class MyConfiguration {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[use-new-requestmatchers]]
|
||||
== Use the new `requestMatchers` methods
|
||||
|
||||
|
||||
@@ -7,8 +7,20 @@ The following steps relate to changes around how to configure CSRF.
|
||||
In Spring Security 5, the default behavior is that the `CsrfToken` will be loaded on every request.
|
||||
This means that in a typical setup, the `HttpSession` must be read for every request even if it is unnecessary.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Some examples of where it should be unnecessary to read the session include endpoints marked `permitAll()` such as static assets, static HTML pages, single-page applications hosted under the same domain/server, etc.
|
||||
====
|
||||
|
||||
In Spring Security 6, the default is that the lookup of the `CsrfToken` will be deferred until it is needed.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `CsrfToken` is needed whenever a request is made with an HTTP verb that would change the state of the application.
|
||||
This is covered in detail in xref:features/exploits/csrf.adoc#csrf-protection-idempotent[Safe Methods Must be Idempotent].
|
||||
Additionally, it is needed by any request that renders the token to the response, such as a web page with a `<form>` tag that includes a hidden `<input>` for the CSRF token.
|
||||
====
|
||||
|
||||
To opt into the new Spring Security 6 default, the following configuration can be used.
|
||||
|
||||
[[servlet-opt-in-defer-loading-csrf-token]]
|
||||
@@ -18,7 +30,7 @@ To opt into the new Spring Security 6 default, the following configuration can b
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
public SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf");
|
||||
@@ -61,7 +73,156 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
If this breaks your application, then you can explicitly opt into the 5.8 defaults using the following configuration:
|
||||
[NOTE]
|
||||
====
|
||||
When the `CsrfToken` is deferred (the default in Spring Security 6), some applications may break due to the fact that they were designed with non-deferred CSRF tokens.
|
||||
See <<servlet-defer-loading-csrf-token-opt-out,Opt-out Steps>> below for more information.
|
||||
====
|
||||
|
||||
[[servlet-defer-loading-csrf-token-opt-out]]
|
||||
=== Opt-out Steps
|
||||
|
||||
If configuring the `CsrfToken` to be deferred gives you trouble, take a look at these scenarios for optimal opt out behavior:
|
||||
|
||||
==== I am using a Single-Page Application with `CookieCsrfTokenRepository`
|
||||
|
||||
If you are using a single-page app (SPA) to connect to a backend protected by Spring Security along with `CookieCsrfTokenRepository.withHttpOnlyFalse()`, you may find that the CSRF token is no longer returned to your application as a cookie on the first request to the server.
|
||||
|
||||
In this case, you have several options for restoring the behavior your client-side application expects.
|
||||
One option is to add a `Filter` that eagerly renders the `CsrfToken` to the response regardless of which request is made first, like so:
|
||||
|
||||
.Add a `Filter` to return a cookie on the response
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf");
|
||||
http
|
||||
// ...
|
||||
.csrf((csrf) -> csrf
|
||||
.csrfTokenRepository(tokenRepository)
|
||||
.csrfTokenRequestHandler(requestHandler)
|
||||
)
|
||||
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private static final class CsrfCookieFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
// Render the token value to a cookie by causing the deferred token to be loaded
|
||||
csrfToken.getToken();
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
val tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
|
||||
val requestHandler = CsrfTokenRequestAttributeHandler()
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf")
|
||||
http {
|
||||
csrf {
|
||||
csrfTokenRepository = tokenRepository
|
||||
csrfTokenRequestHandler = requestHandler
|
||||
}
|
||||
addFilterAfter<BasicAuthenticationFilter>(CsrfCookieFilter())
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
class CsrfCookieFilter : OncePerRequestFilter() {
|
||||
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
|
||||
val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken
|
||||
// Render the token value to a cookie by causing the deferred token to be loaded
|
||||
csrfToken.token
|
||||
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The option above does not require changes to the single-page application, but does cause the `CsrfToken` to be loaded on every request.
|
||||
If you do not wish to add a `Filter` to eagerly load tokens on every request, additional options are listed below.
|
||||
|
||||
==== I am using a Single-Page Application with `HttpSessionCsrfTokenRepository`
|
||||
|
||||
If you are using sessions, your application will benefit from deferred tokens.
|
||||
Instead of opting out, another option is to add a new `@RestController` with a `/csrf` endpoint, like so:
|
||||
|
||||
.Add a `/csrf` endpoint
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
||||
@GetMapping("/csrf")
|
||||
public CsrfToken csrf(CsrfToken csrfToken) {
|
||||
return csrfToken;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
class CsrfController {
|
||||
|
||||
@GetMapping("/csrf")
|
||||
fun csrf(csrfToken: CsrfToken): CsrfToken {
|
||||
return csrfToken
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
You may consider adding `.requestMatchers("/csrf").permitAll()` if the endpoint above is required prior to authenticating with the server.
|
||||
====
|
||||
|
||||
The `/csrf` endpoint would need to be consumed by the client-side application in order to bootstrap the application for subsequent requests.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Instructions for calling the `/csrf` endpoint on application launch are specific to your client-side framework and therefore outside the scope of this document.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
While this requires changes to your single-page application, the benefit is that the CSRF token is only loaded once and the token can continue to be deferred.
|
||||
This approach works particularly well with applications that use `HttpSessionCsrfTokenRepository` and do benefit from deferred tokens by allowing the `HttpSession` not to be read on every request.
|
||||
====
|
||||
|
||||
If you simply wish to opt out of deferred tokens altogether, that option is listed next.
|
||||
|
||||
==== I need to opt out of deferred tokens for another reason
|
||||
|
||||
If deferred tokens break your application for another reason, then you can explicitly opt into the 5.8 defaults using the following configuration:
|
||||
|
||||
.Explicit Configure `CsrfToken` with 5.8 Defaults
|
||||
====
|
||||
@@ -69,7 +230,7 @@ If this breaks your application, then you can explicitly opt into the 5.8 defaul
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
@@ -86,7 +247,7 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
val requestHandler = CsrfTokenRequestAttributeHandler()
|
||||
// set the name of the attribute the CsrfToken will be populated on
|
||||
requestHandler.setCsrfRequestAttributeName(null)
|
||||
@@ -115,6 +276,12 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
By setting the `csrfRequestAttributeName` to `null`, the `CsrfToken` must first be loaded to determine what attribute name to use.
|
||||
This causes the `CsrfToken` to be loaded on every request.
|
||||
====
|
||||
|
||||
== Protect against CSRF BREACH
|
||||
|
||||
If the steps for <<Defer Loading CsrfToken>> work for you, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration:
|
||||
@@ -240,6 +407,83 @@ open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
----
|
||||
====
|
||||
|
||||
This is the RECOMMENDED way to configure Spring Security to work with a client-side application that uses cookie values, because it continues to allow the response to return a randomized value for the CSRF token in case the application returns HTML or other responses that could be vulnerable to BREACH without your knowledge.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
BREACH protection works to protect the token when it is included in a response body that can be GZIP compressed, which generally does not include headers and cookies.
|
||||
====
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Any token value returned by the server can be used successfully by the client-side application because the underlying (raw) CSRF token does not change.
|
||||
It is not required for an AngularJS (or similar) application to refresh the CSRF token before/after every request.
|
||||
====
|
||||
|
||||
If you simply wish to opt out of CSRF BREACH protection altogether, that option is listed next.
|
||||
|
||||
==== I need to opt out of CSRF BREACH protection for another reason
|
||||
|
||||
If CSRF BREACH protection does not work for you for another reason, you can opt out using the configuration from the <<servlet-opt-in-defer-loading-csrf-token>> section.
|
||||
|
||||
== CSRF BREACH with WebSocket support
|
||||
|
||||
If the steps for <<Protect against CSRF BREACH>> work for normal HTTP requests and you are using xref:servlet/integrations/websocket.adoc[WebSocket Security] support, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` with xref:servlet/integrations/websocket.adoc#websocket-sameorigin-csrf[Stomp headers].
|
||||
|
||||
.WebSocket Security BREACH Protection
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
ChannelInterceptor csrfChannelInterceptor() {
|
||||
return new XorCsrfChannelInterceptor();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun csrfChannelInterceptor(): ChannelInterceptor {
|
||||
return XorCsrfChannelInterceptor()
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="csrfChannelInterceptor"
|
||||
class="org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor"/>
|
||||
----
|
||||
====
|
||||
|
||||
If configuring CSRF BREACH protection for WebSocket Security gives you trouble, you can configure the 5.8 default using the following configuration:
|
||||
|
||||
.Configure WebSocket Security with 5.8 default
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
ChannelInterceptor csrfChannelInterceptor() {
|
||||
return new CsrfChannelInterceptor();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun csrfChannelInterceptor(): ChannelInterceptor {
|
||||
return CsrfChannelInterceptor()
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<b:bean id="csrfChannelInterceptor"
|
||||
class="org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor"/>
|
||||
----
|
||||
====
|
||||
|
||||
@@ -265,6 +265,60 @@ The code below demonstrates how to customize the `RequestCache` implementation t
|
||||
|
||||
include::partial$servlet/architecture/request-cache-continue.adoc[]
|
||||
|
||||
[[requestcache-prevent-saved-request]]
|
||||
==== Prevent the Request From Being Saved
|
||||
|
||||
There are a number of reasons you may want to not store the user's unauthenticated request in the session.
|
||||
You may want to offload that storage onto the user's browser or store it in a database.
|
||||
Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login.
|
||||
|
||||
To do that, you can use {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[the `NullRequestCache` implementation].
|
||||
|
||||
.Prevent the Request From Being Saved
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
RequestCache nullRequestCache = new NullRequestCache();
|
||||
http
|
||||
// ...
|
||||
.requestCache((cache) -> cache
|
||||
.requestCache(nullRequestCache)
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
|
||||
val nullRequestCache = NullRequestCache()
|
||||
http {
|
||||
requestCache {
|
||||
requestCache = nullRequestCache
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http auto-config="true">
|
||||
<!-- ... -->
|
||||
<request-cache ref="nullRequestCache"/>
|
||||
</http>
|
||||
|
||||
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
[[requestcacheawarefilter]]
|
||||
=== RequestCacheAwareFilter
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -548,7 +548,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Advisor customAuthorize(AuthorizationManager<MethodInvocationResult> rules) {
|
||||
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
|
||||
AnnotationMatchingPointcut pattern = new AnnotationMatchingPointcut(MySecurityAnnotation.class);
|
||||
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
|
||||
return interceptor;
|
||||
@@ -564,7 +564,7 @@ class MethodSecurityConfig {
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
fun customAuthorize(rules : AuthorizationManager<MethodInvocationResult>) : Advisor {
|
||||
val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java);
|
||||
val pattern = AnnotationMatchingPointcut(MySecurityAnnotation::class.java);
|
||||
val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules);
|
||||
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
|
||||
return interceptor;
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
[[servlet-saml2login-metadata]]
|
||||
= Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
= Saml 2.0 Metadata
|
||||
|
||||
Spring Security can <<parsing-asserting-party-metadata,parse asserting party metadata>> to produce an `AssertingPartyDetails` instance as well as <<publishing-relying-party-metadata,publish relying party metadata>> from a `RelyingPartyRegistration` instance.
|
||||
|
||||
[[parsing-asserting-party-metadata]]
|
||||
== Parsing `<saml2:IDPSSODescriptor>` metadata
|
||||
|
||||
You can parse an asserting party's metadata xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[using `RelyingPartyRegistrations`].
|
||||
|
||||
When using the OpenSAML vendor support, the resulting `AssertingPartyDetails` will be of type `OpenSamlAssertingPartyDetails`.
|
||||
This means you'll be able to do get the underlying OpenSAML XMLObject by doing the following:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails)
|
||||
registration.getAssertingPartyDetails();
|
||||
EntityDescriptor openSamlEntityDescriptor = details.getEntityDescriptor();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val details: OpenSamlAssertingPartyDetails =
|
||||
registration.getAssertingPartyDetails() as OpenSamlAssertingPartyDetails;
|
||||
val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor();
|
||||
----
|
||||
====
|
||||
|
||||
[[publishing-relying-party-metadata]]
|
||||
== Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
|
||||
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ public class WithMockUserTests {
|
||||
@ContextConfiguration
|
||||
class WithMockUserTests {
|
||||
----
|
||||
====
|
||||
|
||||
This is a basic example of how to setup Spring Security Test. The highlights are:
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): String? {
|
||||
----
|
||||
====
|
||||
|
||||
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2User` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
|
||||
In that case, we can tell Spring Security to include a default `OAuth2User` using the `oauth2Login` xref:servlet/test/mockmvc/request-post-processors.adoc[`RequestPostProcessor`], like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
|
||||
@@ -1,78 +1,25 @@
|
||||
plugins {
|
||||
id 'org.antora' version '1.0.0-alpha.3'
|
||||
id 'org.antora' version '1.0.0'
|
||||
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.docs'
|
||||
apply plugin: 'java'
|
||||
|
||||
antora {
|
||||
version = '3.2.0-alpha.2'
|
||||
playbook = file('local-antora-playbook.yml')
|
||||
options = ['--clean', '--stacktrace']
|
||||
environment = [
|
||||
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
|
||||
'ALGOLIA_APP_ID': '244V8V9FGG',
|
||||
'ALGOLIA_INDEX_NAME': 'security-docs'
|
||||
]
|
||||
dependencies = [
|
||||
'@antora/collector-extension': '1.0.0-alpha.3'
|
||||
]
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-security'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
}
|
||||
|
||||
tasks.register('generateAntora') {
|
||||
group = 'Documentation'
|
||||
description = 'Generates the antora.yml for dynamic properties'
|
||||
doLast {
|
||||
def docsTag = snapshotBuild ? 'current' : project.version
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
|
||||
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
def ymlVersions = resolvedVersions(project.configurations.testRuntimeClasspath).call()
|
||||
.collect(v -> " ${v.getKey()}: ${v.getValue()}")
|
||||
.join('\n')
|
||||
def outputFile = layout.buildDirectory.file('generateAntora/antora.yml').get().asFile
|
||||
mkdir(outputFile.getParentFile())
|
||||
def mainVersion = project.version
|
||||
def prerelease = null
|
||||
def versionComponents = mainVersion.split(/(?=-)/)
|
||||
if (versionComponents.length > 1) {
|
||||
if (versionComponents[1] == '-SNAPSHOT') {
|
||||
mainVersion = versionComponents[0]
|
||||
prerelease = "'-SNAPSHOT'"
|
||||
} else {
|
||||
prerelease = 'true'
|
||||
}
|
||||
}
|
||||
def antoraYmlText = file('antora.yml').text
|
||||
layout.buildDirectory.file('.antora.yml').get().asFile.text = antoraYmlText
|
||||
antoraYmlText = antoraYmlText.lines().collect { l ->
|
||||
if (l.startsWith('version: ')) {
|
||||
return prerelease == null ? "version: '${mainVersion}'" : "version: '${mainVersion}'\nprerelease: ${prerelease}"
|
||||
}
|
||||
if (l.startsWith('title: ')) return "title: ${project.parent.description}"
|
||||
return l == 'ext:' || l.getAt(0) == ' ' ? null : l
|
||||
}.findAll(Objects::nonNull).join('\n')
|
||||
outputFile.text = """$antoraYmlText
|
||||
asciidoc:
|
||||
attributes:
|
||||
icondir: icons
|
||||
gh-old-samples-url: $ghOldSamplesUrl
|
||||
gh-samples-url: $ghSamplesUrl
|
||||
gh-url: $ghUrl
|
||||
security-api-url: $securityApiUrl
|
||||
security-reference-url: $securityReferenceUrl
|
||||
spring-framework-api-url: $springFrameworkApiUrl
|
||||
spring-framework-reference-url: $springFrameworkReferenceUrl
|
||||
spring-security-version: ${project.version}
|
||||
${ymlVersions}
|
||||
"""
|
||||
}
|
||||
tasks.named("generateAntoraYml") {
|
||||
asciidocAttributes = project.provider( { generateAttributes() } )
|
||||
asciidocAttributes.putAll(providers.provider( { resolvedVersions(project.configurations.testRuntimeClasspath) }))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -82,12 +29,33 @@ dependencies {
|
||||
testImplementation 'org.springframework:spring-core'
|
||||
}
|
||||
|
||||
def generateAttributes() {
|
||||
def docsTag = snapshotBuild ? 'current' : project.version
|
||||
def ghTag = snapshotBuild ? 'main' : project.version
|
||||
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
|
||||
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
|
||||
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
|
||||
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
|
||||
def securityApiUrl = "$securityDocsUrl/api/"
|
||||
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
|
||||
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
|
||||
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
|
||||
|
||||
return ['gh-old-samples-url': ghOldSamplesUrl.toString(),
|
||||
'gh-samples-url': ghSamplesUrl.toString(),
|
||||
'gh-url': ghUrl.toString(),
|
||||
'security-api-url': securityApiUrl.toString(),
|
||||
'security-reference-url': securityReferenceUrl.toString(),
|
||||
'spring-framework-api-url': springFrameworkApiUrl.toString(),
|
||||
'spring-framework-reference-url': springFrameworkReferenceUrl.toString(),
|
||||
'spring-security-version': project.version]
|
||||
+ resolvedVersions(project.configurations.testRuntimeClasspath)
|
||||
}
|
||||
|
||||
def resolvedVersions(Configuration configuration) {
|
||||
return {
|
||||
configuration.resolvedConfiguration
|
||||
return configuration.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.collectEntries { [(it.name + '-version'): it.moduleVersion.id.version] }
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
aspectjVersion=1.9.9.1
|
||||
aspectjVersion=1.9.19
|
||||
springJavaformatVersion=0.0.35
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.24
|
||||
springFrameworkVersion=5.3.25
|
||||
openSamlVersion=3.4.6
|
||||
version=5.8.1
|
||||
version=5.8.2
|
||||
kotlinVersion=1.7.22
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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.messaging.web.csrf;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
|
||||
/**
|
||||
* {@link ChannelInterceptor} that validates a CSRF token masked by the
|
||||
* {@link org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler} in
|
||||
* the header of any {@link SimpMessageType#CONNECT} message.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 5.8
|
||||
*/
|
||||
public final class XorCsrfChannelInterceptor implements ChannelInterceptor {
|
||||
|
||||
private final MessageMatcher<Object> matcher = new SimpMessageTypeMatcher(SimpMessageType.CONNECT);
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
if (!this.matcher.matches(message)) {
|
||||
return message;
|
||||
}
|
||||
Map<String, Object> sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
|
||||
CsrfToken expectedToken = (sessionAttributes != null)
|
||||
? (CsrfToken) sessionAttributes.get(CsrfToken.class.getName()) : null;
|
||||
if (expectedToken == null) {
|
||||
throw new MissingCsrfTokenException(null);
|
||||
}
|
||||
String actualToken = SimpMessageHeaderAccessor.wrap(message)
|
||||
.getFirstNativeHeader(expectedToken.getHeaderName());
|
||||
String actualTokenValue = XorCsrfTokenUtils.getTokenValue(actualToken, expectedToken.getToken());
|
||||
boolean csrfCheckPassed = equalsConstantTime(expectedToken.getToken(), actualTokenValue);
|
||||
if (!csrfCheckPassed) {
|
||||
throw new InvalidCsrfTokenException(expectedToken, actualToken);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant time comparison to prevent against timing attacks.
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @return
|
||||
*/
|
||||
private static boolean equalsConstantTime(String expected, String actual) {
|
||||
if (expected == actual) {
|
||||
return true;
|
||||
}
|
||||
if (expected == null || actual == null) {
|
||||
return false;
|
||||
}
|
||||
// Encode after ensure that the string is not null
|
||||
byte[] expectedBytes = Utf8.encode(expected);
|
||||
byte[] actualBytes = Utf8.encode(actual);
|
||||
return MessageDigest.isEqual(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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.messaging.web.csrf;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
|
||||
/**
|
||||
* Copied from
|
||||
* {@link org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler}.
|
||||
*
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-projects/spring-security/issues/12378">gh-12378</a>
|
||||
*/
|
||||
final class XorCsrfTokenUtils {
|
||||
|
||||
private XorCsrfTokenUtils() {
|
||||
}
|
||||
|
||||
static String getTokenValue(String actualToken, String token) {
|
||||
byte[] actualBytes;
|
||||
try {
|
||||
actualBytes = Base64.getUrlDecoder().decode(actualToken);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
return xoredCsrf;
|
||||
}
|
||||
|
||||
}
|
||||
+18
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -24,15 +24,18 @@ import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
/**
|
||||
* Copies a CsrfToken from the HttpServletRequest's attributes to the WebSocket
|
||||
* attributes. This is used as the expected CsrfToken when validating connection requests
|
||||
* to ensure only the same origin connects.
|
||||
* Loads a CsrfToken from the HttpServletRequest and HttpServletResponse to populate the
|
||||
* WebSocket attributes. This is used as the expected CsrfToken when validating connection
|
||||
* requests to ensure only the same origin connects.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Steve Riesenberg
|
||||
* @since 4.0
|
||||
*/
|
||||
public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor {
|
||||
@@ -41,11 +44,19 @@ public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
|
||||
Map<String, Object> attributes) {
|
||||
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
CsrfToken token = (CsrfToken) httpRequest.getAttribute(CsrfToken.class.getName());
|
||||
if (token == null) {
|
||||
DeferredCsrfToken deferredCsrfToken = (DeferredCsrfToken) httpRequest
|
||||
.getAttribute(DeferredCsrfToken.class.getName());
|
||||
if (deferredCsrfToken == null) {
|
||||
return true;
|
||||
}
|
||||
attributes.put(CsrfToken.class.getName(), token);
|
||||
CsrfToken csrfToken = deferredCsrfToken.get();
|
||||
// Ensure the values of the CsrfToken are copied into a new token so the old token
|
||||
// is available for garbage collection.
|
||||
// This is required because the original token could hold a reference to the
|
||||
// HttpServletRequest/Response of the handshake request.
|
||||
CsrfToken resolvedCsrfToken = new DefaultCsrfToken(csrfToken.getHeaderName(), csrfToken.getParameterName(),
|
||||
csrfToken.getToken());
|
||||
attributes.put(CsrfToken.class.getName(), resolvedCsrfToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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.messaging.web.csrf;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link XorCsrfChannelInterceptor}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class XorCsrfChannelInterceptorTests {
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = "wpe7zB62-NCpcA==";
|
||||
|
||||
private static final String INVALID_XOR_CSRF_TOKEN_VALUE = "KneoaygbRZtfHQ==";
|
||||
|
||||
private CsrfToken token;
|
||||
|
||||
private SimpMessageHeaderAccessor messageHeaders;
|
||||
|
||||
private MessageChannel channel;
|
||||
|
||||
private XorCsrfChannelInterceptor interceptor;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.token = new DefaultCsrfToken("header", "param", "token");
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
|
||||
this.messageHeaders.setSessionAttributes(new HashMap<>());
|
||||
this.channel = mock(MessageChannel.class);
|
||||
this.interceptor = new XorCsrfChannelInterceptor();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithValidTokenThenSuccess() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithInvalidTokenThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), INVALID_XOR_CSRF_TOKEN_VALUE);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithNoTokenThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithMissingTokenThenThrowsMissingCsrfTokenException() {
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(MissingCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenConnectWithNullSessionAttributesThenThrowsMissingCsrfTokenException() {
|
||||
this.messageHeaders.setSessionAttributes(null);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(MissingCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenAckThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT_ACK);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenDisconnectThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.DISCONNECT);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenHeartbeatThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.HEARTBEAT);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenMessageThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenOtherThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.OTHER);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenUnsubscribeThenIgnores() {
|
||||
this.messageHeaders = SimpMessageHeaderAccessor.create(SimpMessageType.UNSUBSCRIBE);
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
private Message<String> message() {
|
||||
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
|
||||
}
|
||||
|
||||
}
|
||||
+32
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -31,6 +31,7 @@ import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -72,10 +73,38 @@ public class CsrfTokenHandshakeInterceptorTests {
|
||||
@Test
|
||||
public void beforeHandshake() throws Exception {
|
||||
CsrfToken token = new DefaultCsrfToken("header", "param", "token");
|
||||
this.httpRequest.setAttribute(CsrfToken.class.getName(), token);
|
||||
this.httpRequest.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(token));
|
||||
this.interceptor.beforeHandshake(this.request, this.response, this.wsHandler, this.attributes);
|
||||
assertThat(this.attributes.keySet()).containsOnly(CsrfToken.class.getName());
|
||||
assertThat(this.attributes.values()).containsOnly(token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.attributes.get(CsrfToken.class.getName());
|
||||
assertThat(csrfToken.getHeaderName()).isEqualTo(token.getHeaderName());
|
||||
assertThat(csrfToken.getParameterName()).isEqualTo(token.getParameterName());
|
||||
assertThat(csrfToken.getToken()).isEqualTo(token.getToken());
|
||||
// Ensure the values of the CsrfToken are copied into a new token so the old token
|
||||
// is available for garbage collection.
|
||||
// This is required because the original token could hold a reference to the
|
||||
// HttpServletRequest/Response of the handshake request.
|
||||
assertThat(csrfToken).isNotSameAs(token);
|
||||
}
|
||||
|
||||
private static final class TestDeferredCsrfToken implements DeferredCsrfToken {
|
||||
|
||||
private final CsrfToken csrfToken;
|
||||
|
||||
private TestDeferredCsrfToken(CsrfToken csrfToken) {
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken get() {
|
||||
return this.csrfToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -60,6 +60,7 @@ import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.cache.support.SimpleValueWrapper;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -700,6 +701,7 @@ public class NimbusJwtDecoderTests {
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
Cache cache = mock(Cache.class);
|
||||
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
|
||||
given(cache.get(eq(JWK_SET_URI))).willReturn(new SimpleValueWrapper(JWK_SET));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK));
|
||||
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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.saml2.provider.service.registration;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
class OpenSamlMetadataRelyingPartyRegistrationConverter {
|
||||
|
||||
private final OpenSamlMetadataAssertingPartyDetailsConverter converter = new OpenSamlMetadataAssertingPartyDetailsConverter();
|
||||
|
||||
Collection<RelyingPartyRegistration.Builder> convert(InputStream source) {
|
||||
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
|
||||
for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) {
|
||||
builders.add(new RelyingPartyRegistration.Builder(builder));
|
||||
}
|
||||
return builders;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -89,8 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
|
||||
@Override
|
||||
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
|
||||
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||
return RelyingPartyRegistration
|
||||
.withAssertingPartyDetails(this.converter.convert(inputMessage.getBody()).iterator().next().build());
|
||||
return new RelyingPartyRegistration.Builder(this.converter.convert(inputMessage.getBody()).iterator().next());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+25
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -30,6 +30,7 @@ import java.util.function.Function;
|
||||
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -986,6 +987,14 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private AssertingPartyDetails.Builder assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder();
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
private Builder(AssertingPartyDetails.Builder assertingPartyDetailsBuilder) {
|
||||
this.assertingPartyDetailsBuilder = assertingPartyDetailsBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the asserting party's <a href=
|
||||
* "https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.9%20EntityDescriptor">EntityID</a>.
|
||||
@@ -1048,7 +1057,7 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private String registrationId;
|
||||
private Converter<ProviderDetails, String> registrationId = ProviderDetails::getEntityId;
|
||||
|
||||
private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
|
||||
|
||||
@@ -1068,12 +1077,17 @@ public final class RelyingPartyRegistration {
|
||||
|
||||
private String nameIdFormat = null;
|
||||
|
||||
private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();
|
||||
private ProviderDetails.Builder providerDetails;
|
||||
|
||||
private Collection<org.springframework.security.saml2.credentials.Saml2X509Credential> credentials = new LinkedHashSet<>();
|
||||
|
||||
private Builder(String registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
this.registrationId = (party) -> registrationId;
|
||||
this.providerDetails = new ProviderDetails.Builder();
|
||||
}
|
||||
|
||||
Builder(AssertingPartyDetails.Builder builder) {
|
||||
this.providerDetails = new ProviderDetails.Builder(builder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1082,7 +1096,7 @@ public final class RelyingPartyRegistration {
|
||||
* @return this object
|
||||
*/
|
||||
public Builder registrationId(String id) {
|
||||
this.registrationId = id;
|
||||
this.registrationId = (party) -> id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1405,11 +1419,12 @@ public final class RelyingPartyRegistration {
|
||||
this.singleLogoutServiceBindings.add(Saml2MessageBinding.POST);
|
||||
}
|
||||
|
||||
return new RelyingPartyRegistration(this.registrationId, this.entityId,
|
||||
this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding,
|
||||
this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation,
|
||||
this.singleLogoutServiceBindings, this.providerDetails.build(), this.nameIdFormat, this.credentials,
|
||||
this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
ProviderDetails party = this.providerDetails.build();
|
||||
String registrationId = this.registrationId.convert(party);
|
||||
return new RelyingPartyRegistration(registrationId, this.entityId, this.assertionConsumerServiceLocation,
|
||||
this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation,
|
||||
this.singleLogoutServiceResponseLocation, this.singleLogoutServiceBindings, party,
|
||||
this.nameIdFormat, this.credentials, this.decryptionX509Credentials, this.signingX509Credentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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,13 +18,11 @@ package org.springframework.security.saml2.provider.service.registration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails;
|
||||
|
||||
/**
|
||||
* A utility class for constructing instances of {@link RelyingPartyRegistration}
|
||||
@@ -36,7 +34,7 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
*/
|
||||
public final class RelyingPartyRegistrations {
|
||||
|
||||
private static final OpenSamlMetadataAssertingPartyDetailsConverter assertingPartyMetadataConverter = new OpenSamlMetadataAssertingPartyDetailsConverter();
|
||||
private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
|
||||
|
||||
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
@@ -215,11 +213,7 @@ public final class RelyingPartyRegistrations {
|
||||
* @since 5.7
|
||||
*/
|
||||
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
|
||||
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
|
||||
for (AssertingPartyDetails.Builder builder : assertingPartyMetadataConverter.convert(source)) {
|
||||
builders.add(RelyingPartyRegistration.withAssertingPartyDetails(builder.build()));
|
||||
}
|
||||
return builders;
|
||||
return relyingPartyRegistrationConverter.convert(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
@@ -674,7 +675,7 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
|
||||
attributeMap.addAll(attribute.getName(), attributeValues);
|
||||
}
|
||||
}
|
||||
return attributeMap;
|
||||
return new LinkedHashMap<>(attributeMap); // gh-11785
|
||||
}
|
||||
|
||||
private static List<String> getSessionIndexes(Assertion assertion) {
|
||||
|
||||
+19
@@ -32,6 +32,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
@@ -68,6 +69,7 @@ import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
@@ -349,6 +351,23 @@ public class OpenSaml4AuthenticationProviderTests {
|
||||
assertThat(principal.getSessionIndexes()).contains("session-index");
|
||||
}
|
||||
|
||||
// gh-11785
|
||||
@Test
|
||||
public void deserializeWhenAssertionContainsAttributesThenWorks() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ClassLoader loader = getClass().getClassLoader();
|
||||
mapper.registerModules(SecurityJackson2Modules.getModules(loader));
|
||||
Response response = response();
|
||||
Assertion assertion = assertion();
|
||||
List<AttributeStatement> attributes = TestOpenSamlObjects.attributeStatements();
|
||||
assertion.getAttributeStatements().addAll(attributes);
|
||||
response.getAssertions().add(signed(assertion));
|
||||
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||
Authentication authentication = this.provider.authenticate(token);
|
||||
String result = mapper.writeValueAsString(authentication);
|
||||
mapper.readValue(result, Authentication.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAssertionContainsCustomAttributesThenItSucceeds() {
|
||||
Response response = response();
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* https://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.saml2.provider.service.registration;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
|
||||
|
||||
private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
|
||||
|
||||
private String metadata;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
ClassPathResource resource = new ClassPathResource("test-metadata.xml");
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
|
||||
this.metadata = reader.lines().collect(Collectors.joining());
|
||||
}
|
||||
}
|
||||
|
||||
// gh-12667
|
||||
@Test
|
||||
public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception {
|
||||
try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) {
|
||||
this.converter.convert(source)
|
||||
.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails())
|
||||
.isInstanceOf(OpenSamlAssertingPartyDetails.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -59,6 +59,8 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -146,6 +148,8 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
|
||||
private AuthenticationFailureHandler failureHandler;
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
|
||||
@@ -183,6 +187,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
context.setAuthentication(targetUser);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", targetUser));
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
// redirect to target url
|
||||
this.successHandler.onAuthenticationSuccess(request, response, targetUser);
|
||||
}
|
||||
@@ -200,6 +205,7 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
context.setAuthentication(originalUser);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", originalUser));
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
// redirect to target url
|
||||
this.successHandler.onAuthenticationSuccess(request, response, originalUser);
|
||||
return;
|
||||
@@ -525,6 +531,19 @@ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEv
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
|
||||
* switch user success. The default is
|
||||
* {@link RequestAttributeSecurityContextRepository}.
|
||||
* @param securityContextRepository the {@link SecurityContextRepository} to use.
|
||||
* Cannot be null.
|
||||
* @since 5.7.7
|
||||
*/
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
private static RequestMatcher createMatcher(String pattern) {
|
||||
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -108,6 +108,7 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
|
||||
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
|
||||
this.requestHandler.handle(request, response, deferredCsrfToken::get);
|
||||
if (!this.requireCsrfProtectionMatcher.matches(request)) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -226,7 +226,8 @@ public class DefaultSavedRequest implements SavedRequest {
|
||||
if (!propertyEquals(this.pathInfo, request.getPathInfo())) {
|
||||
return false;
|
||||
}
|
||||
if (!propertyEquals(this.queryString, request.getQueryString())) {
|
||||
if (!propertyEquals(createQueryString(this.queryString, this.matchingRequestParameterName),
|
||||
request.getQueryString())) {
|
||||
return false;
|
||||
}
|
||||
if (!propertyEquals(this.requestURI, request.getRequestURI())) {
|
||||
|
||||
+60
@@ -16,15 +16,18 @@
|
||||
|
||||
package org.springframework.security.web.authentication.switchuser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
@@ -47,11 +50,15 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.util.FieldUtils;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -503,6 +510,59 @@ public class SwitchUserFilterTests {
|
||||
filter.setSwitchFailureUrl("/foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenDefaultSecurityContextRepositoryThenRequestAttributeRepository() {
|
||||
SwitchUserFilter switchUserFilter = new SwitchUserFilter();
|
||||
assertThat(ReflectionTestUtils.getField(switchUserFilter, "securityContextRepository"))
|
||||
.isInstanceOf(RequestAttributeSecurityContextRepository.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenSwitchUserThenSaveSecurityContext() throws ServletException, IOException {
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain filterChain = new MockFilterChain();
|
||||
request.setParameter(SwitchUserFilter.SPRING_SECURITY_SWITCH_USERNAME_KEY, "jacklord");
|
||||
request.setRequestURI("/login/impersonate");
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
filter.setSecurityContextRepository(securityContextRepository);
|
||||
filter.setUserDetailsService(new MockUserDetailsService());
|
||||
filter.setTargetUrl("/target");
|
||||
filter.afterPropertiesSet();
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(securityContextRepository).saveContext(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenExitUserThenSaveSecurityContext() throws ServletException, IOException {
|
||||
UsernamePasswordAuthenticationToken source = UsernamePasswordAuthenticationToken.authenticated("dano",
|
||||
"hawaii50", ROLES_12);
|
||||
// set current user (Admin)
|
||||
List<GrantedAuthority> adminAuths = new ArrayList<>(ROLES_12);
|
||||
adminAuths.add(new SwitchUserGrantedAuthority("PREVIOUS_ADMINISTRATOR", source));
|
||||
UsernamePasswordAuthenticationToken admin = UsernamePasswordAuthenticationToken.authenticated("jacklord",
|
||||
"hawaii50", adminAuths);
|
||||
SecurityContextHolder.getContext().setAuthentication(admin);
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain filterChain = new MockFilterChain();
|
||||
request.setParameter(SwitchUserFilter.SPRING_SECURITY_SWITCH_USERNAME_KEY, "jacklord");
|
||||
request.setRequestURI("/logout/impersonate");
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
filter.setSecurityContextRepository(securityContextRepository);
|
||||
filter.setUserDetailsService(new MockUserDetailsService());
|
||||
filter.setTargetUrl("/target");
|
||||
filter.afterPropertiesSet();
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(securityContextRepository).saveContext(any(), any(), any());
|
||||
}
|
||||
|
||||
private class MockUserDetailsService implements UserDetailsService {
|
||||
|
||||
private String password = "hawaii50";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -126,11 +126,12 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterAccessDeniedNoTokenPresent() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.deniedHandler).handle(eq(this.request), eq(this.response), any(InvalidCsrfTokenException.class));
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -138,12 +139,13 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterAccessDeniedIncorrectTokenPresent() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken() + " INVALID");
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.deniedHandler).handle(eq(this.request), eq(this.response), any(InvalidCsrfTokenException.class));
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -151,12 +153,13 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterAccessDeniedIncorrectTokenPresentHeader() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.addHeader(this.token.getHeaderName(), this.token.getToken() + " INVALID");
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.deniedHandler).handle(eq(this.request), eq(this.response), any(InvalidCsrfTokenException.class));
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -165,13 +168,14 @@ public class CsrfFilterTests {
|
||||
public void doFilterAccessDeniedIncorrectTokenPresentHeaderPreferredOverParameter()
|
||||
throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken());
|
||||
this.request.addHeader(this.token.getHeaderName(), this.token.getToken() + " INVALID");
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.deniedHandler).handle(eq(this.request), eq(this.response), any(InvalidCsrfTokenException.class));
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -179,11 +183,12 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterNotCsrfRequestExistingToken() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -191,11 +196,12 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterNotCsrfRequestGenerateToken() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, true));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -203,12 +209,13 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterIsCsrfRequestExistingTokenHeader() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.addHeader(this.token.getHeaderName(), this.token.getToken());
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -217,13 +224,14 @@ public class CsrfFilterTests {
|
||||
public void doFilterIsCsrfRequestExistingTokenHeaderPreferredOverInvalidParam()
|
||||
throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken() + " INVALID");
|
||||
this.request.addHeader(this.token.getHeaderName(), this.token.getToken());
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -231,12 +239,13 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterIsCsrfRequestExistingToken() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken());
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
verify(this.tokenRepository, never()).saveToken(any(CsrfToken.class), any(HttpServletRequest.class),
|
||||
@@ -246,12 +255,13 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterIsCsrfRequestGenerateToken() throws ServletException, IOException {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, true));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken());
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
// LazyCsrfTokenRepository requires the response as an attribute
|
||||
assertThat(this.request.getAttribute(HttpServletResponse.class.getName())).isEqualTo(this.response);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
@@ -316,11 +326,12 @@ public class CsrfFilterTests {
|
||||
this.filter = new CsrfFilter(this.tokenRepository);
|
||||
this.filter.setRequireCsrfProtectionMatcher(this.requestMatcher);
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -344,22 +355,24 @@ public class CsrfFilterTests {
|
||||
given(token.getToken()).willReturn(null);
|
||||
given(token.getHeaderName()).willReturn(this.token.getHeaderName());
|
||||
given(token.getParameterName()).willReturn(this.token.getParameterName());
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
filter.doFilterInternal(this.request, this.response, this.filterChain);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenRequestHandlerThenUsed() throws Exception {
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
CsrfTokenRequestHandler requestHandler = mock(CsrfTokenRequestHandler.class);
|
||||
this.filter = createCsrfFilter(this.tokenRepository);
|
||||
this.filter.setRequestHandler(requestHandler);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken());
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.tokenRepository).loadDeferredToken(this.request, this.response);
|
||||
verify(requestHandler).handle(eq(this.request), eq(this.response), any());
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
@@ -368,14 +381,15 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterWhenXorCsrfTokenRequestAttributeHandlerAndValidTokenThenSuccess() throws Exception {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
|
||||
requestHandler.setCsrfRequestAttributeName(this.token.getParameterName());
|
||||
this.filter.setRequestHandler(requestHandler);
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(this.token.getParameterName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
|
||||
@@ -394,12 +408,13 @@ public class CsrfFilterTests {
|
||||
@Test
|
||||
public void doFilterWhenXorCsrfTokenRequestAttributeHandlerAndRawTokenThenAccessDeniedException() throws Exception {
|
||||
given(this.requestMatcher.matches(this.request)).willReturn(true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(this.token, false));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(this.token, false);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
|
||||
this.filter.setRequestHandler(requestHandler);
|
||||
this.request.setParameter(this.token.getParameterName(), this.token.getToken());
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.deniedHandler).handle(eq(this.request), eq(this.response), any(AccessDeniedException.class));
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -424,10 +439,11 @@ public class CsrfFilterTests {
|
||||
requestHandler.setCsrfRequestAttributeName(csrfAttrName);
|
||||
filter.setRequestHandler(requestHandler);
|
||||
CsrfToken expectedCsrfToken = mock(CsrfToken.class);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response))
|
||||
.willReturn(new TestDeferredCsrfToken(expectedCsrfToken, true));
|
||||
DeferredCsrfToken deferredCsrfToken = new TestDeferredCsrfToken(expectedCsrfToken, true);
|
||||
given(this.tokenRepository.loadDeferredToken(this.request, this.response)).willReturn(deferredCsrfToken);
|
||||
|
||||
filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
|
||||
verifyNoInteractions(expectedCsrfToken);
|
||||
CsrfToken tokenFromRequest = (CsrfToken) this.request.getAttribute(csrfAttrName);
|
||||
|
||||
+18
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -115,6 +115,23 @@ public class HttpSessionRequestCacheTests {
|
||||
cache.setMatchingRequestParameterName("success");
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setQueryString("success"); // gh-12665
|
||||
requestToMatch.setParameter("success", "");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
assertThat(matchingRequest).isNotNull();
|
||||
}
|
||||
|
||||
// gh-12665
|
||||
@Test
|
||||
public void getMatchingRequestWhenMatchingRequestParameterNameSetAndParameterExistAndQueryThenLookedUp() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setQueryString("param=true");
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
cache.setMatchingRequestParameterName("success");
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setQueryString("param=true&success");
|
||||
requestToMatch.setParameter("success", "");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
|
||||
Reference in New Issue
Block a user