You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shiro.apache.org by bd...@apache.org on 2020/10/17 15:03:47 UTC

[shiro] 01/01: [SHIRO-789] Add SameSite option to AbstractShiroWebConfiguration.buildCookie

This is an automated email from the ASF dual-hosted git repository.

bdemers pushed a commit to branch SHIRO-789
in repository https://gitbox.apache.org/repos/asf/shiro.git

commit 9154b0ea43535a1952f156de4929da207335cced
Author: Benjamin Marwell <bm...@gmail.com>
AuthorDate: Thu Aug 20 08:28:00 2020 +0200

    [SHIRO-789] Add SameSite option to AbstractShiroWebConfiguration.buildCookie
    
      Functional changes:
      - automatically set secure cookie on SameSiteOptions.NONE
      - added test for the default value = LAX
      - added test class for STRICT value
    
      Other changes:
      - add javadoc cookie description from developer.mozilla.org
      - add missing overrides
      - remove unused imports in AbstractShiroWebConfiguration and its test
---
 .../web/config/AbstractShiroWebConfiguration.java  | 19 +++++++-
 .../web/config/ShiroWebConfigurationTest.groovy    | 24 ++++++----
 .../ShiroWebConfigurationTestSameSiteStrict.java   | 54 ++++++++++++++++++++++
 ...roWebConfigurationTestSameSiteStrict.properties | 21 +++++++++
 .../java/org/apache/shiro/web/servlet/Cookie.java  | 21 ++++++++-
 .../org/apache/shiro/web/servlet/SimpleCookie.java | 27 +++++++++++
 6 files changed, 154 insertions(+), 12 deletions(-)

diff --git a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java
index 414d7b1..ae2afb5 100644
--- a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java
+++ b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java
@@ -66,6 +66,9 @@ public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
     @Value("#{ @environment['shiro.sessionManager.cookie.secure'] ?: false }")
     protected boolean sessionIdCookieSecure;
 
+    @Value("#{ @environment['shiro.sessionManager.cookie.sameSite'] ?: T(org.apache.shiro.web.servlet.Cookie.SameSiteOptions).LAX  }")
+    protected Cookie.SameSiteOptions sessionIdCookieSameSite;
+
 
     // RememberMe Cookie info
 
@@ -84,6 +87,9 @@ public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
     @Value("#{ @environment['shiro.rememberMeManager.cookie.secure'] ?: false }")
     protected boolean rememberMeCookieSecure;
 
+    @Value("#{ @environment['shiro.rememberMeManager.cookie.sameSite'] ?: T(org.apache.shiro.web.servlet.Cookie.SameSiteOptions).LAX }")
+    protected Cookie.SameSiteOptions rememberMeSameSite;
+
 
     protected SessionManager nativeSessionManager() {
         DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
@@ -104,7 +110,8 @@ public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
                 sessionIdCookieMaxAge,
                 sessionIdCookiePath,
                 sessionIdCookieDomain,
-                sessionIdCookieSecure);
+                sessionIdCookieSecure,
+                sessionIdCookieSameSite);
     }
 
     protected Cookie rememberMeCookieTemplate() {
@@ -113,16 +120,22 @@ public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
                 rememberMeCookieMaxAge,
                 rememberMeCookiePath,
                 rememberMeCookieDomain,
-                rememberMeCookieSecure);
+                rememberMeCookieSecure,
+                rememberMeSameSite);
     }
 
     protected Cookie buildCookie(String name, int maxAge, String path, String domain, boolean secure) {
+        return buildCookie(name, maxAge, path, domain, secure, Cookie.SameSiteOptions.LAX);
+    }
+
+    protected Cookie buildCookie(String name, int maxAge, String path, String domain, boolean secure, Cookie.SameSiteOptions sameSiteOption) {
         Cookie cookie = new SimpleCookie(name);
         cookie.setHttpOnly(true);
         cookie.setMaxAge(maxAge);
         cookie.setPath(path);
         cookie.setDomain(domain);
         cookie.setSecure(secure);
+        cookie.setSameSite(sameSiteOption);
 
         return cookie;
     }
@@ -135,6 +148,7 @@ public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
         return new ServletContainerSessionManager();
     }
 
+    @Override
     protected RememberMeManager rememberMeManager() {
         CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
         cookieRememberMeManager.setCookie(rememberMeCookieTemplate());
@@ -151,6 +165,7 @@ public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
         return new DefaultWebSessionStorageEvaluator();
     }
 
