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
This commit is contained in:
Alex Montoya 2022-11-17 21:43:07 -08:00 committed by Josh Cummings
parent cd0f02de49
commit b79ba89eeb
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
4 changed files with 263 additions and 77 deletions

View File

@ -15,14 +15,15 @@
*/ */
package org.springframework.security.web.csrf; package org.springframework.security.web.csrf;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils; import org.springframework.web.util.WebUtils;
@ -34,6 +35,7 @@ import org.springframework.web.util.WebUtils;
* *
* @author Rob Winch * @author Rob Winch
* @author Steve Riesenberg * @author Steve Riesenberg
* @author Alex Montoya
* @since 4.1 * @since 4.1
*/ */
public final class CookieCsrfTokenRepository implements CsrfTokenRepository { public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
@ -63,7 +65,17 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
private int cookieMaxAge = -1; 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 @Override
@ -74,15 +86,17 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
@Override @Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = (token != null) ? token.getToken() : ""; String tokenValue = (token != null) ? token.getToken() : "";
Cookie cookie = new Cookie(this.cookieName, tokenValue);
cookie.setSecure((this.secure != null) ? this.secure : request.isSecure()); ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from(this.cookieName, tokenValue)
cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)); .secure(this.secure != null ? this.secure : request.isSecure())
cookie.setMaxAge((token != null) ? this.cookieMaxAge : 0); .path(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request))
cookie.setHttpOnly(this.cookieHttpOnly); .maxAge(token != null ? this.cookieMaxAge : 0)
if (StringUtils.hasLength(this.cookieDomain)) { .httpOnly(this.cookieHttpOnly)
cookie.setDomain(this.cookieDomain); .domain(this.cookieDomain);
}
response.addCookie(cookie); this.cookieCustomizer.accept(cookieBuilder);
response.setHeader(HttpHeaders.SET_COOKIE, cookieBuilder.build().toString());
// Set request attribute to signal that response has blank cookie value, // Set request attribute to signal that response has blank cookie value,
// which allows loadToken to return null when token has been removed // 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 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* <code>true</code>.
* @param cookieHttpOnly <code>true</code> sets the HttpOnly attribute,
* <code>false</code> does not set it
*/ */
@Deprecated(since = "6.1")
public void setCookieHttpOnly(boolean cookieHttpOnly) { public void setCookieHttpOnly(boolean cookieHttpOnly) {
this.cookieHttpOnly = 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 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* from.
* @param cookieDomain the domain of the cookie that the expected CSRF token is saved
* to and read from
* @since 5.2 * @since 5.2
*/ */
@Deprecated(since = "6.1")
public void setCookieDomain(String cookieDomain) { public void setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain; this.cookieDomain = cookieDomain;
} }
/** /**
* Sets secure flag of the cookie that the expected CSRF token is saved to and read * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* 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
* @since 5.4 * @since 5.4
*/ */
@Deprecated(since = "6.1")
public void setSecure(Boolean secure) { public void setSecure(Boolean secure) {
this.secure = secure; this.secure = secure;
} }
/** /**
* Sets maximum age in seconds for the cookie that the expected CSRF token is saved to * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* 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}
* @since 5.5 * @since 5.5
*/ */
@Deprecated(since = "6.1")
public void setCookieMaxAge(int cookieMaxAge) { public void setCookieMaxAge(int cookieMaxAge) {
Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero"); Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
this.cookieMaxAge = cookieMaxAge; this.cookieMaxAge = cookieMaxAge;
} }
} }

View File

