Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ef659a643 | |||
| f9f6bd3e3e | |||
| a93cc3f0a0 | |||
| 2fcf48a11b | |||
| 0398d86f39 | |||
| c9112dfaab | |||
| 4156766fe0 | |||
| f125785328 | |||
| 4643c0d999 | |||
| d2d82b5e94 | |||
| d4d03ea146 | |||
| f2c4656abd | |||
| edff50cb08 | |||
| 6ba88e6545 | |||
| a3855ea092 | |||
| f57b433fb1 | |||
| 30566eba9e | |||
| c373f6afd9 | |||
| d245a54951 | |||
| 5f599d3d77 | |||
| f49a933490 | |||
| 8961264ec5 | |||
| aaa6983fda | |||
| 6f1c9184db | |||
| 8fb9c91acf | |||
| 01c3a25d7f | |||
| e8a251aa7f | |||
| e008b93715 | |||
| 8174ed2291 | |||
| e7c1e2d78d | |||
| 3f2ff0572e | |||
| d127ad310f | |||
| b0c823970b | |||
| 67a0a1a010 | |||
| bb644fe371 | |||
| eef519adc5 | |||
| a1b282ff03 | |||
| 2db4430dcd | |||
| cedb9fd199 | |||
| 0baf650f38 | |||
| 000b4bc495 | |||
| c4f68d83bf | |||
| 5ccf414f02 | |||
| 82c86b822f | |||
| e59f71f036 | |||
| ce222de7e6 | |||
| 4f3faa78f7 | |||
| 8ab0bf76a3 | |||
| 2876605324 | |||
| bf2951b5af | |||
| ca1961d35e | |||
| 821db0a1ea | |||
| 6f5c633241 | |||
| 45b81b194b | |||
| 8ca726f4fa | |||
| e7d65966fd | |||
| 0d4c619648 | |||
| 2b36499700 | |||
| 7dd5cc6082 | |||
| 52ed165476 | |||
| c15f45d9ee | |||
| 3572111cf5 | |||
| 7409d14504 | |||
| 7dc3759f5d | |||
| 4a8961bc80 | |||
| aad3e61f4d | |||
| 4264258c31 | |||
| eaa9692c0c | |||
| 51d5fb9c03 | |||
| 179428f7da | |||
| 13487be268 | |||
| 1363a4eece | |||
| c3563df25a | |||
| 66711f2365 | |||
| 33e72b35f9 | |||
| c306df9b46 | |||
| 9603a68e8f | |||
| d42405de42 | |||
| 7db357d36a | |||
| 1084cdb75a | |||
| 5beabbe357 | |||
| f5bc6ce665 | |||
| 6f72c07747 | |||
| 108b03da3a | |||
| 4cbb057b97 | |||
| 20b745e0b7 | |||
| 8758a00e90 | |||
| c0f7cecc6d | |||
| 22ffa833ca | |||
| e0697de7b2 | |||
| 090c5f96ce | |||
| f41b77a4db | |||
| 4d2dab9b6b | |||
| 21ceb333a8 | |||
| 6f43104eb3 | |||
| 2028507bf8 | |||
| 4e80338a9b | |||
| 2c8854bb7f | |||
| 5b6b3d585f | |||
| d1fc789ae2 | |||
| ae46032ced | |||
| ffdb397830 | |||
| d2d2ad6e96 | |||
| 88a8ef647b | |||
| 748e912685 | |||
| 5e1db6a771 | |||
| 225dc593a8 | |||
| c308e4665a | |||
| 930cc68768 | |||
| 9535566f84 | |||
| 27b3f4d403 | |||
| 9c0a35a6f6 | |||
| fda0e9a2b6 | |||
| 3cfaf0d11d | |||
| 4551025992 | |||
| 892bbcfe0f | |||
| 7497ff9023 | |||
| 7bd6deccc3 | |||
| b9f9139f5e | |||
| 5406fed5dc | |||
| fbfa13bd47 | |||
| b3aaa4d5ff | |||
| 992bda32f9 | |||
| 0c6b427132 | |||
| 0f7d17bae6 | |||
| 44d5cb0a63 | |||
| c11248b589 | |||
| afb28348cb | |||
| 972075677f | |||
| 314ae69b70 | |||
| 11614edcfe |
@@ -55,6 +55,7 @@ jobs:
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Set up gradle user name
|
||||
|
||||
+7
-6
@@ -30,7 +30,7 @@ In the instructions below, https://vimeo.com/34436402[`./gradlew`] is invoked fr
|
||||
a cross-platform, self-contained bootstrap mechanism for the build.
|
||||
|
||||
=== Prerequisites
|
||||
https://help.github.com/set-up-git-redirect[Git] and the https://www.oracle.com/technetwork/java/javase/downloads[JDK17 build].
|
||||
https://docs.github.com/en/get-started/quickstart/set-up-git[Git] and the https://www.oracle.com/java/technologies/downloads/#java17[JDK17 build].
|
||||
|
||||
Be sure that your `JAVA_HOME` environment variable points to the `jdk-17` folder extracted from the JDK download.
|
||||
|
||||
@@ -40,13 +40,15 @@ Be sure that your `JAVA_HOME` environment variable points to the `jdk-17` folder
|
||||
git clone git@github.com:spring-projects/spring-security.git
|
||||
----
|
||||
|
||||
=== Install all spring-\* jars into your local Maven cache
|
||||
=== Install all `spring-*.jar` into your local Maven repository.
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
./gradlew publishToMavenLocal
|
||||
----
|
||||
|
||||
=== Compile and test; build all jars, distribution zips, and docs
|
||||
=== Compile and test; build all JARs, distribution zips, and docs
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
./gradlew build
|
||||
@@ -59,18 +61,17 @@ You can build the reference docs for this branch by running the following comman
|
||||
./gradlew :spring-security-docs:antora
|
||||
----
|
||||
|
||||
That command publishes the docs site to the _docs/build/site_ directory.
|
||||
That command publishes the docs site to the `_docs/build/site_` directory.
|
||||
The https://github.com/spring-projects/spring-security/tree/docs-build[playbook branch] describes how to build the reference docs in detail.
|
||||
|
||||
Discover more commands with `./gradlew tasks`.
|
||||
See also the https://github.com/spring-projects/spring-framework/wiki/Gradle-build-and-release-FAQ[Gradle build and release FAQ].
|
||||
|
||||
== Getting Support
|
||||
Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring Security tags on Stack Overflow].
|
||||
https://spring.io/services[Commercial support] is available too.
|
||||
|
||||
== Contributing
|
||||
https://help.github.com/articles/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc[contributor guidelines] for details.
|
||||
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc[contributor guidelines] for details.
|
||||
|
||||
== License
|
||||
Spring Security is Open Source software released under the
|
||||
|
||||
+1
-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.6-rc1"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.netflix.nebula:nebula-project-plugin:8.2.0"
|
||||
|
||||
@@ -87,19 +87,19 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.1'
|
||||
implementation 'io.projectreactor:reactor-core:3.5.3'
|
||||
implementation 'org.gretty:gretty:3.0.9'
|
||||
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
|
||||
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
|
||||
|
||||
+16
-2
@@ -61,6 +61,14 @@ import org.springframework.util.ClassUtils;
|
||||
public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuilder<B>>
|
||||
extends SecurityConfigurerAdapter<AuthenticationManager, B> {
|
||||
|
||||
private static final String APACHEDS_CLASSNAME = "org.apache.directory.server.core.DefaultDirectoryService";
|
||||
|
||||
private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer";
|
||||
|
||||
private static final boolean apacheDsPresent;
|
||||
|
||||
private static final boolean unboundIdPresent;
|
||||
|
||||
private String groupRoleAttribute = "cn";
|
||||
|
||||
private String groupSearchBase = "";
|
||||
@@ -91,6 +99,12 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
|
||||
|
||||
private GrantedAuthoritiesMapper authoritiesMapper;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = LdapAuthenticationProviderConfigurer.class.getClassLoader();
|
||||
apacheDsPresent = ClassUtils.isPresent(APACHEDS_CLASSNAME, classLoader);
|
||||
unboundIdPresent = ClassUtils.isPresent(UNBOUNDID_CLASSNAME, classLoader);
|
||||
}
|
||||
|
||||
private LdapAuthenticationProvider build() throws Exception {
|
||||
BaseLdapPathContextSource contextSource = getContextSource();
|
||||
LdapAuthenticator ldapAuthenticator = createLdapAuthenticator(contextSource);
|
||||
@@ -562,13 +576,13 @@ public class LdapAuthenticationProviderConfigurer<B extends ProviderManagerBuild
|
||||
}
|
||||
|
||||
private void startEmbeddedLdapServer() throws Exception {
|
||||
if (ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader())) {
|
||||
if (apacheDsPresent) {
|
||||
ApacheDSContainer apacheDsContainer = new ApacheDSContainer(this.root, this.ldif);
|
||||
apacheDsContainer.setPort(getPort());
|
||||
postProcess(apacheDsContainer);
|
||||
this.port = apacheDsContainer.getLocalPort();
|
||||
}
|
||||
else if (ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) {
|
||||
else if (unboundIdPresent) {
|
||||
UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif);
|
||||
unboundIdContainer.setPort(getPort());
|
||||
postProcess(unboundIdContainer);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 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.
|
||||
@@ -73,6 +73,6 @@ public @interface EnableReactiveMethodSecurity {
|
||||
* used.
|
||||
* @since 5.8
|
||||
*/
|
||||
boolean useAuthorizationManager() default false;
|
||||
boolean useAuthorizationManager() default true;
|
||||
|
||||
}
|
||||
|
||||
+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.
|
||||
@@ -3051,7 +3051,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;
|
||||
|
||||
+6
-2
@@ -56,7 +56,9 @@ import org.springframework.security.web.access.expression.DefaultWebSecurityExpr
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.debug.DebugFilter;
|
||||
import org.springframework.security.web.firewall.CompositeRequestRejectedHandler;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
|
||||
import org.springframework.security.web.firewall.ObservationMarkingRequestRejectedHandler;
|
||||
import org.springframework.security.web.firewall.RequestRejectedHandler;
|
||||
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||
@@ -309,8 +311,10 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
|
||||
filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
|
||||
}
|
||||
else if (!this.observationRegistry.isNoop()) {
|
||||
filterChainProxy
|
||||
.setRequestRejectedHandler(new ObservationMarkingRequestRejectedHandler(this.observationRegistry));
|
||||
CompositeRequestRejectedHandler requestRejectedHandler = new CompositeRequestRejectedHandler(
|
||||
new ObservationMarkingRequestRejectedHandler(this.observationRegistry),
|
||||
new HttpStatusRequestRejectedHandler());
|
||||
filterChainProxy.setRequestRejectedHandler(requestRejectedHandler);
|
||||
}
|
||||
filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
|
||||
filterChainProxy.afterPropertiesSet();
|
||||
|
||||
+8
-2
@@ -51,12 +51,18 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@Import(OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class)
|
||||
final class OAuth2ClientConfiguration {
|
||||
|
||||
private static final boolean webMvcPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = OAuth2ClientConfiguration.class.getClassLoader();
|
||||
webMvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", classLoader);
|
||||
}
|
||||
|
||||
static class OAuth2ClientWebMvcImportSelector implements ImportSelector {
|
||||
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
|
||||
if (!ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet",
|
||||
getClass().getClassLoader())) {
|
||||
if (!webMvcPresent) {
|
||||
return new String[0];
|
||||
}
|
||||
return new String[] { "org.springframework.security.config.annotation.web.configuration."
|
||||
|
||||
+8
-1
@@ -30,9 +30,16 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class SpringWebMvcImportSelector implements ImportSelector {
|
||||
|
||||
private static final boolean webMvcPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = SpringWebMvcImportSelector.class.getClassLoader();
|
||||
webMvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", classLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
|
||||
if (!ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", getClass().getClassLoader())) {
|
||||
if (!webMvcPresent) {
|
||||
return new String[0];
|
||||
}
|
||||
return new String[] {
|
||||
|
||||
+8
-3
@@ -40,14 +40,20 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
*/
|
||||
public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<CorsConfigurer<H>, H> {
|
||||
|
||||
private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
|
||||
|
||||
private static final String CORS_CONFIGURATION_SOURCE_BEAN_NAME = "corsConfigurationSource";
|
||||
|
||||
private static final String CORS_FILTER_BEAN_NAME = "corsFilter";
|
||||
|
||||
private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
|
||||
|
||||
private static final boolean mvcPresent;
|
||||
|
||||
private CorsConfigurationSource configurationSource;
|
||||
|
||||
static {
|
||||
mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, CorsConfigurer.class.getClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
@@ -84,7 +90,6 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
|
||||
CorsConfigurationSource.class);
|
||||
return new CorsFilter(configurationSource);
|
||||
}
|
||||
boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, context.getClassLoader());
|
||||
if (mvcPresent) {
|
||||
return MvcCorsFilter.getMvcCorsFilter(context);
|
||||
}
|
||||
|
||||
+3
@@ -380,6 +380,9 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
http.setSharedObject(SecurityContextRepository.class, defaultRepository);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.sessionManagementSecurityContextRepository = securityContextRepository;
|
||||
}
|
||||
RequestCache requestCache = http.getSharedObject(RequestCache.class);
|
||||
if (requestCache == null) {
|
||||
if (stateless) {
|
||||
|
||||
+9
-2
@@ -47,10 +47,17 @@ import org.springframework.web.reactive.result.method.annotation.ArgumentResolve
|
||||
*/
|
||||
final class ReactiveOAuth2ClientImportSelector implements ImportSelector {
|
||||
|
||||
private static final boolean oauth2ClientPresent;
|
||||
|
||||
static {
|
||||
oauth2ClientPresent = ClassUtils.isPresent(
|
||||
"org.springframework.security.oauth2.client.registration.ClientRegistration",
|
||||
ReactiveOAuth2ClientImportSelector.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
|
||||
if (!ClassUtils.isPresent("org.springframework.security.oauth2.client.registration.ClientRegistration",
|
||||
getClass().getClassLoader())) {
|
||||
if (!oauth2ClientPresent) {
|
||||
return new String[0];
|
||||
}
|
||||
return new String[] { "org.springframework.security.config.annotation.web.reactive."
|
||||
|
||||
+6
-2
@@ -53,13 +53,17 @@ class WebFluxSecurityConfiguration {
|
||||
|
||||
public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME = "org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository";
|
||||
|
||||
private static final boolean isOAuth2Present = ClassUtils.isPresent(
|
||||
REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, WebFluxSecurityConfiguration.class.getClassLoader());
|
||||
private static final boolean isOAuth2Present;
|
||||
|
||||
private List<SecurityWebFilterChain> securityWebFilterChains;
|
||||
|
||||
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
|
||||
|
||||
static {
|
||||
isOAuth2Present = ClassUtils.isPresent(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME,
|
||||
WebFluxSecurityConfiguration.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
ApplicationContext context;
|
||||
|
||||
|
||||
+11
-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.
|
||||
@@ -41,7 +41,7 @@ import org.springframework.security.messaging.access.intercept.AuthorizationChan
|
||||
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
|
||||
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
|
||||
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor;
|
||||
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
@@ -59,6 +59,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
|
||||
@@ -69,7 +71,7 @@ final class WebSocketMessageBrokerSecurityConfiguration
|
||||
|
||||
private final SecurityContextChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor();
|
||||
|
||||
private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
|
||||
private ChannelInterceptor csrfChannelInterceptor = new XorCsrfChannelInterceptor();
|
||||
|
||||
private AuthorizationManager<Message<?>> authorizationManager = ANY_MESSAGE_AUTHENTICATED;
|
||||
|
||||
@@ -90,6 +92,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;
|
||||
}
|
||||
|
||||
AuthorizationManager<Message<?>> manager = this.authorizationManager;
|
||||
if (!this.observationRegistry.isNoop()) {
|
||||
manager = new ObservationAuthorizationManager<>(this.observationRegistry, manager);
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.aot.hint;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
|
||||
/**
|
||||
* {@link RuntimeHintsRegistrar} for
|
||||
* {@link org.springframework.security.config.web.server.ServerHttpSecurity}
|
||||
*
|
||||
* @author Marcus Da Coregio
|
||||
* @since 6.0.2
|
||||
*/
|
||||
class OAuth2LoginRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
hints.reflection().registerTypeIfPresent(classLoader, "org.springframework.security.oauth2.jwt.JwtDecoder",
|
||||
MemberCategory.INVOKE_PUBLIC_METHODS);
|
||||
}
|
||||
|
||||
}
|
||||
+8
-3
@@ -80,6 +80,8 @@ final class AuthenticationConfigBuilder {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static final boolean webMvcPresent;
|
||||
|
||||
private static final String ATT_REALM = "realm";
|
||||
|
||||
private static final String DEF_REALM = "Realm";
|
||||
@@ -213,6 +215,11 @@ final class AuthenticationConfigBuilder {
|
||||
|
||||
private final List<BeanDefinition> csrfIgnoreRequestMatchers = new ManagedList<>();
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = AuthenticationConfigBuilder.class.getClassLoader();
|
||||
webMvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", classLoader);
|
||||
}
|
||||
|
||||
AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc,
|
||||
SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager,
|
||||
BeanMetadataElement authenticationFilterSecurityContextHolderStrategyRef,
|
||||
@@ -409,9 +416,7 @@ final class AuthenticationConfigBuilder {
|
||||
if (!this.oauth2LoginEnabled && !this.oauth2ClientEnabled) {
|
||||
return;
|
||||
}
|
||||
boolean webmvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet",
|
||||
getClass().getClassLoader());
|
||||
if (webmvcPresent) {
|
||||
if (webMvcPresent) {
|
||||
this.pc.getReaderContext()
|
||||
.registerWithGeneratedName(new RootBeanDefinition(OAuth2ClientWebMvcSecurityPostProcessor.class));
|
||||
}
|
||||
|
||||
+9
-3
@@ -36,12 +36,19 @@ import org.springframework.web.filter.CorsFilter;
|
||||
*/
|
||||
public class CorsBeanDefinitionParser {
|
||||
|
||||
private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
|
||||
|
||||
private static final String ATT_SOURCE = "configuration-source-ref";
|
||||
|
||||
private static final String ATT_REF = "ref";
|
||||
|
||||
private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
|
||||
|
||||
private static final boolean mvcPresent;
|
||||
|
||||
static {
|
||||
mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
|
||||
CorsBeanDefinitionParser.class.getClassLoader());
|
||||
}
|
||||
|
||||
public BeanMetadataElement parse(Element element, ParserContext parserContext) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
@@ -64,7 +71,6 @@ public class CorsBeanDefinitionParser {
|
||||
if (StringUtils.hasText(configurationSourceRef)) {
|
||||
return new RuntimeBeanReference(configurationSourceRef);
|
||||
}
|
||||
boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, getClass().getClassLoader());
|
||||
if (!mvcPresent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+8
-4
@@ -66,14 +66,19 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
private static final String REQUEST_DATA_VALUE_PROCESSOR = "requestDataValueProcessor";
|
||||
|
||||
private static final String DISPATCHER_SERVLET_CLASS_NAME = "org.springframework.web.servlet.DispatcherServlet";
|
||||
|
||||
private static final String ATT_MATCHER = "request-matcher-ref";
|
||||
|
||||
private static final String ATT_REPOSITORY = "token-repository-ref";
|
||||
|
||||
private static final String ATT_REQUEST_HANDLER = "request-handler-ref";
|
||||
|
||||
private static final boolean webMvcPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = CsrfBeanDefinitionParser.class.getClassLoader();
|
||||
webMvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", classLoader);
|
||||
}
|
||||
|
||||
private String csrfRepositoryRef;
|
||||
|
||||
private BeanDefinition csrfFilter;
|
||||
@@ -90,8 +95,7 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader());
|
||||
if (webmvcPresent) {
|
||||
if (webMvcPresent) {
|
||||
if (!pc.getRegistry().containsBeanDefinition(REQUEST_DATA_VALUE_PROCESSOR)) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(CsrfRequestDataValueProcessor.class);
|
||||
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(beanDefinition,
|
||||
|
||||
+9
-2
@@ -42,6 +42,8 @@ public class EmbeddedLdapServerContextSourceFactoryBean
|
||||
|
||||
private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer";
|
||||
|
||||
private static final boolean unboundIdPresent;
|
||||
|
||||
private static final int DEFAULT_PORT = 33389;
|
||||
|
||||
private static final int RANDOM_PORT = 0;
|
||||
@@ -60,6 +62,11 @@ public class EmbeddedLdapServerContextSourceFactoryBean
|
||||
|
||||
private EmbeddedLdapServerContainer container;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = EmbeddedLdapServerContextSourceFactoryBean.class.getClassLoader();
|
||||
unboundIdPresent = ClassUtils.isPresent(UNBOUNDID_CLASSNAME, classLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EmbeddedLdapServerContextSourceFactoryBean that will use an embedded LDAP
|
||||
* server to perform LDAP authentication. This requires a dependency on
|
||||
@@ -120,7 +127,7 @@ public class EmbeddedLdapServerContextSourceFactoryBean
|
||||
|
||||
@Override
|
||||
public DefaultSpringSecurityContextSource getObject() throws Exception {
|
||||
if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) {
|
||||
if (!unboundIdPresent) {
|
||||
throw new IllegalStateException("Embedded LDAP server is not provided");
|
||||
}
|
||||
this.container = getContainer();
|
||||
@@ -156,7 +163,7 @@ public class EmbeddedLdapServerContextSourceFactoryBean
|
||||
}
|
||||
|
||||
private EmbeddedLdapServerContainer getContainer() {
|
||||
if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) {
|
||||
if (!unboundIdPresent) {
|
||||
throw new IllegalStateException("Embedded LDAP server is not provided");
|
||||
}
|
||||
UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif);
|
||||
|
||||
+14
-4
@@ -86,6 +86,16 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
|
||||
private static final String UNBOUNDID_CONTAINER_CLASSNAME = "org.springframework.security.ldap.server.UnboundIdContainer";
|
||||
|
||||
private static final boolean unboundIdPresent;
|
||||
|
||||
private static final boolean apacheDsPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = LdapServerBeanDefinitionParser.class.getClassLoader();
|
||||
unboundIdPresent = ClassUtils.isPresent(UNBOUNID_CLASSNAME, classLoader);
|
||||
apacheDsPresent = ClassUtils.isPresent(APACHEDS_CLASSNAME, classLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BeanDefinition parse(Element elt, ParserContext parserContext) {
|
||||
String url = elt.getAttribute(ATT_URL);
|
||||
@@ -184,11 +194,11 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
|
||||
private boolean isApacheDsEnabled(String mode) {
|
||||
return "apacheds".equals(mode) || ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader());
|
||||
return "apacheds".equals(mode) || apacheDsPresent;
|
||||
}
|
||||
|
||||
private boolean isUnboundidEnabled(String mode) {
|
||||
return "unboundid".equals(mode) || ClassUtils.isPresent(UNBOUNID_CLASSNAME, getClass().getClassLoader());
|
||||
return "unboundid".equals(mode) || unboundIdPresent;
|
||||
}
|
||||
|
||||
private String getPort(Element element) {
|
||||
@@ -222,11 +232,11 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser {
|
||||
}
|
||||
|
||||
private int getPort() {
|
||||
if (ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader())) {
|
||||
if (apacheDsPresent) {
|
||||
ApacheDSContainer apacheDSContainer = this.applicationContext.getBean(ApacheDSContainer.class);
|
||||
return apacheDSContainer.getLocalPort();
|
||||
}
|
||||
if (ClassUtils.isPresent(UNBOUNID_CLASSNAME, getClass().getClassLoader())) {
|
||||
if (unboundIdPresent) {
|
||||
UnboundIdContainer unboundIdContainer = this.applicationContext.getBean(UnboundIdContainer.class);
|
||||
return unboundIdContainer.getPort();
|
||||
}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
|
||||
org.springframework.security.config.annotation.authentication.configuration.AuthenticationManagerBeanRegistrationAotProcessor
|
||||
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.security.config.aot.hint.OAuth2LoginRuntimeHints
|
||||
|
||||
+2
-3
@@ -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.
|
||||
@@ -78,8 +78,7 @@ public class EnableReactiveMethodSecurityTests {
|
||||
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
|
||||
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
|
||||
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
|
||||
+ ".Publisher (i.e. Mono / Flux) or the function must be a Kotlin coroutine "
|
||||
+ "function in order to support Reactor Context");
|
||||
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+47
-1
@@ -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.
|
||||
@@ -29,6 +29,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.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -44,12 +45,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 HttpSecurityDeferAddFilterTest {
|
||||
|
||||
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();
|
||||
@@ -216,6 +234,34 @@ public class HttpSecurityDeferAddFilterTest {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@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 {
|
||||
|
||||
|
||||
+39
@@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.web.builders;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.ObservationTextPublisher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -104,6 +106,15 @@ public class WebSecurityTests {
|
||||
|
||||
@Test
|
||||
public void requestRejectedHandlerInvoked() throws ServletException, IOException {
|
||||
loadConfig(DefaultConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/\u0019path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRequestRejectedHandlerInvoked() throws ServletException, IOException {
|
||||
loadConfig(RequestRejectedHandlerConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/\u0019path");
|
||||
@@ -111,6 +122,16 @@ public class WebSecurityTests {
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// gh-12548
|
||||
@Test
|
||||
public void requestRejectedHandlerInvokedWhenOperationalObservationRegistry() throws ServletException, IOException {
|
||||
loadConfig(ObservationRegistryConfig.class);
|
||||
this.request.setServletPath("/spring");
|
||||
this.request.setRequestURI("/spring/\u0019path");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoringMvcMatcherServletPath() throws Exception {
|
||||
loadConfig(MvcMatcherServletPathConfig.class, LegacyMvcMatchingConfig.class);
|
||||
@@ -143,6 +164,11 @@ public class WebSecurityTests {
|
||||
this.context.getAutowireCapableBeanFactory().autowireBean(this);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class DefaultConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@@ -243,4 +269,17 @@ public class WebSecurityTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class ObservationRegistryConfig {
|
||||
|
||||
@Bean
|
||||
ObservationRegistry observationRegistry() {
|
||||
ObservationRegistry observationRegistry = ObservationRegistry.create();
|
||||
observationRegistry.observationConfig().observationHandler(new ObservationTextPublisher());
|
||||
return observationRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -125,6 +125,18 @@ public class SessionManagementConfigurerTests {
|
||||
this.mvc.perform(get("/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sessionManagementWhenSecurityContextRepositoryIsConfiguredThenUseIt() throws Exception {
|
||||
SessionManagementSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPO = mock(SecurityContextRepository.class);
|
||||
given(SessionManagementSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPO
|
||||
.loadDeferredContext(any(HttpServletRequest.class)))
|
||||
.willReturn(new TestDeferredSecurityContext(mock(SecurityContext.class), false));
|
||||
this.spring.register(SessionManagementSecurityContextRepositoryConfig.class).autowire();
|
||||
this.mvc.perform(get("/"));
|
||||
verify(SessionManagementSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPO)
|
||||
.containsContext(any(HttpServletRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sessionManagementWhenInvokedTwiceThenUsesOriginalSessionCreationPolicy() throws Exception {
|
||||
this.spring.register(InvokeTwiceDoesNotOverride.class).autowire();
|
||||
|
||||
+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.
|
||||
@@ -60,6 +60,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;
|
||||
@@ -78,6 +79,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 {
|
||||
|
||||
@@ -283,7 +285,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));
|
||||
}
|
||||
@@ -305,7 +307,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;
|
||||
}
|
||||
|
||||
}
|
||||
+10
-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.
|
||||
@@ -66,9 +66,10 @@ import org.springframework.security.messaging.access.intercept.AuthorizationChan
|
||||
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
|
||||
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
|
||||
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
|
||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
|
||||
import org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor;
|
||||
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;
|
||||
@@ -91,9 +92,12 @@ 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 {
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = "wpe7zB62-NCpcA==";
|
||||
|
||||
AnnotationConfigWebApplicationContext context;
|
||||
|
||||
Authentication messageUser;
|
||||
@@ -196,7 +200,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
MessageChannel messageChannel = clientInboundChannel();
|
||||
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
|
||||
.getInterceptors().stream().map(ChannelInterceptor::getClass);
|
||||
assertThat(interceptors).contains(CsrfChannelInterceptor.class);
|
||||
assertThat(interceptors).contains(XorCsrfChannelInterceptor.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -236,7 +240,7 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
public void messagesContextWebSocketUseSecurityContextHolderStrategy() {
|
||||
loadConfig(WebSocketSecurityConfig.class, SecurityContextChangedListenerConfig.class);
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
|
||||
headers.setNativeHeader(this.token.getHeaderName(), this.token.getToken());
|
||||
headers.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
Message<?> message = message(headers, "/authenticated");
|
||||
headers.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
MessageChannel messageChannel = clientInboundChannel();
|
||||
@@ -366,7 +370,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));
|
||||
}
|
||||
@@ -388,7 +392,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;
|
||||
}
|
||||
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.aot.hint;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2LoginRuntimeHints}
|
||||
*
|
||||
* @author Marcus Da Coregio
|
||||
*/
|
||||
class OAuth2LoginRuntimeHintsTests {
|
||||
|
||||
private final RuntimeHints hints = new RuntimeHints();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories").load(RuntimeHintsRegistrar.class)
|
||||
.forEach((registrar) -> registrar.registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jwtDecoderHasHints() {
|
||||
assertThat(RuntimeHintsPredicates.reflection().onType(JwtDecoder.class)
|
||||
.withMemberCategories(MemberCategory.INVOKE_PUBLIC_METHODS)).accepts(this.hints);
|
||||
}
|
||||
|
||||
}
|
||||
+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 {
|
||||
|
||||
|
||||
+4
-3
@@ -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.
|
||||
@@ -38,7 +38,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration
|
||||
class KotlinEnableReactiveMethodSecurityTests {
|
||||
// no authorization manager due to https://github.com/spring-projects/spring-security/issues/12080
|
||||
class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests {
|
||||
|
||||
private lateinit var delegate: KotlinReactiveMessageService
|
||||
|
||||
@@ -211,7 +212,7 @@ class KotlinEnableReactiveMethodSecurityTests {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveMethodSecurity
|
||||
@EnableReactiveMethodSecurity(useAuthorizationManager = false)
|
||||
open class Config {
|
||||
var delegate = mockk<KotlinReactiveMessageService>()
|
||||
|
||||
+11
-7
@@ -18,6 +18,7 @@ package org.springframework.security.authentication;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -48,13 +49,16 @@ public class ObservationReactiveAuthenticationManager implements ReactiveAuthent
|
||||
AuthenticationObservationContext context = new AuthenticationObservationContext();
|
||||
context.setAuthenticationRequest(authentication);
|
||||
context.setAuthenticationManagerClass(this.delegate.getClass());
|
||||
Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry).start();
|
||||
return this.delegate.authenticate(authentication).doOnSuccess((result) -> {
|
||||
context.setAuthenticationResult(result);
|
||||
observation.stop();
|
||||
}).doOnCancel(observation::stop).doOnError((t) -> {
|
||||
observation.error(t);
|
||||
observation.stop();
|
||||
return Mono.deferContextual((contextView) -> {
|
||||
Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry)
|
||||
.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
|
||||
return this.delegate.authenticate(authentication).doOnSuccess((result) -> {
|
||||
context.setAuthenticationResult(result);
|
||||
observation.stop();
|
||||
}).doOnCancel(observation::stop).doOnError((t) -> {
|
||||
observation.error(t);
|
||||
observation.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+14
-10
@@ -18,6 +18,7 @@ package org.springframework.security.authorization;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
@@ -50,16 +51,19 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
|
||||
context.setAuthentication(auth);
|
||||
return context.getAuthentication();
|
||||
});
|
||||
Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry).start();
|
||||
return this.delegate.check(wrapped, object).doOnSuccess((decision) -> {
|
||||
context.setDecision(decision);
|
||||
if (decision == null || !decision.isGranted()) {
|
||||
observation.error(new AccessDeniedException("Access Denied"));
|
||||
}
|
||||
observation.stop();
|
||||
}).doOnCancel(observation::stop).doOnError((t) -> {
|
||||
observation.error(t);
|
||||
observation.stop();
|
||||
return Mono.deferContextual((contextView) -> {
|
||||
Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry)
|
||||
.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
|
||||
return this.delegate.check(wrapped, object).doOnSuccess((decision) -> {
|
||||
context.setDecision(decision);
|
||||
if (decision == null || !decision.isGranted()) {
|
||||
observation.error(new AccessDeniedException("Access Denied"));
|
||||
}
|
||||
observation.stop();
|
||||
}).doOnCancel(observation::stop).doOnError((t) -> {
|
||||
observation.error(t);
|
||||
observation.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+25
-5
@@ -89,6 +89,26 @@ public final class SecurityJackson2Modules {
|
||||
|
||||
private static final String saml2Jackson2ModuleClass = "org.springframework.security.saml2.jackson2.Saml2Jackson2Module";
|
||||
|
||||
private static final boolean webServletPresent;
|
||||
|
||||
private static final boolean oauth2ClientPresent;
|
||||
|
||||
private static final boolean javaTimeJacksonPresent;
|
||||
|
||||
private static final boolean ldapJacksonPresent;
|
||||
|
||||
private static final boolean saml2JacksonPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = SecurityJackson2Modules.class.getClassLoader();
|
||||
webServletPresent = ClassUtils.isPresent("jakarta.servlet.http.Cookie", classLoader);
|
||||
oauth2ClientPresent = ClassUtils.isPresent("org.springframework.security.oauth2.client.OAuth2AuthorizedClient",
|
||||
classLoader);
|
||||
javaTimeJacksonPresent = ClassUtils.isPresent(javaTimeJackson2ModuleClass, classLoader);
|
||||
ldapJacksonPresent = ClassUtils.isPresent(ldapJackson2ModuleClass, classLoader);
|
||||
saml2JacksonPresent = ClassUtils.isPresent(saml2Jackson2ModuleClass, classLoader);
|
||||
}
|
||||
|
||||
private SecurityJackson2Modules() {
|
||||
}
|
||||
|
||||
@@ -125,19 +145,19 @@ public final class SecurityJackson2Modules {
|
||||
for (String className : securityJackson2ModuleClasses) {
|
||||
addToModulesList(loader, modules, className);
|
||||
}
|
||||
if (ClassUtils.isPresent("jakarta.servlet.http.Cookie", loader)) {
|
||||
if (webServletPresent) {
|
||||
addToModulesList(loader, modules, webServletJackson2ModuleClass);
|
||||
}
|
||||
if (ClassUtils.isPresent("org.springframework.security.oauth2.client.OAuth2AuthorizedClient", loader)) {
|
||||
if (oauth2ClientPresent) {
|
||||
addToModulesList(loader, modules, oauth2ClientJackson2ModuleClass);
|
||||
}
|
||||
if (ClassUtils.isPresent(javaTimeJackson2ModuleClass, loader)) {
|
||||
if (javaTimeJacksonPresent) {
|
||||
addToModulesList(loader, modules, javaTimeJackson2ModuleClass);
|
||||
}
|
||||
if (ClassUtils.isPresent(ldapJackson2ModuleClass, loader)) {
|
||||
if (ldapJacksonPresent) {
|
||||
addToModulesList(loader, modules, ldapJackson2ModuleClass);
|
||||
}
|
||||
if (ClassUtils.isPresent(saml2Jackson2ModuleClass, loader)) {
|
||||
if (saml2JacksonPresent) {
|
||||
addToModulesList(loader, modules, saml2Jackson2ModuleClass);
|
||||
}
|
||||
return modules;
|
||||
|
||||
+8
-8
@@ -10,12 +10,12 @@ dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:$reactorVersion")
|
||||
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:2022.0.0")
|
||||
api platform("org.springframework.data:spring-data-bom:2022.0.2")
|
||||
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.14.1")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.14.2")
|
||||
constraints {
|
||||
api "ch.qos.logback:logback-classic:1.4.5"
|
||||
api "com.google.inject:guice:3.0"
|
||||
@@ -25,12 +25,12 @@ dependencies {
|
||||
api "com.squareup.okhttp3:okhttp:3.14.9"
|
||||
api "com.unboundid:unboundid-ldapsdk:6.0.7"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.13.3"
|
||||
api "io.mockk:mockk:1.13.4"
|
||||
api "io.micrometer:micrometer-observation:$micrometerVersion"
|
||||
api "jakarta.annotation:jakarta.annotation-api:2.1.1"
|
||||
api "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0"
|
||||
api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.0"
|
||||
api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.1"
|
||||
api "jakarta.servlet:jakarta.servlet-api:6.0.0"
|
||||
api "jakarta.xml.bind:jakarta.xml.bind-api:4.0.0"
|
||||
api "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
@@ -55,7 +55,7 @@ dependencies {
|
||||
api "org.eclipse.jetty:jetty-servlet:11.0.13"
|
||||
api "jakarta.persistence:jakarta.persistence-api:3.1.0"
|
||||
api "org.hamcrest:hamcrest:2.2"
|
||||
api "org.hibernate.orm:hibernate-core:6.1.6.Final"
|
||||
api "org.hibernate.orm:hibernate-core:6.1.7.Final"
|
||||
api "org.hsqldb:hsqldb:2.7.1"
|
||||
api "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
api "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
@@ -68,12 +68,12 @@ dependencies {
|
||||
api "org.skyscreamer:jsonassert:1.5.1"
|
||||
api "org.slf4j:log4j-over-slf4j:1.7.36"
|
||||
api "org.slf4j:slf4j-api:2.0.6"
|
||||
api "org.springframework.ldap:spring-ldap-core:3.0.0"
|
||||
api "org.springframework.ldap:spring-ldap-core:3.0.1"
|
||||
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
api 'org.apache.maven.resolver:maven-resolver-connector-basic:1.8.2'
|
||||
api 'org.apache.maven.resolver:maven-resolver-impl:1.8.2'
|
||||
api 'org.apache.maven.resolver:maven-resolver-transport-http:1.8.2'
|
||||
api 'org.apache.maven:maven-resolver-provider:3.8.6'
|
||||
api 'org.apache.maven:maven-resolver-provider:3.8.7'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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 |
@@ -5,6 +5,7 @@
|
||||
* xref:migration/index.adoc[Migrating to 6.0]
|
||||
** xref:migration/servlet/index.adoc[Servlet Migrations]
|
||||
*** xref:migration/servlet/session-management.adoc[Session Management]
|
||||
*** xref:migration/servlet/exploits.adoc[Exploit Protection]
|
||||
*** xref:migration/servlet/authentication.adoc[Authentication]
|
||||
*** xref:migration/servlet/authorization.adoc[Authorization]
|
||||
** xref:migration/reactive.adoc[Reactive Migrations]
|
||||
|
||||
@@ -336,7 +336,7 @@ https://docs.spring.io/spring-security/site/docs/5.0.x/api/org/springframework/s
|
||||
The `BCryptPasswordEncoder` implementation uses the widely supported https://en.wikipedia.org/wiki/Bcrypt[bcrypt] algorithm to hash the passwords.
|
||||
To make it more resistant to password cracking, bcrypt is deliberately slow.
|
||||
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system.
|
||||
The default implementationThe default implementation of `BCryptPasswordEncoder` uses strength 10 as mentioned in the Javadoc of https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html[`BCryptPasswordEncoder`]. You are encouraged to
|
||||
The default implementation of `BCryptPasswordEncoder` uses strength 10 as mentioned in the Javadoc of https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html[`BCryptPasswordEncoder`]. You are encouraged to
|
||||
tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password.
|
||||
|
||||
.BCryptPasswordEncoder
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0.
|
||||
Use 5.8 and
|
||||
ifdef::spring-security-version[]
|
||||
xref:5.8.0@migration/index.adoc[its preparation steps]
|
||||
xref:5.8.2@migration/index.adoc[its preparation steps]
|
||||
endif::[]
|
||||
ifndef::spring-security-version[]
|
||||
its preparation steps
|
||||
endif::[]
|
||||
to simplify updating to 6.0
|
||||
to simplify updating to 6.0.
|
||||
|
||||
After updating to 5.8, follow this guide to perform any remaining migration or cleanup steps.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ The following steps relate to how to finish migrating authentication support.
|
||||
{security-api-url}org/springframework/security/web/authentication/AuthenticationFilter.html[`AuthenticationFilter`] propagates {security-api-url}org/springframework/security/authentication/AuthenticationServiceException.html[``AuthenticationServiceException``]s to the {security-api-url}org/springframework/security/authentication/AuthenticationEntryPoint.html[`AuthenticationEntryPoint`].
|
||||
Because ``AuthenticationServiceException``s represent a server-side error instead of a client-side error, in 6.0, this changes to propagate them to the container.
|
||||
|
||||
So, if you opted into this behavior by setting `rethrowAuthenticationServiceException` too `true`, you can now remove it like so:
|
||||
So, if you opted into this behavior by setting `rethrowAuthenticationServiceException` to `true`, you can now remove it like so:
|
||||
|
||||
====
|
||||
.Java
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
= Exploit Protection Migrations
|
||||
|
||||
The 5.8 migration guide contains several steps for
|
||||
ifdef::spring-security-version[]
|
||||
xref:5.8.2@migration/servlet/exploits.adoc[exploit protection migrations] when updating to 6.0.
|
||||
endif::[]
|
||||
ifndef::spring-security-version[]
|
||||
exploit protection migrations when updating to 6.0.
|
||||
endif::[]
|
||||
You are encouraged to follow those steps first.
|
||||
|
||||
The following steps relate to how to finish migrating exploit protection support.
|
||||
|
||||
== Defer Loading CsrfToken
|
||||
|
||||
In Spring Security 5.8, the default `CsrfTokenRequestHandler` for making the `CsrfToken` available to the application is `CsrfTokenRequestAttributeHandler`.
|
||||
The default for the field `csrfRequestAttributeName` is `null`, which causes the CSRF token to be loaded on every request.
|
||||
|
||||
In Spring Security 6, `csrfRequestAttributeName` defaults to `_csrf`.
|
||||
If you configured the following only for the purpose of updating to 6.0, you can now remove it:
|
||||
|
||||
requestHandler.setCsrfRequestAttributeName("_csrf");
|
||||
|
||||
== Protect against CSRF BREACH
|
||||
|
||||
In Spring Security 5.8, the default `CsrfTokenRequestHandler` for making the `CsrfToken` available to the application is `CsrfTokenRequestAttributeHandler`.
|
||||
`XorCsrfTokenRequestAttributeHandler` was added to allow opting into CSRF BREACH support.
|
||||
|
||||
In Spring Security 6, `XorCsrfTokenRequestAttributeHandler` is the default `CsrfTokenRequestHandler` for making the `CsrfToken` available.
|
||||
If you configured the `XorCsrfTokenRequestAttributeHandler` only for the purpose of updating to 6.0, you can remove it completely.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If you have set the `csrfRequestAttributeName` to `null` in order to opt out of deferred tokens, or if you have configured a `CsrfTokenRequestHandler` for any other reason, you can leave the configuration in place.
|
||||
====
|
||||
|
||||
== CSRF BREACH with WebSocket support
|
||||
|
||||
In Spring Security 5.8, the default `ChannelInterceptor` for making the `CsrfToken` available with xref:servlet/integrations/websocket.adoc[WebSocket Security] is `CsrfChannelInterceptor`.
|
||||
`XorCsrfChannelInterceptor` was added to allow opting into CSRF BREACH support.
|
||||
|
||||
In Spring Security 6, `XorCsrfChannelInterceptor` is the default `ChannelInterceptor` for making the `CsrfToken` available.
|
||||
If you configured the `XorCsrfChannelInterceptor` only for the purpose of updating to 6.0, you can remove it completely.
|
||||
@@ -109,14 +109,14 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
[[webflux-csrf-configure-request-handler]]
|
||||
==== Configure ServerCsrfTokenRequestHandler
|
||||
|
||||
Spring Security's https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html[`CsrfWebFilter`] exposes a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html[`Mono<CsrfToken>`] as a `ServerWebExchange` attribute named `org.springframework.security.web.server.csrf.CsrfToken` with the help of a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestHandler.html[`ServerCsrfTokenRequestHandler`].
|
||||
The default implementation is `ServerCsrfTokenRequestAttributeHandler`.
|
||||
Spring Security's https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html[`CsrfWebFilter`] exposes a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfToken.html[`Mono<CsrfToken>`] as a `ServerWebExchange` attribute named `org.springframework.security.web.server.csrf.CsrfToken` with the help of a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestHandler.html[`ServerCsrfTokenRequestHandler`].
|
||||
In 5.8, the default implementation was `ServerCsrfTokenRequestAttributeHandler`, which simply makes the `Mono<CsrfToken>` available as an exchange attribute.
|
||||
|
||||
An alternate implementation `XorServerCsrfTokenRequestAttributeHandler` is available to provide protection for BREACH (see https://github.com/spring-projects/spring-security/issues/4001[gh-4001]).
|
||||
As of 6.0, the default implementation is `XorServerCsrfTokenRequestAttributeHandler`, which provides protection for BREACH (see https://github.com/spring-projects/spring-security/issues/4001[gh-4001]).
|
||||
|
||||
You can configure `XorServerCsrfTokenRequestAttributeHandler` using the following Java configuration:
|
||||
If you wish to disable BREACH protection of the `CsrfToken` and revert to the 5.8 default, you can configure `ServerCsrfTokenRequestAttributeHandler` using the following Java configuration:
|
||||
|
||||
.Configure BREACH protection
|
||||
.Disable BREACH protection
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
@@ -126,7 +126,7 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
|
||||
http
|
||||
// ...
|
||||
.csrf(csrf -> csrf
|
||||
.csrfTokenRequestHandler(new XorServerCsrfTokenRequestAttributeHandler())
|
||||
.csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler())
|
||||
)
|
||||
return http.build();
|
||||
}
|
||||
@@ -140,7 +140,7 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain
|
||||
return http {
|
||||
// ...
|
||||
csrf {
|
||||
csrfTokenRequestHandler = XorServerCsrfTokenRequestAttributeHandler()
|
||||
csrfTokenRequestHandler = ServerCsrfTokenRequestAttributeHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +271,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
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
|
||||
|
||||
. `SessionAuthenticationStrategy` is notified of a new login.
|
||||
See the {security-api-url}springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[`SessionAuthenticationStrategy`] interface in the Javadoc.
|
||||
. The ref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
. The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
|
||||
See the {security-api-url}springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] class in the Javadoc.
|
||||
. `RememberMeServices.loginSuccess` is invoked.
|
||||
If remember me is not configured, this is a no-op.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -396,10 +396,6 @@ fun doSomething(contact: Contact?)
|
||||
----
|
||||
====
|
||||
|
||||
Here we are accessing another built-in expression, `authentication`, which is the `Authentication` stored in the security context.
|
||||
You can also access its "principal" property directly, using the expression `principal`.
|
||||
The value will often be a `UserDetails` instance, so you might use an expression like `principal.username` or `principal.enabled`.
|
||||
|
||||
.[[el-pre-post-annotations-post]]
|
||||
Here, we access another built-in expression, `authentication`, which is the `Authentication` stored in the security context.
|
||||
You can also access its `principal` property directly, by using the `principal` expression.
|
||||
|
||||
@@ -562,7 +562,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;
|
||||
@@ -579,7 +579,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;
|
||||
|
||||
@@ -168,13 +168,13 @@ class SecurityConfig {
|
||||
==== Configure CsrfTokenRequestHandler
|
||||
|
||||
Spring Security's https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfFilter.html[`CsrfFilter`] exposes a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html[`CsrfToken`] as an `HttpServletRequest` attribute named `_csrf` with the help of a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfTokenRequestHandler.html[CsrfTokenRequestHandler].
|
||||
The default implementation is `CsrfTokenRequestAttributeHandler`.
|
||||
In 5.8, the default implementation was `CsrfTokenRequestAttributeHandler` which simply makes the `_csrf` attribute available as a request attribute.
|
||||
|
||||
An alternate implementation `XorCsrfTokenRequestAttributeHandler` is available to provide protection for BREACH (see https://github.com/spring-projects/spring-security/issues/4001[gh-4001]).
|
||||
As of 6.0, the default implementation is `XorCsrfTokenRequestAttributeHandler`, which provides protection for BREACH (see https://github.com/spring-projects/spring-security/issues/4001[gh-4001]).
|
||||
|
||||
You can configure `XorCsrfTokenRequestAttributeHandler` in XML using the following:
|
||||
If you wish to disable BREACH protection of the `CsrfToken` and revert to the 5.8 default, you can configure `CsrfTokenRequestAttributeHandler` in XML using the following:
|
||||
|
||||
.Configure BREACH protection XML Configuration
|
||||
.Disable BREACH protection XML Configuration
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
@@ -183,13 +183,13 @@ You can configure `XorCsrfTokenRequestAttributeHandler` in XML using the followi
|
||||
<csrf request-handler-ref="requestHandler"/>
|
||||
</http>
|
||||
<b:bean id="requestHandler"
|
||||
class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"/>
|
||||
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"/>
|
||||
----
|
||||
====
|
||||
|
||||
You can configure `XorCsrfTokenRequestAttributeHandler` in Java Configuration using the following:
|
||||
You can configure `CsrfTokenRequestAttributeHandler` in Java Configuration using the following:
|
||||
|
||||
.Configure BREACH protection
|
||||
.Disable BREACH protection
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
@@ -201,7 +201,7 @@ public class WebSecurityConfig {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf
|
||||
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
|
||||
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
@@ -218,7 +218,7 @@ class SecurityConfig {
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
csrf {
|
||||
csrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
|
||||
csrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
|
||||
@@ -418,7 +418,7 @@ Now that we have a tenant-aware processor and a tenant-aware validator, we can p
|
||||
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
|
||||
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
|
||||
(JwtValidators.createDefault(), this.jwtValidator);
|
||||
(JwtValidators.createDefault(), jwtValidator);
|
||||
decoder.setJwtValidator(validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
aspectjVersion=1.9.9.1
|
||||
reactorVersion=2022.0.1
|
||||
aspectjVersion=1.9.19
|
||||
reactorVersion=2022.0.3
|
||||
springJavaformatVersion=0.0.35
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=6.0.3
|
||||
micrometerVersion=1.10.2
|
||||
springFrameworkVersion=6.0.5
|
||||
micrometerVersion=1.10.4
|
||||
openSamlVersion=4.1.1
|
||||
version=6.0.1
|
||||
version=6.0.2
|
||||
kotlinVersion=1.7.22
|
||||
samplesBranch=main
|
||||
org.gradle.jvmargs=-Xmx3g -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-7
@@ -25,7 +25,6 @@ import io.r2dbc.h2.H2ConnectionFactory;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import io.r2dbc.spi.Result;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -115,14 +114,12 @@ public class R2dbcReactiveOAuth2AuthorizedClientServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // until https://github.com/reactor/reactor-core/issues/3307 is resolved
|
||||
public void loadAuthorizedClientWhenDoesNotExistThenReturnNull() {
|
||||
this.authorizedClientService.loadAuthorizedClient("registration-not-found", "principalName")
|
||||
.as(StepVerifier::create).expectNextCount(0).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // until https://github.com/reactor/reactor-core/issues/3307 is resolved
|
||||
public void loadAuthorizedClientWhenExistsThenReturnAuthorizedClient() {
|
||||
Authentication principal = createPrincipal();
|
||||
OAuth2AuthorizedClient expected = createAuthorizedClient(principal, this.clientRegistration);
|
||||
@@ -153,7 +150,6 @@ public class R2dbcReactiveOAuth2AuthorizedClientServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // until https://github.com/reactor/reactor-core/issues/3307 is resolved
|
||||
public void loadAuthorizedClientWhenExistsButNotFoundInClientRegistrationRepositoryThenThrowDataRetrievalFailureException() {
|
||||
given(this.clientRegistrationRepository.findByRegistrationId(any())).willReturn(Mono.empty());
|
||||
Authentication principal = createPrincipal();
|
||||
@@ -190,7 +186,6 @@ public class R2dbcReactiveOAuth2AuthorizedClientServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // until https://github.com/reactor/reactor-core/issues/3307 is resolved
|
||||
public void saveAuthorizedClientWhenSaveThenLoadReturnsSaved() {
|
||||
Authentication principal = createPrincipal();
|
||||
final OAuth2AuthorizedClient expected = createAuthorizedClient(principal, this.clientRegistration);
|
||||
@@ -249,7 +244,6 @@ public class R2dbcReactiveOAuth2AuthorizedClientServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // until https://github.com/reactor/reactor-core/issues/3307 is resolved
|
||||
public void saveAuthorizedClientWhenSaveClientWithExistingPrimaryKeyThenUpdate() {
|
||||
// Given a saved authorized client
|
||||
Authentication principal = createPrincipal();
|
||||
@@ -303,7 +297,6 @@ public class R2dbcReactiveOAuth2AuthorizedClientServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // until https://github.com/reactor/reactor-core/issues/3307 is resolved
|
||||
public void removeAuthorizedClientWhenExistsThenRemoved() {
|
||||
Authentication principal = createPrincipal();
|
||||
OAuth2AuthorizedClient authorizedClient = createAuthorizedClient(principal, this.clientRegistration);
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import org.springframework.util.ClassUtils;
|
||||
* Utility methods for {@link HttpMessageConverter}'s.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author luamas
|
||||
* @since 5.1
|
||||
*/
|
||||
final class HttpMessageConverters {
|
||||
@@ -42,7 +43,7 @@ final class HttpMessageConverters {
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
|
||||
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
|
||||
}
|
||||
|
||||
private HttpMessageConverters() {
|
||||
|
||||
+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));
|
||||
|
||||
|
||||
+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();
|
||||
|
||||
+5
-1
@@ -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.
|
||||
@@ -23,6 +23,7 @@ import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.test.context.aot.TestRuntimeHintsRegistrar;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link TestRuntimeHintsRegistrar} implementation that register runtime hints for
|
||||
@@ -35,6 +36,9 @@ class WebTestUtilsTestRuntimeHints implements TestRuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, Class<?> testClass, ClassLoader classLoader) {
|
||||
if (!ClassUtils.isPresent("jakarta.servlet.Filter", classLoader)) {
|
||||
return;
|
||||
}
|
||||
registerFilterChainProxyHints(hints);
|
||||
registerSecurityContextRepositoryHints(hints);
|
||||
registerCsrfTokenRepositoryHints(hints);
|
||||
|
||||
+6
-2
@@ -43,14 +43,18 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
final class WithUserDetailsSecurityContextFactory implements WithSecurityContextFactory<WithUserDetails> {
|
||||
|
||||
private static final boolean reactorPresent = ClassUtils.isPresent("reactor.core.publisher.Mono",
|
||||
WithUserDetailsSecurityContextFactory.class.getClassLoader());
|
||||
private static final boolean reactorPresent;
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private BeanFactory beans;
|
||||
|
||||
static {
|
||||
reactorPresent = ClassUtils.isPresent("reactor.core.publisher.Mono",
|
||||
WithUserDetailsSecurityContextFactory.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
WithUserDetailsSecurityContextFactory(BeanFactory beans) {
|
||||
this.beans = beans;
|
||||
|
||||
+3
-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.
|
||||
@@ -182,7 +182,7 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
|
||||
parentBefore.setFilterName(this.name);
|
||||
parentBefore.setChainPosition(this.position);
|
||||
}
|
||||
parent.before().event(Observation.Event.of(this.name + " before"));
|
||||
parent.before().event(Observation.Event.of(this.name + ".before", "before " + this.name));
|
||||
this.filter.doFilter(request, response, chain);
|
||||
parent.start();
|
||||
if (parent.after().getContext() instanceof FilterChainObservationContext parentAfter) {
|
||||
@@ -190,7 +190,7 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F
|
||||
parentAfter.setFilterName(this.name);
|
||||
parentAfter.setChainPosition(this.size - this.position + 1);
|
||||
}
|
||||
parent.after().event(Observation.Event.of(this.name + " after"));
|
||||
parent.after().event(Observation.Event.of(this.name + ".after", "after " + this.name));
|
||||
}
|
||||
|
||||
private AroundFilterObservation parent(HttpServletRequest request) {
|
||||
|
||||
+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.
|
||||
@@ -107,6 +107,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()) {
|
||||
@@ -170,7 +171,7 @@ public final class CsrfFilter extends OncePerRequestFilter {
|
||||
* {@link CsrfToken} available as a request attribute.
|
||||
*
|
||||
* <p>
|
||||
* The default is {@link CsrfTokenRequestAttributeHandler}.
|
||||
* The default is {@link XorCsrfTokenRequestAttributeHandler}.
|
||||
* </p>
|
||||
* @param requestHandler the {@link CsrfTokenRequestHandler} to use
|
||||
* @since 5.8
|
||||
|
||||
+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.
|
||||
@@ -225,7 +225,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())) {
|
||||
|
||||
+19
-11
@@ -27,6 +27,7 @@ import io.micrometer.common.KeyValues;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -73,20 +74,22 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
}
|
||||
|
||||
private WebFilterChain wrapSecured(WebFilterChain original) {
|
||||
return (exchange) -> {
|
||||
return (exchange) -> Mono.deferContextual((contextView) -> {
|
||||
AroundWebFilterObservation parent = observation(exchange);
|
||||
Observation parentObservation = contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
|
||||
Observation observation = Observation.createNotStarted(SECURED_OBSERVATION_NAME, this.registry)
|
||||
.contextualName("secured request");
|
||||
.contextualName("secured request").parentObservation(parentObservation);
|
||||
return parent.wrap(WebFilterObservation.create(observation).wrap(original)).filter(exchange);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private WebFilterChain wrapUnsecured(WebFilterChain original) {
|
||||
return (exchange) -> {
|
||||
return (exchange) -> Mono.deferContextual((contextView) -> {
|
||||
Observation parentObservation = contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
|
||||
Observation observation = Observation.createNotStarted(UNSECURED_OBSERVATION_NAME, this.registry)
|
||||
.contextualName("unsecured request");
|
||||
.contextualName("unsecured request").parentObservation(parentObservation);
|
||||
return WebFilterObservation.create(observation).wrap(original).filter(exchange);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private List<ObservationWebFilter> wrap(List<WebFilter> filters) {
|
||||
@@ -186,8 +189,11 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
if (this.position == 1) {
|
||||
AroundWebFilterObservation parent = parent(exchange);
|
||||
return parent.wrap(this::wrapFilter).filter(exchange, chain);
|
||||
return Mono.deferContextual((contextView) -> {
|
||||
Observation parentObservation = contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null);
|
||||
AroundWebFilterObservation parent = parent(exchange, parentObservation);
|
||||
return parent.wrap(this::wrapFilter).filter(exchange, chain);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return wrapFilter(exchange, chain);
|
||||
@@ -211,11 +217,13 @@ public final class ObservationWebFilterChainDecorator implements WebFilterChainP
|
||||
});
|
||||
}
|
||||
|
||||
private AroundWebFilterObservation parent(ServerWebExchange exchange) {
|
||||
private AroundWebFilterObservation parent(ServerWebExchange exchange, Observation parentObservation) {
|
||||
WebFilterChainObservationContext beforeContext = WebFilterChainObservationContext.before();
|
||||
WebFilterChainObservationContext afterContext = WebFilterChainObservationContext.after();
|
||||
Observation before = Observation.createNotStarted(this.convention, () -> beforeContext, this.registry);
|
||||
Observation after = Observation.createNotStarted(this.convention, () -> afterContext, this.registry);
|
||||
Observation before = Observation.createNotStarted(this.convention, () -> beforeContext, this.registry)
|
||||
.parentObservation(parentObservation);
|
||||
Observation after = Observation.createNotStarted(this.convention, () -> afterContext, this.registry)
|
||||
.parentObservation(parentObservation);
|
||||
AroundWebFilterObservation parent = AroundWebFilterObservation.create(before, after);
|
||||
exchange.getAttributes().put(ATTRIBUTE, parent);
|
||||
return parent;
|
||||
|
||||
@@ -104,7 +104,7 @@ public class CsrfWebFilter implements WebFilter {
|
||||
* Specifies a {@link ServerCsrfTokenRequestHandler} that is used to make the
|
||||
* {@code CsrfToken} available as an exchange attribute.
|
||||
* <p>
|
||||
* The default is {@link ServerCsrfTokenRequestAttributeHandler}.
|
||||
* The default is {@link XorServerCsrfTokenRequestAttributeHandler}.
|
||||
* @param requestHandler the {@link ServerCsrfTokenRequestHandler} to use
|
||||
* @since 5.8
|
||||
*/
|
||||
|
||||
+27
-1
@@ -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.
|
||||
@@ -16,17 +16,24 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationHandler;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@@ -61,4 +68,23 @@ public class ObservationFilterChainDecoratorTests {
|
||||
verifyNoInteractions(handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void decorateFiltersWhenDefaultsThenObserves() throws Exception {
|
||||
ObservationHandler<?> handler = mock(ObservationHandler.class);
|
||||
given(handler.supportsContext(any())).willReturn(true);
|
||||
ObservationRegistry registry = ObservationRegistry.create();
|
||||
registry.observationConfig().observationHandler(handler);
|
||||
ObservationFilterChainDecorator decorator = new ObservationFilterChainDecorator(registry);
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
Filter filter = mock(Filter.class);
|
||||
FilterChain decorated = decorator.decorate(chain, List.of(filter));
|
||||
decorated.doFilter(new MockHttpServletRequest("GET", "/"), new MockHttpServletResponse());
|
||||
verify(handler, times(2)).onStart(any());
|
||||
ArgumentCaptor<Observation.Event> event = ArgumentCaptor.forClass(Observation.Event.class);
|
||||
verify(handler, times(2)).onEvent(event.capture(), any());
|
||||
List<Observation.Event> events = event.getAllValues();
|
||||
assertThat(events.get(0).getName()).isEqualTo(filter.getClass().getSimpleName() + ".before");
|
||||
assertThat(events.get(1).getName()).isEqualTo(filter.getClass().getSimpleName() + ".after");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+60
@@ -16,14 +16,17 @@
|
||||
|
||||
package org.springframework.security.web.authentication.switchuser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.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;
|
||||
@@ -46,11 +49,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;
|
||||
@@ -502,6 +509,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.
|
||||
@@ -127,11 +127,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.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
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);
|
||||
}
|
||||
@@ -139,12 +140,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.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
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);
|
||||
}
|
||||
@@ -152,12 +154,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.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
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);
|
||||
}
|
||||
@@ -166,8 +169,8 @@ 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);
|
||||
CsrfTokenRequestHandler handler = new XorCsrfTokenRequestAttributeHandler();
|
||||
handler.handle(this.request, this.response, () -> this.token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.request.getAttribute(CsrfToken.class.getName());
|
||||
@@ -176,6 +179,7 @@ public class CsrfFilterTests {
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
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);
|
||||
}
|
||||
@@ -183,11 +187,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.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -195,11 +200,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.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -207,8 +213,8 @@ 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);
|
||||
CsrfTokenRequestHandler handler = new XorCsrfTokenRequestAttributeHandler();
|
||||
handler.handle(this.request, this.response, () -> this.token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.request.getAttribute(CsrfToken.class.getName());
|
||||
@@ -216,6 +222,7 @@ public class CsrfFilterTests {
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -224,8 +231,8 @@ 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);
|
||||
CsrfTokenRequestHandler handler = new XorCsrfTokenRequestAttributeHandler();
|
||||
handler.handle(this.request, this.response, () -> this.token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.request.getAttribute(CsrfToken.class.getName());
|
||||
@@ -234,6 +241,7 @@ public class CsrfFilterTests {
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
verify(this.filterChain).doFilter(this.request, this.response);
|
||||
verifyNoMoreInteractions(this.deniedHandler);
|
||||
}
|
||||
@@ -241,8 +249,8 @@ 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);
|
||||
CsrfTokenRequestHandler handler = new XorCsrfTokenRequestAttributeHandler();
|
||||
handler.handle(this.request, this.response, () -> this.token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.request.getAttribute(CsrfToken.class.getName());
|
||||
@@ -250,6 +258,7 @@ public class CsrfFilterTests {
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
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),
|
||||
@@ -259,8 +268,8 @@ 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);
|
||||
CsrfTokenRequestHandler handler = new XorCsrfTokenRequestAttributeHandler();
|
||||
handler.handle(this.request, this.response, () -> this.token);
|
||||
CsrfToken csrfToken = (CsrfToken) this.request.getAttribute(CsrfToken.class.getName());
|
||||
@@ -268,6 +277,7 @@ public class CsrfFilterTests {
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThatCsrfToken(this.request.getAttribute(this.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
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);
|
||||
@@ -332,11 +342,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.csrfAttrName)).isNotNull();
|
||||
assertThatCsrfToken(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute(DeferredCsrfToken.class.getName())).isSameAs(deferredCsrfToken);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
verifyNoMoreInteractions(this.filterChain);
|
||||
}
|
||||
@@ -360,22 +371,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);
|
||||
@@ -384,11 +397,12 @@ 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);
|
||||
this.filter.doFilter(this.request, this.response, this.filterChain);
|
||||
assertThat(this.request.getAttribute(CsrfToken.class.getName())).isNotNull();
|
||||
assertThat(this.request.getAttribute("_csrf")).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);
|
||||
|
||||
@@ -407,10 +421,11 @@ 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);
|
||||
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);
|
||||
}
|
||||
@@ -435,10 +450,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);
|
||||
|
||||
+19
-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.
|
||||
@@ -114,6 +114,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());
|
||||
@@ -128,6 +145,7 @@ public class HttpSessionRequestCacheTests {
|
||||
cache.saveRequest(request, new MockHttpServletResponse());
|
||||
assertThat(request.getSession().getAttribute(HttpSessionRequestCache.SAVED_REQUEST)).isNotNull();
|
||||
MockHttpServletRequest requestToMatch = new MockHttpServletRequest();
|
||||
requestToMatch.setQueryString("success");
|
||||
requestToMatch.setParameter("success", "");
|
||||
requestToMatch.setSession(request.getSession());
|
||||
HttpServletRequest matchingRequest = cache.getMatchingRequest(requestToMatch, new MockHttpServletResponse());
|
||||
|
||||
Reference in New Issue
Block a user