diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java index fc3d9c1380..8d38dc1c93 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -41,6 +41,7 @@ import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.WebListenerRegistrar; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -73,9 +74,11 @@ public class ServletWebServerFactoryAutoConfiguration { @Bean public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, - ObjectProvider webListenerRegistrars) { + ObjectProvider webListenerRegistrars, + ObjectProvider cookieSameSiteSuppliers) { return new ServletWebServerFactoryCustomizer(serverProperties, - webListenerRegistrars.orderedStream().collect(Collectors.toList())); + webListenerRegistrars.orderedStream().collect(Collectors.toList()), + cookieSameSiteSuppliers.orderedStream().collect(Collectors.toList())); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java index ae259d1a17..f3f007cc6a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -24,11 +24,13 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.WebListenerRegistrar; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.core.Ordered; +import org.springframework.util.CollectionUtils; /** - * {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} to servlet web - * servers. + * {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} and + * {@link WebListenerRegistrar WebListenerRegistrars} to servlet web servers. * * @author Brian Clozel * @author Stephane Nicoll @@ -41,7 +43,9 @@ public class ServletWebServerFactoryCustomizer private final ServerProperties serverProperties; - private final Iterable webListenerRegistrars; + private final List webListenerRegistrars; + + private final List cookieSameSiteSuppliers; public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) { this(serverProperties, Collections.emptyList()); @@ -49,8 +53,14 @@ public class ServletWebServerFactoryCustomizer public ServletWebServerFactoryCustomizer(ServerProperties serverProperties, List webListenerRegistrars) { + this(serverProperties, webListenerRegistrars, null); + } + + ServletWebServerFactoryCustomizer(ServerProperties serverProperties, + List webListenerRegistrars, List cookieSameSiteSuppliers) { this.serverProperties = serverProperties; this.webListenerRegistrars = webListenerRegistrars; + this.cookieSameSiteSuppliers = cookieSameSiteSuppliers; } @Override @@ -77,6 +87,9 @@ public class ServletWebServerFactoryCustomizer for (WebListenerRegistrar registrar : this.webListenerRegistrars) { registrar.register(factory); } + if (!CollectionUtils.isEmpty(this.cookieSameSiteSuppliers)) { + factory.setCookieSameSiteSuppliers(this.cookieSameSiteSuppliers); + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java index 046ab5d402..d4c963ecaf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java @@ -51,7 +51,9 @@ import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -369,6 +371,14 @@ class ServletWebServerFactoryAutoConfigurationTests { .run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class)); } + @Test + void cookieSameSiteSuppliersAreApplied() { + this.contextRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> { + AbstractServletWebServerFactory webServerFactory = context.getBean(AbstractServletWebServerFactory.class); + assertThat(webServerFactory.getCookieSameSiteSuppliers()).hasSize(2); + }); + } + private ContextConsumer verifyContext() { return this::verifyContext; } @@ -656,4 +666,19 @@ class ServletWebServerFactoryAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CookieSameSiteSupplierConfiguration { + + @Bean + CookieSameSiteSupplier cookieSameSiteSupplier1() { + return CookieSameSiteSupplier.ofLax().whenHasName("test1"); + } + + @Bean + CookieSameSiteSupplier cookieSameSiteSupplier2() { + return CookieSameSiteSupplier.ofNone().whenHasName("test2"); + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index 3f15ebcf4b..3f508c7a5a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -588,6 +588,39 @@ TIP: See the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[` +[[web.servlet.embedded-container.customizing.samesite]] +===== SameSite Cookies +The `SameSite` cookie attribute can be used by web browsers to control if and how cookies are submitted in cross-site requests. +The attribute is particularly relevant for modern web browsers which have started to change the default value that is used when the attribute is missing. + +If you want to change the `SameSite` attribute of your session cookie, you can use the configprop:server.servlet.session.cookie.same-site[] property. +This property is supported by auto-configured Tomcat, Jetty and Undertow servers. +It is also used to configure Spring Session servlet based `SessionRepository` beans. + +For example, if you want your session cookie to have a `SameSite` attribute of `None`, you can add the following to you `application.properties` or `application.yaml` file: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + servlet: + session: + cookie: + same-site: "none" +---- + +If you want to change the `SameSite` attribute on other cookies added to your `HttpServletResponse`, you can use a `CookieSameSiteSupplier`. +The `CookieSameSiteSupplier` is passed a `Cookie` and may return a `SameSite` value, or `null`. + +There are a number of convenience factory and filter methods that you can use to quickly match specific cookies. +For example, adding the following bean will automatically apply a `SameSite` of `Lax` for all cookies with a name that matches the regular expression `myapp.*`. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/web/servlet/embeddedcontainer/customizing/samesite/MySameSiteConfiguration.java[] +---- + + + [[web.servlet.embedded-container.customizing.programmatic]] ===== Programmatic Customization If you need to programmatically configure your embedded servlet container, you can register a Spring bean that implements the `WebServerFactoryCustomizer` interface. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/embeddedcontainer/customizing/samesite/MySameSiteConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/embeddedcontainer/customizing/samesite/MySameSiteConfiguration.java new file mode 100644 index 0000000000..149d1adcc9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/embeddedcontainer/customizing/samesite/MySameSiteConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 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 + * + * https://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.boot.docs.web.servlet.embeddedcontainer.customizing.samesite; + +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MySameSiteConfiguration { + + @Bean + public CookieSameSiteSupplier applicationCookieSameSiteSupplier() { + return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*"); + } + +} 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 d53ef01a11..0fbaf1a37b 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 @@ -33,6 +33,13 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; @@ -41,6 +48,7 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ErrorHandler; @@ -63,16 +71,19 @@ import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; +import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -199,6 +210,9 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor if (StringUtils.hasText(getServerHeader())) { handler = applyWrapper(handler, JettyHandlerWrappers.createServerHeaderHandlerWrapper(getServerHeader())); } + if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) { + handler = applyWrapper(handler, new SuppliedSameSiteCookieHandlerWrapper(getCookieSameSiteSuppliers())); + } return handler; } @@ -245,6 +259,10 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor private void configureSession(WebAppContext context) { SessionHandler handler = context.getSessionHandler(); + SameSite sessionSameSite = getSession().getCookie().getSameSite(); + if (sessionSameSite != null) { + handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name())); + } Duration sessionTimeout = getSession().getTimeout(); handler.setMaxInactiveInterval(isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds()); if (getSession().isPersistent()) { @@ -661,4 +679,66 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor } + /** + * {@link HandlerWrapper} to apply {@link CookieSameSiteSupplier supplied} + * {@link SameSite} cookie values. + */ + private static class SuppliedSameSiteCookieHandlerWrapper extends HandlerWrapper { + + private final List suppliers; + + SuppliedSameSiteCookieHandlerWrapper(List suppliers) { + this.suppliers = suppliers; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + HttpServletResponse wrappedResponse = new ResposeWrapper(response); + super.handle(target, baseRequest, request, wrappedResponse); + } + + class ResposeWrapper extends HttpServletResponseWrapper { + + ResposeWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public void addCookie(Cookie cookie) { + SameSite sameSite = getSameSite(cookie); + if (sameSite != null) { + String comment = HttpCookie.getCommentWithoutAttributes(cookie.getComment()); + String sameSiteComment = getSameSiteComment(sameSite); + cookie.setComment((comment != null) ? comment + sameSiteComment : sameSiteComment); + } + super.addCookie(cookie); + } + + private String getSameSiteComment(SameSite sameSite) { + switch (sameSite) { + case NONE: + return HttpCookie.SAME_SITE_NONE_COMMENT; + case LAX: + return HttpCookie.SAME_SITE_LAX_COMMENT; + case STRICT: + return HttpCookie.SAME_SITE_STRICT_COMMENT; + } + throw new IllegalStateException("Unsupported SameSite value " + sameSite); + } + + private SameSite getSameSite(Cookie cookie) { + for (CookieSameSiteSupplier supplier : SuppliedSameSiteCookieHandlerWrapper.this.suppliers) { + SameSite sameSite = supplier.getSameSite(cookie); + if (sameSite != null) { + return sameSite; + } + } + return null; + } + + } + + } + } 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 35b3b7383a..9e1bced220 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 @@ -34,6 +34,8 @@ import java.util.Set; import java.util.stream.Collectors; import javax.servlet.ServletContainerInitializer; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; import org.apache.catalina.Context; import org.apache.catalina.Engine; @@ -55,26 +57,31 @@ import org.apache.catalina.session.StandardManager; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat.FixContextListener; import org.apache.catalina.util.LifecycleBase; +import org.apache.catalina.util.SessionConfig; import org.apache.catalina.webresources.AbstractResourceSet; import org.apache.catalina.webresources.EmptyResource; import org.apache.catalina.webresources.StandardRoot; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http2.Http2Protocol; +import org.apache.tomcat.util.http.Rfc6265CookieProcessor; import org.apache.tomcat.util.modeler.Registry; import org.apache.tomcat.util.scan.StandardJarScanFilter; import org.springframework.boot.util.LambdaSafe; +import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.NativeDetector; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -381,6 +388,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto context.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); } configureSession(context); + configureCookieProcessor(context); new DisableReferenceClearingContextCustomizer().customize(context); for (String webListenerClassName : getWebListenerClassNames()) { context.addApplicationListener(webListenerClassName); @@ -410,6 +418,21 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto } } + private void configureCookieProcessor(Context context) { + SameSite sessionSameSite = getSession().getCookie().getSameSite(); + List suppliers = new ArrayList<>(); + if (sessionSameSite != null) { + suppliers.add(CookieSameSiteSupplier.of(sessionSameSite) + .whenHasName(() -> SessionConfig.getSessionCookieName(context))); + } + if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) { + suppliers.addAll(getCookieSameSiteSuppliers()); + } + if (!suppliers.isEmpty()) { + context.setCookieProcessor(new SuppliedSameSiteCookieProcessor(suppliers)); + } + } + private void configurePersistSession(Manager manager) { Assert.state(manager instanceof StandardManager, () -> "Unable to persist HTTP session state using manager type " + manager.getClass().getName()); @@ -880,4 +903,39 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto } + /** + * {@link Rfc6265CookieProcessor} that supports {@link CookieSameSiteSupplier + * supplied} {@link SameSite} values. + */ + private static class SuppliedSameSiteCookieProcessor extends Rfc6265CookieProcessor { + + private final List suppliers; + + SuppliedSameSiteCookieProcessor(List suppliers) { + this.suppliers = suppliers; + } + + @Override + public String generateHeader(Cookie cookie, HttpServletRequest request) { + SameSite sameSite = getSameSite(cookie); + if (sameSite == null) { + return super.generateHeader(cookie, request); + } + Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor(); + delegate.setSameSiteCookies(sameSite.attributeValue()); + return delegate.generateHeader(cookie, request); + } + + private SameSite getSameSite(Cookie cookie) { + for (CookieSameSiteSupplier supplier : this.suppliers) { + SameSite sameSite = supplier.getSameSite(cookie); + if (sameSite != null) { + return sameSite; + } + } + return null; + } + + } + } 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 dbbbaf5e1f..f60c40b486 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 @@ -39,6 +39,9 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import io.undertow.Undertow.Builder; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; import io.undertow.server.handlers.resource.FileResourceManager; import io.undertow.server.handlers.resource.Resource; import io.undertow.server.handlers.resource.ResourceChangeListener; @@ -46,6 +49,7 @@ import io.undertow.server.handlers.resource.ResourceManager; import io.undertow.server.handlers.resource.URLResource; import io.undertow.server.session.SessionManager; import io.undertow.servlet.Servlets; +import io.undertow.servlet.api.Deployment; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.ListenerInfo; @@ -56,15 +60,19 @@ import io.undertow.servlet.core.DeploymentImpl; import io.undertow.servlet.handlers.DefaultServlet; import io.undertow.servlet.util.ImmediateInstanceFactory; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings.Mapping; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; +import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * {@link ServletWebServerFactory} that can be used to create @@ -415,9 +423,9 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac } } - private void configureErrorPages(DeploymentInfo servletBuilder) { + private void configureErrorPages(DeploymentInfo deployment) { for (ErrorPage errorPage : getErrorPages()) { - servletBuilder.addErrorPage(getUndertowErrorPage(errorPage)); + deployment.addErrorPage(getUndertowErrorPage(errorPage)); } } @@ -431,9 +439,9 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac return new io.undertow.servlet.api.ErrorPage(errorPage.getPath()); } - private void configureMimeMappings(DeploymentInfo servletBuilder) { + private void configureMimeMappings(DeploymentInfo deployment) { for (Mapping mimeMapping : getMimeMappings()) { - servletBuilder.addMimeMapping(new MimeMapping(mimeMapping.getExtension(), mimeMapping.getMimeType())); + deployment.addMimeMapping(new MimeMapping(mimeMapping.getExtension(), mimeMapping.getMimeType())); } } @@ -458,11 +466,30 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac * @return a new {@link UndertowServletWebServer} instance */ protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) { + List initialHandlerFactories = new ArrayList<>(); + initialHandlerFactories.add(new DeploymentManagerHttpHandlerFactory(manager)); + HttpHandlerFactory cooHandlerFactory = getCookieHandlerFactory(manager.getDeployment()); + if (cooHandlerFactory != null) { + initialHandlerFactories.add(cooHandlerFactory); + } List httpHandlerFactories = this.delegate.createHttpHandlerFactories(this, - new DeploymentManagerHttpHandlerFactory(manager)); + initialHandlerFactories.toArray(new HttpHandlerFactory[0])); return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0); } + private HttpHandlerFactory getCookieHandlerFactory(Deployment deployment) { + SameSite sessionSameSite = getSession().getCookie().getSameSite(); + List suppliers = new ArrayList<>(); + if (sessionSameSite != null) { + String sessionCookieName = deployment.getServletContext().getSessionCookieConfig().getName(); + suppliers.add(CookieSameSiteSupplier.of(sessionSameSite).whenHasName(sessionCookieName)); + } + if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) { + suppliers.addAll(getCookieSameSiteSuppliers()); + } + return (!suppliers.isEmpty()) ? (next) -> new SuppliedSameSiteCookieHandler(next, suppliers) : null; + } + /** * {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer * ServletContextInitializers}. @@ -583,4 +610,59 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac } + /** + * {@link HttpHandler} to apply {@link CookieSameSiteSupplier supplied} + * {@link SameSite} cookie values. + */ + private static class SuppliedSameSiteCookieHandler implements HttpHandler { + + private final HttpHandler next; + + private final List suppliers; + + SuppliedSameSiteCookieHandler(HttpHandler next, List suppliers) { + this.next = next; + this.suppliers = suppliers; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.addResponseCommitListener(this::beforeCommit); + this.next.handleRequest(exchange); + } + + private void beforeCommit(HttpServerExchange exchange) { + for (Cookie cookie : exchange.responseCookies()) { + SameSite sameSite = getSameSite(asServletCookie(cookie)); + if (sameSite != null) { + cookie.setSameSiteMode(sameSite.attributeValue()); + } + } + } + + private javax.servlet.http.Cookie asServletCookie(Cookie cookie) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + javax.servlet.http.Cookie result = new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue()); + map.from(cookie::getComment).to(result::setComment); + map.from(cookie::getDomain).to(result::setDomain); + map.from(cookie::getMaxAge).to(result::setMaxAge); + map.from(cookie::getPath).to(result::setPath); + result.setSecure(cookie.isSecure()); + result.setVersion(cookie.getVersion()); + result.setHttpOnly(cookie.isHttpOnly()); + return result; + } + + private SameSite getSameSite(javax.servlet.http.Cookie cookie) { + for (CookieSameSiteSupplier supplier : this.suppliers) { + SameSite sameSite = supplier.getSameSite(cookie); + if (sameSite != null) { + return sameSite; + } + } + return null; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java index fc4a0aa3d2..b866fc27fc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java @@ -80,6 +80,8 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab private Map initParameters = Collections.emptyMap(); + private List cookieSameSiteSuppliers = new ArrayList<>(); + private final DocumentRoot documentRoot = new DocumentRoot(this.logger); private final StaticResourceJars staticResourceJars = new StaticResourceJars(); @@ -242,6 +244,22 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab return this.initParameters; } + @Override + public void setCookieSameSiteSuppliers(List cookieSameSiteSuppliers) { + Assert.notNull(cookieSameSiteSuppliers, "CookieSameSiteSuppliers must not be null"); + this.cookieSameSiteSuppliers = new ArrayList<>(cookieSameSiteSuppliers); + } + + @Override + public void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers) { + Assert.notNull(cookieSameSiteSuppliers, "CookieSameSiteSuppliers must not be null"); + this.cookieSameSiteSuppliers.addAll(Arrays.asList(cookieSameSiteSuppliers)); + } + + public List getCookieSameSiteSuppliers() { + return this.cookieSameSiteSuppliers; + } + /** * Utility method that can be used by subclasses wishing to combine the specified * {@link ServletContextInitializer} parameters with those defined in this instance. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java index d58d86ec28..98c5c1c922 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -25,6 +25,7 @@ import java.util.Map; import javax.servlet.ServletContext; import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.ServletContextInitializer; @@ -125,4 +126,21 @@ public interface ConfigurableServletWebServerFactory */ void setInitParameters(Map initParameters); + /** + * Sets {@link CookieSameSiteSupplier CookieSameSiteSuppliers} that should be used to + * obtain the {@link SameSite} attribute of any added cookie. This method will replace + * any previously set or added suppliers. + * @param cookieSameSiteSuppliers the suppliers to add + * @see #addCookieSameSiteSuppliers + */ + void setCookieSameSiteSuppliers(List cookieSameSiteSuppliers); + + /** + * Add {@link CookieSameSiteSupplier CookieSameSiteSuppliers} to those that should be + * used to obtain the {@link SameSite} attribute of any added cookie. + * @param cookieSameSiteSuppliers the suppliers to add + * @see #setCookieSameSiteSuppliers + */ + void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers); + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/CookieSameSiteSupplier.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/CookieSameSiteSupplier.java new file mode 100644 index 0000000000..6c7128e895 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/CookieSameSiteSupplier.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2021 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 + * + * https://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.boot.web.servlet.server; + +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * @author Phillip Webb + */ +import javax.servlet.http.Cookie; + +import org.springframework.boot.web.server.Cookie.SameSite; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Strategy interface that can be used with {@link ConfigurableServletWebServerFactory} + * implementations in order to supply custom {@link SameSite} values for specific + * {@link Cookie cookies}. + *

