Add SameSite support in WebFlux SESSION cookies

This commit adds support for the "SameSite" attribute in response
cookies. As explained in rfc6265bis, this attribute can be used to limit
the scope of a cookie so that it can't be attached to a request unless
it is sent from the "same-site".

This feature is currently supported by Google Chrome and Firefox, other
browsers will ignore this attribute.

This feature can help prevent CSRF attacks; this is why this commit adds
this attribute by default for SESSION Cookies in WebFlux.

See: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis

Issue: SPR-16418
This commit is contained in:
Brian Clozel 2018-06-14 11:22:43 +02:00
parent 1e5f8cc232
commit 09d9450154
3 changed files with 62 additions and 10 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@ -29,6 +29,7 @@ import org.springframework.util.StringUtils;
* static method.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
@ -46,12 +47,15 @@ public final class ResponseCookie extends HttpCookie {
private final boolean httpOnly;
@Nullable
private final String sameSite;
/**
* Private constructor. See {@link #from(String, String)}.
*/
private ResponseCookie(String name, String value, Duration maxAge, @Nullable String domain,
@Nullable String path, boolean secure, boolean httpOnly) {
@Nullable String path, boolean secure, boolean httpOnly, @Nullable String sameSite) {
super(name, value);
Assert.notNull(maxAge, "Max age must not be null");
@ -60,6 +64,7 @@ public final class ResponseCookie extends HttpCookie {
this.path = path;
this.secure = secure;
this.httpOnly = httpOnly;
this.sameSite = sameSite;
}
@ -105,6 +110,16 @@ public final class ResponseCookie extends HttpCookie {
return this.httpOnly;
}
/**
* Return the cookie "SameSite" attribute, or {@code null} if not set.
* <p>This limits the scope of the cookie such that it will only be attached to
* same site requests if {@code "Strict"} or cross-site requests if {@code "Lax"}.
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
*/
@Nullable
public String getSameSite() {
return this.sameSite;
}
@Override
public boolean equals(Object other) {
@ -146,13 +161,15 @@ public final class ResponseCookie extends HttpCookie {
headers.setExpires(seconds > 0 ? System.currentTimeMillis() + seconds : 0);
sb.append(headers.getFirst(HttpHeaders.EXPIRES));
}
if (this.secure) {
sb.append("; Secure");
}
if (this.httpOnly) {
sb.append("; HttpOnly");
}
if (StringUtils.hasText(this.sameSite)) {
sb.append("; SameSite=").append(this.sameSite);
}
return sb.toString();
}
@ -180,6 +197,9 @@ public final class ResponseCookie extends HttpCookie {
private boolean httpOnly;
@Nullable
private String sameSite;
@Override
public ResponseCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
@ -216,10 +236,16 @@ public final class ResponseCookie extends HttpCookie {
return this;
}
@Override
public ResponseCookieBuilder sameSite(String sameSite) {
this.sameSite = sameSite;
return this;
}
@Override
public ResponseCookie build() {
return new ResponseCookie(name, value, this.maxAge, this.domain, this.path,
this.secure, this.httpOnly);
this.secure, this.httpOnly, this.sameSite);
}
};
}
@ -266,6 +292,12 @@ public final class ResponseCookie extends HttpCookie {
*/
ResponseCookieBuilder httpOnly(boolean httpOnly);
/**
* Add the "SameSite" attribute to the cookie.
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
*/
ResponseCookieBuilder sameSite(String sameSite);
/**
* Create the HttpCookie.
*/

View File

@ -31,6 +31,7 @@ import org.springframework.web.server.ServerWebExchange;
* Cookie-based {@link WebSessionIdResolver}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
public class CookieWebSessionIdResolver implements WebSessionIdResolver {
@ -39,6 +40,8 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver {
private Duration cookieMaxAge = Duration.ofSeconds(-1);
private String sameSite = "Strict";
/**
* Set the name of the cookie to use for the session id.
@ -74,6 +77,23 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver {
return this.cookieMaxAge;
}
/**
* Set the value for the "SameSite" attribute of the cookie that holds the
* session id. For its meaning and possible values, see
* {@link ResponseCookie#getSameSite()}.
* <p>By default set to {@code "Strict"}
* @param sameSite the SameSite value
*/
public void setSameSite(String sameSite) {
this.sameSite = sameSite;
}
/**
* Return the configured "SameSite" attribute value for the session cookie.
*/
public String getSameSite() {
return sameSite;
}
@Override
public List<String> resolveSessionIds(ServerWebExchange exchange) {
@ -88,21 +108,21 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver {
@Override
public void setSessionId(ServerWebExchange exchange, String id) {
Assert.notNull(id, "'id' is required");
setSessionCookie(exchange, id, getCookieMaxAge());
setSessionCookie(exchange, id, getCookieMaxAge(), getSameSite());
}
@Override
public void expireSession(ServerWebExchange exchange) {
setSessionCookie(exchange, "", Duration.ofSeconds(0));
setSessionCookie(exchange, "", Duration.ofSeconds(0), "");
}
private void setSessionCookie(ServerWebExchange exchange, String id, Duration maxAge) {
private void setSessionCookie(ServerWebExchange exchange, String id, Duration maxAge, String sameSite) {
String name = getCookieName();
boolean secure = "https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme());
String path = exchange.getRequest().getPath().contextPath().value() + "/";
exchange.getResponse().getCookies().set(name,
ResponseCookie.from(name, id).path(path)
.maxAge(maxAge).httpOnly(true).secure(secure).build());
.maxAge(maxAge).httpOnly(true).secure(secure).sameSite(sameSite).build());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@ -44,6 +44,6 @@ public class CookieWebSessionIdResolverTests {
assertEquals(1, cookies.size());
ResponseCookie cookie = cookies.getFirst(this.resolver.getCookieName());
assertNotNull(cookie);
assertEquals("SESSION=123; Path=/; Secure; HttpOnly", cookie.toString());
assertEquals("SESSION=123; Path=/; Secure; HttpOnly; SameSite=Strict", cookie.toString());
}
}