Skip to content

Add support for partitioned cookies #42316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
Expand Down Expand Up @@ -98,6 +98,7 @@ DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties,
map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie);
map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(cookieSerializer::setCookieMaxAge);
map.from(cookie::getSameSite).as(SameSite::attributeValue).to(cookieSerializer::setSameSite);
map.from(cookie::getPartitioned).to(cookieSerializer::setPartitioned);
cookieSerializerCustomizers.orderedStream().forEach((customizer) -> customizer.customize(cookieSerializer));
return cookieSerializer;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
Expand Down Expand Up @@ -76,6 +76,7 @@ private void initializeCookie(ResponseCookieBuilder builder) {
map.from(cookie::getHttpOnly).to(builder::httpOnly);
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
"name": "server.reactive.session.cookie.name",
"description": "Name for the cookie."
},
{
"name": "server.reactive.session.cookie.partitioned",
"description": "Whether the generated cookie carries the Partitioned attribute."
},
{
"name": "server.reactive.session.cookie.path",
"description": "Path of the cookie."
Expand Down Expand Up @@ -229,6 +233,10 @@
"name": "server.servlet.session.cookie.name",
"description": "Name of the cookie."
},
{
"name": "server.servlet.session.cookie.partitioned",
"description": "Whether the generated cookie carries the Partitioned attribute."
},
{
"name": "server.servlet.session.cookie.path",
"description": "Path of the cookie."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() {
.withPropertyValues("server.servlet.session.cookie.name=sid", "server.servlet.session.cookie.domain=spring",
"server.servlet.session.cookie.path=/test", "server.servlet.session.cookie.httpOnly=false",
"server.servlet.session.cookie.secure=false", "server.servlet.session.cookie.maxAge=10s",
"server.servlet.session.cookie.sameSite=strict")
"server.servlet.session.cookie.sameSite=strict", "server.servlet.session.cookie.partitioned=true")
.run((context) -> {
DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieName", "sid");
Expand All @@ -166,6 +166,7 @@ void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() {
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("useSecureCookie", false);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieMaxAge", 10);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", "Strict");
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("partitioned", true);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,8 @@ void customSessionCookieConfigurationShouldBeApplied() {
this.contextRunner.withPropertyValues("server.reactive.session.cookie.name:JSESSIONID",
"server.reactive.session.cookie.domain:.example.com", "server.reactive.session.cookie.path:/example",
"server.reactive.session.cookie.max-age:60", "server.reactive.session.cookie.http-only:false",
"server.reactive.session.cookie.secure:false", "server.reactive.session.cookie.same-site:strict")
"server.reactive.session.cookie.secure:false", "server.reactive.session.cookie.same-site:strict",
"server.reactive.session.cookie.partitioned:true")
.run(assertExchangeWithSession((exchange) -> {
List<ResponseCookie> cookies = exchange.getResponse().getCookies().get("JSESSIONID");
assertThat(cookies).isNotEmpty();
Expand All @@ -654,6 +655,7 @@ void customSessionCookieConfigurationShouldBeApplied() {
assertThat(cookies).allMatch((cookie) -> !cookie.isHttpOnly());
assertThat(cookies).allMatch((cookie) -> !cookie.isSecure());
assertThat(cookies).allMatch((cookie) -> cookie.getSameSite().equals("Strict"));
assertThat(cookies).allMatch(ResponseCookie::isPartitioned);
}));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
Expand Down Expand Up @@ -57,6 +57,11 @@ public class Cookie {
*/
private Boolean secure;

/**
* Whether the generated cookie carries the Partitioned attribute.
*/
private Boolean partitioned;

/**
* Maximum age of the cookie. If a duration suffix is not specified, seconds will be
* used. A positive value indicates when the cookie expires relative to the current
Expand Down Expand Up @@ -127,6 +132,14 @@ public void setSameSite(SameSite sameSite) {
this.sameSite = sameSite;
}

public Boolean getPartitioned() {
return this.partitioned;
}

public void setPartitioned(Boolean partitioned) {
this.partitioned = partitioned;
}

/**
* SameSite values.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
public abstract class AbstractServletWebServerFactory extends AbstractConfigurableWebServerFactory
implements ConfigurableServletWebServerFactory {

static final String PARTITIONED_ATTRIBUTE_NAME = "Partitioned";

protected final Log logger = LogFactory.getLog(getClass());

private String contextPath = "";
Expand Down Expand Up @@ -350,6 +352,9 @@ private void configureSessionCookie(SessionCookieConfig config) {
map.from(cookie::getHttpOnly).to(config::setHttpOnly);
map.from(cookie::getSecure).to(config::setSecure);
map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(config::setMaxAge);
map.from(cookie::getPartitioned)
.as(Object::toString)
.to((partitioned) -> config.setAttribute(PARTITIONED_ATTRIBUTE_NAME, partitioned));
}

private Set<jakarta.servlet.SessionTrackingMode> unwrap(Set<Session.SessionTrackingMode> modes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@ void sessionCookieConfiguration() {
factory.getSession().getCookie().setPath("/testpath");
factory.getSession().getCookie().setHttpOnly(true);
factory.getSession().getCookie().setSecure(true);
factory.getSession().getCookie().setPartitioned(true);
factory.getSession().getCookie().setMaxAge(Duration.ofSeconds(60));
final AtomicReference<SessionCookieConfig> configReference = new AtomicReference<>();
this.webServer = factory.getWebServer((context) -> configReference.set(context.getSessionCookieConfig()));
Expand All @@ -872,6 +873,8 @@ void sessionCookieConfiguration() {
assertThat(sessionCookieConfig.getPath()).isEqualTo("/testpath");
assertThat(sessionCookieConfig.isHttpOnly()).isTrue();
assertThat(sessionCookieConfig.isSecure()).isTrue();
assertThat(sessionCookieConfig.getAttribute(AbstractServletWebServerFactory.PARTITIONED_ATTRIBUTE_NAME))
.isEqualTo("true");
assertThat(sessionCookieConfig.getMaxAge()).isEqualTo(60);
}

Expand Down Expand Up @@ -1166,6 +1169,7 @@ void sessionConfiguration() {
factory.getSession().getCookie().setPath("/testpath");
factory.getSession().getCookie().setHttpOnly(true);
factory.getSession().getCookie().setSecure(true);
factory.getSession().getCookie().setPartitioned(false);
factory.getSession().getCookie().setMaxAge(Duration.ofMinutes(1));
AtomicReference<ServletContext> contextReference = new AtomicReference<>();
factory.getWebServer(contextReference::set).start();
Expand All @@ -1178,6 +1182,8 @@ void sessionConfiguration() {
assertThat(servletContext.getSessionCookieConfig().isHttpOnly()).isTrue();
assertThat(servletContext.getSessionCookieConfig().isSecure()).isTrue();
assertThat(servletContext.getSessionCookieConfig().getMaxAge()).isEqualTo(60);
assertThat(servletContext.getSessionCookieConfig()
.getAttribute(AbstractServletWebServerFactory.PARTITIONED_ATTRIBUTE_NAME)).isEqualTo("false");
}

@Test
Expand Down