From 48c2bac710d8687160f8682649c36f45e4879c25 Mon Sep 17 00:00:00 2001 From: Ivan Skachkov Date: Mon, 3 Aug 2020 20:49:47 +0200 Subject: [PATCH] Add initial reactive redis session support Relates to #263 --- autoconfigure-adapter/build.gradle.kts | 1 + ...activeSessionConfigurationInitializer.java | 45 +++++ .../session/SessionInitializer.java | 36 ++++ kofu/build.gradle.kts | 2 + .../fu/kofu/session/SessionDsl.kt | 98 ++++++++++ .../fu/kofu/webflux/SecurityDsl.kt | 8 + .../org/springframework/fu/kofu/TestUtils.kt | 42 +++++ .../fu/kofu/samples/session.kt | 49 +++++ .../fu/kofu/webflux/SecurityDslTests.kt | 33 +--- .../fu/kofu/webflux/SessionDslTests.kt | 170 ++++++++++++++++++ 10 files changed, 457 insertions(+), 27 deletions(-) create mode 100644 autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfigurationInitializer.java create mode 100644 autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/SessionInitializer.java create mode 100644 kofu/src/main/kotlin/org/springframework/fu/kofu/session/SessionDsl.kt create mode 100644 kofu/src/test/kotlin/org/springframework/fu/kofu/TestUtils.kt create mode 100644 kofu/src/test/kotlin/org/springframework/fu/kofu/samples/session.kt create mode 100644 kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SessionDslTests.kt diff --git a/autoconfigure-adapter/build.gradle.kts b/autoconfigure-adapter/build.gradle.kts index b237c799a..fddc23941 100644 --- a/autoconfigure-adapter/build.gradle.kts +++ b/autoconfigure-adapter/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { compileOnly("javax.servlet:javax.servlet-api") compileOnly("org.springframework:spring-webflux") compileOnly("org.springframework.boot:spring-boot-starter-security") + compileOnly("org.springframework.session:spring-session-data-redis") compileOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo") compileOnly("org.springframework.data:spring-data-mongodb") compileOnly("org.mongodb:mongodb-driver-reactivestreams") diff --git a/autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfigurationInitializer.java b/autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfigurationInitializer.java new file mode 100644 index 000000000..6783483c6 --- /dev/null +++ b/autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfigurationInitializer.java @@ -0,0 +1,45 @@ +package org.springframework.boot.autoconfigure.session; + +import org.springframework.boot.autoconfigure.session.RedisReactiveSessionConfiguration.SpringBootRedisWebSessionConfiguration; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.config.ReactiveSessionRepositoryCustomizer; +import org.springframework.session.data.redis.ReactiveRedisSessionRepository; + +/** + * {@link ApplicationContextInitializer} adapter for {@link RedisReactiveSessionConfiguration}. + */ +public class RedisReactiveSessionConfigurationInitializer { + + private final SessionProperties sessionProperties; + private final RedisSessionProperties redisSessionProperties; + + public RedisReactiveSessionConfigurationInitializer(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) { + this.sessionProperties = sessionProperties; + this.redisSessionProperties = redisSessionProperties; + } + + public void initialize(GenericApplicationContext context) { + SpringBootRedisWebSessionConfiguration springBootRedisWebSessionConfiguration = new SpringBootRedisWebSessionConfiguration(); + + springBootRedisWebSessionConfiguration.customize(sessionProperties, redisSessionProperties); + + context.registerBean(ReactiveSessionRepository.class, () -> { + springBootRedisWebSessionConfiguration.setRedisConnectionFactory( + context.getBeanProvider(ReactiveRedisConnectionFactory.class), // TODO use @SpringSessionRedisConnectionFactory annotation qualifier + context.getBeanProvider(ReactiveRedisConnectionFactory.class) + ); + springBootRedisWebSessionConfiguration.setDefaultRedisSerializer( + (RedisSerializer) context.getBeanProvider(ResolvableType.forClassWithGenerics(RedisSerializer.class, Object.class)).getIfAvailable() + ); + springBootRedisWebSessionConfiguration.setSessionRepositoryCustomizer( + context.getBeanProvider(ResolvableType.forClassWithGenerics(ReactiveSessionRepositoryCustomizer.class, ReactiveRedisSessionRepository.class)) + ); + return springBootRedisWebSessionConfiguration.sessionRepository(); + }); + } +} diff --git a/autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/SessionInitializer.java b/autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/SessionInitializer.java new file mode 100644 index 000000000..81042720c --- /dev/null +++ b/autoconfigure-adapter/src/main/java/org/springframework/boot/autoconfigure/session/SessionInitializer.java @@ -0,0 +1,36 @@ +package org.springframework.boot.autoconfigure.session; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.Session; +import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; +import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionIdResolver; +import org.springframework.web.server.session.WebSessionManager; + +/** + * {@link ApplicationContextInitializer} adapter for {@link SessionAutoConfiguration} and + * {@link EnableSpringWebSession}. + */ +public class SessionInitializer { + public void initialize(GenericApplicationContext context) { + // SpringWebSessionConfiguration + context.registerBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, WebSessionManager.class, () -> { + SpringWebSessionConfiguration springWebSessionConfiguration = new SpringWebSessionConfiguration(); + WebSessionManager webSessionManager = springWebSessionConfiguration.webSessionManager( + (ReactiveSessionRepository) context.getBeanProvider( + ResolvableType.forClassWithGenerics(ReactiveSessionRepository.class, Session.class) + ).getIfAvailable() + ); + WebSessionIdResolver webSessionIdResolver = context.getBeanProvider(WebSessionIdResolver.class).getIfAvailable(); + if (webSessionIdResolver != null) { + ((DefaultWebSessionManager)webSessionManager).setSessionIdResolver(webSessionIdResolver); + } + return webSessionManager; + }); + } +} diff --git a/kofu/build.gradle.kts b/kofu/build.gradle.kts index 2134989e1..12e079ae1 100644 --- a/kofu/build.gradle.kts +++ b/kofu/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { compileOnly("org.springframework:spring-webflux") compileOnly("org.springframework.boot:spring-boot-starter-security") compileOnly("org.springframework.security.dsl:spring-security-kotlin-dsl:0.0.1.BUILD-SNAPSHOT") // needed until spring-security 5.4 + compileOnly("org.springframework.session:spring-session-data-redis") compileOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo") compileOnly("org.springframework.data:spring-data-mongodb") compileOnly("org.springframework.data:spring-data-r2dbc") @@ -39,6 +40,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-webflux") testImplementation("org.springframework.boot:spring-boot-starter-security") testImplementation("org.springframework.security.dsl:spring-security-kotlin-dsl:0.0.1.BUILD-SNAPSHOT") // needed until spring-security 5.4 + testImplementation("org.springframework.session:spring-session-data-redis") testImplementation("org.springframework.boot:spring-boot-starter-tomcat") testImplementation("org.springframework.boot:spring-boot-starter-undertow") testImplementation("org.springframework.boot:spring-boot-starter-jetty") diff --git a/kofu/src/main/kotlin/org/springframework/fu/kofu/session/SessionDsl.kt b/kofu/src/main/kotlin/org/springframework/fu/kofu/session/SessionDsl.kt new file mode 100644 index 000000000..b6d7d1999 --- /dev/null +++ b/kofu/src/main/kotlin/org/springframework/fu/kofu/session/SessionDsl.kt @@ -0,0 +1,98 @@ +package org.springframework.fu.kofu.session + +import org.springframework.boot.autoconfigure.session.RedisReactiveSessionConfigurationInitializer +import org.springframework.boot.autoconfigure.session.RedisSessionProperties +import org.springframework.boot.autoconfigure.session.SessionInitializer +import org.springframework.boot.autoconfigure.session.SessionProperties +import org.springframework.context.support.GenericApplicationContext +import org.springframework.fu.kofu.AbstractDsl +import org.springframework.fu.kofu.ConfigurationDsl +import org.springframework.session.SaveMode +import java.time.Duration + +/** + * Kofu DSL for spring-session. + * + * Configure spring-session. + * + * Required dependencies can be retrieve using `org.springframework.boot:spring-boot-starter-session`. + * + * @author Ivan Skachkov + */ +class SessionDsl(private val init: SessionDsl.() -> Unit) : ConfigurationDsl({}) { + + override fun initialize(context: GenericApplicationContext) { + super.initialize(context) + init() + SessionInitializer().initialize(context) + } +} + +/** + * Configure spring-session. + * + * Requires `org.springframework.boot:spring-boot-starter-session` dependency. + * + * @sample org.springframework.fu.kofu.samples.sessionDsl + * @author Ivan Skachkov + */ +fun ConfigurationDsl.session(dsl: SessionDsl.() -> Unit = {}) { + SessionDsl(dsl).initialize(context) +} + +/** + * Kofu DSL for spring-session-data-redis. + * + * Configure spring-session-data-redis. + * + * Required dependencies can be retrieve using `org.springframework.session:spring-session-data-redis`. + * + * @see SessionProperties, RedisSessionProperties, org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession + * @author Ivan Skachkov + */ +class ReactiveRedisDsl( + private val init: ReactiveRedisDsl.() -> Unit, + private val sessionProperties: SessionProperties, + private val redisSessionProperties: RedisSessionProperties +) : AbstractDsl() { + + var maxInactiveIntervalInSeconds: Duration + get() = sessionProperties.timeout + set(value) { + sessionProperties.timeout = value + } + + /** + * Namespace for keys used to store sessions. + */ + var redisNamespace: String + get() = redisSessionProperties.namespace + set(value) { + redisSessionProperties.namespace = value + } + + /** + * Sessions save mode. Determines how session changes are tracked and saved to the + * session store. + */ + var saveMode: SaveMode + get() = redisSessionProperties.saveMode + set(value) { + redisSessionProperties.saveMode = value + } + + override fun initialize(context: GenericApplicationContext) { + super.initialize(context) + init() + RedisReactiveSessionConfigurationInitializer(sessionProperties, redisSessionProperties).initialize(context) + } +} + +/** + * @see ReactiveRedisDsl + */ +fun SessionDsl.reactiveRedis(dsl: ReactiveRedisDsl.() -> Unit = {}) { + val sessionProperties = configurationProperties(prefix = "spring.session", defaultProperties = SessionProperties()) + val redisSessionProperties = configurationProperties(prefix = "spring.session.redis", defaultProperties = RedisSessionProperties()) + ReactiveRedisDsl(dsl, sessionProperties, redisSessionProperties).initialize(context) +} diff --git a/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt b/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt index 577c41ffd..6c7c769a7 100644 --- a/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt +++ b/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt @@ -23,6 +23,7 @@ import org.springframework.fu.kofu.ConfigurationDsl import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.config.annotation.web.reactive.SecurityInitializer import org.springframework.security.config.annotation.web.reactive.WebFluxSecurityInitializer +import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService import org.springframework.security.core.userdetails.ReactiveUserDetailsService import org.springframework.security.crypto.password.PasswordEncoder @@ -50,6 +51,11 @@ class SecurityDsl(private val init: SecurityDsl.() -> Unit) : AbstractDsl() { var http: ServerHttpSecurityDsl.() -> Unit = {} + /** + * For customizations not available through spring-security-dsl + */ + var securityCustomizer: (http: ServerHttpSecurity) -> ServerHttpSecurity = { it } + override fun initialize(context: GenericApplicationContext) { super.initialize(context) init() @@ -62,6 +68,8 @@ class SecurityDsl(private val init: SecurityDsl.() -> Unit) : AbstractDsl() { ) securityInitializer.initialize(context) + securityCustomizer.invoke(securityInitializer.httpSecurity) + val chain = securityInitializer.httpSecurity.invoke(http) val webFlux = context is ReactiveWebServerApplicationContext diff --git a/kofu/src/test/kotlin/org/springframework/fu/kofu/TestUtils.kt b/kofu/src/test/kotlin/org/springframework/fu/kofu/TestUtils.kt new file mode 100644 index 000000000..7e385798a --- /dev/null +++ b/kofu/src/test/kotlin/org/springframework/fu/kofu/TestUtils.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.fu.kofu + +import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import java.nio.charset.Charset +import java.util.* + +fun userDetailsService(username: String, password: String): MapReactiveUserDetailsService { + @Suppress("DEPRECATION") + val user = User.withDefaultPasswordEncoder() + .username(username) + .password(password) + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) +} + +val username = "user" +val password = "password" +fun repoAuthenticationManager(): UserDetailsRepositoryReactiveAuthenticationManager { + return UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService(username, password)) +} + +fun basicAuth() = + Base64.getEncoder().encode("$username:$password".toByteArray())?.toString(Charset.defaultCharset()) diff --git a/kofu/src/test/kotlin/org/springframework/fu/kofu/samples/session.kt b/kofu/src/test/kotlin/org/springframework/fu/kofu/samples/session.kt new file mode 100644 index 000000000..c19018fed --- /dev/null +++ b/kofu/src/test/kotlin/org/springframework/fu/kofu/samples/session.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.fu.kofu.samples + +import org.springframework.fu.kofu.reactiveWebApplication +import org.springframework.fu.kofu.session.reactiveRedis +import org.springframework.fu.kofu.webflux.security +import org.springframework.fu.kofu.session.session +import org.springframework.fu.kofu.webflux.webFlux +import org.springframework.session.SaveMode +import java.time.Duration + +fun sessionDsl() { + reactiveWebApplication { + session { + reactiveRedis { + maxInactiveIntervalInSeconds = Duration.ofSeconds(Long.MAX_VALUE) + redisNamespace = "spring:session" // default + saveMode = SaveMode.ON_SET_ATTRIBUTE // default + } + } + security { + http = { + anonymous { } + authorizeExchange { + authorize("/view", hasRole("USER")) + authorize("/public-view", permitAll) + } + headers {} + logout {} + } + } + webFlux { } + } +} diff --git a/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt b/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt index 25ecfe1c3..a9b39fb89 100644 --- a/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt +++ b/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt @@ -17,33 +17,25 @@ package org.springframework.fu.kofu.webflux import org.junit.jupiter.api.Test +import org.springframework.fu.kofu.basicAuth import org.springframework.fu.kofu.localServerPort import org.springframework.fu.kofu.reactiveWebApplication -import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService -import org.springframework.security.core.userdetails.User +import org.springframework.fu.kofu.repoAuthenticationManager import org.springframework.test.web.reactive.server.WebTestClient -import java.nio.charset.Charset import java.time.Duration -import java.util.* /** - * @author Jonas Bark, Ivan Skachkov + * @author Jonas Bark + * @author Ivan Skachkov */ class SecurityDslTests { @Test fun `Check spring-security configuration DSL`() { - - val username = "user" - val password = "password" - val repoAuthenticationManager = - UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService(username, password)) - val app = reactiveWebApplication { security { - authenticationManager = repoAuthenticationManager + authenticationManager = repoAuthenticationManager() http = { authorizeExchange { @@ -72,23 +64,10 @@ class SecurityDslTests { client.get().uri("/view").exchange() .expectStatus().isUnauthorized - val basicAuth = - Base64.getEncoder().encode("$username:$password".toByteArray())?.toString(Charset.defaultCharset()) - client.get().uri("/view").header("Authorization", "Basic $basicAuth").exchange() + client.get().uri("/view").header("Authorization", "Basic ${basicAuth()}").exchange() .expectStatus().is2xxSuccessful close() } } - - private fun userDetailsService(username: String, password: String): MapReactiveUserDetailsService { - @Suppress("DEPRECATION") - val user = User.withDefaultPasswordEncoder() - .username(username) - .password(password) - .roles("USER") - .build() - return MapReactiveUserDetailsService(user) - } - } diff --git a/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SessionDslTests.kt b/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SessionDslTests.kt new file mode 100644 index 000000000..d5c6e1327 --- /dev/null +++ b/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SessionDslTests.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.fu.kofu.webflux + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.fu.kofu.* +import org.springframework.fu.kofu.redis.reactiveRedis +import org.springframework.fu.kofu.session.reactiveRedis +import org.springframework.fu.kofu.session.session +import org.springframework.http.HttpHeaders.SET_COOKIE +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY +import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository +import org.springframework.security.web.server.csrf.CsrfToken +import org.springframework.session.ReactiveSessionRepository +import org.springframework.session.Session +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters.fromFormData +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.WebFilter +import org.springframework.web.server.WebFilterChain +import org.testcontainers.containers.GenericContainer +import reactor.core.publisher.Mono +import java.time.Duration + +/** + * @author Ivan Skachkov + */ +class SessionDslTests { + + private lateinit var redis: GenericContainer + + @BeforeAll + fun setup() { + redis = object : GenericContainer("redis:5") { + init { + withExposedPorts(6379) + } + } + redis.start() + } + + @Test + fun `Check spring-session configuration DSL`() { + val app = reactiveWebApplication { + reactiveRedis { + port = redis.firstMappedPort + lettuce() + } + session { + reactiveRedis() + } + security { + authenticationManager = repoAuthenticationManager() + + http = { + authorizeExchange { + authorize("/view", hasRole("USER")) + authorize("/login", permitAll) + } + formLogin { + loginPage = "/login" + } + csrf { + csrfTokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse() + } + logout { } + } + } + webFlux { + port = 0 + router { + GET("/view") { ok().build() } + GET("/login") { ok().build() } + } + codecs { + form() + } + filter() + } + } + + with(app.run()) { + use { + val client = WebTestClient.bindToServer().baseUrl("http://127.0.0.1:$localServerPort") + .responseTimeout(Duration.ofMinutes(10)) // useful for debug + .build() + // Simulating browser - get csrf token + val getLoginResult = client.get().uri("/login") + .exchange() + .expectStatus().is2xxSuccessful + .expectHeader().exists(SET_COOKIE) + .expectBody().returnResult() + + // Example response header -> Set-Cookie: [XSRF-TOKEN=247d87a6-f5d4-4d3a-96f6-f99e14869cd7; Path=/] + val csrfValue = getLoginResult.responseCookies.getFirst("XSRF-TOKEN")!!.value + // Try logging in + val postLoginResult = client.post().uri("/login") + .header("X-XSRF-TOKEN", csrfValue) + .cookie("XSRF-TOKEN", csrfValue) + .body(fromFormData(SPRING_SECURITY_FORM_USERNAME_KEY, username).with(SPRING_SECURITY_FORM_PASSWORD_KEY, password)) + .exchange() + .expectStatus().is3xxRedirection + .expectHeader().exists(SET_COOKIE) + .expectBody().returnResult() + + // Example header -> Set-Cookie: [SESSION=d07c0ebc-82a3-40d6-aeda-d778fa3e9ae1; Path=/; HttpOnly; SameSite=Lax] + val session = postLoginResult.responseCookies.getFirst("SESSION")?.value + assert(session != null) { "Expected session cookie" } + + client.get().uri("/view") + .cookie("SESSION", session!!) + .exchange() + .expectStatus().is2xxSuccessful + + val reactiveRedisSessionRepository = getBean(ReactiveSessionRepository::class.java) + val redisSessionValue: Session? = reactiveRedisSessionRepository.findById(session).block() + assert(redisSessionValue != null) { "Expected redis session" } + assert(!redisSessionValue!!.isExpired) { "Expected not expired session" } + + // Try logging out and check that session is gone from redis + val postLogoutResult = client.post().uri("/logout") + .header("X-XSRF-TOKEN", csrfValue) + .cookie("XSRF-TOKEN", csrfValue) + .cookie("SESSION", session) + .exchange() + .expectStatus().is3xxRedirection + .expectHeader().exists(SET_COOKIE) + .expectBody().returnResult() + // New session is assigned after logout + // Example response cookie -> Set-Cookie: [XSRF-TOKEN=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT, SESSION=57fc7f02-7411-4a36-8510-d2abef8eb132; Path=/; HttpOnly; SameSite=Lax] + val sessionIdAfterLogout = postLogoutResult.responseCookies.getFirst("SESSION")!!.value + assert(sessionIdAfterLogout != session) { "Expected session to be different after logout" } + + // Old session should be gone from redis + val redisSessionValueAfterLogout: Session? = reactiveRedisSessionRepository.findById(session).block() + assert(redisSessionValueAfterLogout == null) { "Expected redis session to be gone after logout" } + } + } + } + + @AfterAll + fun tearDown() { + redis.stop() + } +} + +class CsrfFilter : WebFilter { // https://github.com/spring-projects/spring-security/issues/5766 + override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { + return (exchange.getAttribute>(CsrfToken::class.java.name) ?: Mono.empty()) + .doOnSuccess {} // do nothing, just subscribe :/ + .then(chain.filter(exchange)) + } +}