diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java index 797910b8ea37..73642504ba4e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java @@ -77,12 +77,7 @@ private void initializeCookie(ResponseCookieBuilder builder) { map.from(cookie::getSecure).to(builder::secure); map.from(cookie::getMaxAge).to(builder::maxAge); map.from(cookie::getPartitioned).to(builder::partitioned); - map.from(getSameSite(cookie)).to(builder::sameSite); - } - - private String getSameSite(Cookie properties) { - SameSite sameSite = properties.getSameSite(); - return (sameSite != null) ? sameSite.attributeValue() : null; + map.from(cookie::getSameSite).as(SameSite::attributeValue).to(builder::sameSite); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index 392ec235aeac..42241de134b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -170,6 +170,16 @@ void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() { }); } + @Test + void sessionCookieSameSiteOmittedIsAppliedToAutoConfiguredCookieSerializer() { + this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) + .withPropertyValues("server.servlet.session.cookie.sameSite=omitted") + .run((context) -> { + DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class); + assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", null); + }); + } + @Test void autoConfiguredCookieSerializerIsUsedBySessionRepositoryFilter() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index ac9ed313b725..c7bd34d8be9a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -676,6 +676,15 @@ void customSessionCookieConfigurationShouldBeApplied() { })); } + @Test + void sessionCookieOmittedConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("server.reactive.session.cookie.same-site:omitted") + .run(assertExchangeWithSession((exchange) -> { + List cookies = exchange.getResponse().getCookies().get("SESSION"); + assertThat(cookies).extracting(ResponseCookie::getSameSite).containsOnlyNulls(); + })); + } + @ParameterizedTest @ValueSource(classes = { ServerProperties.class, WebFluxProperties.class }) void propertiesAreNotEnabledInNonWebApplication(Class propertiesClass) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 00c504e7db31..6bdd590c688d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -284,7 +284,7 @@ protected final void configureWebAppContext(WebAppContext context, ServletContex private void configureSession(WebAppContext context) { SessionHandler handler = context.getSessionHandler(); SameSite sessionSameSite = getSession().getCookie().getSameSite(); - if (sessionSameSite != null) { + if (sessionSameSite != null && sessionSameSite != SameSite.OMITTED) { handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name())); } Duration sessionTimeout = getSession().getTimeout(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 6b6b0b62be0a..d10bf99ed4c9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -998,11 +998,12 @@ private static class SuppliedSameSiteCookieProcessor extends Rfc6265CookieProces @Override public String generateHeader(Cookie cookie, HttpServletRequest request) { SameSite sameSite = getSameSite(cookie); - if (sameSite == null) { + String sameSiteValue = (sameSite != null) ? sameSite.attributeValue() : null; + if (sameSiteValue == null) { return super.generateHeader(cookie, request); } Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor(); - delegate.setSameSiteCookies(sameSite.attributeValue()); + delegate.setSameSiteCookies(sameSiteValue); return delegate.generateHeader(cookie, request); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 40597690d775..4bc7f552c942 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -635,7 +635,10 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { private void beforeCommit(HttpServerExchange exchange) { for (Cookie cookie : exchange.responseCookies()) { SameSite sameSite = getSameSite(asServletCookie(cookie)); - if (sameSite != null) { + if (sameSite == SameSite.OMITTED) { + cookie.setSameSite(false); + } + else if (sameSite != null) { cookie.setSameSiteMode(sameSite.attributeValue()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java index dcf8721610b5..26cfa8e35e8c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java @@ -145,6 +145,11 @@ public void setPartitioned(Boolean partitioned) { */ public enum SameSite { + /** + * The SameSite cookie attribute will be omitted when creating the cookie. + */ + OMITTED(null), + /** * Cookies are sent in both first-party and cross-origin requests. */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 49b1340069b2..4546b4b86315 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -881,7 +881,7 @@ void sessionCookieConfiguration() { } @ParameterizedTest - @EnumSource + @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "OMITTED") void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(SameSite sameSite) throws Exception { AbstractServletWebServerFactory factory = getFactory(); factory.getSession().getCookie().setSameSite(sameSite); @@ -896,7 +896,7 @@ void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(S } @ParameterizedTest - @EnumSource + @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "OMITTED") void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookiesWhenUsingCustomName(SameSite sameSite) throws Exception { AbstractServletWebServerFactory factory = getFactory(); @@ -949,6 +949,23 @@ void cookieSameSiteSuppliersShouldNotAffectSessionCookie() throws IOException, U (header) -> assertThat(header).contains("test=test").contains("SameSite=Strict")); } + @Test + void cookieSameSiteSuppliersShouldNotAffectOmittedSameSite() throws IOException, URISyntaxException { + AbstractServletWebServerFactory factory = getFactory(); + factory.getSession().getCookie().setSameSite(SameSite.OMITTED); + factory.getSession().getCookie().setName("SESSIONCOOKIE"); + factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofStrict()); + factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(false), "/")); + this.webServer = factory.getWebServer(); + this.webServer.start(); + ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/")); + assertThat(clientResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + List setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie"); + assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder( + (header) -> assertThat(header).contains("SESSIONCOOKIE").doesNotContain("SameSite"), + (header) -> assertThat(header).contains("test=test").contains("SameSite=Strict")); + } + @Test protected void sslSessionTracking() { AbstractServletWebServerFactory factory = getFactory();