Browse Source

Add setCookieCustomizer to csrf token repository

- Mark setCookieHttpOnly, setCookieDomain, setCookieMaxAge and
setSecure as deprecated.

- Add the method setCookieCustomizer which allows to set properties
to the ResponseCookieBuilder without having to add new setter methods.

Closes gh-12086
Alex Montoya 2 years ago
parent
commit
b79ba89eeb

+ 34 - 43
web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java

@@ -15,14 +15,15 @@
  */
 
 package org.springframework.security.web.csrf;
-
 import java.util.UUID;
+import java.util.function.Consumer;
 
-import jakarta.servlet.ServletRequest;
 import jakarta.servlet.http.Cookie;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseCookie;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.web.util.WebUtils;
@@ -34,6 +35,7 @@ import org.springframework.web.util.WebUtils;
  *
  * @author Rob Winch
  * @author Steve Riesenberg
+ * @author Alex Montoya
  * @since 4.1
  */
 public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
@@ -63,7 +65,17 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
 
 	private int cookieMaxAge = -1;
 
-	public CookieCsrfTokenRepository() {
+	private Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer = (builder) -> {};
+
+	/**
+	 * Add a {@link Consumer} for a {@code ResponseCookieBuilder} that will be invoked
+	 * for each cookie being built, just before the call to {@code build()}.
+	 * @param cookieCustomizer consumer for a cookie builder
+	 * @since 6.1
+	 */
+	public void setCookieCustomizer(Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer) {
+		Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null");
+		this.cookieCustomizer = cookieCustomizer;
 	}
 
 	@Override
@@ -74,15 +86,17 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
 	@Override
 	public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
 		String tokenValue = (token != null) ? token.getToken() : "";
-		Cookie cookie = new Cookie(this.cookieName, tokenValue);
-		cookie.setSecure((this.secure != null) ? this.secure : request.isSecure());
-		cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
-		cookie.setMaxAge((token != null) ? this.cookieMaxAge : 0);
-		cookie.setHttpOnly(this.cookieHttpOnly);
-		if (StringUtils.hasLength(this.cookieDomain)) {
-			cookie.setDomain(this.cookieDomain);
-		}
-		response.addCookie(cookie);
+
+		ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from(this.cookieName, tokenValue)
+				.secure(this.secure != null ? this.secure : request.isSecure())
+				.path(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request))
+				.maxAge(token != null ? this.cookieMaxAge : 0)
+				.httpOnly(this.cookieHttpOnly)
+				.domain(this.cookieDomain);
+
+		this.cookieCustomizer.accept(cookieBuilder);
+
+		response.setHeader(HttpHeaders.SET_COOKIE, cookieBuilder.build().toString());
 
 		// Set request attribute to signal that response has blank cookie value,
 		// which allows loadToken to return null when token has been removed
@@ -143,11 +157,9 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
 	}
 
 	/**
-	 * Sets the HttpOnly attribute on the cookie containing the CSRF token. Defaults to
-	 * <code>true</code>.
-	 * @param cookieHttpOnly <code>true</code> sets the HttpOnly attribute,
-	 * <code>false</code> does not set it
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 */
+	@Deprecated(since = "6.1")
 	public void setCookieHttpOnly(boolean cookieHttpOnly) {
 		this.cookieHttpOnly = cookieHttpOnly;
 	}
@@ -191,51 +203,30 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
 	}
 
 	/**
-	 * Sets the domain of the cookie that the expected CSRF token is saved to and read
-	 * from.
-	 * @param cookieDomain the domain of the cookie that the expected CSRF token is saved
-	 * to and read from
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 * @since 5.2
 	 */
+	@Deprecated(since = "6.1")
 	public void setCookieDomain(String cookieDomain) {
 		this.cookieDomain = cookieDomain;
 	}
 
 	/**
-	 * Sets secure flag of the cookie that the expected CSRF token is saved to and read
-	 * from. By default secure flag depends on {@link ServletRequest#isSecure()}
-	 * @param secure the secure flag of the cookie that the expected CSRF token is saved
-	 * to and read from
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 * @since 5.4
 	 */
