Merge pull request #44714 from filiphr

* gh-44714:
  Polish "Add support for omitting SameSite attribute from session cookie"
  Add support for omitting SameSite attribute from session cookie

Closes gh-44714
This commit is contained in:
Andy Wilkinson 2025-03-20 13:15:24 +00:00
commit 437c259d9e
8 changed files with 61 additions and 20 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2024 the original author or authors. * Copyright 2012-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -77,12 +77,7 @@ public class WebSessionIdResolverAutoConfiguration {
map.from(cookie::getSecure).to(builder::secure); map.from(cookie::getSecure).to(builder::secure);
map.from(cookie::getMaxAge).to(builder::maxAge); map.from(cookie::getMaxAge).to(builder::maxAge);
map.from(cookie::getPartitioned).to(builder::partitioned); map.from(cookie::getPartitioned).to(builder::partitioned);
map.from(getSameSite(cookie)).to(builder::sameSite); map.from(cookie::getSameSite).as(SameSite::attributeValue).to(builder::sameSite);
}
private String getSameSite(Cookie properties) {
SameSite sameSite = properties.getSameSite();
return (sameSite != null) ? sameSite.attributeValue() : null;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2024 the original author or authors. * Copyright 2012-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -170,6 +170,16 @@ class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurationTest
}); });
} }
@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 @Test
void autoConfiguredCookieSerializerIsUsedBySessionRepositoryFilter() { void autoConfiguredCookieSerializerIsUsedBySessionRepositoryFilter() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)

View File

@ -676,6 +676,15 @@ class WebFluxAutoConfigurationTests {
})); }));
} }
@Test
void sessionCookieOmittedConfigurationShouldBeApplied() {
this.contextRunner.withPropertyValues("server.reactive.session.cookie.same-site:omitted")
.run(assertExchangeWithSession((exchange) -> {
List<ResponseCookie> cookies = exchange.getResponse().getCookies().get("SESSION");
assertThat(cookies).extracting(ResponseCookie::getSameSite).containsOnlyNulls();
}));
}
@ParameterizedTest @ParameterizedTest
@ValueSource(classes = { ServerProperties.class, WebFluxProperties.class }) @ValueSource(classes = { ServerProperties.class, WebFluxProperties.class })
void propertiesAreNotEnabledInNonWebApplication(Class<?> propertiesClass) { void propertiesAreNotEnabledInNonWebApplication(Class<?> propertiesClass) {

View File

@ -284,7 +284,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
private void configureSession(WebAppContext context) { private void configureSession(WebAppContext context) {
SessionHandler handler = context.getSessionHandler(); SessionHandler handler = context.getSessionHandler();
SameSite sessionSameSite = getSession().getCookie().getSameSite(); SameSite sessionSameSite = getSession().getCookie().getSameSite();
if (sessionSameSite != null) { if (sessionSameSite != null && sessionSameSite != SameSite.OMITTED) {
handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name())); handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name()));
} }
Duration sessionTimeout = getSession().getTimeout(); Duration sessionTimeout = getSession().getTimeout();

View File

@ -998,11 +998,12 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
@Override @Override
public String generateHeader(Cookie cookie, HttpServletRequest request) { public String generateHeader(Cookie cookie, HttpServletRequest request) {
SameSite sameSite = getSameSite(cookie); SameSite sameSite = getSameSite(cookie);
if (sameSite == null) { String sameSiteValue = (sameSite != null) ? sameSite.attributeValue() : null;
if (sameSiteValue == null) {
return super.generateHeader(cookie, request); return super.generateHeader(cookie, request);
} }
Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor(); Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor();
delegate.setSameSiteCookies(sameSite.attributeValue()); delegate.setSameSiteCookies(sameSiteValue);
return delegate.generateHeader(cookie, request); return delegate.generateHeader(cookie, request);
} }

View File

@ -635,7 +635,10 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
private void beforeCommit(HttpServerExchange exchange) { private void beforeCommit(HttpServerExchange exchange) {
for (Cookie cookie : exchange.responseCookies()) { for (Cookie cookie : exchange.responseCookies()) {
SameSite sameSite = getSameSite(asServletCookie(cookie)); SameSite sameSite = getSameSite(asServletCookie(cookie));
if (sameSite != null) { if (sameSite == SameSite.OMITTED) {
cookie.setSameSite(false);
}
else if (sameSite != null) {
cookie.setSameSiteMode(sameSite.attributeValue()); cookie.setSameSiteMode(sameSite.attributeValue());
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2024 the original author or authors. * Copyright 2012-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -146,19 +146,25 @@ public class Cookie {
public enum SameSite { public enum SameSite {
/** /**
* Cookies are sent in both first-party and cross-origin requests. * SameSite attribute will be omitted when creating the cookie.
*/
OMITTED(null),
/**
* SameSite attribute will be set to None. Cookies are sent in both first-party
* and cross-origin requests.
*/ */
NONE("None"), NONE("None"),
/** /**
* Cookies are sent in a first-party context, also when following a link to the * SameSite attribute will be set to Lax. Cookies are sent in a first-party
* origin site. * context, also when following a link to the origin site.
*/ */
LAX("Lax"), LAX("Lax"),
/** /**
* Cookies are only sent in a first-party context (i.e. not when following a link * SameSite attribute will be set to Strict. Cookies are only sent in a
* to the origin site). * first-party context (i.e. not when following a link to the origin site).
*/ */
STRICT("Strict"); STRICT("Strict");

View File

@ -881,7 +881,7 @@ public abstract class AbstractServletWebServerFactoryTests {
} }
@ParameterizedTest @ParameterizedTest
@EnumSource @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "OMITTED")
void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(SameSite sameSite) throws Exception { void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(SameSite sameSite) throws Exception {
AbstractServletWebServerFactory factory = getFactory(); AbstractServletWebServerFactory factory = getFactory();
factory.getSession().getCookie().setSameSite(sameSite); factory.getSession().getCookie().setSameSite(sameSite);
@ -896,7 +896,7 @@ public abstract class AbstractServletWebServerFactoryTests {
} }
@ParameterizedTest @ParameterizedTest
@EnumSource @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "OMITTED")
void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookiesWhenUsingCustomName(SameSite sameSite) void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookiesWhenUsingCustomName(SameSite sameSite)
throws Exception { throws Exception {
AbstractServletWebServerFactory factory = getFactory(); AbstractServletWebServerFactory factory = getFactory();
@ -949,6 +949,23 @@ public abstract class AbstractServletWebServerFactoryTests {
(header) -> assertThat(header).contains("test=test").contains("SameSite=Strict")); (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<String> 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 @Test
protected void sslSessionTracking() { protected void sslSessionTracking() {
AbstractServletWebServerFactory factory = getFactory(); AbstractServletWebServerFactory factory = getFactory();