You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2021/06/01 21:07:12 UTC

[couchdb] branch 3.x updated: Improve basic auth credentials handling in replicator

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

vatamane pushed a commit to branch 3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/3.x by this push:
     new ecd266b  Improve basic auth credentials handling in replicator
ecd266b is described below

commit ecd266b0e87f44e1080cabdb4c28e4758f5a4406
Author: Nick Vatamaniuc <va...@gmail.com>
AuthorDate: Fri May 28 18:35:44 2021 -0400

    Improve basic auth credentials handling in replicator
    
    Previously, there were two ways to pass in basic auth credentials for
    endpoints -- using URL's userinfo part and encoding the them in an
    `"Authorization": "basic ..."` header. Neither one is ideal for these reasons:
    
     * Passwords in userinfo doesn't allow using ":", "@" and other characters.
       However, even after switching to always unquoting them like we did recently
       [1], would break authentication for usernames or passwords previously
       containing "+" or "%HH" patterns, as "+" might now be decoded to a " ".
    
     * Base64 encoded headers need an extra step to encode them. Also, quite often
       these encoded headers are confused as being "encrypted" and shared in a
       clear channel.
    
    To improve this, revert the recent commit to unquote URL userinfo parts to
    restore backwards compatibility, and introduce a way to pass in basic auth
    credentials in the "auth" object. The "auth" object was already added a while
    back to allow authentication plugins to store their credentials in it. The
    format is:
    
    ```
       "source": {
           "url": "https://host/db",
           "auth": {
               "basic": {
                   "username":"myuser",
                   "password":"mypassword"
               }
           }
       }
    ```
    
    {"auth" : "basic" : {...}} object is checked first, and if credentials are
    provided, they will be used. If they are not then userinfo and basic auth
    header will be parsed.
    
    Internally, there was a good amount duplication related to parsing credentials
    from userinfo and headers in replication ID generation logic and in the auth
    session plugin. As a cleanup, consolidate that logic in the
    `couch_replicator_utils` module.
    
    [1] https://github.com/apache/couchdb/commit/f672b911db19981a81d7fc6ce8ac33b150234fd7
---
 src/couch_replicator/src/couch_replicator.erl      |  10 +-
 .../src/couch_replicator_auth_session.erl          | 204 ++--------------
 src/couch_replicator/src/couch_replicator_docs.erl |   5 +-
 .../src/couch_replicator_httpc.erl                 |   8 +-
 src/couch_replicator/src/couch_replicator_ids.erl  |  76 +++---
 .../src/couch_replicator_scheduler_job.erl         |   4 +-
 .../src/couch_replicator_utils.erl                 | 271 ++++++++++++++++++++-
 7 files changed, 353 insertions(+), 225 deletions(-)

