Skip to content

Commit

Permalink
Refactor common code
Browse files Browse the repository at this point in the history
  • Loading branch information
dvv committed Mar 20, 2013
1 parent 574627e commit e67610c
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 676 deletions.
44 changes: 13 additions & 31 deletions README.md
Expand Up @@ -18,46 +18,28 @@ Router configuration
--------------

```erlang
{"/auth/google/:action", cowboy_social_google, [
{"/auth/google/:action", cowboy_social, [
{provider, cowboy_social_google},
% At the end of the flow this handler will be called as
% Mod:Fun({ok, Auth, Profile}, Req) or Mod:Fun({error, Reason}, Req)
{handler, {Mod, Fun}},
{client_id, <<"440647648374.apps.googleusercontent.com">>},
{client_id, <<"...">>},
{client_secret, <<"...">>},
{scope, <<"https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile">>},
{callback_uri, <<"/auth/google/callback">>}
]}.
{"/auth/github/:action", cowboy_social_github, [
{handler, {Mod, Fun}},
{client_id, <<"883b68d607abddc24f77">>},
{client_secret, <<"...">>},
{scope, <<>>},
{callback_uri, <<"/auth/github/callback">>}
]}.
{"/auth/yandex/:action", cowboy_social_yandex, [
{handler, {Mod, Fun}},
{client_id, <<"f44bd59ddfbe408ab1d29151126385a6">>},
{client_secret, <<"...">>},
{scope, <<>>},
{callback_uri, <<"/auth/yandex/callback">>}
]}.
{"/auth/vkontakte/:action", cowboy_social_vkontakte, [
{handler, {Mod, Fun}},
{client_id, <<"3473116">>},
{client_secret, <<"...">>},
{scope, <<"uid,first_name,last_name,sex,photo">>},
{callback_uri, <<"/auth/vkontakte/callback">>}
]}.
{"/auth/mailru/:action", cowboy_social_mailru, [
{handler, {Mod, Fun}},
{client_id, <<"701614">>},
{client_secret, <<"...">>},
{secret_key, <<"...">>},
{scope, <<>>},
{callback_uri, <<"/auth/mailru/callback">>}
]}.
```

Supported providers
--------------
- Github
- Google
- Mail.ru
- PayPal
- Vkontakte
- Yandex
- add more, this very simple

License (MIT)
-------

Expand Down
2 changes: 1 addition & 1 deletion src/cowboy_request.erl
Expand Up @@ -56,7 +56,7 @@ request(Method, URL, Headers, Body) ->
% pecypc_log:info({reqerr, _Else}),
{error, failed}
end,
% pecypc_log:info({res, _ResHeaders, Result}),
% pecypc_log:info({res, Result}),
Result.

urlencode(Bin) when is_binary(Bin) ->
Expand Down
86 changes: 30 additions & 56 deletions src/cowboy_social.erl
Expand Up @@ -15,99 +15,73 @@
%%

init(_Transport, Req, Opts) ->
{ok, Req, Opts}.
% compose full redirect URI
case key(callback_uri, Opts) of
<< "http://", _/binary >> -> {ok, Req, Opts};
<< "https://", _/binary >> -> {ok, Req, Opts};
Relative ->
{SelfUri, Req2} = cowboy_req:host_url(Req),
{ok, Req2, lists:keyreplace(callback_uri, 1, Opts,
{callback_uri, << SelfUri/binary, Relative/binary >>})}
end.

terminate(_Reason, _Req, _State) ->
ok.

%%
%% {"/auth/:provider/:action", cowboy_social, [...]}.
%%
handle(Req, Opts) ->
Provider = key(provider, Opts),
% extract flow action name
{Action, Req2} = cowboy_req:binding(action, Req),
% construct callback URI
{SelfUri, Req3} = cowboy_req:host_url(Req2),
[FullPath] = cowboy_req:get([path], Req3),
CallbackUrl = << SelfUri/binary, FullPath/binary >>,
% perform flow action
{ok, Req4} = handle_request(Action, Provider, Opts, CallbackUrl, Req3),
{ok, Req4, undefined}.
{ok, Req3} = handle_request(Action, Req2, Opts),
{ok, Req3, undefined}.

