send a session cookie after successful basic auth
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 652fb39..5ffbddd 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -30,7 +30,6 @@
 
 -export([authenticate/2, verify_totp/2]).
 -export([ensure_cookie_auth_secret/0, make_cookie_time/0]).
--export([cookie_auth_cookie/4, cookie_scheme/1]).
 -export([maybe_value/3]).
 
 -export([jwt_authentication_handler/1]).
@@ -114,12 +113,23 @@
                     Password = ?l2b(Pass),
                     case authenticate(Password, UserProps) of
                         true ->
-                            Req#httpd{
+                            Req0 = Req#httpd{
                                 user_ctx = #user_ctx{
                                     name = UserName,
                                     roles = couch_util:get_value(<<"roles">>, UserProps, [])
                                 }
-                            };
+                            },
+                            case chttpd_util:get_chttpd_auth_config("secret") of
+                                undefined ->
+                                    Req0;
+                                SecretStr ->
+                                    Secret = ?l2b(SecretStr),
+                                    UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<"">>),
+                                    FullSecret = <<Secret/binary, UserSalt/binary>>,
+                                    Req0#httpd{
+                                        auth = {FullSecret, true, true}
+                                    }
+                            end;
                         false ->
                             authentication_warning(Req, UserName),
                             throw({unauthorized, <<"Name or password is incorrect.">>})
@@ -331,11 +341,15 @@
         [] ->
             Req;
         Cookie ->
-            [User, TimeStr, HashStr] =
+            % TimestampStr is expanded to be a list of options, separated
+            % by commas. The new second option is 'MustMatchBasic', a 0 or
+            % 1 to indicate if the basic auth username must match the cookie
+            % if present.
+            [User, OptionsStr, HashStr] =
                 try
                     AuthSession = couch_util:decodeBase64Url(Cookie),
                     [_A, _B, _Cs] = re:split(
-                        ?b2l(AuthSession),
+                        AuthSession,
                         ":",
                         [{return, list}, {parts, 3}]
                     )
@@ -344,6 +358,20 @@
                         Reason = <<"Malformed AuthSession cookie. Please clear your cookies.">>,
                         throw({bad_request, Reason})
                 end,
+            [TimeStr, MustMatchBasic] =
+                case re:split(OptionsStr, ",", [{return, list}]) of
+                    [T, M] ->
+                        [T, M];
+                    [T] ->
+                        [T, "0"]
+                end,
+            BasicAuthUser =
+                case basic_name_pw(Req) of
+                    {U, _P} ->
+                        U;
+                    nil ->
+                        nil
+                end,
             % Verify expiry and hash
             CurrentTime = make_cookie_time(),
             HashAlgorithms = couch_util:get_config_hash_algorithms(),
@@ -351,6 +379,9 @@
                 undefined ->
                     couch_log:debug("cookie auth secret is not set", []),
                     Req;
+                _ when MustMatchBasic == "1", BasicAuthUser /= nil, User /= BasicAuthUser ->
+                    % ignoring pre-emptive cookie
+                    Req;
                 SecretStr ->
                     Secret = ?l2b(SecretStr),
                     case AuthModule:get_user_creds(Req, User) of
@@ -361,7 +392,13 @@
                             FullSecret = <<Secret/binary, UserSalt/binary>>,
                             Hash = ?l2b(HashStr),
                             VerifyHash = fun(HashAlg) ->
-                                Hmac = couch_util:hmac(HashAlg, FullSecret, User ++ ":" ++ TimeStr),
+                                Hmac = couch_util:hmac(
+                                    HashAlg,
+                                    FullSecret,
+                                    lists:join(":", [
+                                        User, lists:join(",", [TimeStr, MustMatchBasic])
+                                    ])
+                                ),
                                 couch_passwords:verify(Hmac, Hash)
                             end,
                             Timeout = chttpd_util:get_chttpd_auth_config_integer(
@@ -384,7 +421,9 @@
                                                         <<"roles">>, UserProps, []
                                                     )
                                                 },
-                                                auth = {FullSecret, TimeLeft < Timeout * 0.9}
+                                                auth =
+                                                    {FullSecret, TimeLeft < Timeout * 0.9,
+                                                        MustMatchBasic == "1"}
                                             };
                                         _Else ->
                                             Req
