From 4236a48065a5cbe470cdb0313e120211274b48be Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Thu, 15 Oct 2026 12:12:32 +0200 Subject: [PATCH] ESB-1293 Secured cookies when ENTANDO_SECURE_SECRET_COOKIES is set and REDIS sessions are enabled --- .../redis/session/RedisSessionConfig.java | 68 +++++++++++++++++- .../redis/session/RedisSessionActiveTest.java | 15 +++- .../redis/session/RedisSessionConfigTest.java | 70 +++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfigTest.java diff --git a/redis-plugin/src/main/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfig.java b/redis-plugin/src/main/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfig.java index 64dfe8ef91..de7ef23892 100644 --- a/redis-plugin/src/main/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfig.java +++ b/redis-plugin/src/main/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfig.java @@ -1,12 +1,78 @@ package org.entando.entando.plugins.jpredis.aps.system.redis.session; +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionCookieConfig; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.entando.entando.aps.util.UrlUtils; import org.entando.entando.plugins.jpredis.aps.system.redis.conditions.RedisSessionActive; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; +import org.springframework.session.web.http.CookieSerializer; +import org.springframework.session.web.http.DefaultCookieSerializer; +import org.springframework.web.context.ServletContextAware; +@Slf4j @Configuration @EnableRedisHttpSession @RedisSessionActive(true) -public class RedisSessionConfig { +public class RedisSessionConfig implements ServletContextAware { + static final String ENTANDO_SECURE_SECRET_COOKIES = "ENTANDO_SECURE_SECRET_COOKIES"; + + private ServletContext servletContext; + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Bean + public CookieSerializer cookieSerializer() { + return createCookieSerializer(isSecureCookie(System.getenv(ENTANDO_SECURE_SECRET_COOKIES), + UrlUtils.determineForceHttps())); + } + + /** + * Same rule StartupListener applies to the container session cookie. + */ + static boolean isSecureCookie(String secureFlag, boolean forceHttps) { + return StringUtils.isNotEmpty(secureFlag) ? Boolean.parseBoolean(secureFlag) : forceHttps; + } + + /** + * Copies the servlet session cookie config as Spring Session's default serializer does, adding Secure when + * requested; otherwise Secure follows request.isSecure(). + */ + DefaultCookieSerializer createCookieSerializer(boolean secure) { + DefaultCookieSerializer serializer = new DefaultCookieSerializer(); + SessionCookieConfig config = null; + if (this.servletContext != null) { + try { + config = this.servletContext.getSessionCookieConfig(); + } catch (UnsupportedOperationException ex) { + log.warn("Unable to obtain SessionCookieConfig: {}", ex.getMessage()); + } + } + if (config != null) { + if (config.getName() != null) { + serializer.setCookieName(config.getName()); + } + if (config.getDomain() != null) { + serializer.setDomainName(config.getDomain()); + } + if (config.getPath() != null) { + serializer.setCookiePath(config.getPath()); + } + if (config.getMaxAge() != -1) { + serializer.setCookieMaxAge(config.getMaxAge()); + } + } + if (secure) { + serializer.setUseSecureCookie(true); + log.info("Spring Session cookie Secure flag: TRUE"); + } + return serializer; + } } diff --git a/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionActiveTest.java b/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionActiveTest.java index f02db66975..f1a91e2882 100644 --- a/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionActiveTest.java +++ b/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionActiveTest.java @@ -2,8 +2,8 @@ import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; -import java.util.List; import jakarta.servlet.Filter; +import java.util.List; import org.entando.entando.TestEntandoJndiUtils; import org.entando.entando.aps.system.services.cache.RedisEnvironmentVariables; import org.entando.entando.plugins.jpredis.utils.RedisTestExtension; @@ -14,8 +14,9 @@ import org.mockito.MockedStatic; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.session.SessionRepository; -import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.data.redis.RedisSessionRepository; +import org.springframework.session.web.http.CookieHttpSessionIdResolver; +import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.SessionRepositoryFilter; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; @@ -50,6 +51,9 @@ static void setUp(MockedStatic mockedEnv) { @Autowired private RedisClient redisClient; + @Autowired + private CookieSerializer cookieSerializer; + @Test void testRedisSessionActive(GenericContainer redisContainer) throws Exception { Assertions.assertTrue(springSessionRepositoryFilter instanceof SessionRepositoryFilter); @@ -68,4 +72,11 @@ void testRedisSessionActive(GenericContainer redisContainer) throws Exception { } } } + + @Test + void testSessionFilterUsesCookieSerializerBean() { + Object resolver = ReflectionTestUtils.getField(springSessionRepositoryFilter, "httpSessionIdResolver"); + Assertions.assertTrue(resolver instanceof CookieHttpSessionIdResolver); + Assertions.assertSame(cookieSerializer, ReflectionTestUtils.getField(resolver, "cookieSerializer")); + } } diff --git a/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfigTest.java b/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfigTest.java new file mode 100644 index 0000000000..d6fdaefadb --- /dev/null +++ b/redis-plugin/src/test/java/org/entando/entando/plugins/jpredis/aps/system/redis/session/RedisSessionConfigTest.java @@ -0,0 +1,70 @@ +package org.entando.entando.plugins.jpredis.aps.system.redis.session; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.session.web.http.CookieSerializer.CookieValue; +import org.springframework.session.web.http.DefaultCookieSerializer; + +class RedisSessionConfigTest { + + @Test + void testIsSecureCookie() { + Assertions.assertTrue(RedisSessionConfig.isSecureCookie("true", false)); + Assertions.assertTrue(RedisSessionConfig.isSecureCookie("TRUE", false)); + Assertions.assertFalse(RedisSessionConfig.isSecureCookie("false", true)); + Assertions.assertTrue(RedisSessionConfig.isSecureCookie(null, true)); + Assertions.assertTrue(RedisSessionConfig.isSecureCookie("", true)); + Assertions.assertFalse(RedisSessionConfig.isSecureCookie(null, false)); + Assertions.assertFalse(RedisSessionConfig.isSecureCookie("", false)); + } + + @Test + void testSecureCookieOverHttp() { + String header = writeCookie(new RedisSessionConfig().createCookieSerializer(true), false); + Assertions.assertTrue(header.startsWith("SESSION=")); + } + + @Test + void testNotSecureCookieFollowsRequest() { + RedisSessionConfig config = new RedisSessionConfig(); + Assertions.assertFalse(writeCookie(config.createCookieSerializer(false), false).contains("Secure")); + Assertions.assertTrue(writeCookie(config.createCookieSerializer(false), true).contains("; Secure")); + } + + @Test + void testSessionCookieConfigCopied() { + MockServletContext servletContext = new MockServletContext(); + servletContext.getSessionCookieConfig().setName("CUSTOM"); + servletContext.getSessionCookieConfig().setPath("/ctx"); + servletContext.getSessionCookieConfig().setDomain("example.org"); + servletContext.getSessionCookieConfig().setMaxAge(60); + RedisSessionConfig config = new RedisSessionConfig(); + config.setServletContext(servletContext); + + String header = writeCookie(config.createCookieSerializer(true), false); + + Assertions.assertTrue(header.startsWith("CUSTOM=")); + Assertions.assertTrue(header.contains("; Max-Age=60")); + Assertions.assertTrue(header.contains("; Domain=example.org")); + Assertions.assertTrue(header.contains("; Path=/ctx")); + Assertions.assertTrue(header.contains("; Secure")); + } + + @Test + void testDefaultNameWithoutSessionCookieConfigName() { + RedisSessionConfig config = new RedisSessionConfig(); + config.setServletContext(new MockServletContext()); + Assertions.assertTrue(writeCookie(config.createCookieSerializer(false), false).startsWith("SESSION=")); + } + + private String writeCookie(DefaultCookieSerializer serializer, boolean secureRequest) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setSecure(secureRequest); + MockHttpServletResponse response = new MockHttpServletResponse(); + serializer.writeCookieValue(new CookieValue(request, response, "id")); + return response.getHeader("Set-Cookie"); + } +}