From b79ba89eeb2219fb504a5050cd4a26cf09942f06 Mon Sep 17 00:00:00 2001 From: Alex Montoya Date: Thu, 17 Nov 2022 21:43:07 -0800 Subject: [PATCH] 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 --- .../web/csrf/CookieCsrfTokenRepository.java | 77 +++++------ .../csrf/CookieServerCsrfTokenRepository.java | 58 ++++----- .../csrf/CookieCsrfTokenRepositoryTests.java | 120 +++++++++++++++++- .../CookieServerCsrfTokenRepositoryTests.java | 85 ++++++++++++- 4 files changed, 263 insertions(+), 77 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java index a93ec34220..72adba529e 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java +++ b/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 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 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 - * true. - * @param cookieHttpOnly true sets the HttpOnly attribute, - * false 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. - * - *

- * A positive value indicates that the cookie will expire after that many seconds have - * passed. Note that the value is the maximum age when the cookie will expire, - * not the cookie's current age. - * - *

- * A negative value means that the cookie is not stored persistently and will be - * deleted when the Web browser exits. - * - *

- * 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; } - } diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java index 68fac2fdad..add90288e0 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java +++ b/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 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 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. - * - *

- * A positive value indicates that the cookie will expire after that many seconds have - * passed. Note that the value is the maximum age when the cookie will expire, - * not the cookie's current age. - * - *

- * A negative value means that the cookie is not stored persistently and will be - * deleted when the Web browser exits. - * - *

- * 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; diff --git a/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java index 42cc5b6711..a4d7e2c0a2 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java +++ b/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)); diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java index b190642158..156d6d8cf3 100644 --- a/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java +++ b/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() {