Skip to content
Permalink
Browse files

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
  • Loading branch information...
bclozel committed Jun 14, 2018
1 parent 1e5f8cc commit 09d9450154be796349dabdc606ade57beae08724
@@ -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 @@
* 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 @@

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 @@ private ResponseCookie(String name, String value, Duration maxAge, @Nullable Str
this.path = path;
this.secure = secure;
this.httpOnly = httpOnly;
this.sameSite = sameSite;
}


@@ -105,6 +110,16 @@ public boolean isHttpOnly() {
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 String toString() {
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 static ResponseCookieBuilder from(final String name, final String value)

private boolean httpOnly;

@Nullable
private String sameSite;

@Override
public ResponseCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
@@ -216,10 +236,16 @@ public ResponseCookieBuilder httpOnly(boolean httpOnly) {
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 ResponseCookie build() {
*/
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 @@
* Cookie-based {@link WebSessionIdResolver}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
public class CookieWebSessionIdResolver implements WebSessionIdResolver {
@@ -39,6 +40,8 @@

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 Duration getCookieMaxAge() {
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 Duration getCookieMaxAge() {
@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 void setSessionId() throws Exception {
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());
}
}

0 comments on commit 09d9450

Please sign in to comment.
You can’t perform that action at this time.