+	@Deprecated(since = "6.1")
 	public void setSecure(Boolean secure) {
 		this.secure = secure;
 	}
 
 	/**
-	 * Sets maximum age in seconds for the cookie that the expected CSRF token is saved to
-	 * and read from. By default maximum age value is -1.
-	 *
-	 * <p>
-	 * A positive value indicates that the cookie will expire after that many seconds have
-	 * passed. Note that the value is the <i>maximum</i> age when the cookie will expire,
-	 * not the cookie's current age.
-	 *
-	 * <p>
-	 * A negative value means that the cookie is not stored persistently and will be
-	 * deleted when the Web browser exits.
-	 *
-	 * <p>
-	 * A zero value causes the cookie to be deleted immediately therefore it is not a
-	 * valid value and in that case an {@link IllegalArgumentException} will be thrown.
-	 * @param cookieMaxAge an integer specifying the maximum age of the cookie in seconds;
-	 * if negative, means the cookie is not stored; if zero, the method throws an
-	 * {@link IllegalArgumentException}
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 * @since 5.5
 	 */
+	@Deprecated(since = "6.1")
 	public void setCookieMaxAge(int cookieMaxAge) {
 		Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
 		this.cookieMaxAge = cookieMaxAge;
 	}
-
 }

+ 29 - 29
web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.server.csrf;
 
 import java.util.UUID;
+import java.util.function.Consumer;
 
 import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Schedulers;
@@ -36,6 +37,7 @@ import org.springframework.web.server.ServerWebExchange;
  * @author Eric Deandrea
  * @author Thomas Vitale
  * @author Alonso Araya
+ * @author Alex Montoya
  * @since 5.1
  */
 public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRepository {
@@ -60,6 +62,19 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
 
 	private int cookieMaxAge = -1;
 
+	private Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer = (builder) -> {};
+
+	/**
+	 * Add a {@link Consumer} for a {@code ResponseCookieBuilder} that will be invoked
+	 * for each cookie being built, just before the call to {@code build()}.
+	 * @param cookieCustomizer consumer for a cookie builder
+	 * @since 6.1
+	 */
+	public void setCookieCustomizer(Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer) {
+		Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null");
+		this.cookieCustomizer = cookieCustomizer;
+	}
+
 	/**
 	 * Factory method to conveniently create an instance that has
 	 * {@link #setCookieHttpOnly(boolean)} set to false.
@@ -82,16 +97,18 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
 		return Mono.fromRunnable(() -> {
 			String tokenValue = (token != null) ? token.getToken() : "";
 			// @formatter:off
-			ResponseCookie cookie = ResponseCookie
+			ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie
 					.from(this.cookieName, tokenValue)
 					.domain(this.cookieDomain)
 					.httpOnly(this.cookieHttpOnly)
 					.maxAge(!tokenValue.isEmpty() ? this.cookieMaxAge : 0)
 					.path((this.cookiePath != null) ? this.cookiePath : getRequestContext(exchange.getRequest()))
-					.secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null))
-					.build();
+					.secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null));
+
+			this.cookieCustomizer.accept(cookieBuilder);
+
 			// @formatter:on
-			exchange.getResponse().addCookie(cookie);
+			exchange.getResponse().addCookie(cookieBuilder.build());
 		});
 	}
 
@@ -107,9 +124,9 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
 	}
 
 	/**
-	 * Sets the HttpOnly attribute on the cookie containing the CSRF token
-	 * @param cookieHttpOnly True to mark the cookie as http only. False otherwise.
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 */
+	@Deprecated(since = "6.1")
 	public void setCookieHttpOnly(boolean cookieHttpOnly) {
 		this.cookieHttpOnly = cookieHttpOnly;
 	}
@@ -150,44 +167,27 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
 	}
 
 	/**
-	 * Sets the cookie domain
-	 * @param cookieDomain The cookie domain
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 */
+	@Deprecated(since = "6.1")
 	public void setCookieDomain(String cookieDomain) {
 		this.cookieDomain = cookieDomain;
 	}
 
 	/**
-	 * Sets the cookie secure flag. If not set, the value depends on
-	 * {@link ServerHttpRequest#getSslInfo()}.
-	 * @param secure The value for the secure flag
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 * @since 5.5
 	 */
+	@Deprecated(since = "6.1")
 	public void setSecure(boolean secure) {
 		this.secure = secure;
 	}
 
 	/**
-	 * Sets maximum age in seconds for the cookie that the expected CSRF token is saved to
-	 * and read from. By default maximum age value is -1.
-	 *
-	 * <p>
-	 * A positive value indicates that the cookie will expire after that many seconds have
-	 * passed. Note that the value is the <i>maximum</i> age when the cookie will expire,
-	 * not the cookie's current age.
-	 *
-	 * <p>
-	 * A negative value means that the cookie is not stored persistently and will be
-	 * deleted when the Web browser exits.
-	 *
-	 * <p>
-	 * A zero value causes the cookie to be deleted immediately therefore it is not a
-	 * valid value and in that case an {@link IllegalArgumentException} will be thrown.
-	 * @param cookieMaxAge an integer specifying the maximum age of the cookie in seconds;
-	 * if negative, means the cookie is not stored; if zero, the method throws an
-	 * {@link IllegalArgumentException}
+	 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
 	 * @since 5.8
 	 */
+	@Deprecated(since = "6.1")
 	public void setCookieMaxAge(int cookieMaxAge) {
 		Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
 		this.cookieMaxAge = cookieMaxAge;

+ 116 - 4
web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java

@@ -20,6 +20,7 @@ import jakarta.servlet.http.Cookie;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import org.springframework.mock.web.MockCookie;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 
@@ -29,6 +30,7 @@ import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCs
 
 /**
  * @author Rob Winch
+ * @author Alex Montoya
  * @since 4.1
  */
 public class CookieCsrfTokenRepositoryTests {
@@ -102,7 +104,17 @@ public class CookieCsrfTokenRepositoryTests {
 	}
 
 	@Test
-	public void saveTokenSecureFlagFalse() {
+	void saveTokenSecureFlagTrueUsingCustomizer() {
+		this.request.setSecure(false);
+		this.repository.setCookieCustomizer(customizer -> customizer.secure(Boolean.TRUE));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie.getSecure()).isTrue();
+	}
+
+	@Test
+	void saveTokenSecureFlagFalse() {
 		this.request.setSecure(true);
 		this.repository.setSecure(Boolean.FALSE);
 		CsrfToken token = this.repository.generateToken(this.request);
@@ -112,7 +124,17 @@ public class CookieCsrfTokenRepositoryTests {
 	}
 
 	@Test
-	public void saveTokenNull() {
+	void saveTokenSecureFlagFalseUsingCustomizer() {
+		this.request.setSecure(true);
+		this.repository.setCookieCustomizer(customizer -> customizer.secure(Boolean.FALSE));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie.getSecure()).isFalse();
+	}
+
+	@Test
+	void saveTokenNull() {
 		this.request.setSecure(true);
 		this.repository.saveToken(null, this.request, this.response);
 		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
@@ -133,7 +155,16 @@ public class CookieCsrfTokenRepositoryTests {
 	}
 
 	@Test
-	public void saveTokenHttpOnlyFalse() {
+	void saveTokenHttpOnlyTrueUsingCustomizer() {
+		this.repository.setCookieCustomizer(customizer -> customizer.httpOnly(true));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie.isHttpOnly()).isTrue();
+	}
+
+	@Test
+	void saveTokenHttpOnlyFalse() {
 		this.repository.setCookieHttpOnly(false);
 		CsrfToken token = this.repository.generateToken(this.request);
 		this.repository.saveToken(token, this.request, this.response);
@@ -142,7 +173,16 @@ public class CookieCsrfTokenRepositoryTests {
 	}
 
 	@Test
-	public void saveTokenWithHttpOnlyFalse() {
+	void saveTokenHttpOnlyFalseUsingCustomizer() {
+		this.repository.setCookieCustomizer(customizer -> customizer.httpOnly(false));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie.isHttpOnly()).isFalse();
+	}
+
+	@Test
+	void saveTokenWithHttpOnlyFalse() {
 		this.repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
 		CsrfToken token = this.repository.generateToken(this.request);
 		this.repository.saveToken(token, this.request, this.response);
@@ -190,6 +230,16 @@ public class CookieCsrfTokenRepositoryTests {
 		assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
 	}
 
+	@Test
+	void saveTokenWithCookieDomainUsingCustomizer() {
+		String domainName = "example.com";
+		this.repository.setCookieCustomizer(customizer -> customizer.domain(domainName));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
+	}
+
 	@Test
 	public void saveTokenWithCookieMaxAge() {
 		int maxAge = 1200;
@@ -200,6 +250,46 @@ public class CookieCsrfTokenRepositoryTests {
 		assertThat(tokenCookie.getMaxAge()).isEqualTo(maxAge);
 	}
 
+	@Test
+	void saveTokenWithCookieMaxAgeUsingCustomizer() {
+		int maxAge = 1200;
+		this.repository.setCookieCustomizer(customizer -> customizer.maxAge(maxAge));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie.getMaxAge()).isEqualTo(maxAge);
+	}
+
+	@Test
+	void saveTokenWithSameSiteNull() {
+		String sameSitePolicy = null;
+		this.repository.setCookieCustomizer(customizer -> customizer.sameSite(sameSitePolicy));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(((MockCookie)tokenCookie).getSameSite()).isNull();
+	}
+
+	@Test
+	void saveTokenWithSameSiteStrict() {
+		String sameSitePolicy = "Strict";
+		this.repository.setCookieCustomizer(customizer -> customizer.sameSite(sameSitePolicy));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(((MockCookie)tokenCookie).getSameSite()).isEqualTo(sameSitePolicy);
+	}
+
+	@Test
+	void saveTokenWithSameSiteLax() {
+		String sameSitePolicy = "Lax";
+		this.repository.setCookieCustomizer(customizer -> customizer.sameSite(sameSitePolicy));
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(((MockCookie)tokenCookie).getSameSite()).isEqualTo(sameSitePolicy);
+	}
+
 	@Test
 	public void loadTokenNoCookiesNull() {
 		assertThat(this.repository.loadToken(this.request)).isNull();
@@ -299,6 +389,28 @@ public class CookieCsrfTokenRepositoryTests {
 		assertThat(deferredCsrfToken.isGenerated()).isFalse();
 	}
 
+	@Test
+	void cookieCustomizer() {
+		String domainName = "example.com";
+		String customPath = "/custompath";
+		String sameSitePolicy = "Strict";
+		this.repository.setCookieCustomizer(customizer -> {
+			customizer.domain(domainName);
+			customizer.secure(false);
+			customizer.path(customPath);
+			customizer.sameSite(sameSitePolicy);
+		});
+		CsrfToken token = this.repository.generateToken(this.request);
+		this.repository.saveToken(token, this.request, this.response);
+		Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+		assertThat(tokenCookie).isNotNull();
+		assertThat(tokenCookie.getMaxAge()).isEqualTo(-1);
+		assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
+		assertThat(tokenCookie.getPath()).isEqualTo(customPath);
+		assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.TRUE);
+		assertThat(((MockCookie)tokenCookie).getSameSite()).isEqualTo(sameSitePolicy);
+	}
+
 	@Test
 	public void setCookieNameNullIllegalArgumentException() {
 		assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null));

+ 84 - 1
web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.web.server.csrf;
 
 import java.security.cert.X509Certificate;
 import java.time.Duration;
+import java.time.temporal.ChronoUnit;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
  * @author Eric Deandrea
  * @author Thomas Vitale
  * @author Alonso Araya
+ * @author Alex Montoya
  * @since 5.1
  */
 public class CookieServerCsrfTokenRepositoryTests {
@@ -61,6 +63,8 @@ public class CookieServerCsrfTokenRepositoryTests {
 
 	private String expectedCookieValue = "csrfToken";
 
+	private String expectedSameSitePolicy = null;
+
 	@BeforeEach
 	public void setUp() {
 		this.csrfTokenRepository = new CookieServerCsrfTokenRepository();
@@ -120,6 +124,12 @@ public class CookieServerCsrfTokenRepositoryTests {
 		saveAndAssertExpectedValues(createToken());
 	}
 
+	@Test
+	void saveTokenWhenSameSiteThenCookieSameSite() {
+		setExpectedSameSitePolicy("Lax");
+		saveAndAssertExpectedValues(createToken());
+	}
+
 	@Test
 	public void saveTokenWhenCustomPropertiesThenCustomProperties() {
 		setExpectedDomain("spring.io");
@@ -127,12 +137,48 @@ public class CookieServerCsrfTokenRepositoryTests {
 		setExpectedPath("/some/path");
 		setExpectedHeaderName("headerName");
 		setExpectedParameterName("paramName");
+		setExpectedSameSitePolicy("Strict");
 		setExpectedCookieMaxAge(3600);
 		saveAndAssertExpectedValues(createToken());
 	}
 
 	@Test
-	public void saveTokenWhenSslInfoPresentThenSecure() {
+	void saveTokenWhenCustomPropertiesThenCustomPropertiesUsingCustomizer() {
+		String expectedDomain = "spring.io";
+		int expectedMaxAge = 3600;
+		String expectedPath = "/some/path";
+		String expectedSameSite = "Strict";
+
+		setExpectedCookieName("csrfCookie");
+
+		setExpectedHeaderName("headerName");
+		setExpectedParameterName("paramName");
+
+		CsrfToken token = createToken();
+
+		this.csrfTokenRepository.setCookieCustomizer(customizer -> {
+			customizer.domain(expectedDomain);
+			customizer.maxAge(expectedMaxAge);
+			customizer.path(expectedPath);
+			customizer.sameSite(expectedSameSite);
+		});
+
+		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
+		this.csrfTokenRepository.saveToken(exchange, token).block();
+		ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.getMaxAge()).isEqualTo(Duration.of(expectedMaxAge, ChronoUnit.SECONDS));
+		assertThat(cookie.getDomain()).isEqualTo(expectedDomain);
+		assertThat(cookie.getPath()).isEqualTo(expectedPath);
+		assertThat(cookie.getSameSite()).isEqualTo(expectedSameSite);
+		assertThat(cookie.isSecure()).isEqualTo(this.expectedSecure);
+		assertThat(cookie.isHttpOnly()).isEqualTo(this.expectedHttpOnly);
+		assertThat(cookie.getName()).isEqualTo(this.expectedCookieName);
+		assertThat(cookie.getValue()).isEqualTo(this.expectedCookieValue);
+	}
+
+	@Test
+	void saveTokenWhenSslInfoPresentThenSecure() {
 		this.request.sslInfo(new MockSslInfo());
 		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
 		this.csrfTokenRepository.saveToken(exchange, createToken()).block();
@@ -160,6 +206,16 @@ public class CookieServerCsrfTokenRepositoryTests {
 		assertThat(cookie.isSecure()).isTrue();
 	}
 
+	@Test
+	void saveTokenWhenSecureFlagTrueThenSecureUsingCustomizer() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
+		this.csrfTokenRepository.setCookieCustomizer(customizer -> customizer.secure(true));
+		this.csrfTokenRepository.saveToken(exchange, createToken()).block();
+		ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.isSecure()).isTrue();
+	}
+
 	@Test
 	public void saveTokenWhenSecureFlagFalseThenNotSecure() {
 		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
@@ -170,6 +226,16 @@ public class CookieServerCsrfTokenRepositoryTests {
 		assertThat(cookie.isSecure()).isFalse();
 	}
 
+	@Test
+	void saveTokenWhenSecureFlagFalseThenNotSecureUsingCustomizer() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
+		this.csrfTokenRepository.setCookieCustomizer(customizer -> customizer.secure(false));
+		this.csrfTokenRepository.saveToken(exchange, createToken()).block();
+		ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.isSecure()).isFalse();
+	}
+
 	@Test
 	public void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() {
 		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
@@ -181,6 +247,17 @@ public class CookieServerCsrfTokenRepositoryTests {
 		assertThat(cookie.isSecure()).isFalse();
 	}
 
+	@Test
+	void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecureUsingCustomizer() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
+		this.request.sslInfo(new MockSslInfo());
+		this.csrfTokenRepository.setCookieCustomizer(customizer -> customizer.secure(false));
+		this.csrfTokenRepository.saveToken(exchange, createToken()).block();
+		ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.isSecure()).isFalse();
+	}
+
 	@Test
 	public void loadTokenWhenCookieExistThenTokenFound() {
 		loadAndAssertExpectedValues();
@@ -248,6 +325,11 @@ public class CookieServerCsrfTokenRepositoryTests {
 		this.expectedMaxAge = Duration.ofSeconds(expectedCookieMaxAge);
 	}
 
+	private void setExpectedSameSitePolicy(String sameSitePolicy){
+		this.csrfTokenRepository.setCookieCustomizer(customizer -> customizer.sameSite(sameSitePolicy));
+		this.expectedSameSitePolicy = sameSitePolicy;
+	}
+
 	private void setExpectedCookieValue(String expectedCookieValue) {
 		this.expectedCookieValue = expectedCookieValue;
 	}
@@ -284,6 +366,7 @@ public class CookieServerCsrfTokenRepositoryTests {
 		assertThat(cookie.isHttpOnly()).isEqualTo(this.expectedHttpOnly);
 		assertThat(cookie.getName()).isEqualTo(this.expectedCookieName);
 		assertThat(cookie.getValue()).isEqualTo(this.expectedCookieValue);
+		assertThat(cookie.getSameSite()).isEqualTo(this.expectedSameSitePolicy);
 	}
 
 	private void generateTokenAndAssertExpectedValues() {