@ -17,6 +17,7 @@
package org.springframework.security.web.server.csrf; package org.springframework.security.web.server.csrf;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
@ -36,6 +37,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Eric Deandrea * @author Eric Deandrea
* @author Thomas Vitale * @author Thomas Vitale
* @author Alonso Araya * @author Alonso Araya
* @author Alex Montoya
* @since 5.1 * @since 5.1
*/ */
public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRepository { public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRepository {
@ -60,6 +62,19 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
private int cookieMaxAge = -1; 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 * Factory method to conveniently create an instance that has
* {@link #setCookieHttpOnly(boolean)} set to false. * {@link #setCookieHttpOnly(boolean)} set to false.
@ -82,16 +97,18 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
return Mono.fromRunnable(() -> { return Mono.fromRunnable(() -> {
String tokenValue = (token != null) ? token.getToken() : ""; String tokenValue = (token != null) ? token.getToken() : "";
// @formatter:off // @formatter:off
ResponseCookie cookie = ResponseCookie ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie
.from(this.cookieName, tokenValue) .from(this.cookieName, tokenValue)
.domain(this.cookieDomain) .domain(this.cookieDomain)
.httpOnly(this.cookieHttpOnly) .httpOnly(this.cookieHttpOnly)
.maxAge(!tokenValue.isEmpty() ? this.cookieMaxAge : 0) .maxAge(!tokenValue.isEmpty() ? this.cookieMaxAge : 0)
.path((this.cookiePath != null) ? this.cookiePath : getRequestContext(exchange.getRequest())) .path((this.cookiePath != null) ? this.cookiePath : getRequestContext(exchange.getRequest()))
.secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null)) .secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null));
.build();
this.cookieCustomizer.accept(cookieBuilder);
// @formatter:on // @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 * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* @param cookieHttpOnly True to mark the cookie as http only. False otherwise.
*/ */
@Deprecated(since = "6.1")
public void setCookieHttpOnly(boolean cookieHttpOnly) { public void setCookieHttpOnly(boolean cookieHttpOnly) {
this.cookieHttpOnly = cookieHttpOnly; this.cookieHttpOnly = cookieHttpOnly;
} }
@ -150,44 +167,27 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep
} }
/** /**
* Sets the cookie domain * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* @param cookieDomain The cookie domain
*/ */
@Deprecated(since = "6.1")
public void setCookieDomain(String cookieDomain) { public void setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain; this.cookieDomain = cookieDomain;
} }
/** /**
* Sets the cookie secure flag. If not set, the value depends on * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* {@link ServerHttpRequest#getSslInfo()}.
* @param secure The value for the secure flag
* @since 5.5 * @since 5.5
*/ */
@Deprecated(since = "6.1")
public void setSecure(boolean secure) { public void setSecure(boolean secure) {
this.secure = secure; this.secure = secure;
} }
/** /**
* Sets maximum age in seconds for the cookie that the expected CSRF token is saved to * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
* 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}
* @since 5.8 * @since 5.8
*/ */
@Deprecated(since = "6.1")
public void setCookieMaxAge(int cookieMaxAge) { public void setCookieMaxAge(int cookieMaxAge) {
Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero"); Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
this.cookieMaxAge = cookieMaxAge; this.cookieMaxAge = cookieMaxAge;

View File

@ -20,6 +20,7 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockCookie;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
@ -29,6 +30,7 @@ import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCs
/** /**
* @author Rob Winch * @author Rob Winch
* @author Alex Montoya
* @since 4.1 * @since 4.1
*/ */
public class CookieCsrfTokenRepositoryTests { public class CookieCsrfTokenRepositoryTests {
@ -102,7 +104,17 @@ public class CookieCsrfTokenRepositoryTests {
} }
@Test @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.request.setSecure(true);
this.repository.setSecure(Boolean.FALSE); this.repository.setSecure(Boolean.FALSE);
CsrfToken token = this.repository.generateToken(this.request); CsrfToken token = this.repository.generateToken(this.request);
@ -112,7 +124,17 @@ public class CookieCsrfTokenRepositoryTests {
} }
@Test @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.request.setSecure(true);
this.repository.saveToken(null, this.request, this.response); this.repository.saveToken(null, this.request, this.response);
Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
@ -133,7 +155,16 @@ public class CookieCsrfTokenRepositoryTests {
} }
@Test @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); this.repository.setCookieHttpOnly(false);
CsrfToken token = this.repository.generateToken(this.request); CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, this.response); this.repository.saveToken(token, this.request, this.response);
@ -142,7 +173,16 @@ public class CookieCsrfTokenRepositoryTests {
} }
@Test @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(); this.repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfToken token = this.repository.generateToken(this.request); CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, this.response); this.repository.saveToken(token, this.request, this.response);
@ -190,6 +230,16 @@ public class CookieCsrfTokenRepositoryTests {
assertThat(tokenCookie.getDomain()).isEqualTo(domainName); 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 @Test
public void saveTokenWithCookieMaxAge() { public void saveTokenWithCookieMaxAge() {
int maxAge = 1200; int maxAge = 1200;
@ -200,6 +250,46 @@ public class CookieCsrfTokenRepositoryTests {
assertThat(tokenCookie.getMaxAge()).isEqualTo(maxAge); 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 @Test
public void loadTokenNoCookiesNull() { public void loadTokenNoCookiesNull() {
assertThat(this.repository.loadToken(this.request)).isNull(); assertThat(this.repository.loadToken(this.request)).isNull();
@ -299,6 +389,28 @@ public class CookieCsrfTokenRepositoryTests {
assertThat(deferredCsrfToken.isGenerated()).isFalse(); 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 @Test
public void setCookieNameNullIllegalArgumentException() { public void setCookieNameNullIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null)); assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null));

View File

@ -18,6 +18,7 @@ package org.springframework.security.web.server.csrf;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Eric Deandrea * @author Eric Deandrea
* @author Thomas Vitale * @author Thomas Vitale
* @author Alonso Araya * @author Alonso Araya
* @author Alex Montoya
* @since 5.1 * @since 5.1
*/ */
public class CookieServerCsrfTokenRepositoryTests { public class CookieServerCsrfTokenRepositoryTests {
@ -61,6 +63,8 @@ public class CookieServerCsrfTokenRepositoryTests {
private String expectedCookieValue = "csrfToken"; private String expectedCookieValue = "csrfToken";
private String expectedSameSitePolicy = null;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
this.csrfTokenRepository = new CookieServerCsrfTokenRepository(); this.csrfTokenRepository = new CookieServerCsrfTokenRepository();
@ -120,6 +124,12 @@ public class CookieServerCsrfTokenRepositoryTests {
saveAndAssertExpectedValues(createToken()); saveAndAssertExpectedValues(createToken());
} }
@Test
void saveTokenWhenSameSiteThenCookieSameSite() {
setExpectedSameSitePolicy("Lax");
saveAndAssertExpectedValues(createToken());
}
@Test @Test
public void saveTokenWhenCustomPropertiesThenCustomProperties() { public void saveTokenWhenCustomPropertiesThenCustomProperties() {
setExpectedDomain("spring.io"); setExpectedDomain("spring.io");
@ -127,12 +137,48 @@ public class CookieServerCsrfTokenRepositoryTests {
setExpectedPath("/some/path"); setExpectedPath("/some/path");
setExpectedHeaderName("headerName"); setExpectedHeaderName("headerName");
setExpectedParameterName("paramName"); setExpectedParameterName("paramName");
setExpectedSameSitePolicy("Strict");
setExpectedCookieMaxAge(3600); setExpectedCookieMaxAge(3600);
saveAndAssertExpectedValues(createToken()); saveAndAssertExpectedValues(createToken());
} }
@Test @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()); this.request.sslInfo(new MockSslInfo());
MockServerWebExchange exchange = MockServerWebExchange.from(this.request); MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
this.csrfTokenRepository.saveToken(exchange, createToken()).block(); this.csrfTokenRepository.saveToken(exchange, createToken()).block();
@ -160,6 +206,16 @@ public class CookieServerCsrfTokenRepositoryTests {
assertThat(cookie.isSecure()).isTrue(); 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 @Test
public void saveTokenWhenSecureFlagFalseThenNotSecure() { public void saveTokenWhenSecureFlagFalseThenNotSecure() {
MockServerWebExchange exchange = MockServerWebExchange.from(this.request); MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
@ -170,6 +226,16 @@ public class CookieServerCsrfTokenRepositoryTests {
assertThat(cookie.isSecure()).isFalse(); 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 @Test
public void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() { public void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() {
MockServerWebExchange exchange = MockServerWebExchange.from(this.request); MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
@ -181,6 +247,17 @@ public class CookieServerCsrfTokenRepositoryTests {
assertThat(cookie.isSecure()).isFalse(); 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 @Test
public void loadTokenWhenCookieExistThenTokenFound() { public void loadTokenWhenCookieExistThenTokenFound() {
loadAndAssertExpectedValues(); loadAndAssertExpectedValues();
@ -248,6 +325,11 @@ public class CookieServerCsrfTokenRepositoryTests {
this.expectedMaxAge = Duration.ofSeconds(expectedCookieMaxAge); 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) { private void setExpectedCookieValue(String expectedCookieValue) {
this.expectedCookieValue = expectedCookieValue; this.expectedCookieValue = expectedCookieValue;
} }
@ -284,6 +366,7 @@ public class CookieServerCsrfTokenRepositoryTests {
assertThat(cookie.isHttpOnly()).isEqualTo(this.expectedHttpOnly); assertThat(cookie.isHttpOnly()).isEqualTo(this.expectedHttpOnly);
assertThat(cookie.getName()).isEqualTo(this.expectedCookieName); assertThat(cookie.getName()).isEqualTo(this.expectedCookieName);
assertThat(cookie.getValue()).isEqualTo(this.expectedCookieValue); assertThat(cookie.getValue()).isEqualTo(this.expectedCookieValue);
assertThat(cookie.getSameSite()).isEqualTo(this.expectedSameSitePolicy);
} }
private void generateTokenAndAssertExpectedValues() { private void generateTokenAndAssertExpectedValues() {