Add SameSite option to cookies
diff --git a/web/src/main/java/org/apache/shiro/web/servlet/Cookie.java b/web/src/main/java/org/apache/shiro/web/servlet/Cookie.java
index 065b51d..94b18a0 100644
--- a/web/src/main/java/org/apache/shiro/web/servlet/Cookie.java
+++ b/web/src/main/java/org/apache/shiro/web/servlet/Cookie.java
@@ -47,6 +47,12 @@
*/
public static final String ROOT_PATH = "/";
+ public enum SameSiteOptions {
+ NONE,
+ LAX,
+ STRICT,
+ }
+
String getName();
void setName(String name);
@@ -83,6 +89,10 @@
boolean isHttpOnly();
+ void setSameSite(SameSiteOptions sameSite);
+
+ SameSiteOptions getSameSite();
+
void saveTo(HttpServletRequest request, HttpServletResponse response);
void removeFrom(HttpServletRequest request, HttpServletResponse response);
diff --git a/web/src/main/java/org/apache/shiro/web/servlet/SimpleCookie.java b/web/src/main/java/org/apache/shiro/web/servlet/SimpleCookie.java
index c8d1420..7022586 100644
--- a/web/src/main/java/org/apache/shiro/web/servlet/SimpleCookie.java
+++ b/web/src/main/java/org/apache/shiro/web/servlet/SimpleCookie.java
@@ -67,6 +67,7 @@
protected static final String COMMENT_ATTRIBUTE_NAME = "Comment";
protected static final String SECURE_ATTRIBUTE_NAME = "Secure";
protected static final String HTTP_ONLY_ATTRIBUTE_NAME = "HttpOnly";
+ protected static final String SAME_SITE_ATTRIBUTE_NAME = "SameSite";
private static final transient Logger log = LoggerFactory.getLogger(SimpleCookie.class);
@@ -79,11 +80,13 @@
private int version;
private boolean secure;
private boolean httpOnly;
+ private SameSiteOptions sameSite;
public SimpleCookie() {
this.maxAge = DEFAULT_MAX_AGE;
this.version = DEFAULT_VERSION;
this.httpOnly = true; //most of the cookies ever used by Shiro should be as secure as possible.
+ this.sameSite = SameSiteOptions.LAX;
}
public SimpleCookie(String name) {
@@ -101,6 +104,7 @@
this.version = Math.max(DEFAULT_VERSION, cookie.getVersion());
this.secure = cookie.isSecure();
this.httpOnly = cookie.isHttpOnly();
+ this.sameSite = cookie.getSameSite();
}
public String getName() {
@@ -178,6 +182,14 @@
this.httpOnly = httpOnly;
}
+ public SameSiteOptions getSameSite() {
+ return sameSite;
+ }
+
+ public void setSameSite(SameSiteOptions sameSite) {
+ this.sameSite = sameSite;
+ }
+
/**
* Returns the Cookie's calculated path setting. If the {@link javax.servlet.http.Cookie#getPath() path} is {@code null}, then the
* {@code request}'s {@link javax.servlet.http.HttpServletRequest#getContextPath() context path}
@@ -211,15 +223,16 @@
int version = getVersion();
boolean secure = isSecure();
boolean httpOnly = isHttpOnly();
+ SameSiteOptions sameSite = getSameSite();
- addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
+ addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly, sameSite);
}
private void addCookieHeader(HttpServletResponse response, String name, String value, String comment,
String domain, String path, int maxAge, int version,
- boolean secure, boolean httpOnly) {
+ boolean secure, boolean httpOnly, SameSiteOptions sameSite) {
- String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
+ String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly, sameSite);
response.addHeader(COOKIE_HEADER_NAME, headerValue);
if (log.isDebugEnabled()) {
@@ -238,6 +251,13 @@
String domain, String path, int maxAge, int version,
boolean secure, boolean httpOnly) {
+ return buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly, getSameSite());
+ }
+
+ protected String buildHeaderValue(String name, String value, String comment,
+ String domain, String path, int maxAge, int version,
+ boolean secure, boolean httpOnly, SameSiteOptions sameSite) {
+
if (!StringUtils.hasText(name)) {
throw new IllegalStateException("Cookie name cannot be null/empty.");
}
@@ -255,6 +275,7 @@
appendVersion(sb, version);
appendSecure(sb, secure);
appendHttpOnly(sb, httpOnly);
+ appendSameSite(sb, sameSite);
return sb.toString();
@@ -328,6 +349,13 @@
}
}
+ private void appendSameSite(StringBuilder sb, SameSiteOptions sameSite) {
+ if (sameSite != null) {
+ sb.append(ATTRIBUTE_DELIMITER);
+ sb.append(SAME_SITE_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(sameSite.toString().toLowerCase(Locale.ENGLISH));
+ }
+ }
+
/**
* Check whether the given {@code cookiePath} matches the {@code requestPath}
*
@@ -369,8 +397,9 @@
int version = getVersion();
boolean secure = isSecure();
boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all
+ SameSiteOptions sameSite = null;
- addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
+ addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly, sameSite);
log.trace("Removed '{}' cookie by setting maxAge=0", name);
}
diff --git a/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy b/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy
index 526636c..841569f 100644
--- a/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy
+++ b/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy
@@ -78,6 +78,7 @@
expect(cookie.getVersion()).andReturn(SimpleCookie.DEFAULT_VERSION);
expect(cookie.isSecure()).andReturn(true);
expect(cookie.isHttpOnly()).andReturn(true);
+ expect(cookie.getSameSite()).andReturn(Cookie.SameSiteOptions.LAX);
replay(cookie);
diff --git a/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java b/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java
index fba8f55..81d9c5a 100644
--- a/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java
+++ b/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java
@@ -71,6 +71,7 @@
expect(cookie.getVersion()).andReturn(SimpleCookie.DEFAULT_VERSION);
expect(cookie.isSecure()).andReturn(false);
expect(cookie.isHttpOnly()).andReturn(true);
+ expect(cookie.getSameSite()).andReturn(org.apache.shiro.web.servlet.Cookie.SameSiteOptions.LAX);
UsernamePasswordToken token = new UsernamePasswordToken("user", "secret");
token.setRememberMe(true);
diff --git a/web/src/test/java/org/apache/shiro/web/servlet/SimpleCookieTest.java b/web/src/test/java/org/apache/shiro/web/servlet/SimpleCookieTest.java
index 3a272aa..4d26f86 100644
--- a/web/src/test/java/org/apache/shiro/web/servlet/SimpleCookieTest.java
+++ b/web/src/test/java/org/apache/shiro/web/servlet/SimpleCookieTest.java
@@ -25,6 +25,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import java.util.Locale;
import static org.easymock.EasyMock.*;
@@ -59,7 +60,7 @@
String path = "/somepath";
String headerValue = this.cookie.buildHeaderValue(name, value, null, null, path,
- 0, SimpleCookie.DEFAULT_VERSION, false, false);
+ 0, SimpleCookie.DEFAULT_VERSION, false, false, null);
String expectedStart = new StringBuilder()
.append(name).append(SimpleCookie.NAME_VALUE_DELIMITER).append(value)
@@ -89,6 +90,9 @@
.append(SimpleCookie.PATH_ATTRIBUTE_NAME).append(SimpleCookie.NAME_VALUE_DELIMITER).append(Cookie.ROOT_PATH)
.append(SimpleCookie.ATTRIBUTE_DELIMITER)
.append(SimpleCookie.HTTP_ONLY_ATTRIBUTE_NAME)
+ .append(SimpleCookie.ATTRIBUTE_DELIMITER)
+ .append(SimpleCookie.SAME_SITE_ATTRIBUTE_NAME).append(SimpleCookie.NAME_VALUE_DELIMITER)
+ .append(Cookie.SameSiteOptions.LAX.toString().toLowerCase(Locale.ENGLISH))
.toString();
expect(mockRequest.getContextPath()).andReturn(contextPath);