├── .gitignore ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── index.test.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2019-2020 David Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # should-send-same-site-none 2 | 3 | The module comes with: 4 | 5 | - A small **utility function** `isSameSiteNoneCompatible` for detecting incompatible user agents (browsers) for the `SameSite=None` cookie attribute. 6 | 7 | - A **Express middleware** `shouldSendSameSiteNone` for automatically removing `SameSite=None` from response header when reqesting client is incompatible with `SameSite=None`. (Note: You are still responsible for adding the 'Secure' cookie attribute whenever applicable.) 8 | 9 | ## Background 10 | 11 | With Chrome 80 in February 2020, Chrome will treat cookies that have no declared SameSite value as `SameSite=Lax` cookies. Other browser vendors are expected to follow Google’s lead. (See this [Blog Post](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html)). 12 | 13 | If you manage cross-site cookies, you will need to apply the SameSite=None; Secure setting to those cookies. However, some browsers, including some versions of Chrome, Safari and UC Browser, might handle the None value in unintended ways, requiring developers to code exceptions for those clients. 14 | 15 | `isSameSiteNoneCompatible` utility function detects incompatible user agents based on a [list of known incompatible clients](https://www.chromium.org/updates/same-site/incompatible-clients) and returns `true` if the given user-agent string is compatible with `SameSite=None` cookie attribute. 16 | 17 | For Express.js, `shouldSendSameSiteNone` middleware automatically removes `SameSite=None` from set-cookie response header when the reqesting client is incompatible with `SameSite=None`. 18 | 19 | ## Usage 20 | 21 | #### Function: `isSameSiteNoneCompatible` 22 | 23 | ``` 24 | 25 | import { isSameSiteNoneCompatible } from 'should-send-same-site-none'; 26 | 27 | const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14) ....'; 28 | 29 | if (isSameSiteNoneCompatible(ua)) { 30 | console.log("Yes, the browser is compatible and we can set SameSite=None cookies"); 31 | } 32 | 33 | 34 | ``` 35 | 36 | #### Middleware: `shouldSendSameSiteNone` 37 | 38 | ``` 39 | const express = require('express'); 40 | const { shouldSendSameSiteNone } = require('should-send-same-site-none'); 41 | const app = express(); 42 | 43 | // Apply middleware before routes 44 | app.use(shouldSendSameSiteNone); 45 | 46 | app.get('/', function (req, res) { 47 | // Set cookie with SameSite='None' as needed; 48 | res.cookie("foo", "bar", { sameSite: "none", secure: true }); 49 | res.send('hello world'); 50 | }); 51 | 52 | app.listen(3000); 53 | 54 | 55 | ``` 56 | 57 | ## Running tests 58 | 59 | ``` 60 | npm run test 61 | 62 | 63 | PASS ./index.test.js 64 | 65 | ✓ Test Chrome 50 @ Win10 (true) (4ms) 66 | ✓ Test Chrome 67 @ Win10 (true) (1ms) 67 | ✓ Test Chrome 60 @ IOS (true) 68 | ✓ Test Chrome @ Mac (true) 69 | ✓ Test UC Browser 12.13.2 @ Andriod (true) (1ms) 70 | ✓ Test UC Browser 12.13.4 @ Andriod (true) 71 | ✓ Test Safari @ Mac 13 (true) 72 | ✓ Test Safari @ Mac 15.5 (true) (1ms) 73 | ✓ Test Safari @ ios 13 (true) 74 | ✓ Test Chrome 51 (false) 75 | ✓ Test Chrome 52 @ Win 10 (false) 76 | ✓ Test Chrome 53 @ Win 10 (false) 77 | ✓ Test Chrome 54 (false) 78 | ✓ Test Chrome 55 @ Mac (false) 79 | ✓ Test Chrome 56 @ Linux (false) (1ms) 80 | ✓ Test Chrome 57 @ Win 7 (false) 81 | ✓ Test Chrome 58 @ Android (false) 82 | ✓ Test Chrome 59 @ Win7 (false) 83 | ✓ Test Chrome 60 @ Win10 (false) (1ms) 84 | ✓ Test Chrome 61 @ Win10 (false) 85 | ✓ Test Chrome 62 @ Win10 (false) 86 | ✓ Test Chrome 63 @ Win7 (false) 87 | ✓ Test Chrome 64 @ Win7 (false) (1ms) 88 | ✓ Test Chrome 65 (false) 89 | ✓ Test Chrome 66 @ Win10 (false) 90 | ✓ Test Chrome 66 Webview (false) 91 | ✓ Test UC Browser @ 10.7 (false) 92 | ✓ Test UC Browser 12 @ Android (false) (1ms) 93 | ✓ Test UC Browser 11.5 @ iOS 11 (false) (1ms) 94 | ✓ Test Safari @ Mac 10.14 (false) (1ms) 95 | ✓ Test Embeded @ Mac 10.4 (false) 96 | ✓ Test Safari @ iOS 12 (false) 97 | ✓ Test Chrome @ iOS 12 (false) 98 | ✓ Test Firefox @ iOS 12 (false) 99 | ``` 100 | 101 | ## Note 102 | 103 | The approach for detecting incompatible clients are taken from this [update](https://www.chromium.org/updates/same-site/incompatible-clients). 104 | 105 | The following incompatible clients were accounted for at the time of writing: 106 | 107 | - Versions of Chrome from Chrome 51 to Chrome 66 (inclusive on both ends). These Chrome versions will reject a cookie with `SameSite=None`. This also affects older versions of Chromium-derived browsers, as well as Android WebView. This behavior was correct according to the version of the cookie specification at that time, but with the addition of the new "None" value to the specification, this behavior has been updated in Chrome 67 and newer. (Prior to Chrome 51, the SameSite attribute was ignored entirely and all cookies were treated as if they were `SameSite=None`.) 108 | - Versions of UC Browser on Android prior to version 12.13.2. Older versions will reject a cookie with `SameSite=None`. This behavior was correct according to the version of the cookie specification at that time, but with the addition of the new "None" value to the specification, this behavior has been updated in newer versions of UC Browser. 109 | - Versions of Safari and embedded browsers on MacOS 10.14 and all browsers on iOS 12. These versions will erroneously treat cookies marked with `SameSite=None` as if they were marked `SameSite=Strict`. This bug has been fixed on newer versions of iOS and MacOS. 110 | 111 | Compatibilities of the following clients are unclear: 112 | 113 | 1. Versions of Chrome from Chrome 51 to Chrome 66 on **IOS device**; (Assumed compatible) 114 | 2. Versions of UC Browser on other non-Android platforms (e.g. IOS) prior to version 12.13.2. (Assumed Incompatible) 115 | 116 | Please file an issue if additional incompatible clients are identified. 117 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'should-send-same-site-none' { 2 | import { RequestHandler } from 'express'; 3 | 4 | export const shouldSendSameSiteNone: RequestHandler; 5 | export function isSameSiteNoneCompatible(useragent: string): boolean; 6 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function intToString(intValue) { 2 | return String(intValue); 3 | } 4 | 5 | function stringToInt(strValue) { 6 | return parseInt(strValue, 10) || 0; 7 | } 8 | 9 | // Don’t send `SameSite=None` to known incompatible clients. 10 | function isSameSiteNoneCompatible(useragent) { 11 | return !isSameSiteNoneIncompatible(String(useragent)); 12 | } 13 | 14 | // Classes of browsers known to be incompatible. 15 | function isSameSiteNoneIncompatible(useragent) { 16 | return ( 17 | hasWebKitSameSiteBug(useragent) || 18 | dropsUnrecognizedSameSiteCookies(useragent) 19 | ); 20 | } 21 | 22 | function hasWebKitSameSiteBug(useragent) { 23 | return ( 24 | isIosVersion(12, useragent) || 25 | (isMacosxVersion(10, 14, useragent) && 26 | (isSafari(useragent) || isMacEmbeddedBrowser(useragent))) 27 | ); 28 | } 29 | 30 | function dropsUnrecognizedSameSiteCookies(useragent) { 31 | return ( 32 | (isChromiumBased(useragent) && 33 | isChromiumVersionAtLeast(51, useragent) && 34 | !isChromiumVersionAtLeast(67, useragent)) || 35 | (isUcBrowser(useragent) && !isUcBrowserVersionAtLeast(12, 13, 2, useragent)) 36 | ); 37 | } 38 | 39 | // Regex parsing of User-Agent string. 40 | function regexContains(stringValue, regex) { 41 | var matches = stringValue.match(regex); 42 | return matches !== null; 43 | } 44 | 45 | function extractRegexMatch(stringValue, regex, offsetIndex) { 46 | var matches = stringValue.match(regex); 47 | 48 | if (matches !== null && matches[offsetIndex] !== undefined) { 49 | return matches[offsetIndex]; 50 | } 51 | 52 | return null; 53 | } 54 | 55 | function isIosVersion(major, useragent) { 56 | var regex = /\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\//; 57 | // Extract digits from first capturing group. 58 | return extractRegexMatch(useragent, regex, 1) === intToString(major); 59 | } 60 | 61 | function isMacosxVersion(major, minor, useragent) { 62 | var regex = /\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\//; 63 | // Extract digits from first and second capturing groups. 64 | return ( 65 | extractRegexMatch(useragent, regex, 1) === intToString(major) && 66 | extractRegexMatch(useragent, regex, 2) === intToString(minor) 67 | ); 68 | } 69 | 70 | function isSafari(useragent) { 71 | var safari_regex = /Version\/.* Safari\//; 72 | return useragent.match(safari_regex) !== null && !isChromiumBased(useragent); 73 | } 74 | 75 | function isMacEmbeddedBrowser(useragent) { 76 | var regex = /^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$/; 77 | 78 | return regexContains(useragent, regex); 79 | } 80 | 81 | function isChromiumBased(useragent) { 82 | const regex = /Chrom(e|ium)/; 83 | return regexContains(useragent, regex); 84 | } 85 | 86 | function isChromiumVersionAtLeast(major, useragent) { 87 | var regex = /Chrom[^ \/]+\/(\d+)[\.\d]* /; 88 | // Extract digits from first capturing group. 89 | var version = stringToInt(extractRegexMatch(useragent, regex, 1)); 90 | return version >= major; 91 | } 92 | 93 | function isUcBrowser(useragent) { 94 | var regex = /UCBrowser\//; 95 | return regexContains(useragent, regex); 96 | } 97 | 98 | function isUcBrowserVersionAtLeast(major, minor, build, useragent) { 99 | var regex = /UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* /; 100 | // Extract digits from three capturing groups. 101 | var major_version = stringToInt(extractRegexMatch(useragent, regex, 1)); 102 | var minor_version = stringToInt(extractRegexMatch(useragent, regex, 2)); 103 | var build_version = stringToInt(extractRegexMatch(useragent, regex, 3)); 104 | if (major_version !== major) { 105 | return major_version > major; 106 | } 107 | if (minor_version != minor) { 108 | return minor_version > minor; 109 | } 110 | 111 | return build_version >= build; 112 | } 113 | 114 | var shouldSendSameSiteNone = function(req, res, next) { 115 | var writeHead = res.writeHead; 116 | res.writeHead = function() { 117 | var ua = req.get("user-agent"); 118 | var isCompatible = isSameSiteNoneCompatible(ua); 119 | var cookies = res.get("Set-Cookie"); 120 | var removeSameSiteNone = function(str) { 121 | return str.replace(/;\s*SameSite\s*=\s*None\s*(?=;|$)/ig, ""); 122 | }; 123 | if (!isCompatible && cookies) { 124 | if (Array.isArray(cookies)) { 125 | cookies = cookies.map(removeSameSiteNone); 126 | } else { 127 | cookies = removeSameSiteNone(cookies); 128 | } 129 | res.set("Set-Cookie", cookies); 130 | } 131 | 132 | writeHead.apply(this, arguments); 133 | }; 134 | next(); 135 | }; 136 | 137 | module.exports = { 138 | shouldSendSameSiteNone: shouldSendSameSiteNone, 139 | isSameSiteNoneCompatible: isSameSiteNoneCompatible 140 | }; 141 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const supertest = require("supertest"); 3 | var http = require("http"); 4 | const { 5 | isSameSiteNoneCompatible, 6 | shouldSendSameSiteNone 7 | } = require("./index"); 8 | 9 | const negativeTestCases = { 10 | "Chrome 51": 11 | "Mozilla/5.0 doogiePIM/1.0.4.2 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.84 Safari/537.36", 12 | "Chrome 52 @ Win 10": 13 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36", 14 | "Chrome 53 @ Win 10": 15 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2883.87 Safari/537.36", 16 | "Chrome 54": "Mozilla/5.0 Chrome/54.0.2840.99 Safari/537.36", 17 | "Chrome 55 @ Mac": 18 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36", 19 | "Chrome 56 @ Linux": 20 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36", 21 | "Chrome 57 @ Win 7": 22 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36", 23 | "Chrome 58 @ Android": 24 | "Mozilla/5.0 (Linux; Android 8.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Klar/1.0 Chrome/58.0.3029.121 Mobile Safari/537.36", 25 | "Chrome 59 @ Win7": 26 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", 27 | "Chrome 60 @ Win10": 28 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36", 29 | "Chrome 61 @ Win10": 30 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36", 31 | "Chrome 62 @ Win10": 32 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3165.0 Safari/537.36", 33 | "Chrome 63 @ Win7": 34 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3213.3 Safari/537.36", 35 | "Chrome 64 @ Win7": 36 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36", 37 | "Chrome 65": 38 | "Mozilla/5.0 (Win) AppleWebKit/1000.0 (KHTML, like Gecko) Chrome/65.663 Safari/1000.01", 39 | "Chrome 66 @ Win10": 40 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3334.0 Safari/537.36", 41 | "Chrome 66 Webview": 42 | "Mozilla/5.0 (Linux; Android 4.4.4; One Build/KTU84L.H4) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.0.0 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/28.0.0.20.16;]", 43 | "UC Browser @ 10.7": 44 | "UCWEB/2.0 (MIDP-2.0; U; Adr 4.0.4; en-US; ZTE_U795) U2/1.0.0 UCBrowser/10.7.6.805 U2/1.0.0 Mobile", 45 | "UC Browser 12 @ Android": 46 | "Mozilla/5.0 (Linux; U; Android 7.1.1; en-US; Lenovo K8 Note Build/NMB26.54-74) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.0.0.1088 Mobile Safari/537.36", 47 | "UC Browser 11.5 @ iOS 11": 48 | "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X; zh-CN) AppleWebKit/537.51.1 (KHTML, like Gecko) Mobile/15A5304i UCBrowser/11.5.7.986 Mobile AliApp(TUnionSDK/0.1.15)", 49 | "Safari @ Mac 10.14": 50 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15", 51 | "Embeded @ Mac 10.4": 52 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14) AppleWebKit/537.36 (KHTML, like Gecko)", 53 | "Safari @ iOS 12": 54 | "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/ 604.1.21 (KHTML, like Gecko) Version/ 12.0 Mobile/17A6278a Safari/602.1.26", 55 | "Chrome @ iOS 12": 56 | "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/70.0.3538.75 Mobile/15E148 Safari/605.1", 57 | "Firefox @ iOS 12": 58 | "Mozilla/5.0 (iPad; CPU OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/13.2b11866 Mobile/16A366 Safari/605.1.15" 59 | }; 60 | 61 | const positiveTestCases = { 62 | "Chrome 50 @ Win10": 63 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", 64 | "Chrome 67 @ Win10": 65 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.2526.73 Safari/537.36", 66 | "Chrome 60 @ IOS": 67 | "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) CriOS/60.0.3112.72 Mobile/15A5327g Safari/602.1", 68 | "Chrome @ Mac": 69 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.29 Safari/537.36", 70 | "UC Browser 12.13.2 @ Andriod": 71 | "Mozilla/5.0 (Linux; U; Android 8.0.0; en-US; Pixel XL Build/OPR3.170623.007) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/12.13.2.1005 U3/0.8.0 Mobile Safari/534.30", 72 | "UC Browser 12.13.4 @ Andriod": 73 | "Mozilla/5.0 (Linux; U; Android 8.0.0; en-US; Pixel XL Build/OPR3.170623.007) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/12.13.4.1005 U3/0.8.0 Mobile Safari/534.30", 74 | "Safari @ Mac 13": 75 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15", 76 | "Safari @ Mac 15.5": 77 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/601.1.39 (KHTML, like Gecko) Version/10.1.2 Safari/601.1.39", 78 | "Safari @ ios 13": 79 | "Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/66.6 Mobile/14A5297c Safari/602.1", 80 | "Null": null, 81 | }; 82 | 83 | describe("isSameSiteNoneCompatible", () => { 84 | for (const i in positiveTestCases) { 85 | if (positiveTestCases.hasOwnProperty(i)) { 86 | it(`Test ${i} (true)`, () => { 87 | expect(isSameSiteNoneCompatible(positiveTestCases[i])).toBe(true); 88 | }); 89 | } 90 | } 91 | 92 | for (const i in negativeTestCases) { 93 | if (negativeTestCases.hasOwnProperty(i)) { 94 | it(`Test ${i} (false)`, () => { 95 | expect(isSameSiteNoneCompatible(negativeTestCases[i])).toBe(false); 96 | }); 97 | } 98 | } 99 | }); 100 | 101 | describe("shouldSendSameSiteNone with mutiple cookies", () => { 102 | let app, server; 103 | beforeEach(done => { 104 | app = new express(); 105 | app.use(shouldSendSameSiteNone); 106 | app.get("/", (req, res, next) => { 107 | res.set("Set-Cookie", "a=b;samesite = none ;secure"); 108 | res.cookie("foo", "bar", { sameSite: "none" }); 109 | res.cookie("koo", "mar", { sameSite: "none" }); 110 | res.send("ok"); 111 | }); 112 | server = http.createServer(app); 113 | server.listen(done); 114 | }); 115 | 116 | afterEach(done => { 117 | server.close(done); 118 | }); 119 | 120 | for (const i in negativeTestCases) { 121 | if (negativeTestCases.hasOwnProperty(i)) { 122 | it(`Remove SameSite=None attributes in ${i}`, async done => { 123 | const response = await supertest(app) 124 | .get("/") 125 | .set("User-Agent", negativeTestCases[i]); 126 | const expected = ["a=b;secure", "foo=bar; Path=/", "koo=mar; Path=/"]; 127 | expect(response.header["set-cookie"]).toEqual(expected); 128 | expect(response.text).toEqual("ok"); 129 | done(); 130 | }); 131 | } 132 | } 133 | 134 | for (const i in positiveTestCases) { 135 | if (positiveTestCases.hasOwnProperty(i)) { 136 | it(`Keep SameSite=None attributes in ${i}`, async done => { 137 | const response = await supertest(app) 138 | .get("/") 139 | .set("User-Agent", positiveTestCases[i]); 140 | const expected = [ 141 | "a=b;samesite = none ;secure", 142 | "foo=bar; Path=/; SameSite=None", 143 | "koo=mar; Path=/; SameSite=None" 144 | ]; 145 | expect(response.header["set-cookie"]).toEqual(expected); 146 | expect(response.text).toEqual("ok"); 147 | done(); 148 | }); 149 | } 150 | } 151 | }); 152 | 153 | describe("shouldSendSameSiteNone with single cookies", () => { 154 | let app, server; 155 | beforeEach(done => { 156 | app = new express(); 157 | app.use(shouldSendSameSiteNone); 158 | app.get("/", (req, res, next) => { 159 | res.cookie("foo", "bar", { sameSite: "none" }); 160 | res.send("ok"); 161 | }); 162 | server = http.createServer(app); 163 | server.listen(done); 164 | }); 165 | 166 | afterEach(done => { 167 | server.close(done); 168 | }); 169 | 170 | for (const i in negativeTestCases) { 171 | if (negativeTestCases.hasOwnProperty(i)) { 172 | it(`Remove SameSite=None attributes in ${i}`, async done => { 173 | const response = await supertest(app) 174 | .get("/") 175 | .set("User-Agent", negativeTestCases[i]); 176 | const expected = ["foo=bar; Path=/"]; 177 | expect(response.header["set-cookie"]).toEqual(expected); 178 | expect(response.text).toEqual("ok"); 179 | done(); 180 | }); 181 | } 182 | } 183 | 184 | for (const i in positiveTestCases) { 185 | if (positiveTestCases.hasOwnProperty(i)) { 186 | it(`Keep SameSite=None attributes in ${i}`, async done => { 187 | const response = await supertest(app) 188 | .get("/") 189 | .set("User-Agent", positiveTestCases[i]); 190 | const expected = ["foo=bar; Path=/; SameSite=None"]; 191 | expect(response.header["set-cookie"]).toEqual(expected); 192 | expect(response.text).toEqual("ok"); 193 | done(); 194 | }); 195 | } 196 | } 197 | }); 198 | 199 | describe("shouldSendSameSiteNone with no cookies", () => { 200 | let app, server; 201 | beforeEach(done => { 202 | app = new express(); 203 | app.use(shouldSendSameSiteNone); 204 | app.get("/", (req, res, next) => { 205 | res.send("ok"); 206 | }); 207 | server = http.createServer(app); 208 | server.listen(done); 209 | }); 210 | 211 | afterEach(done => { 212 | server.close(done); 213 | }); 214 | 215 | for (const i in negativeTestCases) { 216 | if (negativeTestCases.hasOwnProperty(i)) { 217 | it(`Remove SameSite=None attributes in ${i}`, async done => { 218 | const response = await supertest(app) 219 | .get("/") 220 | .set("User-Agent", negativeTestCases[i]); 221 | expect(response.header["set-cookie"]).toEqual(undefined); 222 | expect(response.text).toEqual("ok"); 223 | done(); 224 | }); 225 | } 226 | } 227 | 228 | for (const i in positiveTestCases) { 229 | if (positiveTestCases.hasOwnProperty(i)) { 230 | it(`Keep SameSite=None attributes in ${i}`, async done => { 231 | const response = await supertest(app) 232 | .get("/") 233 | .set("User-Agent", positiveTestCases[i]); 234 | expect(response.header["set-cookie"]).toEqual(undefined); 235 | expect(response.text).toEqual("ok"); 236 | done(); 237 | }); 238 | } 239 | } 240 | }); 241 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "should-send-same-site-none", 3 | "version": "2.0.5", 4 | "description": "A simple utility to detect incompatible user agents for `SameSite=None` cookie attribute", 5 | "keywords": [ 6 | "Express", 7 | "samesite", 8 | "cookie", 9 | "middleware" 10 | ], 11 | "main": "index.js", 12 | "types": "index.d.ts", 13 | "scripts": { 14 | "test": "jest --detectOpenHandles" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/linsight/should-send-same-site-none.git" 19 | }, 20 | "author": "David Lin", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/express": "^4.17.1", 24 | "express": "^4.17.1", 25 | "jest": "^25.5.1", 26 | "supertest": "^4.0.2" 27 | } 28 | } 29 | --------------------------------------------------------------------------------