diff --git a/src/couch_replicator/src/couch_replicator.erl b/src/couch_replicator/src/couch_replicator.erl
index b169dcc..4136281 100644
--- a/src/couch_replicator/src/couch_replicator.erl
+++ b/src/couch_replicator/src/couch_replicator.erl
@@ -373,13 +373,13 @@ t_strip_local_db_creds() ->
 t_strip_http_basic_creds() ->
     ?_test(begin
         Url1 = <<"http://adm:pass@host/db">>,
-        ?assertEqual(<<"http://adm:*****@host/db/">>, strip_url_creds(Url1)),
+        ?assertEqual(<<"http://host/db/">>, strip_url_creds(Url1)),
         Url2 = <<"https://adm:pass@host/db">>,
-        ?assertEqual(<<"https://adm:*****@host/db/">>, strip_url_creds(Url2)),
+        ?assertEqual(<<"https://host/db/">>, strip_url_creds(Url2)),
         Url3 = <<"http://adm:pass@host:80/db">>,
-        ?assertEqual(<<"http://adm:*****@host:80/db/">>, strip_url_creds(Url3)),
+        ?assertEqual(<<"http://host:80/db/">>, strip_url_creds(Url3)),
         Url4 = <<"http://adm:pass@host/db?a=b&c=d">>,
-        ?assertEqual(<<"http://adm:*****@host/db?a=b&c=d">>,
+        ?assertEqual(<<"http://host/db?a=b&c=d">>,
             strip_url_creds(Url4))
     end).
 
@@ -387,7 +387,7 @@ t_strip_http_basic_creds() ->
 t_strip_http_props_creds() ->
     ?_test(begin
         Props1 = {[{<<"url">>, <<"http://adm:pass@host/db">>}]},
-        ?assertEqual(<<"http://adm:*****@host/db/">>, strip_url_creds(Props1)),
+        ?assertEqual(<<"http://host/db/">>, strip_url_creds(Props1)),
         Props2 = {[ {<<"url">>, <<"http://host/db">>},
             {<<"headers">>, {[{<<"Authorization">>, <<"Basic pa55">>}]}}
         ]},
diff --git a/src/couch_replicator/src/couch_replicator_auth_session.erl b/src/couch_replicator/src/couch_replicator_auth_session.erl
index 8daa7bc..4f70cd6 100644
--- a/src/couch_replicator/src/couch_replicator_auth_session.erl
+++ b/src/couch_replicator/src/couch_replicator_auth_session.erl
@@ -78,7 +78,6 @@
 
 -type headers() :: [{string(), string()}].
 -type code() :: non_neg_integer().
--type creds() :: {string() | undefined, string() | undefined}.
 -type time_sec() :: non_neg_integer().
 -type age() :: time_sec() | undefined.
 
@@ -238,59 +237,15 @@ init_state(#httpdb{} = HttpDb) ->
 
 -spec extract_creds(#httpdb{}) ->
     {ok, string(), string(), #httpdb{}} | {error, term()}.
-extract_creds(#httpdb{url = Url, headers = Headers} = HttpDb) ->
-    {{HeadersUser, HeadersPass}, HeadersNoCreds} =
-            couch_replicator_utils:remove_basic_auth_from_headers(Headers),
-    case extract_creds_from_url(Url) of
-        {ok, UrlUser, UrlPass, UrlNoCreds} ->
-            case pick_creds({UrlUser, UrlPass}, {HeadersUser, HeadersPass}) of
-                {ok, User, Pass} ->
-                    HttpDb1 = HttpDb#httpdb{
-                        url = UrlNoCreds,
-                        headers = HeadersNoCreds
-                    },
-                    {ok, User, Pass, HttpDb1};
-                {error, Error} ->
-                    {error, Error}
-            end;
-        {error, Error} ->
-            {error, Error}
-    end.
-
-
-% Credentials could be specified in the url and/or in the headers.
-%  * If no credentials specified return error.
-%  * If specified in url but not in headers, pick url creds.
-%  * Otherwise pick headers creds.
-%
--spec pick_creds(creds(), creds()) ->
-    {ok, string(), string()} | {error, missing_credentials}.
-pick_creds({undefined, _}, {undefined, _}) ->
-    {error, missing_credentials};
-pick_creds({UrlUser, UrlPass}, {undefined, _}) ->
-    {ok, UrlUser, UrlPass};
-pick_creds({_, _}, {HeadersUser, HeadersPass}) ->
-    {ok, HeadersUser, HeadersPass}.
-
-
--spec extract_creds_from_url(string()) ->
-    {ok, string() | undefined, string() | undefined, string()} |
-    {error, term()}.
-extract_creds_from_url(Url) ->
-    case ibrowse_lib:parse_url(Url) of
-        {error, Error} ->
-            {error, Error};
-        #url{username = undefined, password = undefined} ->
-            {ok, undefined, undefined, Url};
-        #url{protocol = Proto, username = User, password = Pass} ->
-            % Excise user and pass parts from the url. Try to keep the host,
-            % port and path as they were in the original.
-            Prefix = lists:concat([Proto, "://", User, ":", Pass, "@"]),
-            Suffix = lists:sublist(Url, length(Prefix) + 1, length(Url) + 1),
-            NoCreds = lists:concat([Proto, "://", Suffix]),
-            User1 = chttpd:unquote(User),
-            Pass1 = chttpd:unquote(Pass),
-            {ok, User1, Pass1, NoCreds}
+extract_creds(#httpdb{} = HttpDb) ->
+    case couch_replicator_utils:get_basic_auth_creds(HttpDb) of
+        {undefined, undefined} ->
+            % Return error. Session plugin should ignore this replication
+            % endpoint as there are no valid creds which can be used
+            {error, missing_credentials};
+        {User, Pass} when is_list(User), is_list(Pass) ->
+            HttpDb1 = couch_replicator_utils:remove_basic_auth_creds(HttpDb),
+            {ok, User, Pass, HttpDb1}
     end.
 
 
@@ -569,116 +524,15 @@ get_session_url_test_() ->
     ]].
 
 
-extract_creds_success_test_() ->
-    DefaultHeaders = (#httpdb{})#httpdb.headers,
-    [?_assertEqual({ok, User, Pass, HttpDb2}, extract_creds(HttpDb1)) ||
-        {HttpDb1, {User, Pass, HttpDb2}} <- [
-        {
-            #httpdb{url = "http://u:p@x.y/db"},
-            {"u", "p", #httpdb{url = "http://x.y/db"}}
-        },
-        {
-            #httpdb{url = "http://u%40:p%40@x.y/db"},
-            {"u@", "p@", #httpdb{url = "http://x.y/db"}}
-        },
-        {
-            #httpdb{url = "http://u%40u:p%40p@x.y/db"},
-            {"u@u", "p@p", #httpdb{url = "http://x.y/db"}}
-        },
-        {
-            #httpdb{url = "http://u%40%401:p%40%401@x.y/db"},
-            {"u@@1", "p@@1", #httpdb{url = "http://x.y/db"}}
-        },
-        {
-            #httpdb{url = "http://u%40%2540:p%40%2540@x.y/db"},
-            {"u@%40", "p@%40", #httpdb{url = "http://x.y/db"}}
-        },
-        {
-            #httpdb{url = "http://u:p@h:80/db"},
-            {"u", "p", #httpdb{url = "http://h:80/db"}}
-        },
-        {
-            #httpdb{url = "http://u%3A:p%3A@h:80/db"},
-            {"u:", "p:", #httpdb{url = "http://h:80/db"}}
-        },
-        {
-            #httpdb{url = "https://u:p@h/db"},
-            {"u", "p", #httpdb{url = "https://h/db"}}
-        },
-        {
-            #httpdb{url = "https://u%2F:p%2F@h/db"},
-            {"u/", "p/", #httpdb{url = "https://h/db"}}
-        },
-        {
-            #httpdb{url = "http://u:p@127.0.0.1:5984/db"},
-            {"u", "p", #httpdb{url = "http://127.0.0.1:5984/db"}}
-        },
-        {
-            #httpdb{url = "http://u:p@[2001:db8:a1b:12f9::1]/db"},
-            {"u", "p", #httpdb{url = "http://[2001:db8:a1b:12f9::1]/db"}}
-        },
-        {
-            #httpdb{url = "http://u:p@[2001:db8:a1b:12f9::1]:81/db"},
-            {"u", "p", #httpdb{url = "http://[2001:db8:a1b:12f9::1]:81/db"}}
-        },
-        {
-            #httpdb{url = "http://u:p%3A%2F%5B%5D%40@[2001:db8:a1b:12f9::1]:81/db"},
-            {"u", "p:/[]@", #httpdb{url = "http://[2001:db8:a1b:12f9::1]:81/db"}}
-        },
-        {
-            #httpdb{url = "http://u:p@x.y/db/other?query=Z&query=w"},
-            {"u", "p", #httpdb{url = "http://x.y/db/other?query=Z&query=w"}}
-        },
-        {
-            #httpdb{url = "http://u:p%3F@x.y/db/other?query=Z&query=w"},
-            {"u", "p?", #httpdb{url = "http://x.y/db/other?query=Z&query=w"}}
-        },
-        {
-            #httpdb{
-                url = "http://h/db",
-                headers = DefaultHeaders ++ [
-                    {"Authorization", "Basic " ++ b64creds("u", "p")}
-                ]
-            },
-            {"u", "p", #httpdb{url = "http://h/db"}}
-        },
-        {
-            #httpdb{
-                url = "http://h/db",
-                headers = DefaultHeaders ++ [
-                    {"Authorization", "Basic " ++ b64creds("u", "p@")}
-                ]
-            },
-            {"u", "p@", #httpdb{url = "http://h/db"}}
-        },
-        {
-            #httpdb{
-                url = "http://h/db",
-                headers = DefaultHeaders ++ [
-                    {"Authorization", "Basic " ++ b64creds("u", "p@%40")}
-                ]
-            },
-            {"u", "p@%40", #httpdb{url = "http://h/db"}}
-        },
-        {
-            #httpdb{
-                url = "http://h/db",
-                headers = DefaultHeaders ++ [
-                    {"aUthoriZation", "bASIC " ++ b64creds("U", "p")}
-                ]
-            },
-            {"U", "p", #httpdb{url = "http://h/db"}}
-        },
-        {
-            #httpdb{
-                url = "http://u1:p1@h/db",
-                headers = DefaultHeaders ++ [
-                    {"Authorization", "Basic " ++ b64creds("u2", "p2")}
-                ]
-            },
-            {"u2", "p2", #httpdb{url = "http://h/db"}}
-        }
-    ]].
+extract_creds_success_test() ->
+    HttpDb = #httpdb{auth_props = [
+        {<<"basic">>, {[
+            {<<"username">>, <<"u2">>},
+            {<<"password">>, <<"p2">>}
+        ]}}
+    ]},
+    ?assertEqual({ok, "u2", "p2", #httpdb{}}, extract_creds(HttpDb)),
+    ?assertEqual({error, missing_credentials}, extract_creds(#httpdb{})).
 
 
 cookie_update_test_() ->
@@ -795,7 +649,7 @@ t_process_ok_no_cookie() ->
 t_init_state_fails_on_401() ->
     ?_test(begin
         mock_http_401_response(),
-        {error, Error} = init_state(#httpdb{url = "http://u:p@h"}),
+        {error, Error} = init_state(httpdb("http://u:p@h")),
         SessionUrl =  "http://h/_session",
         ?assertEqual({session_request_unauthorized, SessionUrl, "u"}, Error)
     end).
@@ -805,32 +659,36 @@ t_init_state_401_with_require_valid_user() ->
     ?_test(begin
         mock_http_401_response_with_require_valid_user(),
         ?assertMatch({ok, #httpdb{}, #state{cookie = "Cookie"}},
-            init_state(#httpdb{url = "http://u:p@h"}))
+            init_state(httpdb("http://u:p@h")))
     end).
 
 
 t_init_state_404() ->
     ?_test(begin
         mock_http_404_response(),
-        ?assertEqual(ignore, init_state(#httpdb{url = "http://u:p@h"}))
+        ?assertEqual(ignore, init_state(httpdb("http://u:p@h")))
     end).
 
 
 t_init_state_no_creds() ->
     ?_test(begin
-        ?_assertEqual(ignore, init_state(#httpdb{url = "http://h"}))
+        ?_assertEqual(ignore, init_state(httpdb("http://h")))
     end).
 
 
 t_init_state_http_error() ->
     ?_test(begin
         mock_http_error_response(),
-        {error, Error} = init_state(#httpdb{url = "http://u:p@h"}),
+        {error, Error} = init_state(httpdb("http://u:p@h")),
         SessionUrl = "http://h/_session",
         ?assertEqual({session_request_failed, SessionUrl, "u", x}, Error)
     end).
 
 
+httpdb(Url) ->
+    couch_replicator_utils:normalize_basic_auth(#httpdb{url = Url}).
+
+
 setup_all() ->
     meck:expect(couch_replicator_httpc_pool, get_worker, 1, {ok, worker}),
     meck:expect(couch_replicator_httpc_pool, release_worker_sync, 2, ok),
@@ -885,14 +743,6 @@ mock_http_error_response() ->
     meck:expect(ibrowse, send_req_direct, 7, {error, x}).
 
 
-extract_creds_error_test_() ->
-    [?_assertMatch({error, Error}, extract_creds(HttpDb)) ||
-        {HttpDb, Error} <- [
-        {#httpdb{url = "some_junk"}, invalid_uri},
-        {#httpdb{url = "http://h/db"}, missing_credentials}
-    ]].
-
-
 parse_max_age_test_() ->
     [?_assertEqual(R, parse_max_age(mochiweb_headers:make([{"Max-Age", A}])))
         ||  {A, R} <- [
diff --git a/src/couch_replicator/src/couch_replicator_docs.erl b/src/couch_replicator/src/couch_replicator_docs.erl
index 6190632..3087195 100644
--- a/src/couch_replicator/src/couch_replicator_docs.erl
+++ b/src/couch_replicator/src/couch_replicator_docs.erl
@@ -408,7 +408,7 @@ parse_rep_db({Props}, Proxy, Options) ->
     {BinHeaders} = get_value(<<"headers">>, Props, {[]}),
     Headers = lists:ukeysort(1, [{?b2l(K), ?b2l(V)} || {K, V} <- BinHeaders]),
     DefaultHeaders = (#httpdb{})#httpdb.headers,
-    #httpdb{
+    HttpDb = #httpdb{
         url = Url,
         auth_props = AuthProps,
         headers = lists:ukeymerge(1, Headers, DefaultHeaders),
@@ -419,7 +419,8 @@ parse_rep_db({Props}, Proxy, Options) ->
         http_connections = get_value(http_connections, Options),
         retries = get_value(retries, Options),
         proxy_url = ProxyURL
-    };
+    },
+    couch_replicator_utils:normalize_basic_auth(HttpDb);
 
 parse_rep_db(<<"http://", _/binary>> = Url, Proxy, Options) ->
     parse_rep_db({[{<<"url">>, Url}]}, Proxy, Options);
diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl
index 6012b52..a2af518 100644
--- a/src/couch_replicator/src/couch_replicator_httpc.erl
+++ b/src/couch_replicator/src/couch_replicator_httpc.erl
@@ -112,7 +112,13 @@ send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) ->
         end
     end,
     {ok, Worker} = couch_replicator_httpc_pool:get_worker(HttpDb#httpdb.httpc_pool),
-    IbrowseOptions = [
+    BasicAuthOpts = case couch_replicator_utils:get_basic_auth_creds(HttpDb) of
+        {undefined, undefined} ->
+            [];
+        {User, Pass} when is_list(User), is_list(Pass) ->
+            [{basic_auth, {User, Pass}}]
+    end,
+    IbrowseOptions = BasicAuthOpts ++ [
         {response_format, binary}, {inactivity_timeout, HttpDb#httpdb.timeout} |
         lists:ukeymerge(1, get_value(ibrowse_options, Params, []),
             HttpDb#httpdb.ibrowse_options)
diff --git a/src/couch_replicator/src/couch_replicator_ids.erl b/src/couch_replicator/src/couch_replicator_ids.erl
index 04e71c3..80ff001 100644
--- a/src/couch_replicator/src/couch_replicator_ids.erl
+++ b/src/couch_replicator/src/couch_replicator_ids.erl
@@ -134,21 +134,10 @@ get_rep_endpoint(#httpdb{url=Url, headers=Headers}) ->
 
 get_v4_endpoint(#httpdb{} = HttpDb) ->
     {remote, Url, Headers} = get_rep_endpoint(HttpDb),
-    {{UserFromHeaders, _}, HeadersWithoutBasicAuth} =
-        couch_replicator_utils:remove_basic_auth_from_headers(Headers),
-    {UserFromUrl, Host, NonDefaultPort, Path} = get_v4_url_info(Url),
-    User = pick_defined_value([UserFromUrl, UserFromHeaders]),
+    {User, _} = couch_replicator_utils:get_basic_auth_creds(HttpDb),
+    {Host, NonDefaultPort, Path} = get_v4_url_info(Url),
     OAuth = undefined, % Keep this to ensure checkpoints don't change
-    {remote, User, Host, NonDefaultPort, Path, HeadersWithoutBasicAuth, OAuth}.
-
-
-pick_defined_value(Values) ->
-    case [V || V <- Values, V /= undefined] of
-        [] ->
-            undefined;
-        DefinedValues ->
-            hd(DefinedValues)
-    end.
+    {remote, User, Host, NonDefaultPort, Path, Headers, OAuth}.
 
 
 get_v4_url_info(Url) when is_binary(Url) ->
@@ -158,16 +147,15 @@ get_v4_url_info(Url) ->
         {error, invalid_uri} ->
             % Tolerate errors here to avoid a bad user document
             % crashing the replicator
-            {undefined, Url, undefined, undefined};
+            {Url, undefined, undefined};
         #url{
             protocol = Schema,
-            username = User,
             host = Host,
             port = Port,
             path = Path
         } ->
             NonDefaultPort = get_non_default_port(Schema, Port),
-            {User, Host, NonDefaultPort, Path}
+            {Host, NonDefaultPort, Path}
     end.
 
 
@@ -197,71 +185,81 @@ replication_id_convert_test_() ->
 
 http_v4_endpoint_test_() ->
     [?_assertMatch({remote, User, Host, Port, Path, HeadersNoAuth, undefined},
-        get_v4_endpoint(#httpdb{url = Url, headers = Headers})) ||
-            {{User, Host, Port, Path, HeadersNoAuth}, {Url, Headers}} <- [
+        begin
+            HttpDb = #httpdb{url = Url, headers = Headers, auth_props = Auth},
+            HttpDb1 = couch_replicator_utils:normalize_basic_auth(HttpDb),
+            get_v4_endpoint(HttpDb1)
+        end) ||
+            {{User, Host, Port, Path, HeadersNoAuth}, {Url, Headers, Auth}} <- [
                 {
                     {undefined, "host", default, "/", []},
-                    {"http://host", []}
+                    {"http://host", [], []}
                 },
                 {
                     {undefined, "host", default, "/", []},
-                    {"https://host", []}
+                    {"https://host", [], []}
                 },
                 {
                     {undefined, "host", default, "/", []},
-                    {"http://host:5984", []}
+                    {"http://host:5984", [], []}
                 },
                 {
                     {undefined, "host", 1, "/", []},
-                    {"http://host:1", []}
+                    {"http://host:1", [], []}
                 },
                 {
                     {undefined, "host", 2, "/", []},
-                    {"https://host:2", []}
+                    {"https://host:2", [], []}
                 },
                 {
                     {undefined, "host", default, "/", [{"h","v"}]},
-                    {"http://host", [{"h","v"}]}
+                    {"http://host", [{"h","v"}], []}
                 },
                 {
                     {undefined, "host", default, "/a/b", []},
-                    {"http://host/a/b", []}
+                    {"http://host/a/b", [], []}
                 },
                 {
                     {"user", "host", default, "/", []},
-                    {"http://user:pass@host", []}
+                    {"http://user:pass@host", [], []}
                 },
                 {
                     {"user", "host", 3, "/", []},
-                    {"http://user:pass@host:3", []}
+                    {"http://user:pass@host:3", [], []}
                 },
                 {
                     {"user", "host", default, "/", []},
-                    {"http://user:newpass@host", []}
+                    {"http://user:newpass@host", [], []}
                 },
                 {
                     {"user", "host", default, "/", []},
-                    {"http://host", [basic_auth("user","pass")]}
+                    {"http://host", [basic_auth("user","pass")], []}
                 },
                 {
                     {"user", "host", default, "/", []},
-                    {"http://host", [basic_auth("user","newpass")]}
+                    {"http://host", [basic_auth("user","newpass")], []}
                 },
                 {
-                    {"user1", "host", default, "/", []},
-                    {"http://user1:pass1@host", [basic_auth("user2","pass2")]}
+                    {"user3", "host", default, "/", []},
+                    {"http://user1:pass1@host", [basic_auth("user2","pass2")],
+                        auth_props("user3", "pass3")}
+                },
+                {
+                    {"user2", "host", default, "/", [{"h", "v"}]},
+                    {"http://host", [{"h", "v"}, basic_auth("user","pass")],
+                        auth_props("user2", "pass2")}
                 },
                 {
                     {"user", "host", default, "/", [{"h", "v"}]},
-                    {"http://host", [{"h", "v"}, basic_auth("user","pass")]}
+                    {"http://host", [{"h", "v"}], auth_props("user", "pass")}
                 },
                 {
                     {undefined, "random_junk", undefined, undefined},
-                    {"random_junk", []}
+                    {"random_junk", [], []}
                 },
                 {
                     {undefined, "host", default, "/", []},
-                    {"http://host", [{"Authorization", "Basic bad"}]}
+                    {"http://host", [{"Authorization", "Basic bad"}], []}
                 }
         ]
     ].
@@ -272,4 +270,10 @@ basic_auth(User, Pass) ->
     {"Authorization", "Basic " ++ B64Auth}.
 
 
+auth_props(User, Pass) when is_list(User), is_list(Pass) ->
+    [{<<"basic">>, {[
+       {<<"username">>, list_to_binary(User)},
+       {<<"password">>, list_to_binary(Pass)}
+    ]}}].
+
 -endif.
diff --git a/src/couch_replicator/src/couch_replicator_scheduler_job.erl b/src/couch_replicator/src/couch_replicator_scheduler_job.erl
index 1d328d0..db8edfb 100644
--- a/src/couch_replicator/src/couch_replicator_scheduler_job.erl
+++ b/src/couch_replicator/src/couch_replicator_scheduler_job.erl
@@ -1072,8 +1072,8 @@ scheduler_job_format_status_test() ->
         highest_seq_done = <<"5">>
     },
     Format = format_status(opts_ignored, [pdict, State]),
-    ?assertEqual("http://u:*****@h1/d1/", proplists:get_value(source, Format)),
-    ?assertEqual("http://u:*****@h2/d2/", proplists:get_value(target, Format)),
+    ?assertEqual("http://h1/d1/", proplists:get_value(source, Format)),
+    ?assertEqual("http://h2/d2/", proplists:get_value(target, Format)),
     ?assertEqual({"base", "+ext"}, proplists:get_value(rep_id, Format)),
     ?assertEqual([{create_target, true}], proplists:get_value(options, Format)),
     ?assertEqual(<<"mydoc">>, proplists:get_value(doc_id, Format)),
diff --git a/src/couch_replicator/src/couch_replicator_utils.erl b/src/couch_replicator/src/couch_replicator_utils.erl
index 5f608de..dbadb37 100644
--- a/src/couch_replicator/src/couch_replicator_utils.erl
+++ b/src/couch_replicator/src/couch_replicator_utils.erl
@@ -23,12 +23,15 @@
    pp_rep_id/1,
    iso8601/1,
    filter_state/3,
-   remove_basic_auth_from_headers/1,
    normalize_rep/1,
-   ejson_state_info/1
+   ejson_state_info/1,
+   get_basic_auth_creds/1,
+   remove_basic_auth_creds/1,
+   normalize_basic_auth/1
 ]).
 
 
+-include_lib("ibrowse/include/ibrowse.hrl").
 -include_lib("couch/include/couch_db.hrl").
 -include("couch_replicator.hrl").
 -include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl").
@@ -191,6 +194,98 @@ ejson_state_info(Info) ->
     {[{<<"error">>, ErrMsg}]}.
 
 
+-spec get_basic_auth_creds(#httpdb{}) ->
+    {string(), string()} | {undefined, undefined}.
+get_basic_auth_creds(#httpdb{auth_props = AuthProps}) ->
+    case couch_util:get_value(<<"basic">>, AuthProps) of
+        undefined ->
+            {undefined, undefined};
+        {UserPass} when is_list(UserPass)  ->
+            User = couch_util:get_value(<<"username">>, UserPass),
+            Pass = couch_util:get_value(<<"password">>, UserPass),
+            case {User, Pass} of
+                _ when is_binary(User), is_binary(Pass) ->
+                    {binary_to_list(User), binary_to_list(Pass)};
+                _Other ->
+                    {undefined, undefined}
+            end;
+        _Other ->
+            {undefined, undefined}
+    end.
+
+
+-spec remove_basic_auth_creds(#httpd{}) -> #httpdb{}.
+remove_basic_auth_creds(#httpdb{auth_props = Props} = HttpDb) ->
+    Props1 = lists:keydelete(<<"basic">>, 1, Props),
+    HttpDb#httpdb{auth_props = Props1}.
+
+
+-spec set_basic_auth_creds(string(), string(), #httpd{}) -> #httpdb{}.
+set_basic_auth_creds(undefined, undefined, #httpdb{} = HttpDb) ->
+    HttpDb;
+set_basic_auth_creds(User, Pass, #httpdb{} = HttpDb)
+        when is_list(User), is_list(Pass) ->
+    HttpDb1 = remove_basic_auth_creds(HttpDb),
+    Props = HttpDb1#httpdb.auth_props,
+    UserPass = {[
+        {<<"username">>, list_to_binary(User)},
+        {<<"password">>, list_to_binary(Pass)}
+    ]},
+    Props1 = lists:keystore(<<"basic">>, 1, Props, {<<"basic">>, UserPass}),
+    HttpDb1#httpdb{auth_props = Props1}.
+
+
+-spec extract_creds_from_url(string()) ->
+    {ok, {string() | undefined, string() | undefined}, string()} |
+    {error, term()}.
+extract_creds_from_url(Url) ->
+    case ibrowse_lib:parse_url(Url) of
+        {error, Error} ->
+            {error, Error};
+        #url{username = undefined, password = undefined} ->
+            {ok, {undefined, undefined}, Url};
+        #url{protocol = Proto, username = User, password = Pass} ->
+            % Excise user and pass parts from the url. Try to keep the host,
+            % port and path as they were in the original.
+            Prefix = lists:concat([Proto, "://", User, ":", Pass, "@"]),
+            Suffix = lists:sublist(Url, length(Prefix) + 1, length(Url) + 1),
+            NoCreds = lists:concat([Proto, "://", Suffix]),
+            {ok, {User, Pass}, NoCreds}
+    end.
+
+
+% Normalize basic auth credentials so they are set only in the auth props
+% object. If multiple basic auth credentials are provided, the resulting
+% credentials are picked in the following order.
+%  1) {"auth": "basic": {"username":.., "password": ...} ...}
+%  2) URL userinfo part
+%  3) "Authentication" : "basic $base64" headers
+%
+-spec normalize_basic_auth(#httpdb{}) -> #httpdb{}.
+normalize_basic_auth(#httpdb{} = HttpDb) ->
+    #httpdb{url = Url, headers = Headers} = HttpDb,
+    {HeaderCreds, HeadersNoCreds} = remove_basic_auth_from_headers(Headers),
+    {UrlCreds, UrlWithoutCreds} = case extract_creds_from_url(Url) of
+        {ok, Creds = {_, _}, UrlNoCreds} ->
+            {Creds, UrlNoCreds};
+        {error, _Error} ->
+            % Don't crash replicator if user provided an invalid
+            % userinfo part
+            {undefined, undefined}
+    end,
+    AuthCreds = {_, _} = get_basic_auth_creds(HttpDb),
+    HttpDb1 = remove_basic_auth_creds(HttpDb#httpdb{
+        url = UrlWithoutCreds,
+        headers = HeadersNoCreds
+    }),
+    {User, Pass} = case {AuthCreds, UrlCreds, HeaderCreds} of
+        {{U, P}, {_, _}, {_, _}} when is_list(U), is_list(P) -> {U, P};
+        {{_, _}, {U, P}, {_, _}} when is_list(U), is_list(P) -> {U, P};
+        {{_, _}, {_, _}, {U, P}} -> {U, P}
+    end,
+    set_basic_auth_creds(User, Pass, HttpDb1).
+
+
 -ifdef(TEST).
 
 -include_lib("eunit/include/eunit.hrl").
@@ -269,4 +364,176 @@ normalize_rep_test_() ->
         end)
     }.
 
+
+get_basic_auth_creds_test() ->
+    Check = fun(Props) ->
+        get_basic_auth_creds(#httpdb{auth_props = Props})
+    end,
+
+    ?assertEqual({undefined, undefined}, Check([])),
+
+    ?assertEqual({undefined, undefined}, Check([null])),
+
+    ?assertEqual({undefined, undefined}, Check([{<<"other">>, <<"x">>}])),
+
+    ?assertEqual({undefined, undefined}, Check([{<<"basic">>, []}])),
+
+    UserPass1 = {[{<<"username">>, <<"u">>}, {<<"password">>, <<"p">>}]},
+    ?assertEqual({"u", "p"}, Check([{<<"basic">>, UserPass1}])),
+
+    UserPass3 = {[{<<"username">>, <<"u">>}, {<<"password">>, null}]},
+    ?assertEqual({undefined, undefined}, Check([{<<"basic">>, UserPass3}])).
+
+
+remove_basic_auth_creds_test() ->
+    Check = fun(Props) ->
+        HttpDb = remove_basic_auth_creds(#httpdb{auth_props = Props}),
+        HttpDb#httpdb.auth_props
+    end,
+
+    ?assertEqual([], Check([])),
+
+    ?assertEqual([{<<"other">>, {[]}}], Check([{<<"other">>, {[]}}])),
+
+    ?assertEqual([], Check([
+        {<<"basic">>, {[
+            {<<"username">>, <<"u">>},
+            {<<"password">>, <<"p">>}
+        ]}}
+    ])),
+
+    ?assertEqual([{<<"other">>, {[]}}], Check([
+        {<<"basic">>, {[
+            {<<"username">>, <<"u">>},
+            {<<"password">>, <<"p">>}
+        ]}},
+        {<<"other">>, {[]}}
+    ])).
+
+
+set_basic_auth_creds_test() ->
+    Check = fun(User, Pass, Props) ->
+        HttpDb = set_basic_auth_creds(User, Pass, #httpdb{auth_props = Props}),
+        HttpDb#httpdb.auth_props
+    end,
+
+    ?assertEqual([], Check(undefined, undefined, [])),
+
+    ?assertEqual([{<<"other">>, {[]}}], Check(undefined, undefined,
+        [{<<"other">>, {[]}}])),
+
+    ?assertEqual([
+        {<<"basic">>, {[
+            {<<"username">>, <<"u">>},
+            {<<"password">>, <<"p">>}
+        ]}}
+    ], Check("u", "p", [])),
+
+    ?assertEqual([
+        {<<"other">>, {[]}},
+        {<<"basic">>, {[
+            {<<"username">>, <<"u">>},
+            {<<"password">>, <<"p">>}
+        ]}}
+    ], Check("u", "p", [{<<"other">>, {[]}}])).
+
+
+normalize_basic_creds_test_() ->
+    DefaultHeaders = (#httpdb{})#httpdb.headers,
+    [?_assertEqual(Expect, normalize_basic_auth(Input)) || {Input, Expect} <- [
+        {
+            #httpdb{url = "http://u:p@x.y/db"},
+            #httpdb{url = "http://x.y/db", auth_props = auth_props("u", "p")}
+        },
+        {
+            #httpdb{url = "http://u:p@h:80/db"},
+            #httpdb{url = "http://h:80/db", auth_props = auth_props("u", "p")}
+        },
+        {
+            #httpdb{url = "https://u:p@h/db"},
+            #httpdb{url = "https://h/db", auth_props = auth_props("u", "p")}
+        },
+        {
+            #httpdb{url = "http://u:p@[2001:db8:a1b:12f9::1]/db"},
+            #httpdb{url = "http://[2001:db8:a1b:12f9::1]/db",
+                auth_props = auth_props("u", "p")}
+        },
+        {
+            #httpdb{
+                url = "http://h/db",
+                headers = DefaultHeaders ++ [
+                    {"Authorization", "Basic " ++ b64creds("u", "p")}
+                ]
+            },
+            #httpdb{url = "http://h/db", auth_props = auth_props("u", "p")}
+        },
+        {
+            #httpdb{
+                url = "http://h/db",
+                headers = DefaultHeaders ++ [
+                    {"Authorization", "Basic " ++ b64creds("u", "p@")}
+                ]
+            },
+            #httpdb{url = "http://h/db", auth_props = auth_props("u", "p@")}
+        },
+        {
+            #httpdb{
+                url = "http://h/db",
+                headers = DefaultHeaders ++ [
+                    {"Authorization", "Basic " ++ b64creds("u", "p@%40")}
+                ]
+            },
+            #httpdb{url = "http://h/db", auth_props = auth_props("u", "p@%40")}
+        },
+        {
+            #httpdb{
+                url = "http://h/db",
+                headers = DefaultHeaders ++ [
+                    {"aUthoriZation", "bASIC " ++ b64creds("U", "p")}
+                ]
+            },
+            #httpdb{url = "http://h/db", auth_props = auth_props("U", "p")}
+        },
+        {
+            #httpdb{
+                url = "http://u1:p1@h/db",
+                headers = DefaultHeaders ++ [
+                    {"Authorization", "Basic " ++ b64creds("u2", "p2")}
+                ]
+            },
+            #httpdb{url ="http://h/db", auth_props = auth_props("u1", "p1")}
+        },
+        {
+            #httpdb{
+                url = "http://u1:p1@h/db",
+                auth_props = [{<<"basic">>, {[
+                    {<<"username">>, <<"u2">>},
+                    {<<"password">>, <<"p2">>}
+                ]}}]
+            },
+            #httpdb{url = "http://h/db", auth_props = auth_props("u2", "p2")}
+        },
+        {
+            #httpdb{
+                url = "http://u1:p1@h/db",
+                auth_props = [{<<"basic">>, {[
+                    {<<"username">>, <<"u2">>},
+                    {<<"password">>, <<"p2">>}
+                ]}}],
+                headers = DefaultHeaders ++ [
+                    {"Authorization", "Basic " ++ b64creds("u3", "p3")}
+                ]
+            },
+            #httpdb{url = "http://h/db", auth_props = auth_props("u2", "p2")}
+        }
+    ]].
+
+
+auth_props(User, Pass) when is_list(User), is_list(Pass) ->
+    [{<<"basic">>, {[
+       {<<"username">>, list_to_binary(User)},
+       {<<"password">>, list_to_binary(Pass)}
+    ]}}].
+
+
 -endif.