@@ -398,7 +437,11 @@
 
 cookie_auth_header(#httpd{user_ctx = #user_ctx{name = null}}, _Headers) ->
     [];
-cookie_auth_header(#httpd{user_ctx = #user_ctx{name = User}, auth = {Secret, true}} = Req, Headers) ->
+cookie_auth_header(
+    #httpd{user_ctx = #user_ctx{name = User}, auth = {Secret, _SendCookie = true, MustMatchBasic}} =
+        Req,
+    Headers
+) ->
     % Note: we only set the AuthSession cookie if:
     %  * a valid AuthSession cookie has been received
     %  * we are outside a 10% timeout window
@@ -412,20 +455,28 @@
     if
         AuthSession == undefined ->
             TimeStamp = make_cookie_time(),
-            [cookie_auth_cookie(Req, ?b2l(User), Secret, TimeStamp)];
+            [cookie_auth_cookie(Req, User, Secret, TimeStamp, MustMatchBasic)];
         true ->
             []
     end;
 cookie_auth_header(_Req, _Headers) ->
     [].
 
-cookie_auth_cookie(Req, User, Secret, TimeStamp) ->
-    SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
+cookie_auth_cookie(Req, User, Secret, TimeStamp, MustMatchBasic) ->
+    MustMatchBasicStr =
+        case MustMatchBasic of
+            true -> "1";
+            false -> "0"
+        end,
+    SessionData = lists:join(":", [
+        User,
+        lists:join(",", [erlang:integer_to_list(TimeStamp, 16), MustMatchBasicStr])
+    ]),
     [HashAlgorithm | _] = couch_util:get_config_hash_algorithms(),
     Hash = couch_util:hmac(HashAlgorithm, Secret, SessionData),
     mochiweb_cookies:cookie(
         "AuthSession",
-        couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
+        couch_util:encodeBase64Url(lists:join(":", [SessionData, Hash])),
         cookie_attributes(Req)
     ).
 
@@ -493,7 +544,7 @@
             UserSalt = couch_util:get_value(<<"salt">>, UserProps),
             CurrentTime = make_cookie_time(),
             Cookie = cookie_auth_cookie(
-                Req, ?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime
+                Req, UserName, <<Secret/binary, UserSalt/binary>>, CurrentTime, false
             ),
             % TODO document the "next" feature in Futon
             {Code, Headers} =
diff --git a/src/docs/src/api/server/authn.rst b/src/docs/src/api/server/authn.rst
index 7744d12..789eec2 100644
--- a/src/docs/src/api/server/authn.rst
+++ b/src/docs/src/api/server/authn.rst
@@ -27,6 +27,13 @@
 Basic Authentication
 ====================
 
+.. versionchanged:: 3.4 In order to aid transition to stronger password hashing
+    without causing a performance penalty, CouchDB will send a Set-Cookie header
+    when a request authenticates successfully with Basic authentication. All browsers
+    and many http libraries will automatically send this cookie on subsequent requests.
+    The cost of verifying the cookie is significantly less than PBKDF2 with a high
+    iteration count, for example.
+
 `Basic authentication`_ (:rfc:`2617`) is a quick and simple way to authenticate
 with CouchDB. The main drawback is the need to send user credentials with each
 request which may be insecure and could hurt operation performance (since
diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex
index d9751c4..efdfe31 100644
--- a/test/elixir/lib/couch.ex
+++ b/test/elixir/lib/couch.ex
@@ -138,18 +138,21 @@
   end
 
   def set_auth_options(options) do
-    if Keyword.get(options, :cookie) == nil do
-      headers = Keyword.get(options, :headers, [])
-      if headers[:basic_auth] != nil or headers[:authorization] != nil
-         or List.keymember?(headers, :"X-Auth-CouchDB-UserName", 0) do
+    cond do
+      Keyword.get(options, :no_auth, false) ->
         options
-      else
-        username = System.get_env("EX_USERNAME") || "adm"
-        password = System.get_env("EX_PASSWORD") || "pass"
-        Keyword.put(options, :basic_auth, {username, password})
-      end
-    else
-      options
+      Keyword.get(options, :cookie) == nil ->
+        headers = Keyword.get(options, :headers, [])
+        if headers[:basic_auth] != nil or headers[:authorization] != nil
+          or List.keymember?(headers, :"X-Auth-CouchDB-UserName", 0) do
+          options
+        else
+          username = System.get_env("EX_USERNAME") || "adm"
+          password = System.get_env("EX_PASSWORD") || "pass"
+          Keyword.put(options, :basic_auth, {username, password})
+        end
+      true ->
+        options
     end
   end
 
diff --git a/test/elixir/test/cookie_auth_test.exs b/test/elixir/test/cookie_auth_test.exs
index 6e42963..000b1d6 100644
--- a/test/elixir/test/cookie_auth_test.exs
+++ b/test/elixir/test/cookie_auth_test.exs
@@ -392,4 +392,46 @@
     # log in one last time so run_on_modified_server can clean up the admin account
     login("jan", "apple")
   end
+
+  test "basic+cookie auth interaction" do
+    # performing a successful basic authentication will create a session cookie
+    resp = Couch.get(
+      "/_all_dbs",
+      no_auth: true,
+      headers: [authorization: "Basic #{:base64.encode("jan:apple")}"])
+    assert resp.status_code == 200
+
+    # extract cookie value
+    cookie = resp.headers[:"set-cookie"]
+    [token | _] = String.split(cookie, ";")
+
+    # Cookie is usable on its own
+    resp = Couch.get(
+      "/_session",
+      no_auth: true,
+      headers: [cookie: token])
+    assert resp.status_code == 200
+    assert resp.body["userCtx"]["name"]  == "jan"
+    assert resp.body["info"]["authenticated"] == "cookie"
+
+    # Cookie is usable with basic auth if usernames match
+    resp = Couch.get(
+      "/_session",
+      no_auth: true,
+      headers: [
+        authorization: "Basic #{:base64.encode("jan:apple")}",
+        cookie: token])
+    assert resp.status_code == 200
+    assert resp.body["userCtx"]["name"] == "jan"
+    assert resp.body["info"]["authenticated"] == "cookie"
+
+    # Cookie is not usable with basic auth if usernames don't match
+    resp = Couch.get(
+      "/_session",
+      no_auth: true,
+      headers: [
+        authorization: "Basic #{:base64.encode("notjan:banana")}",
+        cookie: token])
+    assert resp.status_code == 401
+  end
 end