| % Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| % use this file except in compliance with the License. You may obtain a copy of |
| % the License at |
| % |
| % http://www.apache.org/licenses/LICENSE-2.0 |
| % |
| % Unless required by applicable law or agreed to in writing, software |
| % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| % License for the specific language governing permissions and limitations under |
| % the License. |
| |
| -module(couch_httpd_oauth). |
| |
| -include_lib("couch/include/couch_db.hrl"). |
| -include("couch_js_functions.hrl"). |
| |
| -export([oauth_authentication_handler/1, handle_oauth_req/1]). |
| |
| -define(OAUTH_DDOC_ID, <<"_design/oauth">>). |
| -define(OAUTH_VIEW_NAME, <<"oauth_credentials">>). |
| |
| -record(callback_params, { |
| consumer, |
| token, |
| token_secret, |
| url, |
| signature, |
| params, |
| username |
| }). |
| |
| % OAuth auth handler using per-node user db |
| oauth_authentication_handler(Req) -> |
| serve_oauth(Req, fun oauth_auth_callback/2, true). |
| |
| |
| oauth_auth_callback(Req, #callback_params{token_secret = undefined}) -> |
| couch_httpd:send_error( |
| Req, 400, <<"invalid_token">>, <<"Invalid OAuth token.">>); |
| |
| oauth_auth_callback(#httpd{mochi_req = MochiReq} = Req, CbParams) -> |
| Method = atom_to_list(MochiReq:get(method)), |
| #callback_params{ |
| consumer = Consumer, |
| token = Token, |
| token_secret = TokenSecret, |
| url = Url, |
| signature = Sig, |
| params = Params, |
| username = User |
| } = CbParams, |
| case oauth:verify(Sig, Method, Url, Params, Consumer, TokenSecret) of |
| true -> |
| set_user_ctx(Req, User); |
| false -> |
| couch_log:debug("OAuth handler: signature verification failed for" |
| " user `~p`~n" |
| "Received signature is `~p`~n" |
| "HTTP method is `~p`~n" |
| "URL is `~p`~n" |
| "Parameters are `~p`~n" |
| "Consumer is `~p`, token secret is `~p`~n" |
| "Expected signature was `~p`~n", |
| [User, Sig, Method, Url, Params, Consumer, TokenSecret, |
| oauth:sign(Method, Url, Params, Consumer, Token, |
| TokenSecret)]), |
| Req |
| end. |
| |
| |
| % Look up the consumer key and get the roles to give the consumer |
| set_user_ctx(_Req, undefined) -> |
| throw({bad_request, unknown_oauth_token}); |
| set_user_ctx(Req, Name) -> |
| case couch_auth_cache:get_user_creds(Name) of |
| nil -> |
| couch_log:debug("OAuth handler: user `~p` credentials not found", |
| [Name]), |
| Req; |
| {ok, User, _AuthCtx} -> |
| Roles = couch_util:get_value(<<"roles">>, User, []), |
| Req#httpd{user_ctx=#user_ctx{name=Name, roles=Roles}} |
| end. |
| |
| % OAuth request_token |
| handle_oauth_req(#httpd{path_parts=[_OAuth, <<"request_token">>], method=Method}=Req1) -> |
| serve_oauth(Req1, fun(Req, CbParams) -> |
| #callback_params{ |
| consumer = Consumer, |
| token_secret = TokenSecret, |
| url = Url, |
| signature = Sig, |
| params = Params |
| } = CbParams, |
| case oauth:verify( |
| Sig, atom_to_list(Method), Url, Params, Consumer, TokenSecret) of |
| true -> |
| ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>); |
| false -> |
| invalid_signature(Req) |
| end |
| end, false); |
| handle_oauth_req(#httpd{path_parts=[_OAuth, <<"authorize">>]}=Req) -> |
| {ok, serve_oauth_authorize(Req)}; |
| handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>], method='GET'}=Req1) -> |
| serve_oauth(Req1, fun(Req, CbParams) -> |
| #callback_params{ |
| consumer = Consumer, |
| token = Token, |
| url = Url, |
| signature = Sig, |
| params = Params |
| } = CbParams, |
| case Token of |
| "requestkey" -> |
| case oauth:verify( |
| Sig, "GET", Url, Params, Consumer, "requestsecret") of |
| true -> |
| ok(Req, |
| <<"oauth_token=accesskey&oauth_token_secret=accesssecret">>); |
| false -> |
| invalid_signature(Req) |
| end; |
| _ -> |
| couch_httpd:send_error( |
| Req, 400, <<"invalid_token">>, <<"Invalid OAuth token.">>) |
| end |
| end, false); |
| handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>]}=Req) -> |
| couch_httpd:send_method_not_allowed(Req, "GET"). |
| |
| invalid_signature(Req) -> |
| couch_httpd:send_error(Req, 400, <<"invalid_signature">>, <<"Invalid signature value.">>). |
| |
| % This needs to be protected i.e. force user to login using HTTP Basic Auth or form-based login. |
| serve_oauth_authorize(#httpd{method=Method}=Req1) -> |
| case Method of |
| 'GET' -> |
| % Confirm with the User that they want to authenticate the Consumer |
| serve_oauth(Req1, fun(Req, CbParams) -> |
| #callback_params{ |
| consumer = Consumer, |
| token_secret = TokenSecret, |
| url = Url, |
| signature = Sig, |
| params = Params |
| } = CbParams, |
| case oauth:verify( |
| Sig, "GET", Url, Params, Consumer, TokenSecret) of |
| true -> |
| ok(Req, <<"oauth_token=requestkey&", |
| "oauth_token_secret=requestsecret">>); |
| false -> |
| invalid_signature(Req) |
| end |
| end, false); |
| 'POST' -> |
| % If the User has confirmed, we direct the User back to the Consumer with a verification code |
| serve_oauth(Req1, fun(Req, CbParams) -> |
| #callback_params{ |
| consumer = Consumer, |
| token_secret = TokenSecret, |
| url = Url, |
| signature = Sig, |
| params = Params |
| } = CbParams, |
| case oauth:verify( |
| Sig, "POST", Url, Params, Consumer, TokenSecret) of |
| true -> |
| %redirect(oauth_callback, oauth_token, oauth_verifier), |
| ok(Req, <<"oauth_token=requestkey&", |
| "oauth_token_secret=requestsecret">>); |
| false -> |
| invalid_signature(Req) |
| end |
| end, false); |
| _ -> |
| couch_httpd:send_method_not_allowed(Req1, "GET,POST") |
| end. |
| |
| serve_oauth(#httpd{mochi_req=MochiReq}=Req, Fun, FailSilently) -> |
| % 1. In the HTTP Authorization header as defined in OAuth HTTP Authorization Scheme. |
| % 2. As the HTTP POST request body with a content-type of application/x-www-form-urlencoded. |
| % 3. Added to the URLs in the query part (as defined by [RFC3986] section 3). |
| AuthHeader = case MochiReq:get_header_value("authorization") of |
| undefined -> |
| ""; |
| Else -> |
| [Head | Tail] = re:split(Else, "\\s", [{parts, 2}, {return, list}]), |
| case [string:to_lower(Head) | Tail] of |
| ["oauth", Rest] -> Rest; |
| _ -> "" |
| end |
| end, |
| HeaderParams = oauth:header_params_decode(AuthHeader), |
| %Realm = couch_util:get_value("realm", HeaderParams), |
| |
| % get requested path |
| RequestedPath = case MochiReq:get_header_value("x-couchdb-requested-path") of |
| undefined -> |
| case MochiReq:get_header_value("x-couchdb-vhost-path") of |
| undefined -> |
| MochiReq:get(raw_path); |
| VHostPath -> |
| VHostPath |
| end; |
| RequestedPath0 -> |
| RequestedPath0 |
| end, |
| {_, QueryString, _} = mochiweb_util:urlsplit_path(RequestedPath), |
| |
| Params = proplists:delete("realm", HeaderParams) ++ mochiweb_util:parse_qs(QueryString), |
| |
| couch_log:debug("OAuth Params: ~p", [Params]), |
| case couch_util:get_value("oauth_version", Params, "1.0") of |
| "1.0" -> |
| case couch_util:get_value("oauth_consumer_key", Params, undefined) of |
| undefined -> |
| case FailSilently of |
| true -> Req; |
| false -> couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer.">>) |
| end; |
| ConsumerKey -> |
| Url = couch_httpd:absolute_uri(Req, RequestedPath), |
| case get_callback_params(ConsumerKey, Params, Url) of |
| {ok, CallbackParams} -> |
| Fun(Req, CallbackParams); |
| invalid_consumer_token_pair -> |
| couch_httpd:send_error( |
| Req, 400, |
| <<"invalid_consumer_token_pair">>, |
| <<"Invalid consumer and token pair.">>); |
| {error, {Error, Reason}} -> |
| couch_httpd:send_error(Req, 400, Error, Reason) |
| end |
| end; |
| _ -> |
| couch_httpd:send_error(Req, 400, <<"invalid_oauth_version">>, <<"Invalid OAuth version.">>) |
| end. |
| |
| |
| get_callback_params(ConsumerKey, Params, Url) -> |
| Token = couch_util:get_value("oauth_token", Params), |
| SigMethod = sig_method(Params), |
| CbParams0 = #callback_params{ |
| token = Token, |
| signature = couch_util:get_value("oauth_signature", Params), |
| params = proplists:delete("oauth_signature", Params), |
| url = Url |
| }, |
| case oauth_credentials_info(Token, ConsumerKey) of |
| nil -> |
| invalid_consumer_token_pair; |
| {error, _} = Err -> |
| Err; |
| {OauthCreds} -> |
| User = couch_util:get_value(<<"username">>, OauthCreds, []), |
| ConsumerSecret = ?b2l(couch_util:get_value( |
| <<"consumer_secret">>, OauthCreds, <<>>)), |
| TokenSecret = ?b2l(couch_util:get_value( |
| <<"token_secret">>, OauthCreds, <<>>)), |
| case (User =:= []) orelse (ConsumerSecret =:= []) orelse |
| (TokenSecret =:= []) of |
| true -> |
| invalid_consumer_token_pair; |
| false -> |
| CbParams = CbParams0#callback_params{ |
| consumer = {ConsumerKey, ConsumerSecret, SigMethod}, |
| token_secret = TokenSecret, |
| username = User |
| }, |
| couch_log:debug("Got OAuth credentials, for ConsumerKey `~p` and " |
| "Token `~p`, from the views, User: `~p`, " |
| "ConsumerSecret: `~p`, TokenSecret: `~p`", |
| [ConsumerKey, Token, User, ConsumerSecret, |
| TokenSecret]), |
| {ok, CbParams} |
| end |
| end. |
| |
| |
| sig_method(Params) -> |
| sig_method_1(couch_util:get_value("oauth_signature_method", Params)). |
| sig_method_1("PLAINTEXT") -> |
| plaintext; |
| % sig_method_1("RSA-SHA1") -> |
| % rsa_sha1; |
| sig_method_1("HMAC-SHA1") -> |
| hmac_sha1; |
| sig_method_1(_) -> |
| undefined. |
| |
| |
| ok(#httpd{mochi_req=MochiReq}, Body) -> |
| {ok, MochiReq:respond({200, [], Body})}. |
| |
| |
| oauth_credentials_info(Token, ConsumerKey) -> |
| case use_auth_db() of |
| {ok, Db} -> |
| Result = case query_oauth_view(Db, [?l2b(ConsumerKey), ?l2b(Token)]) of |
| [] -> |
| nil; |
| [Creds] -> |
| Creds; |
| [_ | _] -> |
| Reason = iolist_to_binary( |
| io_lib:format("Found multiple OAuth credentials for the pair " |
| " (consumer_key: `~p`, token: `~p`)", [ConsumerKey, Token])), |
| {error, {<<"oauth_token_consumer_key_pair">>, Reason}} |
| end, |
| couch_db:close(Db), |
| Result; |
| nil -> |
| { |
| case config:get("oauth_consumer_secrets", ConsumerKey) of |
| undefined -> []; |
| ConsumerSecret -> [{<<"consumer_secret">>, ?l2b(ConsumerSecret)}] |
| end |
| ++ |
| case config:get("oauth_token_secrets", Token) of |
| undefined -> []; |
| TokenSecret -> [{<<"token_secret">>, ?l2b(TokenSecret)}] |
| end |
| ++ |
| case config:get("oauth_token_users", Token) of |
| undefined -> []; |
| User -> [{<<"username">>, ?l2b(User)}] |
| end |
| } |
| end. |
| |
| |
| use_auth_db() -> |
| case config:get("couch_httpd_oauth", "use_users_db", "false") of |
| "false" -> |
| nil; |
| "true" -> |
| AuthDb = open_auth_db(), |
| {ok, _AuthDb2} = ensure_oauth_views_exist(AuthDb) |
| end. |
| |
| |
| open_auth_db() -> |
| DbName = ?l2b(config:get("couch_httpd_auth", "authentication_db")), |
| {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_CTX]), |
| AuthDb. |
| |
| |
| ensure_oauth_views_exist(AuthDb) -> |
| case couch_db:open_doc(AuthDb, ?OAUTH_DDOC_ID, []) of |
| {ok, _DDoc} -> |
| {ok, AuthDb}; |
| _ -> |
| {ok, DDoc} = get_oauth_ddoc(), |
| {ok, _Rev} = couch_db:update_doc(AuthDb, DDoc, []), |
| {ok, _AuthDb2} = couch_db:reopen(AuthDb) |
| end. |
| |
| |
| get_oauth_ddoc() -> |
| Json = {[ |
| {<<"_id">>, ?OAUTH_DDOC_ID}, |
| {<<"language">>, <<"javascript">>}, |
| {<<"views">>, |
| {[ |
| {?OAUTH_VIEW_NAME, |
| {[ |
| {<<"map">>, ?OAUTH_MAP_FUN} |
| ]} |
| } |
| ]} |
| } |
| ]}, |
| {ok, couch_doc:from_json_obj(Json)}. |
| |
| |
| query_oauth_view(Db, Key) -> |
| ViewOptions = [ |
| {start_key, Key}, |
| {end_key, Key} |
| ], |
| Callback = fun({row, Row}, Acc) -> |
| {ok, [couch_util:get_value(value, Row) | Acc]}; |
| (_, Acc) -> |
| {ok, Acc} |
| end, |
| {ok, Result} = couch_mrview:query_view( |
| Db, ?OAUTH_DDOC_ID, ?OAUTH_VIEW_NAME, ViewOptions, Callback, []), |
| Result. |