From d65f8671b16fca98a99c80a3a7c2a2d4e6e957f7 Mon Sep 17 00:00:00 2001
From: Patrick <patrick@cirrusidentity.com>
Date: Tue, 15 Dec 2020 13:56:38 -0800
Subject: [PATCH] Allow easy enabling of SameSite = 'None' (#1382)

---
 config-templates/config.php             |  4 +-
 lib/SimpleSAML/Utils/HTTP.php           | 45 +++++++++++++++++++++
 tests/lib/SimpleSAML/Utils/HTTPTest.php | 53 +++++++++++++++++++++++++
 3 files changed, 100 insertions(+), 2 deletions(-)

diff --git a/config-templates/config.php b/config-templates/config.php
index b573f432d..50e03fdcc 100644
--- a/config-templates/config.php
+++ b/config-templates/config.php
@@ -564,7 +564,7 @@ $config = [
      * Example:
      *  'session.cookie.samesite' => 'None',
      */
-    'session.cookie.samesite' => null,
+    'session.cookie.samesite' => \SimpleSAML\Utils\HTTP::canSetSameSiteNone() ? 'None' : null,
 
     /*
      * Options to override the default settings for php sessions.
@@ -792,7 +792,7 @@ $config = [
     'language.cookie.secure' => false,
     'language.cookie.httponly' => false,
     'language.cookie.lifetime' => (60 * 60 * 24 * 900),
-    'language.cookie.samesite' => null,
+    'language.cookie.samesite' => \SimpleSAML\Utils\HTTP::canSetSameSiteNone() ? 'None' : null,
 
     /**
      * Custom getLanguage function called from SimpleSAML\Locale\Language::getLanguage().
diff --git a/lib/SimpleSAML/Utils/HTTP.php b/lib/SimpleSAML/Utils/HTTP.php
index 23c7bdf84..d74031ef0 100644
--- a/lib/SimpleSAML/Utils/HTTP.php
+++ b/lib/SimpleSAML/Utils/HTTP.php
@@ -18,6 +18,51 @@ use SimpleSAML\XHTML\Template;
  */
 class HTTP
 {
+    /**
+     * Determine if the user agent can support cookies being sent with SameSite equal to "None".
+     * Browsers with out support may drop the cookie and or treat is a stricter setting
+     * Browsers with support may have additional requirements on setting it on non-secure websites.
+     *
+     * Based on the Azure teams experience rolling out support and Chromium's advice
+     * https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
+     * https://www.chromium.org/updates/same-site/incompatible-clients
+     * @return bool true if user agent supports a None value for SameSite.
+     */
+    public static function canSetSameSiteNone(): bool
+    {
+        $useragent = $_SERVER['HTTP_USER_AGENT'] ?? null;
+        if (!$useragent) {
+            return true;
+        }
+        // All iOS 12 based browsers have no support
+        if (strpos($useragent, "CPU iPhone OS 12") !== false || strpos($useragent, "iPad; CPU OS 12") !== false) {
+            return false;
+        }
+
+        // Safari Mac OS X 10.14 has no support
+        // - Safari on Mac OS X.
+        if (strpos($useragent, "Macintosh; Intel Mac OS X 10_14") !== false) {
+            // regular safari
+            if (strpos($useragent, "Version/") !== false && strpos($useragent, "Safari") !== false) {
+                return false;
+            } elseif (preg_match('|AppleWebKit/[\.\d]+ \(KHTML, like Gecko\)$|', $useragent)) {
+                return false;
+            }
+        }
+
+        // Chrome based UCBrowser may have support (>= 12.13.2) even though its chrome version is old
+        $matches = [];
+        if (preg_match('|UCBrowser/(\d+\.\d+\.\d+)[\.\d]*|', $useragent, $matches)) {
+            return version_compare($matches[1], '12.13.2', '>=');
+        }
+
+        // Chrome 50-69 may have broken SameSite=None and don't require it to be set
+        if (strpos($useragent, "Chrome/5") !== false || strpos($useragent, "Chrome/6") !== false) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Obtain a URL where we can redirect to securely post a form with the given data to a specific destination.
      *
diff --git a/tests/lib/SimpleSAML/Utils/HTTPTest.php b/tests/lib/SimpleSAML/Utils/HTTPTest.php
index e60dae5c8..f6f90fd64 100644
--- a/tests/lib/SimpleSAML/Utils/HTTPTest.php
+++ b/tests/lib/SimpleSAML/Utils/HTTPTest.php
@@ -526,4 +526,57 @@ class HTTPTest extends ClearStateTestCase
         $this->assertMatchesRegularExpression('/\b[Ss]ame[Ss]ite=Lax(;|$)/', $headers[2]);
         $this->assertMatchesRegularExpression('/\b[Ss]ame[Ss]ite=Strict(;|$)/', $headers[3]);
     }
+
+    /**
+     * Test detecting if user agent supports None
+     * @dataProvider detectSameSiteProvider
+     * @param null|string $userAgent The user agent. Null means not set, like with CLI
+     * @param bool $supportsNone None can be set as a SameSite flag
+     */
+    public function testDetectSameSiteNoneBehavior(?string $userAgent, bool $supportsNone): void
+    {
+        if ($userAgent) {
+            $_SERVER['HTTP_USER_AGENT'] = $userAgent;
+        }
+        $this->assertEquals($supportsNone, HTTP::canSetSameSiteNone(), $userAgent ?? 'No user agent set');
+    }
+
+    public function detectSameSiteProvider(): array
+    {
+        // @codingStandardsIgnoreStart
+        return [
+          [null, true],
+          ['some-new-browser', true],
+            //Browsers that can handle 'None'
+            // Chrome
+            ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', true],
+            // Chome on windows
+            ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36', true],
+            // Chrome linux
+            ['Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0', true],
+             // Safari iOS 13
+            ['Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.2 Mobile/15E148 Safari/604.1', true],
+            // Mac OS X with support
+            ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Safari/605.1.15', true],
+            // UC Browser with support
+            ['Mozilla/5.0 (Linux; U; Android 9; en-US; SM-A705FN Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.13.2.1208 Mobile Safari/537.36', true],
+            ['Mozilla/5.0 (Linux; U; Android 10; en-US; RMX2020 Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.13.5.1209 Mobile Safari/537.36', true],
+            // Embedded Mac with support
+            ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko)', true],
+            // Browser without support
+            // Old Safari on mac
+            ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15', false],
+            // Old Safari on iOS 12 (phone and ipad
+            ['Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1', false],
+            ['Mozilla/5.0 (iPad; CPU OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/16A5288q Safari/605.1.15', false],
+            // Chromium without support
+            ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/65.0.3325.181 Chrome/65.0.3325.181 Safari/537.36', false],
+            // UC Browser without support
+            ['Mozilla/5.0 (Linux; U; Android 8.1.0; zh-CN; EML-AL00 Build/HUAWEIEML-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 baidu.sogo.uc.UCBrowser/11.9.4.974 UWS/2.13.1.48 Mobile Safari/537.36 AliApp(DingTalk/4.5.11) com.alibaba.android.rimet/10487439 Channel/227200 language/zh-CN', false],
+            ['Mozilla/5.0 (Linux; U; Android 7.1.1; en-US; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.13.0.1207 Mobile Safari/537.36', false],
+            // old embedded browser
+            ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/605.1.15 (KHTML, like Gecko)', false]
+        ];
+        // @codingStandardsIgnoreEnd
+    }
 }
-- 
GitLab