| % 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("couch_db.hrl"). |
| |
| -export([oauth_authentication_handler/1, handle_oauth_req/1, consumer_lookup/2]). |
| |
| % OAuth auth handler using per-node user db |
| oauth_authentication_handler(#httpd{mochi_req=MochiReq}=Req) -> |
| serve_oauth(Req, fun(URL, Params, Consumer, Signature) -> |
| AccessToken = couch_util:get_value("oauth_token", Params), |
| case couch_config:get("oauth_token_secrets", AccessToken) of |
| undefined -> |
| couch_httpd:send_error(Req, 400, <<"invalid_token">>, |
| <<"Invalid OAuth token.">>); |
| TokenSecret -> |
| ?LOG_DEBUG("OAuth URL is: ~p", [URL]), |
| case oauth:verify(Signature, atom_to_list(MochiReq:get(method)), URL, Params, Consumer, TokenSecret) of |
| true -> |
| set_user_ctx(Req, AccessToken); |
| false -> |
| Req |
| end |
| end |
| end, true). |
| |
| % Look up the consumer key and get the roles to give the consumer |
| set_user_ctx(Req, AccessToken) -> |
| % TODO move to db storage |
| Name = case couch_config:get("oauth_token_users", AccessToken) of |
| undefined -> throw({bad_request, unknown_oauth_token}); |
| Value -> ?l2b(Value) |
| end, |
| case couch_auth_cache:get_user_creds(Name) of |
| nil -> Req; |
| User -> |
| 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}=Req) -> |
| serve_oauth(Req, fun(URL, Params, Consumer, Signature) -> |
| AccessToken = couch_util:get_value("oauth_token", Params), |
| TokenSecret = couch_config:get("oauth_token_secrets", AccessToken), |
| case oauth:verify(Signature, 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'}=Req) -> |
| serve_oauth(Req, fun(URL, Params, Consumer, Signature) -> |
| case oauth:token(Params) of |
| "requestkey" -> |
| case oauth:verify(Signature, "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}=Req) -> |
| case Method of |
| 'GET' -> |
| % Confirm with the User that they want to authenticate the Consumer |
| serve_oauth(Req, fun(URL, Params, Consumer, Signature) -> |
| AccessToken = couch_util:get_value("oauth_token", Params), |
| TokenSecret = couch_config:get("oauth_token_secrets", AccessToken), |
| case oauth:verify(Signature, "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(Req, fun(URL, Params, Consumer, Signature) -> |
| AccessToken = couch_util:get_value("oauth_token", Params), |
| TokenSecret = couch_config:get("oauth_token_secrets", AccessToken), |
| case oauth:verify(Signature, "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(Req, "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_uri:params_from_header_string(AuthHeader), |
| %Realm = couch_util:get_value("realm", HeaderParams), |
| Params = proplists:delete("realm", HeaderParams) ++ MochiReq:parse_qs(), |
| ?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 -> |
| SigMethod = couch_util:get_value("oauth_signature_method", Params), |
| case consumer_lookup(ConsumerKey, SigMethod) of |
| none -> |
| couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer (key or signature method).">>); |
| Consumer -> |
| Signature = couch_util:get_value("oauth_signature", Params), |
| URL = couch_httpd:absolute_uri(Req, MochiReq:get(raw_path)), |
| Fun(URL, proplists:delete("oauth_signature", Params), |
| Consumer, Signature) |
| end |
| end; |
| _ -> |
| couch_httpd:send_error(Req, 400, <<"invalid_oauth_version">>, <<"Invalid OAuth version.">>) |
| end. |
| |
| consumer_lookup(Key, MethodStr) -> |
| SignatureMethod = case MethodStr of |
| "PLAINTEXT" -> plaintext; |
| "HMAC-SHA1" -> hmac_sha1; |
| %"RSA-SHA1" -> rsa_sha1; |
| _Else -> undefined |
| end, |
| case SignatureMethod of |
| undefined -> none; |
| _SupportedMethod -> |
| case couch_config:get("oauth_consumer_secrets", Key, undefined) of |
| undefined -> none; |
| Secret -> {Key, Secret, SignatureMethod} |
| end |
| end. |
| |
| ok(#httpd{mochi_req=MochiReq}, Body) -> |
| {ok, MochiReq:respond({200, [], Body})}. |