%%
%% redirect to provider authorization page, expect it to redirect
%% to our next handler
%%
handle_request(<<"login">>, P, O, U, Req) ->
AuthUrl = << (cowboy_social_providers:authorize_url(P))/binary, $?,
(cowboy_request:urlencode([
{<<"client_id">>, key(client_id, O)},
{<<"redirect_uri">>, binary:replace(U, <<"/login">>, <<"/callback">>)},
{<<"response_type">>, <<"code">>},
{<<"scope">>, key(scope, O)}
]))/binary >>,
cowboy_req:reply(303, [{<<"location">>, AuthUrl}], <<>>, Req);
handle_request(<<"login">>, Req, Opts) ->
cowboy_req:reply(302, [
{<<"location">>, (key(provider, Opts)):get_authorize_url(Opts)}
], <<>>, Req);

%%
%% provider redirected back to us with authorization code
%%
handle_request(<<"callback">>, P, O, U, Req) ->
handle_request(<<"callback">>, Req, Opts) ->
case cowboy_req:qs_val(<<"code">>, Req) of
{undefined, Req2} ->
finish({error, nocode}, Req2);
finish({error, nocode}, Req2, Opts);
{Code, Req2} ->
get_access_token(P, O, U, Code, Req2)
get_access_token(Code, Req2, Opts)
end;

%%
%% catchall
%%
handle_request(_, _, _, _, Req) ->
handle_request(_, Req, _) ->
{ok, Req2} = cowboy_req:reply(404, [], <<>>, Req),
{ok, Req2, undefined}.

%%
%% exchange authorization code for auth token
%%
get_access_token(P, O, U, Code, Req) ->
case cowboy_request:post_for_json(cowboy_social_providers:token_url(P), [
{<<"code">>, Code},
{<<"client_id">>, key(client_id, O)},
{<<"client_secret">>, key(client_secret, O)},
{<<"redirect_uri">>, U},
{<<"grant_type">>, <<"authorization_code">>}
])
of
{ok, Auth} ->
get_user_profile(P, O, Auth, Req);
_ ->
finish({error, notoken}, Req)
end.
get_access_token(Code, Req, Opts) ->
{ok, Auth} = (key(provider, Opts)):get_access_token(Code, Opts),
get_user_profile(Auth, Req, Opts).

%%
%% use auth tocken to extract info from user profile
%%
get_user_profile(P, O, Auth, Req) ->
AccessToken = key(<<"access_token">>, Auth),
case cowboy_request:get_json(cowboy_social_providers:profile_url(P), [
{<<"access_token">>, AccessToken}
| cowboy_social_providers:custom_data(P, AccessToken, O)
])
of
{ok, Profile} ->
finish({ok, cowboy_social_providers:normalize_profile(P, Auth, Profile)},
Req);
_ ->
finish({error, noprofile}, Req)
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).

%%
%% finalize application flow by calling callback handler
%%
finish(Status, Req) ->
{{M, F}, Req2} = cowboy_req:meta(callback, Req),
M:F(Status, Req2).
finish(Status, Req, Opts) ->
{M, F} = key(handler, Opts),
M:F(Status, Req).

%%
%%------------------------------------------------------------------------------
Expand Down
154 changes: 40 additions & 114 deletions src/cowboy_social_github.erl
Expand Up @@ -5,104 +5,62 @@
-module(cowboy_social_github).
-author('Vladimir Dronnikov <dronnikov@gmail.com>').

-behaviour(cowboy_http_handler).
-export([init/3, terminate/3, handle/2]).
-export([
get_authorize_url/1,
get_access_token/2,
get_user_profile/2
]).

%%
%%------------------------------------------------------------------------------
%% OAUTH2 Application flow
%%------------------------------------------------------------------------------
%%