+    @Override
     protected SessionsSecurityManager createSecurityManager() {
 
         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
diff --git a/support/spring/src/test/groovy/org/apache/shiro/spring/web/config/ShiroWebConfigurationTest.groovy b/support/spring/src/test/groovy/org/apache/shiro/spring/web/config/ShiroWebConfigurationTest.groovy
index eb52198..1515b30 100644
--- a/support/spring/src/test/groovy/org/apache/shiro/spring/web/config/ShiroWebConfigurationTest.groovy
+++ b/support/spring/src/test/groovy/org/apache/shiro/spring/web/config/ShiroWebConfigurationTest.groovy
@@ -18,24 +18,17 @@
  */
 package org.apache.shiro.spring.web.config
 
-import org.apache.shiro.event.EventBus
-import org.apache.shiro.event.support.DefaultEventBus
 import org.apache.shiro.mgt.SecurityManager
 import org.apache.shiro.mgt.SessionStorageEvaluator
 import org.apache.shiro.realm.text.TextConfigurationRealm
-import org.apache.shiro.spring.config.EventBusTestConfiguration
-import org.apache.shiro.spring.config.RealmTestConfiguration
-import org.apache.shiro.spring.config.ShiroAnnotationProcessorConfiguration
-import org.apache.shiro.spring.config.ShiroBeanConfiguration
-import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
 import org.apache.shiro.spring.testconfig.EventBusTestConfiguration
 import org.apache.shiro.spring.testconfig.RealmTestConfiguration
-import org.apache.shiro.web.mgt.CookieRememberMeManager
 import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator
 import org.apache.shiro.web.mgt.WebSecurityManager
 import org.apache.shiro.web.servlet.Cookie
-
 import org.junit.Test
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.function.Executable
 import org.junit.runner.RunWith
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Qualifier
@@ -103,6 +96,13 @@ public class ShiroWebConfigurationTest {
     }
 
     @Test
+    void testSameSiteOptionExpression() {
+        ExpressionParser parser = new SpelExpressionParser();
+        Executable expressionParser = () -> parser.parseExpression("T(org.apache.shiro.web.servlet.Cookie.SameSiteOptions).LAX")
+        Assertions.assertDoesNotThrow expressionParser;
+    }
+
+    @Test
     public void rememberMeCookie() {
         assertEquals "rememberMe", rememberMeCookie.name
     }
@@ -112,4 +112,10 @@ public class ShiroWebConfigurationTest {
         assertSame "JSESSIONID", sessionCookieTemplate.name
 
     }
+
+    @Test
+    void sameSiteOption() {
+        assertSame Cookie.SameSiteOptions.LAX, rememberMeCookie.sameSite
+        assertSame Cookie.SameSiteOptions.LAX, sessionCookieTemplate.sameSite
+    }
 }
