summaryrefslogtreecommitdiff
path: root/src/couchdb/couch_httpd_auth.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/couchdb/couch_httpd_auth.erl')
-rw-r--r--src/couchdb/couch_httpd_auth.erl294
1 files changed, 119 insertions, 175 deletions
diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl
index 554886ca..7023e7f3 100644
--- a/src/couchdb/couch_httpd_auth.erl
+++ b/src/couchdb/couch_httpd_auth.erl
@@ -16,9 +16,9 @@
-export([default_authentication_handler/1,special_test_authentication_handler/1]).
-export([cookie_authentication_handler/1]).
-export([null_authentication_handler/1]).
+-export([proxy_authentification_handler/1]).
-export([cookie_auth_header/2]).
-export([handle_session_req/1]).
--export([ensure_users_db_exists/1, get_user/1]).
-import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
@@ -43,11 +43,11 @@ special_test_authentication_handler(Req) ->
Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
end.
-basic_username_pw(Req) ->
+basic_name_pw(Req) ->
AuthorizationHeader = header_value(Req, "Authorization"),
case AuthorizationHeader of
"Basic " ++ Base64Value ->
- case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of
+ case string:tokens(?b2l(base64:decode(Base64Value)),":") of
["_", "_"] ->
% special name and pass to be logged out
nil;
@@ -63,20 +63,20 @@ basic_username_pw(Req) ->
end.
default_authentication_handler(Req) ->
- case basic_username_pw(Req) of
+ case basic_name_pw(Req) of
{User, Pass} ->
- case get_user(?l2b(User)) of
+ case couch_auth_cache:get_user_creds(User) of
nil ->
throw({unauthorized, <<"Name or password is incorrect.">>});
UserProps ->
- UserSalt = proplists:get_value(<<"salt">>, UserProps, <<>>),
+ UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<>>),
PasswordHash = hash_password(?l2b(Pass), UserSalt),
- case proplists:get_value(<<"password_sha">>, UserProps, nil) of
- ExpectedHash when ExpectedHash == PasswordHash ->
+ ExpectedHash = couch_util:get_value(<<"password_sha">>, UserProps, nil),
+ case couch_util:verify(ExpectedHash, PasswordHash) of
+ true ->
Req#httpd{user_ctx=#user_ctx{
name=?l2b(User),
- roles=proplists:get_value(<<"roles">>, UserProps, []),
- user_doc={UserProps}
+ roles=couch_util:get_value(<<"roles">>, UserProps, [])
}};
_Else ->
throw({unauthorized, <<"Name or password is incorrect.">>})
@@ -99,176 +99,106 @@ default_authentication_handler(Req) ->
null_authentication_handler(Req) ->
Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
-% maybe we can use hovercraft to simplify running this view query
-% rename to get_user_from_users_db
-get_user(UserName) ->
- case couch_config:get("admins", ?b2l(UserName)) of
- "-hashed-" ++ HashedPwdAndSalt ->
- % the username is an admin, now check to see if there is a user doc
- % which has a matching username, salt, and password_sha
- [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
- case get_user_props_from_db(UserName) of
- nil ->
- [{<<"roles">>, [<<"_admin">>]},
- {<<"salt">>, ?l2b(Salt)},
- {<<"password_sha">>, ?l2b(HashedPwd)}];
- UserProps when is_list(UserProps) ->
- DocRoles = proplists:get_value(<<"roles">>, UserProps),
- [{<<"roles">>, [<<"_admin">> | DocRoles]},
- {<<"salt">>, ?l2b(Salt)},
- {<<"password_sha">>, ?l2b(HashedPwd)},
- {<<"user_doc">>, {UserProps}}]
- end;
- Else ->
- get_user_props_from_db(UserName)
- end.
-
-get_user_props_from_db(UserName) ->
- DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
- {ok, Db} = ensure_users_db_exists(?l2b(DbName)),
- DocId = <<"org.couchdb.user:", UserName/binary>>,
- try couch_httpd_db:couch_doc_open(Db, DocId, nil, []) of
- #doc{}=Doc ->
- {DocProps} = couch_query_servers:json_doc(Doc),
- DocProps
- catch
- throw:Throw ->
- nil
- end.
-
-% this should handle creating the ddoc
-ensure_users_db_exists(DbName) ->
- case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
- {ok, Db} ->
- ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
- {ok, Db};
- _Error ->
- {ok, Db} = couch_db:create(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
- ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
- {ok, Db}
+%% @doc proxy auth handler.
+%
+% This handler allows creation of a userCtx object from a user authenticated remotly.
+% The client just pass specific headers to CouchDB and the handler create the userCtx.
+% Headers name can be defined in local.ini. By thefault they are :
+%
+% * X-Auth-CouchDB-UserName : contain the username, (x_auth_username in
+% couch_httpd_auth section)
+% * X-Auth-CouchDB-Roles : contain the user roles, list of roles separated by a
+% comma (x_auth_roles in couch_httpd_auth section)
+% * X-Auth-CouchDB-Token : token to authenticate the authorization (x_auth_token
+% in couch_httpd_auth section). This token is an hmac-sha1 created from secret key
+% and username. The secret key should be the same in the client and couchdb node. s
+% ecret key is the secret key in couch_httpd_auth section of ini. This token is optional
+% if value of proxy_use_secret key in couch_httpd_auth section of ini isn't true.
+%
+proxy_authentification_handler(Req) ->
+ case proxy_auth_user(Req) of
+ nil -> Req;
+ Req2 -> Req2
end.
-ensure_auth_ddoc_exists(Db, DDocId) ->
- try couch_httpd_db:couch_doc_open(Db, DDocId, nil, []) of
- _Foo -> ok
- catch
- _:Error ->
- % create the design document
- {ok, AuthDesign} = auth_design_doc(DDocId),
- {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []),
- ok
+proxy_auth_user(Req) ->
+ XHeaderUserName = couch_config:get("couch_httpd_auth", "x_auth_username",
+ "X-Auth-CouchDB-UserName"),
+ XHeaderRoles = couch_config:get("couch_httpd_auth", "x_auth_roles",
+ "X-Auth-CouchDB-Roles"),
+ XHeaderToken = couch_config:get("couch_httpd_auth", "x_auth_token",
+ "X-Auth-CouchDB-Token"),
+ case header_value(Req, XHeaderUserName) of
+ undefined -> nil;
+ UserName ->
+ Roles = case header_value(Req, XHeaderRoles) of
+ undefined -> [];
+ Else ->
+ [?l2b(R) || R <- string:tokens(Else, ",")]
+ end,
+ case couch_config:get("couch_httpd_auth", "proxy_use_secret", "false") of
+ "true" ->
+ case couch_config:get("couch_httpd_auth", "secret", nil) of
+ nil ->
+ Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), roles=Roles}};
+ Secret ->
+ ExpectedToken = couch_util:to_hex(crypto:sha_mac(Secret, UserName)),
+ case header_value(Req, XHeaderToken) of
+ Token when Token == ExpectedToken ->
+ Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName),
+ roles=Roles}};
+ _ -> nil
+ end
+ end;
+ _ ->
+ Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), roles=Roles}}
+ end
end.
-% add the validation function here
-auth_design_doc(DocId) ->
- DocProps = [
- {<<"_id">>, DocId},
- {<<"language">>,<<"javascript">>},
- {<<"views">>,
- {[{<<"users">>,
- {[{<<"map">>,
- <<"function (doc) {\n if (doc.type == \"user\") {\n emit(doc.username, doc);\n}\n}">>
- }]}
- }]}
- },
- {
- <<"validate_doc_update">>,
- <<"function(newDoc, oldDoc, userCtx) {
- if (newDoc.type != 'user') {
- return;
- } // we only validate user docs for now
- if (!newDoc.username) {
- throw({forbidden : 'doc.username is required'});
- }
- if (!(newDoc.roles && (typeof newDoc.roles.length != 'undefined') )) {
- throw({forbidden : 'doc.roles must be an array'});
- }
- if (newDoc._id != 'org.couchdb.user:'+newDoc.username) {
- throw({forbidden : 'Docid must be of the form org.couchdb.user:username'});
- }
- if (oldDoc) { // validate all updates
- if (oldDoc.username != newDoc.username) {
- throw({forbidden : 'Usernames may not be changed.'});
- }
- }
- if (newDoc.password_sha && !newDoc.salt) {
- throw({forbidden : 'Users with password_sha must have a salt. See /_utils/script/couch.js for example code.'});
- }
- if (userCtx.roles.indexOf('_admin') == -1) { // not an admin
- if (oldDoc) { // validate non-admin updates
- if (userCtx.name != newDoc.username) {
- throw({forbidden : 'You may only update your own user document.'});
- }
- // validate role updates
- var oldRoles = oldDoc.roles.sort();
- var newRoles = newDoc.roles.sort();
- if (oldRoles.length != newRoles.length) {
- throw({forbidden : 'Only _admin may edit roles'});
- }
- for (var i=0; i < oldRoles.length; i++) {
- if (oldRoles[i] != newRoles[i]) {
- throw({forbidden : 'Only _admin may edit roles'});
- }
- };
- } else if (newDoc.roles.length > 0) {
- throw({forbidden : 'Only _admin may set roles'});
- }
- }
- // no system roles in users db
- for (var i=0; i < newDoc.roles.length; i++) {
- if (newDoc.roles[i][0] == '_') {
- throw({forbidden : 'No system roles (starting with underscore) in users db.'});
- }
- };
- // no system names as usernames
- if (newDoc.username[0] == '_') {
- throw({forbidden : 'Username may not start with underscore.'});
- }
- }">>
- }],
- {ok, couch_doc:from_json_obj({DocProps})}.
cookie_authentication_handler(#httpd{mochi_req=MochiReq}=Req) ->
case MochiReq:get_cookie_value("AuthSession") of
undefined -> Req;
[] -> Req;
- Cookie ->
+ Cookie ->
[User, TimeStr | HashParts] = try
AuthSession = couch_util:decodeBase64Url(Cookie),
- [A, B | Cs] = string:tokens(?b2l(AuthSession), ":")
+ [_A, _B | _Cs] = string:tokens(?b2l(AuthSession), ":")
catch
- _:Error ->
+ _:_Error ->
Reason = <<"Malformed AuthSession cookie. Please clear your cookies.">>,
throw({bad_request, Reason})
end,
% Verify expiry and hash
- {NowMS, NowS, _} = erlang:now(),
- CurrentTime = NowMS * 1000000 + NowS,
+ CurrentTime = make_cookie_time(),
case couch_config:get("couch_httpd_auth", "secret", nil) of
- nil ->
+ nil ->
?LOG_ERROR("cookie auth secret is not set",[]),
Req;
SecretStr ->
Secret = ?l2b(SecretStr),
- case get_user(?l2b(User)) of
+ case couch_auth_cache:get_user_creds(User) of
nil -> Req;
UserProps ->
- UserSalt = proplists:get_value(<<"salt">>, UserProps, <<"">>),
+ UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<"">>),
FullSecret = <<Secret/binary, UserSalt/binary>>,
ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr),
Hash = ?l2b(string:join(HashParts, ":")),
Timeout = to_int(couch_config:get("couch_httpd_auth", "timeout", 600)),
?LOG_DEBUG("timeout ~p", [Timeout]),
case (catch erlang:list_to_integer(TimeStr, 16)) of
- TimeStamp when CurrentTime < TimeStamp + Timeout
- andalso ExpectedHash == Hash ->
- TimeLeft = TimeStamp + Timeout - CurrentTime,
- ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
- Req#httpd{user_ctx=#user_ctx{
- name=?l2b(User),
- roles=proplists:get_value(<<"roles">>, UserProps, []),
- user_doc=proplists:get_value(<<"user_doc">>, UserProps, null)
- }, auth={FullSecret, TimeLeft < Timeout*0.9}};
+ TimeStamp when CurrentTime < TimeStamp + Timeout ->
+ case couch_util:verify(ExpectedHash, Hash) of
+ true ->
+ TimeLeft = TimeStamp + Timeout - CurrentTime,
+ ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
+ Req#httpd{user_ctx=#user_ctx{
+ name=?l2b(User),
+ roles=couch_util:get_value(<<"roles">>, UserProps, [])
+ }, auth={FullSecret, TimeLeft < Timeout*0.9}};
+ _Else ->
+ Req
+ end;
_Else ->
Req
end
@@ -285,12 +215,11 @@ cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}, H
% or logout handler.
% The login and logout handlers need to set the AuthSession cookie
% themselves.
- CookieHeader = proplists:get_value("Set-Cookie", Headers, ""),
+ CookieHeader = couch_util:get_value("Set-Cookie", Headers, ""),
Cookies = mochiweb_cookies:parse_cookie(CookieHeader),
- AuthSession = proplists:get_value("AuthSession", Cookies),
+ AuthSession = couch_util:get_value("AuthSession", Cookies),
if AuthSession == undefined ->
- {NowMS, NowS, _} = erlang:now(),
- TimeStamp = NowMS * 1000000 + NowS,
+ TimeStamp = make_cookie_time(),
[cookie_auth_cookie(?b2l(User), Secret, TimeStamp)];
true ->
[]
@@ -322,26 +251,27 @@ ensure_cookie_auth_secret() ->
handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
ReqBody = MochiReq:recv_body(),
Form = case MochiReq:get_primary_header_value("content-type") of
+ % content type should be json
"application/x-www-form-urlencoded" ++ _ ->
mochiweb_util:parse_qs(ReqBody);
_ ->
[]
end,
- UserName = ?l2b(proplists:get_value("username", Form, "")),
- Password = ?l2b(proplists:get_value("password", Form, "")),
+ UserName = ?l2b(couch_util:get_value("name", Form, "")),
+ Password = ?l2b(couch_util:get_value("password", Form, "")),
?LOG_DEBUG("Attempt Login: ~s",[UserName]),
- User = case get_user(UserName) of
+ User = case couch_auth_cache:get_user_creds(UserName) of
nil -> [];
Result -> Result
end,
- UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
+ UserSalt = couch_util:get_value(<<"salt">>, User, <<>>),
PasswordHash = hash_password(Password, UserSalt),
- case proplists:get_value(<<"password_sha">>, User, nil) of
- ExpectedHash when ExpectedHash == PasswordHash ->
+ ExpectedHash = couch_util:get_value(<<"password_sha">>, User, nil),
+ case couch_util:verify(ExpectedHash, PasswordHash) of
+ true ->
% setup the session cookie
Secret = ?l2b(ensure_cookie_auth_secret()),
- {NowMS, NowS, _} = erlang:now(),
- CurrentTime = NowMS * 1000000 + NowS,
+ CurrentTime = make_cookie_time(),
Cookie = cookie_auth_cookie(?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
% TODO document the "next" feature in Futon
{Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
@@ -353,9 +283,8 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
{[
{ok, true},
- {name, proplists:get_value(<<"username">>, User, null)},
- {roles, proplists:get_value(<<"roles">>, User, [])},
- {user_doc, proplists:get_value(<<"user_doc">>, User, null)}
+ {name, couch_util:get_value(<<"name">>, User, null)},
+ {roles, couch_util:get_value(<<"roles">>, User, [])}
]});
_Else ->
% clear the session
@@ -363,6 +292,7 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
send_json(Req, 401, [Cookie], {[{error, <<"unauthorized">>},{reason, <<"Name or password is incorrect.">>}]})
end;
% get user info
+% GET /_session
handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
Name = UserCtx#user_ctx.name,
ForceLogin = couch_httpd:qs_value(Req, "basic", "false"),
@@ -371,15 +301,20 @@ handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
throw({unauthorized, <<"Please login.">>});
{Name, _} ->
send_json(Req, {[
+ % remove this ok
{ok, true},
- {name, Name},
- {roles, UserCtx#user_ctx.roles},
+ {<<"userCtx">>, {[
+ {name, Name},
+ {roles, UserCtx#user_ctx.roles}
+ ]}},
{info, {[
- {user_db, ?l2b(couch_config:get("couch_httpd_auth", "authentication_db"))},
- {handlers, [?l2b(H) || H <- couch_httpd:make_fun_spec_strs(
+ {authentication_db, ?l2b(couch_config:get("couch_httpd_auth", "authentication_db"))},
+ {authentication_handlers, [auth_name(H) || H <- couch_httpd:make_fun_spec_strs(
couch_config:get("httpd", "authentication_handlers"))]}
- ] ++ maybe_value(authenticated, UserCtx#user_ctx.handler)}}
- ] ++ maybe_value(user_doc, UserCtx#user_ctx.user_doc)})
+ ] ++ maybe_value(authenticated, UserCtx#user_ctx.handler, fun(Handler) ->
+ auth_name(?b2l(Handler))
+ end)}}
+ ]})
end;
% logout by deleting the session
handle_session_req(#httpd{method='DELETE'}=Req) ->
@@ -394,12 +329,21 @@ handle_session_req(#httpd{method='DELETE'}=Req) ->
handle_session_req(Req) ->
send_method_not_allowed(Req, "GET,HEAD,POST,DELETE").
-maybe_value(Key, undefined) -> [];
-maybe_value(Key, Else) -> [{Key, Else}].
+maybe_value(_Key, undefined, _Fun) -> [];
+maybe_value(Key, Else, Fun) ->
+ [{Key, Fun(Else)}].
+
+auth_name(String) when is_list(String) ->
+ [_,_,_,_,_,Name|_] = re:split(String, "[\\W_]", [{return, list}]),
+ ?l2b(Name).
to_int(Value) when is_binary(Value) ->
- to_int(?b2l(Value));
+ to_int(?b2l(Value));
to_int(Value) when is_list(Value) ->
list_to_integer(Value);
to_int(Value) when is_integer(Value) ->
Value.
+
+make_cookie_time() ->
+ {NowMS, NowS, _} = erlang:now(),
+ NowMS * 1000000 + NowS.