+ * Basic CookieSameSiteSupplier implementations can be constructed using the {@code of...} + * factory methods, typically combined with name matching. For example:

+ * CookieSameSiteSupplier.ofLax().whenHasName("mycookie");
+ * 
+ * + * @author Phillip Webb + * @since 2.6.0 + * @see ConfigurableServletWebServerFactory#addCookieSameSiteSuppliers(CookieSameSiteSupplier...) + */ +@FunctionalInterface +public interface CookieSameSiteSupplier { + + /** + * Get the {@link SameSite} values that should be used for the given {@link Cookie}. + * @param cookie the cookie to check + * @return the {@link SameSite} value to use or {@code null} if the next supplier + * should be checked + */ + SameSite getSameSite(Cookie cookie); + + /** + * Limit this supplier so that it's only called if the Cookie has the given name. + * @param name the name to check + * @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the + * name matches + */ + default CookieSameSiteSupplier whenHasName(String name) { + Assert.hasText(name, "Name must not be empty"); + return when((cookie) -> ObjectUtils.nullSafeEquals(cookie.getName(), name)); + } + + /** + * Limit this supplier so that it's only called if the Cookie has the given name. + * @param nameSupplier a supplier providing the name to check + * @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the + * name matches + */ + default CookieSameSiteSupplier whenHasName(Supplier nameSupplier) { + Assert.notNull(nameSupplier, "NameSupplier must not be empty"); + return when((cookie) -> ObjectUtils.nullSafeEquals(cookie.getName(), nameSupplier.get())); + } + + /** + * Limit this supplier so that it's only called if the Cookie name matches the given + * regex. + * @param regex the regex pattern that must match + * @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the + * name matches the regex + */ + default CookieSameSiteSupplier whenHasNameMatching(String regex) { + Assert.hasText(regex, "Regex must not be empty"); + return whenHasNameMatching(Pattern.compile(regex)); + } + + /** + * Limit this supplier so that it's only called if the Cookie name matches the given + * {@link Pattern}. + * @param pattern the regex pattern that must match + * @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the + * name matches the pattern + */ + default CookieSameSiteSupplier whenHasNameMatching(Pattern pattern) { + Assert.notNull(pattern, "Pattern must not be null"); + return when((cookie) -> pattern.matcher(cookie.getName()).matches()); + } + + /** + * Limit this supplier so that it's only called if the predicate accepts the Cookie. + * @param predicate the predicate used to match the cookie + * @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the + * cookie matches the predicate + */ + default CookieSameSiteSupplier when(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + return (cookie) -> predicate.test(cookie) ? getSameSite(cookie) : null; + } + + /** + * Return a new {@link CookieSameSiteSupplier} that always returns + * {@link SameSite#NONE}. + * @return the supplier instance + */ + static CookieSameSiteSupplier ofNone() { + return of(SameSite.NONE); + } + + /** + * Return a new {@link CookieSameSiteSupplier} that always returns + * {@link SameSite#LAX}. + * @return the supplier instance + */ + static CookieSameSiteSupplier ofLax() { + return of(SameSite.LAX); + } + + /** + * Return a new {@link CookieSameSiteSupplier} that always returns + * {@link SameSite#STRICT}. + * @return the supplier instance + */ + static CookieSameSiteSupplier ofStrict() { + return of(SameSite.STRICT); + } + + /** + * Return a new {@link CookieSameSiteSupplier} that always returns the given + * {@link SameSite} value. + * @param sameSite the value to return + * @return the supplier instance + */ + static CookieSameSiteSupplier of(SameSite sameSite) { + Assert.notNull(sameSite, "SameSite must not be null"); + return (cookie) -> sameSite; + } + +} 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 d45071c253..f56d26a69d 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 @@ -42,6 +42,7 @@ import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -73,6 +74,7 @@ import javax.servlet.ServletRegistration.Dynamic; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.SessionCookieConfig; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -105,6 +107,8 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.InOrder; import org.springframework.boot.system.ApplicationHome; @@ -114,6 +118,7 @@ import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.boot.testsupport.web.servlet.ExampleFilter; import org.springframework.boot.testsupport.web.servlet.ExampleServlet; import org.springframework.boot.web.server.Compression; +import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.Http2; @@ -810,6 +815,57 @@ public abstract class AbstractServletWebServerFactoryTests { assertThat(sessionCookieConfig.getMaxAge()).isEqualTo(60); } + @ParameterizedTest + @EnumSource(SameSite.class) + void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(SameSite sameSite) throws Exception { + AbstractServletWebServerFactory factory = getFactory(); + factory.getSession().getCookie().setSameSite(sameSite); + factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(false), "/")); + this.webServer = factory.getWebServer(); + this.webServer.start(); + ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/")); + List setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie"); + assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder( + (header) -> assertThat(header).contains("JSESSIONID").contains("SameSite=" + sameSite.attributeValue()), + (header) -> assertThat(header).contains("test=test").doesNotContain("SameSite")); + } + + @ParameterizedTest + @EnumSource(SameSite.class) + void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookiesWhenUsingCustomName(SameSite sameSite) + throws Exception { + AbstractServletWebServerFactory factory = getFactory(); + factory.getSession().getCookie().setName("THESESSION"); + factory.getSession().getCookie().setSameSite(sameSite); + factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(false), "/")); + this.webServer = factory.getWebServer(); + this.webServer.start(); + ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/")); + List setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie"); + assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder( + (header) -> assertThat(header).contains("THESESSION").contains("SameSite=" + sameSite.attributeValue()), + (header) -> assertThat(header).contains("test=test").doesNotContain("SameSite")); + } + + @Test + void cookieSameSiteSuppliers() throws Exception { + AbstractServletWebServerFactory factory = getFactory(); + factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofLax().whenHasName("relaxed")); + factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofNone().whenHasName("empty")); + factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofStrict().whenHasName("controlled")); + factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(true), "/")); + this.webServer = factory.getWebServer(); + this.webServer.start(); + ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/")); + List setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie"); + assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder( + (header) -> assertThat(header).contains("JSESSIONID").doesNotContain("SameSite"), + (header) -> assertThat(header).contains("test=test").doesNotContain("SameSite"), + (header) -> assertThat(header).contains("relaxed=test").contains("SameSite=Lax"), + (header) -> assertThat(header).contains("empty=test").contains("SameSite=None"), + (header) -> assertThat(header).contains("controlled=test").contains("SameSite=Strict")); + } + @Test void sslSessionTracking() { AbstractServletWebServerFactory factory = getFactory(); @@ -1611,4 +1667,25 @@ public abstract class AbstractServletWebServerFactoryTests { } + static final class CookieServlet extends HttpServlet { + + private final boolean addSupplierTestCookies; + + CookieServlet(boolean addSupplierTestCookies) { + this.addSupplierTestCookies = addSupplierTestCookies; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.getSession(true); + resp.addCookie(new Cookie("test", "test")); + if (this.addSupplierTestCookies) { + resp.addCookie(new Cookie("relaxed", "test")); + resp.addCookie(new Cookie("empty", "test")); + resp.addCookie(new Cookie("controlled", "test")); + } + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/CookieSameSiteSupplierTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/CookieSameSiteSupplierTests.java new file mode 100644 index 0000000000..f676d86217 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/CookieSameSiteSupplierTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2012-2021 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 + * + * https://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.boot.web.servlet.server; + +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import javax.servlet.http.Cookie; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.server.Cookie.SameSite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link CookieSameSiteSupplier}. + * + * @author Phillip Webb + */ +class CookieSameSiteSupplierTests { + + @Test + void whenHasNameWhenNameIsNullThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((String) null)) + .withMessage("Name must not be empty"); + } + + @Test + void whenHasNameWhenNameIsEmptyThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName("")) + .withMessage("Name must not be empty"); + } + + @Test + void whenHasNameWhenNameMatchesCallsGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThat(supplier.whenHasName("test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX); + } + + @Test + void whenHasNameWhenNameDoesNotMatchDoesNotCallGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called"); + assertThat(supplier.whenHasName("test").getSameSite(new Cookie("tset", "x"))).isNull(); + } + + @Test + void whenHasSuppliedNameWhenNameIsNullThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((Supplier) null)) + .withMessage("NameSupplier must not be empty"); + } + + @Test + void whenHasSuppliedNameWhenNameMatchesCallsGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX); + } + + @Test + void whenHasSuppliedNameWhenNameDoesNotMatchDoesNotCallGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called"); + assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("tset", "x"))).isNull(); + } + + @Test + void whenHasNameMatchingRegexWhenRegexIsNullThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((String) null)) + .withMessage("Regex must not be empty"); + } + + @Test + void whenHasNameMatchingRegexWhenRegexIsEmptyThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching("")) + .withMessage("Regex must not be empty"); + } + + @Test + void whenHasNameMatchingRegexWhenNameMatchesCallsGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX); + } + + @Test + void whenHasNameMatchingRegexWhenNameDoesNotMatchDoesNotCallGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called"); + assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("tset", "x"))).isNull(); + } + + @Test + void whenHasNameMatchingPatternWhenPatternIsNullThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((Pattern) null)) + .withMessage("Pattern must not be null"); + } + + @Test + void whenHasNameMatchingPatternWhenNameMatchesCallsGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("test", "x"))) + .isEqualTo(SameSite.LAX); + } + + @Test + void whenHasNameMatchingPatternWhenNameDoesNotMatchDoesNotCallGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called"); + assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("tset", "x"))).isNull(); + } + + @Test + void whenWhenPredicateIsNullThrowsException() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.when(null)) + .withMessage("Predicate must not be null"); + } + + @Test + void whenWhenPredicateMatchesCallsGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX; + assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("test", "x"))) + .isEqualTo(SameSite.LAX); + } + + @Test + void whenWhenPredicateDoesNotMatchDoesNotCallGetSameSite() { + CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called"); + assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("tset", "x"))) + .isNull(); + } + + @Test + void ofNoneSuppliesNone() { + assertThat(CookieSameSiteSupplier.ofNone().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.NONE); + } + + @Test + void ofLaxSuppliesLax() { + assertThat(CookieSameSiteSupplier.ofLax().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX); + } + + @Test + void ofStrictSuppliesStrict() { + assertThat(CookieSameSiteSupplier.ofStrict().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.STRICT); + } + + @Test + void ofWhenNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> CookieSameSiteSupplier.of(null)) + .withMessage("SameSite must not be null"); + } + + @Test + void ofSuppliesValue() { + assertThat(CookieSameSiteSupplier.of(SameSite.STRICT).getSameSite(new Cookie("test", "x"))) + .isEqualTo(SameSite.STRICT); + } + +}