diff --git a/support/spring/src/test/java/org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.java b/support/spring/src/test/java/org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.java
new file mode 100644
index 0000000..19d5290
--- /dev/null
+++ b/support/spring/src/test/java/org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.shiro.spring.web.config;
+
+import org.apache.shiro.spring.testconfig.EventBusTestConfiguration;
+import org.apache.shiro.spring.testconfig.RealmTestConfiguration;
+import org.apache.shiro.web.servlet.Cookie;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {EventBusTestConfiguration.class, RealmTestConfiguration.class, ShiroWebConfiguration.class})
+@TestPropertySource
+public class ShiroWebConfigurationTestSameSiteStrict {
+
+    @Autowired
+    ShiroWebConfiguration shiroWebConfiguration;
+
+    @Test
+    void testStrictSameSite() {
+        // given
+        // org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.properties
+
+        // then
+        assertEquals(Cookie.SameSiteOptions.STRICT, shiroWebConfiguration.sessionIdCookieSameSite);
+        assertEquals(Cookie.SameSiteOptions.STRICT, shiroWebConfiguration.rememberMeSameSite);
+
+        assertEquals(Cookie.SameSiteOptions.STRICT, shiroWebConfiguration.sessionCookieTemplate().getSameSite());
+        assertEquals(Cookie.SameSiteOptions.STRICT, shiroWebConfiguration.rememberMeCookieTemplate().getSameSite());
+    }
+}
diff --git a/support/spring/src/test/resources/org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.properties b/support/spring/src/test/resources/org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.properties
new file mode 100644
index 0000000..ffe22c4
--- /dev/null
+++ b/support/spring/src/test/resources/org/apache/shiro/spring/web/config/ShiroWebConfigurationTestSameSiteStrict.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+shiro.sessionManager.cookie.sameSite = STRICT
+shiro.rememberMeManager.cookie.sameSite = STRICT
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 94b18a0..5222e20 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
@@ -35,7 +35,7 @@ public interface Cookie {
      * The value of deleted cookie (with the maxAge 0).
      */
     public static final String DELETED_COOKIE_VALUE = "deleteMe";
-    
+
 
     /**
      * The number of seconds in one year (= 60 * 60 * 24 * 365).
@@ -47,9 +47,28 @@ public interface Cookie {
      */
     public static final String ROOT_PATH = "/";
 
+    /**
+     * The SameSite attribute of the Set-Cookie HTTP response header allows you to declare if your cookie should be restricted to a first-party or same-site context.
+     */
     public enum SameSiteOptions {
+        /**
+         * Cookies will be sent in all contexts, i.e sending cross-origin is allowed.
+         *
+         * <p>None used to be the default value, but recent browser versions made Lax the default value
+         * to have reasonably robust defense against some classes of cross-site request forgery (CSRF) attacks.</p>
+         *
+         * <p>None requires the Secure attribute in latest browser versions. See below for more information.</p>
+         */
         NONE,
+        /**
+         * Cookies are allowed to be sent with top-level navigations and will be sent along with GET requests
+         * initiated by third party website. This is the default value in modern browsers as of 2020.
+         */
         LAX,
+        /**
+         * Cookies will only be sent in a first-party context
+         * and not be sent along with requests initiated by third party websites.
+         */
         STRICT,
     }
 
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 fe28f3d..1c3d504 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
@@ -108,10 +108,12 @@ public class SimpleCookie implements Cookie {
         this.sameSite = cookie.getSameSite();
     }
 
+    @Override
     public String getName() {
         return name;
     }
 
+    @Override
     public void setName(String name) {
         if (!StringUtils.hasText(name)) {
             throw new IllegalArgumentException("Name cannot be null/empty.");
@@ -119,76 +121,98 @@ public class SimpleCookie implements Cookie {
         this.name = name;
     }
 
+    @Override
     public String getValue() {
         return value;
     }
 
+    @Override
     public void setValue(String value) {
         this.value = value;
     }
 
+    @Override
     public String getComment() {
         return comment;
     }
 
+    @Override
     public void setComment(String comment) {
         this.comment = comment;
     }
 
+    @Override
     public String getDomain() {
         return domain;
     }
 
+    @Override
     public void setDomain(String domain) {
         this.domain = domain;
     }
 
+    @Override
     public String getPath() {
         return path;
     }
 
+    @Override
     public void setPath(String path) {
         this.path = path;
     }
 
+    @Override
     public int getMaxAge() {
         return maxAge;
     }
 
+    @Override
     public void setMaxAge(int maxAge) {
         this.maxAge = Math.max(DEFAULT_MAX_AGE, maxAge);
     }
 
+    @Override
     public int getVersion() {
         return version;
     }
 
+    @Override
     public void setVersion(int version) {
         this.version = Math.max(DEFAULT_VERSION, version);
     }
 
+    @Override
     public boolean isSecure() {
         return secure;
     }
 
+    @Override
     public void setSecure(boolean secure) {
         this.secure = secure;
     }
 
+    @Override
     public boolean isHttpOnly() {
         return httpOnly;
     }
 
+    @Override
     public void setHttpOnly(boolean httpOnly) {
         this.httpOnly = httpOnly;
     }
 
+    @Override
     public SameSiteOptions getSameSite() {
         return sameSite;
     }
 
+    @Override
     public void setSameSite(SameSiteOptions sameSite) {
         this.sameSite = sameSite;
+        if (this.sameSite == SameSiteOptions.NONE) {
+            // do not allow invalid cookies. Only secure cookies are allowed if SameSite is set to NONE.
+            setSecure(true);
+        }
     }
 
     /**
@@ -213,6 +237,7 @@ public class SimpleCookie implements Cookie {
         return path;
     }
 
+    @Override
     public void saveTo(HttpServletRequest request, HttpServletResponse response) {
 
         String name = getName();
@@ -388,6 +413,7 @@ public class SimpleCookie implements Cookie {
         return fmt.format(date);
     }
 
+    @Override
     public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
         String name = getName();
         String value = DELETED_COOKIE_VALUE;
@@ -405,6 +431,7 @@ public class SimpleCookie implements Cookie {
         log.trace("Removed '{}' cookie by setting maxAge=0", name);
     }
 
+    @Override
     public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
         String name = getName();
         String value = null;