Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

cleanup

  • Loading branch information...
commit 0cc6aad7ce3316d72c082f2c375abc571b1f6401 1 parent 8094b20
@dvv authored
View
33 src/cowboy_request.erl
@@ -11,6 +11,7 @@
-export([post_for_json/2]).
-export([request/4]).
-export([urlencode/1]).
+-export([make_uri/3]).
-record(client, {
state = wait :: wait | request | response | response_body,
@@ -39,7 +40,7 @@ request(Method, URL, Headers, Body) ->
| Headers
], Body, Client),
Result = case cowboy_client:response(Client2) of
- {ok, 200, _ResHeaders, Client3} ->
+ {ok, _Status, _ResHeaders, Client3} ->
case Client3#client.state of
% @fixme dirty hack, reports only first read chunk
request ->
@@ -47,6 +48,7 @@ request(Method, URL, Headers, Body) ->
response_body ->
case cowboy_client:response_body(Client3) of
{ok, ResBody, _} ->
+ % @todo analyze Status
{ok, ResBody};
Else ->
Else
@@ -54,30 +56,41 @@ request(Method, URL, Headers, Body) ->
end;
_Else ->
% pecypc_log:info({reqerr, _Else}),
- {error, failed}
+ {error, <<"server_error">>}
end,
% pecypc_log:info({res, Result}),
Result.
+make_uri(Scheme, Host, Path) ->
+ << Scheme/binary, "://", Host/binary, Path/binary >>.
+
urlencode(Bin) when is_binary(Bin) ->
cowboy_http:urlencode(Bin);
urlencode(Atom) when is_atom(Atom) ->
urlencode(atom_to_binary(Atom, latin1));
+urlencode(Int) when is_integer(Int) ->
+ urlencode(list_to_binary(integer_to_list(Int)));
+urlencode({K, undefined}) ->
+ << (urlencode(K))/binary, $= >>;
+urlencode({K, V}) ->
+ << (urlencode(K))/binary, $=, (urlencode(V))/binary >>;
urlencode(List) when is_list(List) ->
- urlencode(List, <<>>).
-urlencode([], Acc) ->
- Acc;
-urlencode([{K, V} | T], <<>>) ->
- urlencode(T, << (urlencode(K))/binary, $=, (urlencode(V))/binary >>);
-urlencode([{K, V} | T], Acc) ->
- urlencode(T, << Acc/binary, $&,
- (urlencode(K))/binary, $=, (urlencode(V))/binary >>).
+ binary_join([urlencode(X) || X <- List], << $& >>).
+
+binary_join([], _Sep) ->
+ <<>>;
+binary_join([H], _Sep) ->
+ << H/binary >>;
+binary_join([H | T], Sep) ->
+ << H/binary, Sep/binary, (binary_join(T, Sep))/binary >>.
parse({ok, JSON}) ->
case jsx:decode(JSON, [{error_handler, fun(_, _, _) -> {error, badarg} end}])
of
{error, _} ->
{ok, cowboy_http:x_www_form_urlencoded(JSON)};
+ {incomplete, _} ->
+ {ok, []};
Hash ->
{ok, Hash}
end;
View
297 src/cowboy_social.erl
@@ -5,91 +5,178 @@
-module(cowboy_social).
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
--behaviour(cowboy_http_handler).
--export([init/3, terminate/3, handle/2]).
+% -behaviour(cowboy_rest_handler).
+-export([
+ init/3,
+ terminate/3,
+ rest_init/2,
+ allowed_methods/2,
+ content_types_provided/2
+ ]).
-%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
+-export([
+ get_html/2,
+ get_json/2
+ ]).
+
+-record(state, {
+ provider,
+ action,
+ options
+ }).
init(_Transport, Req, Opts) ->
+ {Provider, Req2} = cowboy_req:binding(provider, Req),
+ {Action, Req3} = cowboy_req:binding(action, Req2, <<"login">>),
+ % @todo unknown provider
+ case lists:keyfind(Provider, 1, Opts) of
+ {_, ProviderOpts} ->
+ {upgrade, protocol, cowboy_rest, Req3, #state{
+ provider = Provider,
+ action = Action,
+ options = ProviderOpts
+ }};
+ false ->
+ {ok, Req4} = cowboy_req:reply(404, [], <<>>, Req3),
+ {shutdown, Req4, undefined}
+ end.
+
+terminate(_Reason, _Req, _State) ->
+ ok.
+
+rest_init(Req, State = #state{options = O}) ->
% compose full redirect URI
- case key(callback_uri, Opts) of
- << "http://", _/binary >> -> {ok, Req, Opts};
- << "https://", _/binary >> -> {ok, Req, Opts};
+ {Req3, O3} = case key(callback_uri, O) of
+ << "http://", _/binary >> ->
+ {Req, O};
+ << "https://", _/binary >> ->
+ {Req, O};
Relative ->
{Headers, Req2} = cowboy_req:headers(Req),
% NB: we use X-Scheme custom header to honor proxies
- {ok, Req2, lists:keyreplace(callback_uri, 1, Opts, {callback_uri,
- << (key(<<"x-scheme">>, Headers, <<"http">>))/binary, "://",
- (key(<<"host">>, Headers))/binary,
- Relative/binary >>})}
- end.
+ O2 = keyreplace(callback_uri, O,
+ cowboy_request:make_uri(
+ key(<<"x-scheme">>, Headers, <<"http">>),
+ key(<<"host">>, Headers),
+ Relative)),
+ {Req2, O2}
+ end,
+ {ok, Req3, State#state{options = O3}}.
-terminate(_Reason, _Req, _State) ->
- ok.
+allowed_methods(Req, State) ->
+ {[<<"GET">>], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[
+ {{<<"text">>, <<"html">>, []}, get_html},
+ {{<<"application">>, <<"json">>, []}, get_json}
+ ], Req, State}.
-handle(Req, Opts) ->
- % extract flow action name
- {Action, Req2} = cowboy_req:binding(action, Req),
- % perform flow action
- {ok, Req3} = handle_request(Action, Req2, Opts),
- {ok, Req3, undefined}.
+get_html(Req, State) ->
+ case get_json(Req, State) of
+ {Result, Req2, State2} when is_binary(Result) ->
+ {ok, Req3} = cowboy_req:reply(200, [], <<
+ "<script>",
+ "window.atoken=", Result/binary, ";",
+ "if(window.atoken&&window.opener){",
+ "window.opener.atoken=window.atoken;"
+ "window.close();",
+ "}",
+ "</script>"
+ >>, Req2),
+ {halt, Req3, State2};
+ {html, Result, Req2, State2} ->
+ {Result, Req2, State2};
+ Else ->
+ Else
+ end.
+
+get_json(Req, State) ->
+ case action(Req, State) of
+ {ok, Result, Req2} ->
+ {jsx:encode(Result), Req2, State};
+ {error, Error, Req2} ->
+ {jsx:encode([{error, Error}]), Req2, State};
+ {html, Result, Req2} ->
+ {html, Result, Req2, State};
+ Else ->
+ Else
+ end.
%%
-%% Redirect to provider authorization page, expect it to redirect
-%% to our next handler
+%% User agent initiates the flow.
%%
-handle_request(<<"login">>, Req, Opts) ->
- cowboy_req:reply(302, [
- {<<"location">>, (key(provider, Opts)):get_authorize_url(Opts)}
- ], <<>>, Req);
+action(Req, #state{action = <<"login">>, provider = P, options = O}) ->
+ {Type, Req2} = cowboy_req:qs_val(<<"response_type">>, Req, <<"code">>),
+ {Opaque, Req3} = cowboy_req:qs_val(<<"state">>, Req2, <<>>),
+ % redirect to provider authorization page
+ % Mod = binary_to_atom(<< "cowboy_social_", Provider/binary >>, latin1),
+ % {ok, Req4} = cowboy_req:reply(302, [
+ % {<<"location">>, Mod:authorize(Opts)}
+ % ], <<>>, Req3),
+ % {halt, Req4, State};
+ redirect(key(authorize_url, O, authorize_url(P)), [
+ {client_id, key(client_id, O)},
+ {redirect_uri, key(callback_uri, O)},
+ {response_type, Type},
+ {scope, << (default_scope(P))/binary,
+ (key(scope, O, <<>>))/binary >>},
+ {state, Opaque}
+ ], Req3);
%%
-%% Provider redirected back to us with authorization code
+%% Provider redirects back to client with authorization code.
+%% Exchange authorization code for access token.
%%
-handle_request(<<"callback">>, Req, Opts) ->
+action(Req, State = #state{action = <<"callback">>}) ->
+ case cowboy_req:qs_val(<<"error">>, Req) of
+ {undefined, Req2} ->
+ check_code(Req2, State);
+ {Error, Req2} ->
+ {error, Error, Req2}
+ end.
+
+check_code(Req, State = #state{provider = P, options = O}) ->
case cowboy_req:qs_val(<<"code">>, Req) of
{undefined, Req2} ->
- finish({error, nocode}, Req2, Opts);
+ check_token(Req2, State);
{Code, Req2} ->
- % get_access_token(Code, Req2, Opts)
- try get_access_token(Code, Req2, Opts) of
- Result -> Result
- catch _:_ ->
- finish({error, notoken}, Req2, Opts)
- end
- end;
-
-%%
-%% Catchall
-%%
-handle_request(_, Req, _) ->
- {ok, Req2} = cowboy_req:reply(404, [], <<>>, Req),
- {ok, Req2, undefined}.
-
-%%
-%% Exchange authorization code for auth token
-%%
-get_access_token(Code, Req, Opts) ->
- {ok, Auth} = (key(provider, Opts)):get_access_token(Code, Opts),
- get_user_profile(Auth, Req, Opts).
+ %% Provider redirected back to the client with authorization code.
+ %% Exchange authorization code for access token.
+ post(key(token_url, O, token_url(P)), [
+ {code, Code},
+ {client_id, key(client_id, O)},
+ {client_secret, key(client_secret, O)},
+ {redirect_uri, key(callback_uri, O)},
+ {grant_type, <<"authorization_code">>}
+ ], Req2)
+ end.
-%%
-%% Use auth token to extract info from user profile
-%%
-get_user_profile(Auth, Req, Opts) ->
- {ok, Profile} = (key(provider, Opts)):get_user_profile(Auth, Opts),
- finish({ok, Auth, Profile}, Req, Opts).
+check_token(Req, State) ->
+ case cowboy_req:qs_val(<<"access_token">>, Req) of
+ {undefined, Req2} ->
+ implicit_flow_stage2(Req2, State);
+ {Token, Req2} ->
+ {TokenType, Req3} = cowboy_req:qs_val(
+ <<"token_type">>, Req2, <<"bearer">>),
+ {ok, [
+ {access_token, Token},
+ {token_type, TokenType}
+ ], Req3}
+ end.
%%
-%% Finalize application flow by calling callback handler
+%% Provider redirected back to the client with access token in URI fragment.
+%% Fragment is stored in UA, this handler should provide UA with a script
+%% to extract access token.
%%
-finish(Status, Req, Opts) ->
- {M, F} = key(handler, Opts),
- M:F(Status, Req, Opts).
+implicit_flow_stage2(Req, _State) ->
+ {html, <<
+ "<!--script>",
+ % "if(window.opener){window.location=window.location.href.replace('#','?')}",
+ "window.location.replace(window.location.href.replace('#','?'))",
+ "</script-->"
+ >>, Req}.
%%
%%------------------------------------------------------------------------------
@@ -97,11 +184,93 @@ finish(Status, Req, Opts) ->
%%------------------------------------------------------------------------------
%%
+redirect(Uri, Params, Req) ->
+ {ok, Req2} = cowboy_req:reply(302, [
+ {<<"location">>,
+ << Uri/binary, $?, (cowboy_request:urlencode(Params))/binary >>}
+ ], <<>>, Req),
+ {halt, Req2, undefined}.
+
+post(Url, Params, Req) ->
+ try cowboy_request:post_for_json(Url, Params) of
+ {ok, Auth} ->
+% pecypc_log:info({auth, Auth}),
+ case lists:keyfind(<<"error">>, 1, Auth) of
+ false ->
+ {ok, [
+ {access_token, key(<<"access_token">>, Auth)},
+ {token_type, key(<<"token_type">>, Auth, <<"bearer">>)}
+ ], Req};
+ {_, Error} ->
+ {error, Error, Req}
+ end;
+ _Else ->
+ {error, <<"authorization_error">>, Req}
+ catch _:_ ->
+ {error, <<"server_error">>, Req}
+ end.
+
key(Key, List) ->
- key(Key, List, <<>>).
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
key(Key, List, Def) ->
case lists:keyfind(Key, 1, List) of
{_, Value} -> Value;
_ -> Def
end.
+
+keyreplace(Key, List, Value) ->
+ lists:keyreplace(Key, 1, List, {Key, Value}).
+
+%%
+%%------------------------------------------------------------------------------
+%% Providers
+%%------------------------------------------------------------------------------
+%%
+
+authorize_url(<<"facebook">>) ->
+ <<"https://www.facebook.com/dialog/oauth">>;
+authorize_url(<<"github">>) ->
+ <<"https://github.com/login/oauth/authorize">>;
+authorize_url(<<"google">>) ->
+ <<"https://accounts.google.com/o/oauth2/auth">>;
+authorize_url(<<"mailru">>) ->
+ <<"https://connect.mail.ru/oauth/authorize">>;
+authorize_url(<<"paypal">>) ->
+ <<"https://identity.x.com/xidentity/resources/authorize">>;
+authorize_url(<<"vkontakte">>) ->
+ <<"https://oauth.vk.com/authorize">>;
+authorize_url(<<"yandex">>) ->
+ <<"https://oauth.yandex.ru/authorize">>;
+authorize_url(_) ->
+ undefined.
+
+token_url(<<"facebook">>) ->
+ <<"https://graph.facebook.com/oauth/access_token">>;
+token_url(<<"github">>) ->
+ <<"https://github.com/login/oauth/access_token">>;
+token_url(<<"google">>) ->
+ <<"https://accounts.google.com/o/oauth2/token">>;
+token_url(<<"mailru">>) ->
+ <<"https://connect.mail.ru/oauth/token">>;
+token_url(<<"paypal">>) ->
+ <<"https://identity.x.com/xidentity/oauthtokenservice">>;
+token_url(<<"vkontakte">>) ->
+ <<"https://oauth.vk.com/access_token">>;
+token_url(<<"yandex">>) ->
+ <<"https://oauth.yandex.ru/token">>;
+token_url(_) ->
+ undefined.
+
+default_scope(<<"facebook">>) ->
+ <<"email">>;
+default_scope(<<"google">>) ->
+ << "https://www.googleapis.com/auth/userinfo.email ",
+ "https://www.googleapis.com/auth/userinfo.profile" >>;
+default_scope(<<"paypal">>) ->
+ <<"https://identity.x.com/xidentity/resources/profile/me">>;
+default_scope(<<"vkontakte">>) ->
+ <<"uid,first_name,last_name,sex,photo">>;
+default_scope(_) ->
+ <<>>.
View
58 src/cowboy_social_facebook.erl
@@ -6,63 +6,28 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << "https://www.facebook.com/dialog/oauth", $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {scope, << "email ", (key(scope, Opts))/binary >>}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- % NB: facebook uses GET
- {ok, Auth} = cowboy_request:get_json(
- <<"https://graph.facebook.com/oauth/access_token">>, [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, <<"Bearer">>},
- {expires, key(<<"expires">>, Auth)}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, _Opts) ->
+user_profile(Auth, _Opts) ->
{ok, Profile} = cowboy_request:get_json(
<<"https://graph.facebook.com/me">>, [
- {access_token, key(access_token, Auth)},
+ {access_token, Auth},
{fields, <<"id,email,name,picture,gender,locale">>}
]),
+ false = lists:keyfind(<<"error">>, 1, Profile),
{ok, [
{id, << "facebook:", (key(<<"id">>, Profile))/binary >>},
{provider, <<"facebook">>},
{email, key(<<"email">>, Profile)},
{name, key(<<"name">>, Profile)},
- {avatar, key(<<"url">>, key(<<"data">>, key(<<"picture">>, Profile)))},
+ {picture, key(<<"url">>, key(<<"data">>, key(<<"picture">>, Profile)))},
{gender, key(<<"gender">>, Profile)},
- {locale, key(<<"locale">>, Profile)}
+ {locale, key(<<"locale">>, Profile)},
+ {raw, Profile}
]}.
%%
@@ -72,10 +37,5 @@ get_user_profile(Auth, _Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
-
-key(Key, List, Def) ->
- case lists:keyfind(Key, 1, List) of
- {_, Value} -> Value;
- _ -> Def
- end.
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
View
59 src/cowboy_social_generic.erl
@@ -6,64 +6,28 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << (key(authorize_url, Opts))/binary, $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {response_type, <<"code">>},
- {scope, key(scope, Opts)}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- {ok, Auth} = cowboy_request:post_for_json(
- key(access_token_url, Opts), [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {grant_type, <<"authorization_code">>}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, key(<<"token_type">>, Auth)},
- {expires_in, key(<<"expires_in">>, Auth)}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, Opts) ->
+user_profile(Auth, Opts) ->
{ok, Profile} = cowboy_request:get_json(
key(profile_url, Opts), [
- {access_token, key(access_token, Auth)}
+ {access_token, Auth}
]),
Name = key(provider_name, Opts),
+ false = lists:keyfind(<<"error">>, 1, Profile),
{ok, [
{id, << Name/binary, ":", (key(<<"id">>, Profile))/binary >>},
{provider, Name},
{email, key(<<"email">>, Profile)},
{name, key(<<"name">>, Profile)},
- {avatar, key(<<"picture">>, Profile)},
+ {picture, key(<<"picture">>, Profile)},
{gender, key(<<"gender">>, Profile)},
- {locale, key(<<"locale">>, Profile)}
+ {locale, key(<<"locale">>, Profile)},
+ {raw, Profile}
]}.
%%
@@ -73,10 +37,5 @@ get_user_profile(Auth, Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
-
-key(Key, List, Def) ->
- case lists:keyfind(Key, 1, List) of
- {_, Value} -> Value;
- _ -> Def
- end.
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
View
55 src/cowboy_social_github.erl
@@ -6,60 +6,26 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << "https://github.com/login/oauth/authorize", $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {scope, key(scope, Opts)}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- {ok, Auth} = cowboy_request:post_for_json(
- <<"https://github.com/login/oauth/access_token">>, [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, key(<<"token_type">>, Auth)},
- {expires_in, 0}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, _Opts) ->
+user_profile(Auth, _Opts) ->
{ok, Profile} = cowboy_request:get_json(
<<"https://api.github.com/user">>, [
- {access_token, key(access_token, Auth)}
+ {access_token, Auth}
]),
+ false = lists:keyfind(<<"message">>, 1, Profile),
{ok, [
{id, << "github:",
(list_to_binary(integer_to_list(key(<<"id">>, Profile))))/binary >>},
{provider, <<"github">>},
{email, key(<<"email">>, Profile)},
{name, key(<<"name">>, Profile)},
- {avatar, key(<<"avatar_url">>, Profile)}
+ {picture, key(<<"avatar_url">>, Profile)},
+ {raw, Profile}
]}.
%%
@@ -69,10 +35,5 @@ get_user_profile(Auth, _Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
-
-key(Key, List, Def) ->
- case lists:keyfind(Key, 1, List) of
- {_, Value} -> Value;
- _ -> Def
- end.
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
View
35 src/cowboy_social_google.erl
@@ -6,21 +6,15 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ % authorize/1,
+ % access_token/2,
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
%% get URL of provider authorization page
%%
-get_authorize_url(Opts) ->
+authorize(Opts) ->
<< "https://accounts.google.com/o/oauth2/auth", $?,
(cowboy_request:urlencode([
{client_id, key(client_id, Opts)},
@@ -34,7 +28,7 @@ get_authorize_url(Opts) ->
%%
%% exchange authorization code for auth token
%%
-get_access_token(Code, Opts) ->
+access_token(Code, Opts) ->
{ok, Auth} = cowboy_request:post_for_json(
<<"https://accounts.google.com/o/oauth2/token">>, [
{code, Code},
@@ -52,19 +46,21 @@ get_access_token(Code, Opts) ->
%%
%% extract info from user profile
%%
-get_user_profile(Auth, _Opts) ->
+user_profile(Token, _Opts) ->
{ok, Profile} = cowboy_request:get_json(
<<"https://www.googleapis.com/oauth2/v1/userinfo">>, [
- {access_token, key(access_token, Auth)}
+ {access_token, Token}
]),
+ false = lists:keyfind(<<"error">>, 1, Profile),
{ok, [
{id, << "google:", (key(<<"id">>, Profile))/binary >>},
{provider, <<"google">>},
{email, key(<<"email">>, Profile)},
{name, key(<<"name">>, Profile)},
- {avatar, key(<<"picture">>, Profile)},
+ {picture, key(<<"picture">>, Profile)},
{gender, key(<<"gender">>, Profile)},
- {locale, key(<<"locale">>, Profile)}
+ {locale, key(<<"locale">>, Profile)},
+ {raw, Profile}
]}.
%%
@@ -74,10 +70,5 @@ get_user_profile(Auth, _Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
-
-key(Key, List, Def) ->
- case lists:keyfind(Key, 1, List) of
- {_, Value} -> Value;
- _ -> Def
- end.
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
View
62 src/cowboy_social_mailru.erl
@@ -6,54 +6,16 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << "https://connect.mail.ru/oauth/authorize", $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {response_type, <<"code">>}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- {ok, Auth} = cowboy_request:post_for_json(
- <<"https://connect.mail.ru/oauth/token">>, [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {grant_type, <<"authorization_code">>}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, key(<<"token_type">>, Auth)},
- {expires_in, key(<<"expires_in">>, Auth)}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, Opts) ->
+user_profile(Auth, Opts) ->
Sig = md5hex(<<
"app_id=", (key(client_id, Opts))/binary,
- "method=users.getInfosecure=1session_key=",
- (key(access_token, Auth))/binary,
+ "method=users.getInfosecure=1session_key=", Auth/binary,
(key(secret_key, Opts))/binary >>),
% NB: provider returns list of data for uids; we need only the first
{ok, [Profile]} = cowboy_request:get_json(
@@ -61,18 +23,21 @@ get_user_profile(Auth, Opts) ->
{app_id, key(client_id, Opts)},
{method, <<"users.getInfo">>},
{secure, <<"1">>},
- {session_key, key(access_token, Auth)},
+ {session_key, Auth},
{sig, Sig}
]),
+ % NB: {<<"error">>, _} means error occured
+ false = is_tuple(Profile),
{ok, [
{id, << "mailru:", (key(<<"uid">>, Profile))/binary >>},
{provider, <<"mailru">>},
{email, key(<<"email">>, Profile)},
{name, << (key(<<"first_name">>, Profile))/binary, " ",
(key(<<"last_name">>, Profile))/binary >>},
- {avatar, key(<<"pic">>, Profile)},
+ {picture, key(<<"pic">>, Profile)},
{gender, case key(<<"sex">>, Profile) of
- 1 -> <<"female">>; _ -> <<"male">> end}
+ 1 -> <<"female">>; _ -> <<"male">> end},
+ {raw, Profile}
]}.
%%
@@ -82,13 +47,8 @@ get_user_profile(Auth, Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
-
-key(Key, List, Def) ->
- case lists:keyfind(Key, 1, List) of
- {_, Value} -> Value;
- _ -> Def
- end.
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
md5hex(Bin) ->
list_to_binary(lists:flatten([io_lib:format("~2.16.0b",[N]) ||
View
34 src/cowboy_social_native.erl
@@ -0,0 +1,34 @@
+%%
+%% @doc Handler for social login via this app provider.
+%%
+
+-module(cowboy_social_native).
+-author('Vladimir Dronnikov <dronnikov@gmail.com>').
+
+-export([
+ user_profile/2
+ ]).
+
+%%
+%% extract info from user profile
+%%
+user_profile(_Auth, _Opts) ->
+ {ok, [
+ {id, << "native:nyi" >>},
+ {provider, <<"native">>},
+ {email, <<"nyi@nyi.nyi">>},
+ {name, <<"nyi">>},
+ {picture, null},
+ {gender, <<"male">>},
+ {locale, <<"ru">>}
+ ]}.
+
+%%
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+%%
+
+% key(Key, List) ->
+% {_, Value} = lists:keyfind(Key, 1, List),
+% Value.
View
57 src/cowboy_social_paypal.erl
@@ -6,55 +6,16 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << "https://identity.x.com/xidentity/resources/authorize", $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {response_type, <<"code">>},
- {scope, << "https://identity.x.com/xidentity/resources/profile/me ",
- (key(scope, Opts))/binary >>}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- {ok, Auth} = cowboy_request:post_for_json(
- <<"https://identity.x.com/xidentity/oauthtokenservice">>, [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {grant_type, <<"authorization_code">>}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, <<"Bearer">>},
- {expires_in, key(<<"expires_in">>, Auth)}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, _Opts) ->
+user_profile(Auth, _Opts) ->
{ok, Result} = cowboy_request:get_json(
<<"https://identity.x.com/xidentity/resources/profile/me">>, [
- {oauth_token, key(access_token, Auth)}
+ {oauth_token, Auth}
]),
% NB: provider returns {status: ..., identity: Profile}
Profile = key(<<"identity">>, Result),
@@ -62,7 +23,8 @@ get_user_profile(Auth, _Opts) ->
{id, << "paypal:", (key(<<"userId">>, Profile))/binary >>},
{provider, <<"paypal">>},
{email, hd(key(<<"emails">>, Profile))},
- {name, key(<<"fullName">>, Profile)}
+ {name, key(<<"fullName">>, Profile)},
+ {raw, Profile}
]}.
%%
@@ -72,10 +34,5 @@ get_user_profile(Auth, _Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
-
-key(Key, List, Def) ->
- case lists:keyfind(Key, 1, List) of
- {_, Value} -> Value;
- _ -> Def
- end.
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
View
77 src/cowboy_social_profile.erl
@@ -0,0 +1,77 @@
+%%
+%% @doc Handler for calling social providers API.
+%%
+
+-module(cowboy_social_profile).
+-author('Vladimir Dronnikov <dronnikov@gmail.com>').
+
+% -behaviour(cowboy_rest_handler).
+-export([
+ init/3,
+ terminate/3,
+ rest_init/2,
+ is_authorized/2,
+ content_types_provided/2
+ ]).
+
+-export([
+ get_json/2
+ ]).
+
+-record(state, {
+ provider,
+ action,
+ options,
+ token
+ }).
+
+init(_Transport, Req, Opts) ->
+ {Provider, Req2} = cowboy_req:binding(provider, Req),
+ {Action, Req3} = cowboy_req:binding(action, Req2),
+ {_, ProviderOpts} = lists:keyfind(Provider, 1, Opts),
+ {upgrade, protocol, cowboy_rest, Req3, #state{
+ provider = Provider,
+ action = Action,
+ options = ProviderOpts
+ }}.
+
+terminate(_Reason, _Req, _State) ->
+ ok.
+
+rest_init(Req, State) ->
+ {ok, Req, State}.
+
+%%
+%% `Authorization: Bearer TOKEN` or `?access_token=TOKEN` required
+%%
+is_authorized(Req, State) ->
+ case cowboy_req:header(<<"authorization">>, Req) of
+ {<< "Bearer ", Bearer/binary >>, Req2} ->
+ {true, Req2, State#state{token = Bearer}};
+ {undefined, Req2} ->
+ case cowboy_req:qs_val(<<"access_token">>, Req2) of
+ {undefined, Req3} ->
+ {{false, <<"Bearer">>}, Req3, State};
+ {Token, Req3} ->
+ {true, Req3, State#state{token = Token}}
+ end;
+ {_, Req2} ->
+ {{false, <<"Bearer">>}, Req2, State}
+ end.
+
+content_types_provided(Req, State) ->
+ {[
+ {{<<"application">>, <<"json">>, []}, get_json}
+ ], Req, State}.
+
+get_json(Req, State = #state{
+ provider = Provider, action = Action, options = Opts, token = Token}) ->
+ % @fixme atoms are not purged!
+ Mod = binary_to_atom(<< "cowboy_social_", Provider/binary >>, latin1),
+ Fun = binary_to_atom(Action, latin1),
+ case Mod:Fun(Token, Opts) of
+ {ok, Result} ->
+ {jsx:encode(Result), Req, State};
+ {error, Error} ->
+ {jsx:encode([{error, Error}]), Req, State}
+ end.
View
474 src/cowboy_social_provider.erl
@@ -1,139 +1,412 @@
-%%
-%% @doc Handler for OAuth2 provider.
-%%
+%%% ----------------------------------------------------------------------------
+%%%
+%%% @doc Skeleton for a OAuth2 provider.
+%%%
+%%% ----------------------------------------------------------------------------
-module(cowboy_social_provider).
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
--behaviour(cowboy_http_handler).
--export([init/3, terminate/3, handle/2]).
+% -behaviour(cowboy_rest_handler).
+-export([
+ init/3,
+ terminate/3,
+ rest_init/2,
+ allowed_methods/2,
+ content_types_accepted/2,
+ content_types_provided/2,
+ post_is_create/2
+ ]).
-%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
+% getters
+-export([
+ get_html/2
+ ]).
+
+% setters
+-export([
+ put_form/2,
+ put_json/2
+ ]).
+
+-record(state, {
+ options,
+ data,
+ client_id,
+ client_secret,
+ redirect_uri,
+ scope,
+ opaque,
+ response_type
+ }).
init(_Transport, Req, Opts) ->
- {ok, Req, Opts}.
+ {upgrade, protocol, cowboy_rest, Req, #state{options = Opts}}.
terminate(_Reason, _Req, _State) ->
ok.
-handle(Req, Opts) ->
- % extract flow action name
- {Action, Req2} = cowboy_req:binding(action, Req),
- % perform flow action
- {ok, Req3} = handle_request(Action, Req2, Opts),
- {ok, Req3, undefined}.
+rest_init(Req, State) ->
+ {ok, Req, State}.
+
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"POST">>], Req, State}.
+
+content_types_accepted(Req, State) ->
+ {[
+ {{<<"application">>, <<"json">>, []}, put_json},
+ {{<<"application">>, <<"x-www-form-urlencoded">>, []}, put_form}
+ ], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[
+ {{<<"text">>, <<"html">>, []}, get_html}
+ ], Req, State}.
+
+%%------------------------------------------------------------------------------
+%% Authorization Request
+%%------------------------------------------------------------------------------
+
+get_html(Req, State) ->
+ case cowboy_req:qs_val(<<"redirect_uri">>, Req) of
+ {undefined, Req2} ->
+ fail(Req2, State#state{data = <<"invalid_request">>});
+ {RedirectUri, Req2} ->
+ check_valid_redirect_uri(Req2, State#state{redirect_uri = RedirectUri})
+ end.
+
+check_valid_redirect_uri(Req, State = #state{redirect_uri = RedirectUri}) ->
+ {ClientId, Req2} = cowboy_req:qs_val(<<"client_id">>, Req),
+ {Opaque, Req3} = cowboy_req:qs_val(<<"state">>, Req2, <<>>),
+ case verify_redirection_uri(ClientId, RedirectUri) of
+ ok ->
+ check_response_type(Req3, State#state{client_id = ClientId,
+ opaque = Opaque});
+ % NB: do not redirect to unauthorized URI
+ {error, mismatch} ->
+ fail(Req3, State#state{data = <<"unauthorized_client">>,
+ opaque = Opaque});
+ % another validation error
+ {error, badarg} ->
+ fail(Req3, State#state{data = <<"invalid_request">>, opaque = Opaque})
+ end.
+
+check_response_type(Req, State) ->
+ case cowboy_req:qs_val(<<"response_type">>, Req) of
+ {<<"code">>, Req2} ->
+ check_scope(Req2, State#state{response_type = <<"code">>});
+ {<<"token">>, Req2} ->
+ check_scope(Req2, State#state{response_type = <<"token">>});
+ {_, Req2} ->
+ fail(Req2, State#state{data = <<"unsupported_response_type">>})
+ end.
+
+check_scope(Req, State = #state{client_id = ClientId}) ->
+ {Scope, Req2} = cowboy_req:qs_val(<<"scope">>, Req),
+ case authorize_client_credentials(ClientId, implicit, Scope) of
+ {ok, _, Scope2} ->
+ authorization_decision(Req2, State#state{scope = Scope2});
+ {error, scope} ->
+ fail(Req, State#state{data = <<"invalid_scope">>});
+ {error, _} ->
+ fail(Req, State#state{data = <<"invalid_request">>})
+ end.
+
+%%------------------------------------------------------------------------------
+%% Authorization Response
+%%------------------------------------------------------------------------------
%%
-%% Client asks user to authorize client and provide access code
+%% @todo this route per se must be accessible by authenticated resourse owners!
%%
-handle_request(<<"authorize">>, Req, Opts) ->
- % extract parameters
- {Data, Req2} = cowboy_req:qs_vals(Req),
- case lists:keyfind(<<"client_id">>, 1, Data) of
- false ->
- handle_flow(username_and_password, Data, Req2, Opts);
- _ ->
- handle_flow(client_id_and_secret, Data, Req2, Opts)
- end;
%%
-%% Client requests access token in exchange to access code.
-%% NB: we read data from body, hence POST
+%% @todo no-cache for these two
%%
-handle_request(<<"access_token">>, Req, Opts) ->
- % extract parameters
+
+authorization_decision(Req, State = #state{response_type = <<"code">>,
+ client_id = ClientId, redirect_uri = RedirectUri,
+ scope = Scope, opaque = Opaque,
+ options = Opts
+ }) ->
+ % respond with form containing authorization code.
+ % NB: flow continues after form submit ok
+ Code = termit:encode_base64(
+ {Opaque, ClientId, RedirectUri, Scope},
+ key(code_secret, Opts)),
+ {<<
+ "<p>Client: \"", ClientId/binary, "\" asks permission for scope:\"", Scope/binary, "\"</p>",
+ "<form action=\"", RedirectUri/binary, "\" method=\"get\">",
+ "<input type=\"hidden\" name=\"code\" value=\"", Code/binary, "\" />",
+ "<input type=\"hidden\" name=\"state\" value=\"", Opaque/binary, "\" />",
+ "<input type=\"submit\" value=\"ok\" />",
+ "</form>",
+ "<form action=\"", RedirectUri/binary, "\" method=\"get\">",
+ "<input type=\"hidden\" name=\"error\" value=\"access_denied\" />",
+ "<input type=\"hidden\" name=\"state\" value=\"", Opaque/binary, "\" />",
+ "<input type=\"submit\" value=\"nak\" />",
+ "</form>"
+ >>, Req, State};
+
+authorization_decision(Req, State = #state{response_type = <<"token">>,
+ client_id = ClientId, redirect_uri = RedirectUri,
+ scope = Scope, opaque = Opaque,
+ options = Opts
+ }) ->
+ % authorize client and get authorized scope
+ case authorize_client_credentials(ClientId, implicit, Scope) of
+ {ok, Identity, Scope2} ->
+ % respond with form containing token
+ Token = token({Identity, Scope2}, Scope2, Opts),
+ TokenBin = urlencode(Token),
+ {<<
+ "<p>Client: \"", ClientId/binary, "\" asks permission for scope:\"", Scope2/binary, "\"</p>",
+ "<form action=\"", RedirectUri/binary, "#", TokenBin/binary, "&state=", Opaque/binary, "\" method=\"get\">",
+ "<input type=\"submit\" value=\"ok\" />",
+ "</form>",
+ "<form action=\"", RedirectUri/binary, "#error=access_denied&state=", Opaque/binary, "\" method=\"get\">",
+ "<input type=\"submit\" value=\"nak\" />",
+ "</form>"
+ >>, Req, State};
+ {error, scope} ->
+ fail(Req, State#state{data = <<"invalid_scope">>});
+ {error, _} ->
+ fail(Req, State#state{data = <<"unauthorized_client">>})
+ end.
+
+%%------------------------------------------------------------------------------
+%% Error Response
+%%------------------------------------------------------------------------------
+
+%% no redirect_uri is known or it's invalid -> respond with error
+fail(Req, State = #state{data = Error, redirect_uri = undefined}) ->
+ {ok, Req2} = cowboy_req:reply(400, [
+ {<<"content-type">>, <<"application/json; charset=UTF-8">>},
+ {<<"cache-control">>, <<"no-store">>},
+ {<<"pragma">>, <<"no-cache">>}
+ ], jsx:encode([{error, Error}]), Req),
+ {halt, Req2, State};
+%% redirect_uri is valid -> pass error to redirect_uri as fragment
+fail(Req, State = #state{data = Error, response_type = <<"token">>,
+ redirect_uri = RedirectUri, opaque = Opaque}) ->
+ % redirect to redirect URI with data urlencoded
+ {ok, Req2} = cowboy_req:reply(302, [
+ {<<"location">>, << RedirectUri/binary, $#,
+ (urlencode([
+ {error, Error},
+ {state, Opaque}
+ ]))/binary >>},
+ {<<"cache-control">>, <<"no-store">>},
+ {<<"pragma">>, <<"no-cache">>}
+ ], <<>>, Req),
+ {halt, Req2, State};
+%% redirect_uri is valid -> pass error to redirect_uri as querystring
+fail(Req, State = #state{data = Error,
+ redirect_uri = RedirectUri, opaque = Opaque}) ->
+ % redirect to redirect URI with data urlencoded
+ {ok, Req2} = cowboy_req:reply(302, [
+ {<<"location">>, << RedirectUri/binary, $?,
+ (urlencode([
+ {error, Error},
+ {state, Opaque}
+ ]))/binary >>},
+ {<<"cache-control">>, <<"no-store">>},
+ {<<"pragma">>, <<"no-cache">>}
+ ], <<>>, Req),
+ {halt, Req2, State}.
+
+%%------------------------------------------------------------------------------
+%% Access Token Request
+%%------------------------------------------------------------------------------
+
+post_is_create(Req, State) ->
+ {true, Req, State}.
+
+put_json(Req, State) ->
+ {ok, JSON, Req2} = cowboy_req:body(Req),
+ case jsx:decode(JSON, [{error_handler, fun(_, _, _) -> {error, badarg} end}])
+ of
+ {error, _} ->
+ {false, Req2, State};
+ {incomplete, _} ->
+ {false, Req2, State};
+ Data ->
+ request_token(Req2, State#state{data = Data})
+ end.
+
+put_form(Req, State) ->
{ok, Data, Req2} = cowboy_req:body_qs(Req),
- % State = key(<<"state">>, Data),
+ request_token(Req2, State#state{data = Data}).
+
+request_token(Req, State = #state{data = Data}) ->
+ % try
+ case lists:keyfind(<<"grant_type">>, 1, Data) of
+ {_, <<"authorization_code">>} ->
+ authorization_code_flow_stage2(Req, State);
+ {_, <<"refresh_token">>} ->
+ refresh_token(Req, State);
+ {_, <<"password">>} ->
+ password_credentials_flow(Req, State);
+ {_, <<"client_credentials">>} ->
+ client_credentials_flow(Req, State);
+ _ ->
+ fail(Req, State#state{data = <<"unsupported_grant_type">>})
+ % end
+ % catch _:_ ->
+ % fail(Req, State#state{data = <<"invalid_request">>})
+ end.
+
+%%------------------------------------------------------------------------------
+%% Access Token Response
+%%------------------------------------------------------------------------------
+
+%%
+%% Exchange authorization code for access token.
+%%
+authorization_code_flow_stage2(Req, State = #state{
+ data = Data, options = Opts
+ }) ->
ClientId = key(<<"client_id">>, Data),
ClientSecret = key(<<"client_secret">>, Data),
RedirectUri = key(<<"redirect_uri">>, Data),
- _GrantType = key(<<"grant_type">>, Data),
% decode token and ensure its validity
- % @todo State instead of _
- {ok, {_, ClientId, RedirectUri, Scope}} =
- % NB: code is expired after code_ttl seconds since issued
- termit:decode_base64(
- key(<<"code">>, Data),
- key(code_secret, Opts),
- key(code_ttl, Opts)
- ),
- % authorize client and get authorized scope
- {ok, Identity, Scope2} =
- authorize_client_credentials(ClientId, ClientSecret, Scope),
- % respond with token
- issue_token({Identity, Scope2}, Req2, Opts);
+ % NB: code is expired after code_ttl seconds since issued
+ case termit:decode_base64(
+ key(<<"code">>, Data),
+ key(code_secret, Opts),
+ key(code_ttl, Opts))
+ of
+ {ok, {_, ClientId, RedirectUri, Scope}} ->
+ % authorize client and get authorized scope
+ case authorize_client_credentials(ClientId, ClientSecret, Scope) of
+ {ok, Identity, Scope2} ->
+ % respond with token
+ % NB: can also issue refresh token
+ issue_token(Req, State, {Identity, Scope2}, Scope2, Opts);
+ {error, scope} ->
+ fail(Req, State#state{data = <<"invalid_scope">>});
+ {error, _} ->
+ fail(Req, State#state{data = <<"unauthorized_client">>})
+ end;
+ {error, _} ->
+ fail(Req, State#state{data = <<"invalid_grant">>})
+ end.
%%
-%% Catchall
+%% Refresh an access token.
%%
-handle_request(_, Req, _) ->
- {ok, Req2} = cowboy_req:reply(404, [], <<>>, Req),
- {ok, Req2, undefined}.
+refresh_token(Req, State = #state{data = Data, options = Opts}) ->
+ case termit:decode_base64(
+ key(<<"refresh_token">>, Data),
+ key(refresh_secret, Opts),
+ key(refresh_ttl, Opts))
+ of
+ {ok, {Identity, Scope}} ->
+ issue_token(Req, State, {Identity, Scope}, Scope, Opts);
+ {error, _} ->
+ fail(Req, State#state{data = <<"invalid_grant">>})
+ end.
+
+%%
+%% Request access token for a resource owner.
+%%
+password_credentials_flow(Req, State = #state{
+ data = Data, options = Opts
+ }) ->
+ % @todo ensure scheme is https
+ case authorize_username_password(
+ key(<<"username">>, Data),
+ key(<<"password">>, Data),
+ key(<<"scope">>, Data))
+ of
+ {ok, Identity, Scope} ->
+ issue_token(Req, State, {Identity, Scope}, Scope, Opts);
+ {error, scope} ->
+ fail(Req, State#state{data = <<"invalid_scope">>});
+ {error, _} ->
+ fail(Req, State#state{data = <<"invalid_client">>})
+ end.
%%
%% Request access code for a client.
%%
-handle_flow(client_id_and_secret, Data, Req, Opts) ->
- ClientId = key(<<"client_id">>, Data),
- RedirectUri = key(<<"redirect_uri">>, Data),
- % State = key(<<"state">>, Data),
- Scope = key(<<"scope">>, Data),
- % redirect URI fits the client?
- case verify_redirection_uri(ClientId, RedirectUri) of
- % yes
- ok ->
- % generate authorization code
- State = nonce(),
- Code = termit:encode_base64(
- {State, ClientId, RedirectUri, Scope},
- key(code_secret, Opts)),
- % show authorization form
- {M, F} = key(authorization_form, Opts),
- M:F(Code, RedirectUri, State, Req);
- % no
- {error, mismatch} ->
- % return error
- cowboy_req:reply(302, [
- {<<"location">>, << RedirectUri/binary, $?, "error=redirect_uri" >>}
- ], Req)
- end;
+client_credentials_flow(Req, State = #state{
+ data = Data, options = Opts
+ }) ->
+ % @todo ensure scheme is https
+ case authorize_client_credentials(
+ key(<<"client_id">>, Data),
+ key(<<"client_secret">>, Data),
+ key(<<"scope">>, Data))
+ of
+ {ok, Identity, Scope} ->
+ issue_token(Req, State, {Identity, Scope}, Scope, Opts);
+ {error, scope} ->
+ fail(Req, State#state{data = <<"invalid_scope">>});
+ {error, _} ->
+ fail(Req, State#state{data = <<"invalid_client">>})
+ end.
%%
-%% Request access token for a user.
+%% Respond with access token.
%%
-handle_flow(username_and_password, Data, Req, Opts) ->
- {ok, Identity, Scope} = authorize_username_password(
- key(<<"username">>, Data),
- key(<<"password">>, Data),
- key(<<"scope">>, Data)),
- issue_token({Identity, Scope}, Req, Opts).
+issue_token(Req, State, Context, Scope, Opts) ->
+ {ok, Req2} = cowboy_req:reply(200, [
+ {<<"content-type">>, <<"application/json; charset=UTF-8">>},
+ {<<"cache-control">>, <<"no-store">>},
+ {<<"pragma">>, <<"no-cache">>}
+ ], jsx:encode(token(Context, Scope, Opts)), Req),
+ {halt, Req2, State}.
+
+token(Data, Scope, Opts) ->
+ AccessToken = termit:encode_base64(Data, key(token_secret, Opts)),
+ [
+ {access_token, AccessToken},
+ {token_type, <<"Bearer">>},
+ {expires_in, key(token_ttl, Opts)},
+ {scope, Scope}
+ ].
+
+token(Data, Scope, Opts, with_refresh) ->
+ AccessToken = termit:encode_base64(Data, key(token_secret, Opts)),
+ RefreshToken = termit:encode_base64(Data, key(refresh_secret, Opts)),
+ [
+ {access_token, AccessToken},
+ {token_type, <<"Bearer">>},
+ {expires_in, key(token_ttl, Opts)},
+ {scope, Scope},
+ {refresh_token, RefreshToken}
+ ].
%%
-%%------------------------------------------------------------------------------
+%% -----------------------------------------------------------------------------
%% Helpers
-%%------------------------------------------------------------------------------
+%% -----------------------------------------------------------------------------
%%
key(Key, List) ->
{_, Value} = lists:keyfind(Key, 1, List),
Value.
-nonce() ->
- base64:encode(crypto:strong_rand_bytes(16)).
+urlencode(Bin) when is_binary(Bin) ->
+ cowboy_http:urlencode(Bin);
+urlencode(Atom) when is_atom(Atom) ->
+ urlencode(atom_to_binary(Atom, latin1));
+urlencode(Int) when is_integer(Int) ->
+ urlencode(list_to_binary(integer_to_list(Int)));
+urlencode({K, undefined}) ->
+ << (urlencode(K))/binary, $= >>;
+urlencode({K, V}) ->
+ << (urlencode(K))/binary, $=, (urlencode(V))/binary >>;
+urlencode(List) when is_list(List) ->
+ binary_join([urlencode(X) || X <- List], << $& >>).
-issue_token(Data, Req, Opts) ->
- Token = termit:encode_base64(Data, key(token_secret, Opts)),
- cowboy_req:reply(200, [
- {<<"content-type">>, <<"application/json">>}
- ], jsx:encode([
- {access_token, Token},
- {token_type, <<"Bearer">>},
- {expires_in, key(token_ttl, Opts)}
- ]), Req).
+binary_join([], _Sep) ->
+ <<>>;
+binary_join([H], _Sep) ->
+ << H/binary >>;
+binary_join([H | T], Sep) ->
+ << H/binary, Sep/binary, (binary_join(T, Sep))/binary >>.
%%
%%------------------------------------------------------------------------------
@@ -142,13 +415,24 @@ issue_token(Data, Req, Opts) ->
%%
% ok | {error, mismatch}
+authorize_username_password(undefined, _Password, _Scope) ->
+ {error, mismatch};
authorize_username_password(Username, _Password, Scope) ->
{ok, {user, Username}, Scope}.
% ok | {error, mismatch}
+authorize_client_credentials(undefined, _ClientSecret, _Scope) ->
+ {error, mismatch};
+authorize_client_credentials(_ClientId, _ClientSecret, undefined) ->
+ {error, scope};
+authorize_client_credentials(_ClientId, _ClientSecret, <<"foo">>) ->
+ {error, scope};
authorize_client_credentials(ClientId, _ClientSecret, Scope) ->
{ok, {client, ClientId}, Scope}.
-% ok | {error, mismatch}
+% ok | {error, mismatch} | {error, badarg}
+verify_redirection_uri(undefined, _RedirectUri) ->
+ {error, badarg};
verify_redirection_uri(_ClientId, _RedirectUri) ->
ok.
+ % {error, mismatch}.
View
57 src/cowboy_social_vkontakte.erl
@@ -6,59 +6,22 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << "https://oauth.vk.com/authorize", $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {response_type, <<"code">>},
- {scope, << "uid,first_name,last_name,sex,photo ",
- (key(scope, Opts))/binary >>}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- {ok, Auth} = cowboy_request:post_for_json(
- <<"https://oauth.vk.com/access_token">>, [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {grant_type, <<"authorization_code">>}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, <<"Bearer">>},
- {expires_in, key(<<"expires_in">>, Auth)}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, Opts) ->
+user_profile(Auth, Opts) ->
{ok, Profiles} = cowboy_request:get_json(
<<"https://api.vk.com/method/users.get">>, [
- {access_token, key(access_token, Auth)},
- {fields, key(scope, Opts)}
+ {access_token, Auth},
+ {fields, key(scope, Opts, <<"uid,first_name,last_name,sex,photo">>)}
]),
% NB: provider returns list of data for uids; we need only the first
+ false = lists:keyfind(<<"error">>, 1, Profiles),
[Profile] = key(<<"response">>, Profiles),
+pecypc_log:info({prof, Profile}),
{ok, [
{id, << "vkontakte:",
(list_to_binary(integer_to_list(key(<<"uid">>, Profile))))/binary >>},
@@ -66,9 +29,10 @@ get_user_profile(Auth, Opts) ->
% {email, key(<<"email">>, Profile)},
{name, << (key(<<"first_name">>, Profile))/binary, " ",
(key(<<"last_name">>, Profile))/binary >>},
- {avatar, key(<<"photo">>, Profile)},
+ {picture, key(<<"photo">>, Profile)},
{gender, case key(<<"sex">>, Profile) of
- 1 -> <<"female">>; _ -> <<"male">> end}
+ 1 -> <<"female">>; _ -> <<"male">> end},
+ {raw, Profile}
]}.
%%
@@ -78,7 +42,8 @@ get_user_profile(Auth, Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
key(Key, List, Def) ->
case lists:keyfind(Key, 1, List) of
View
55 src/cowboy_social_yandex.erl
@@ -6,64 +6,28 @@
-author('Vladimir Dronnikov <dronnikov@gmail.com>').
-export([
- get_authorize_url/1,
- get_access_token/2,
- get_user_profile/2
+ user_profile/2
]).
%%
-%%------------------------------------------------------------------------------
-%% OAUTH2 Application flow
-%%------------------------------------------------------------------------------
-%%
-
-%%
-%% get URL of provider authorization page
-%%
-get_authorize_url(Opts) ->
- << "https://oauth.yandex.ru/authorize", $?,
- (cowboy_request:urlencode([
- {client_id, key(client_id, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {response_type, <<"code">>},
- {scope, key(scope, Opts)}
- ]))/binary >>.
-
-%%
-%% exchange authorization code for auth token
-%%
-get_access_token(Code, Opts) ->
- {ok, Auth} = cowboy_request:post_for_json(
- <<"https://oauth.yandex.ru/token">>, [
- {code, Code},
- {client_id, key(client_id, Opts)},
- {client_secret, key(client_secret, Opts)},
- {redirect_uri, key(callback_uri, Opts)},
- {grant_type, <<"authorization_code">>}
- ]),
- {ok, [
- {access_token, key(<<"access_token">>, Auth)},
- {token_type, key(<<"token_type">>, Auth)},
- {expires_in, 0}
- ]}.
-
-%%
%% extract info from user profile
%%
-get_user_profile(Auth, _Opts) ->
+user_profile(Auth, _Opts) ->
{ok, Profile} = cowboy_request:get_json(
<<"https://login.yandex.ru/info">>, [
- {oauth_token, key(access_token, Auth)},
+ {oauth_token, Auth},
{format, <<"json">>}
]),
+ true = Profile =/= [],
{ok, [
{id, << "yandex:", (key(<<"id">>, Profile))/binary >>},
{provider, <<"yandex">>},
{email, key(<<"default_email">>, Profile)},
- {name, key(<<"real_name">>, Profile)},
- % {avatar, key(<<"picture">>, Profile)},
+ {name, key(<<"display_name">>, Profile)},
+ {picture, key(<<"picture">>, Profile, null)},
{gender, case key(<<"sex">>, Profile) of
- 1 -> <<"female">>; _ -> <<"male">> end}
+ 1 -> <<"female">>; _ -> <<"male">> end},
+ {raw, Profile}
]}.
%%
@@ -73,7 +37,8 @@ get_user_profile(Auth, _Opts) ->
%%
key(Key, List) ->
- key(Key, List, <<>>).
+ {_, Value} = lists:keyfind(Key, 1, List),
+ Value.
key(Key, List, Def) ->
case lists:keyfind(Key, 1, List) of
Please sign in to comment.
Something went wrong with that request. Please try again.