diff options
Diffstat (limited to 'src/couchdb/couch_httpd_auth.erl')
-rw-r--r-- | src/couchdb/couch_httpd_auth.erl | 294 |
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. |