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:
parent
1e5f8cc232
commit
09d9450154
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue