Add SameSite support to auth cookie

Adds a new configuration field, `couch_httpd_auth.same_site` which
sets the `SameSite` attribute of the CouchDB auth cookie. If no
value is set (the default), no `SameSite` attribute is added.

Refs #2221
diff --git a/.gitignore b/.gitignore
index 5eec70f..3fa860c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
 *~
 .venv
 .DS_Store
+.vscode
 .rebar/
 .eunit/
 cover/
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 5fc8e07..a301987 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -245,6 +245,8 @@
 ; secret = 
 ; users_db_public = false
 ; cookie_domain = example.com
+; Set the SameSite cookie property for the auth cookie. If empty, the SameSite property is not set.
+; same_site =
 
 ; CSP (Content Security Policy) Support for _utils
 [csp]
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 515ce61..96de5bf 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -273,7 +273,7 @@
     Hash = crypto:hmac(sha, Secret, SessionData),
     mochiweb_cookies:cookie("AuthSession",
         couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
-        [{path, "/"}] ++ cookie_scheme(Req) ++ max_age() ++ cookie_domain()).
+        [{path, "/"}] ++ cookie_scheme(Req) ++ max_age() ++ cookie_domain() ++ same_site()).
 
 ensure_cookie_auth_secret() ->
     case config:get("couch_httpd_auth", "secret", undefined) of
@@ -457,6 +457,20 @@
         _ -> [{domain, Domain}]
     end.
 
+
+same_site() ->
+    SameSite = config:get("couch_httpd_auth", "same_site", ""),
+    case string:to_lower(SameSite) of
+        "" -> [];
+        "none" -> [{same_site, none}];
+        "lax" -> [{same_site, lax}];
+        "strict" -> [{same_site, strict}];
+        _ ->
+            couch_log:error("invalid config value couch_httpd_auth.same_site: ~p ",[SameSite]),
+            []
+    end.
+
+
 reject_if_totp(User) ->
     case get_totp_config(User) of
         undefined ->
diff --git a/src/couch/test/exunit/same_site_cookie_tests.exs b/src/couch/test/exunit/same_site_cookie_tests.exs
new file mode 100644
index 0000000..bad32ad
--- /dev/null
+++ b/src/couch/test/exunit/same_site_cookie_tests.exs
@@ -0,0 +1,44 @@
+defmodule SameSiteCookieTests do
+  use CouchTestCase
+
+  @moduletag :authentication
+
+  def get_cookie(user, pass) do
+    resp = Couch.post("/_session", body: %{:username => user, :password => pass})
+
+    true = resp.body["ok"]
+    resp.headers[:"set-cookie"]
+  end
+
+  @tag config: [{"admins", "jan", "apple"}, {"couch_httpd_auth", "same_site", "None"}]
+  test "Set same_site None" do
+    cookie = get_cookie("jan", "apple")
+    assert cookie =~ "; SameSite=None"
+  end
+
+  @tag config: [{"admins", "jan", "apple"}, {"couch_httpd_auth", "same_site", ""}]
+  test "same_site not set" do
+    cookie = get_cookie("jan", "apple")
+    assert cookie
+    refute cookie =~ "; SameSite="
+  end
+
+  @tag config: [{"admins", "jan", "apple"}, {"couch_httpd_auth", "same_site", "Strict"}]
+  test "Set same_site Strict" do
+    cookie = get_cookie("jan", "apple")
+    assert cookie =~ "; SameSite=Strict"
+  end
+
+  @tag config: [{"admins", "jan", "apple"}, {"couch_httpd_auth", "same_site", "Lax"}]
+  test "Set same_site Lax" do
+    cookie = get_cookie("jan", "apple")
+    assert cookie =~ "; SameSite=Lax"
+  end
+
+  @tag config: [{"admins", "jan", "apple"}, {"couch_httpd_auth", "same_site", "Invalid"}]
+  test "Set same_site invalid" do
+    cookie = get_cookie("jan", "apple")
+    assert cookie
+    refute cookie =~ "; SameSite="
+  end
+end