init(_Transport, Req, Opts) ->
% compose full redirect URI
{SelfUri, Req2} = cowboy_req:host_url(Req),
{ok, Req2, lists:keyreplace(callback_uri, 1, Opts,
{callback_uri, << SelfUri/binary, (key(callback_uri, Opts))/binary >>})}.

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}.

%%
%% redirect to provider authorization page, expect it to redirect
%% to our next handler
%%
handle_request(<<"login">>, Req, Opts) ->
AuthUrl = << (authorize_url())/binary, $?,
(cowboy_request:urlencode([
{<<"client_id">>, key(client_id, Opts)},
{<<"redirect_uri">>, key(callback_uri, Opts)},
{<<"scope">>, key(scope, Opts)}
]))/binary >>,
cowboy_req:reply(303, [{<<"location">>, AuthUrl}], <<>>, Req);

%%
%% provider redirected back to us with authorization code
%%
handle_request(<<"callback">>, Req, Opts) ->
case cowboy_req:qs_val(<<"code">>, Req) of
{undefined, Req2} ->
finish({error, nocode}, Req2, Opts);
{Code, Req2} ->
get_access_token(Code, Req2, Opts)
end;

%%
%% catchall
%% get URL of provider authorization page
%%
handle_request(_, Req, _) ->
{ok, Req2} = cowboy_req:reply(404, [], <<>>, Req),
{ok, Req2, undefined}.
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, Req, Opts) ->
try cowboy_request:post_for_json(token_url(), [
{<<"code">>, Code},
{<<"client_id">>, key(client_id, Opts)},
{<<"client_secret">>, key(client_secret, Opts)},
{<<"redirect_uri">>, key(callback_uri, Opts)}
])
of
{ok, Auth} ->
get_user_profile(Auth, Req, Opts);
_ ->
finish({error, notoken}, Req, Opts)
catch _:_ ->
finish({error, notoken}, Req, Opts)
end.

%%
%% use auth token to extract info from user profile
%%
get_user_profile(Auth, Req, Opts) ->
AccessToken = key(<<"access_token">>, Auth),
try cowboy_request:get_json(profile_url(), [
{<<"access_token">>, AccessToken}
])
of
{ok, Profile} ->
finish({ok, normalize_auth(Auth), normalize_profile(Profile)}, Req, Opts);
_ ->
finish({error, noprofile}, Req, Opts)
catch _:_ ->
finish({error, noprofile}, Req, Opts)
end.
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}
]}.

%%
%% finalize application flow by calling callback handler
%% extract info from user profile
%%
finish(Status, Req, Opts) ->
{M, F} = key(handler, Opts),
M:F(Status, Req).
get_user_profile(Auth, _Opts) ->
{ok, Profile} = cowboy_request:get_json(
<<"https://api.github.com/user">>, [
{access_token, key(access_token, Auth)}
]),
{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)}
]}.

%%
%%------------------------------------------------------------------------------
Expand All @@ -113,35 +71,3 @@ finish(Status, Req, Opts) ->
key(Key, List) ->
{_, Value} = lists:keyfind(Key, 1, List),
Value.

%%
%%------------------------------------------------------------------------------
%% Provider details
%%------------------------------------------------------------------------------
%%

authorize_url() ->
<<"https://github.com/login/oauth/authorize">>.

token_url() ->
<<"https://github.com/login/oauth/access_token">>.

profile_url() ->
<<"https://api.github.com/user">>.

normalize_auth(Auth) ->
[
{access_token, key(<<"access_token">>, Auth)},
{token_type, key(<<"token_type">>, Auth)},
{expires_in, 0}
].

normalize_profile(Raw) ->
[
{id, << "github:",
(list_to_binary(integer_to_list(key(<<"id">>, Raw))))/binary >>},
{provider, <<"github">>},
{email, key(<<"email">>, Raw)},
{name, key(<<"name">>, Raw)},
{avatar, key(<<"avatar_url">>, Raw)}
].

0 comments on commit e67610c

Please sign in to comment.