Browse files

Add refresh_token support

  • Loading branch information...
1 parent b5623b4 commit 44bb4cd9cf99ce82584800d43ab21e5907e77645 @bipthelin bipthelin committed Nov 27, 2012
Showing with 224 additions and 5 deletions.
  1. +1 −1 rebar.tests.config
  2. +32 −0 src/oauth2.erl
  3. +31 −0 src/oauth2_backend.erl
  4. +47 −1 src/oauth2_response.erl
  5. +113 −3 test/oauth2_tests.erl
View
2 rebar.tests.config
@@ -28,7 +28,7 @@
{clean_files, [".eunit", "ebin/*.beam", "test/*.beam"]}.
{erl_opts, [
- bin_opt_info,
+ %% bin_opt_info,
warn_format,
warn_export_all,
warn_export_vars,
View
32 src/oauth2.erl
@@ -35,6 +35,7 @@
,verify_access_token/1
,verify_access_code/1
,verify_access_code/2
+ ,verify_refresh_issue_access_token/1
,verify_redirection_uri/2
]).
@@ -216,6 +217,36 @@ verify_access_code(AccessCode, Identity) ->
Error -> Error
end.
+%% @doc Verifies an refresh token RefreshToken, returning a new Access Token
+%% if successful. Otherwise, an OAuth2 error code is returned.
+%% @end
+-spec verify_refresh_issue_access_token(RefreshToken)
+ -> {ok, Identity, Response}
+ | {error, Reason} when
+ RefreshToken :: token(),
+ Identity :: term(),
+ Response :: oauth2_response:response(),
+ Reason :: error().
+verify_refresh_issue_access_token(RefreshToken) ->
+ case oauth2_backend:resolve_refresh_token(RefreshToken) of
+ {ok, Context} ->
+ {_, ExpiryAbsolute} = lists:keyfind(<<"expiry_time">>, 1, Context),
+ case ExpiryAbsolute > seconds_since_epoch(0) of
+ true ->
+ {_, Identity} = lists:keyfind(<<"identity">>, 1, Context),
+ {_, ResOwner} = lists:keyfind(<<"resource_owner">>, 1, Context),
+ {_, Scope} = lists:keyfind(<<"scope">>, 1, Context),
+ TTL = oauth2_config:expiry_time(password_credentials),
+ Response = issue_token(Identity, ResOwner, Scope, TTL),
+ {ok, Identity, Response};
+ false ->
+ oauth2_backend:revoke_refresh_token(RefreshToken),
+ {error, access_denied}
+ end;
+ _ ->
+ {error, access_denied}
+ end.
+
%% @doc Verifies an access token AccessToken, returning its associated
%% context if successful. Otherwise, an OAuth2 error code is returned.
%% @end
@@ -282,6 +313,7 @@ issue_token_and_refresh(Identity, ResOwner, Scope, TTL) ->
ExpiryAbsolute = seconds_since_epoch(TTL),
Context = build_context(Identity, ExpiryAbsolute, ResOwner, Scope),
ok = oauth2_backend:associate_access_token(AccessToken, Context),
+ ok = oauth2_backend:associate_refresh_token(RefreshToken, Context),
oauth2_response:new(AccessToken, TTL, ResOwner, Scope, RefreshToken).
-spec issue_token(Identity, ResOwner, Scope, TTL) -> oauth2_response:response() when
View
31 src/oauth2_backend.erl
@@ -31,11 +31,14 @@
authenticate_username_password/3
,authenticate_client/3
,associate_access_token/2
+ ,associate_refresh_token/2
,associate_access_code/2
,resolve_access_token/1
,resolve_access_code/1
+ ,resolve_refresh_token/1
,revoke_access_token/1
,revoke_access_code/1
+ ,revoke_refresh_token/1
,get_redirection_uri/1
]).
@@ -96,6 +99,17 @@ associate_access_code(AccessCode, Context) ->
associate_access_token(AccessToken, Context) ->
?BACKEND:associate_access_token(AccessToken, Context).
+%% @doc Stores a new refresh token RefreshToken, associating it with Context.
+%% The context is a proplist carrying information about the identity
+%% with which the token is associated, when it expires, etc.
+%% @end
+-spec associate_refresh_token(RefreshToken, Context) -> ok | {error, Reason} when
+ RefreshToken :: oauth2:token(),
+ Context :: proplist(atom(), term()),
+ Reason :: notfound.
+associate_refresh_token(RefreshToken, Context) ->
+ ?BACKEND:associate_refresh_token(RefreshToken, Context).
+
%% @doc Looks up an access token AccessToken, returning the corresponding
%% context if a match is found.
%% @end
@@ -116,6 +130,16 @@ resolve_access_token(AccessToken) ->
resolve_access_code(AccessCode) ->
?BACKEND:resolve_access_code(AccessCode).
+%% @doc Looks up an refresh token RefreshToken, returning the corresponding
+%% context if a match is found.
+%% @end
+-spec resolve_refresh_token(RefreshToken) -> {ok, Context} | {error, Reason} when
+ RefreshToken :: oauth2:token(),
+ Context :: proplist(atom(), term()),
+ Reason :: notfound.
+resolve_refresh_token(RefreshToken) ->
+ ?BACKEND:resolve_refresh_token(RefreshToken).
+
%% @doc Revokes an access token AccessToken, so that it cannot be used again.
-spec revoke_access_token(AccessToken) -> ok | {error, Reason} when
AccessToken :: oauth2:token(),
@@ -130,6 +154,13 @@ revoke_access_token(AccessToken) ->
revoke_access_code(AccessCode) ->
?BACKEND:revoke_access_code(AccessCode).
+%% @doc Revokes an refresh token RefreshToken, so that it cannot be used again.
+-spec revoke_refresh_token(RefreshToken) -> ok | {error, Reason} when
+ RefreshToken :: oauth2:token(),
+ Reason :: notfound.
+revoke_refresh_token(RefreshToken) ->
+ ?BACKEND:revoke_refresh_token(RefreshToken).
+
%% @doc Returns the redirection URI associated with the client ClientId.
-spec get_redirection_uri(ClientId) -> Result when
ClientId :: binary(),
View
48 src/oauth2_response.erl
@@ -70,75 +70,121 @@
%%% API functions
%%%===================================================================
+-spec new(AccessToken :: oauth2:token()) -> response().
new(AccessToken) ->
#response{access_token = AccessToken}.
+-spec new(AccessToken, ExpiresIn) -> response() when
+ AccessToken :: oauth2:token(),
+ ExpiresIn :: oauth2:lifetime().
new(AccessToken, ExpiresIn) ->
#response{access_token = AccessToken, expires_in = ExpiresIn}.
+-spec new(AccessToken, ExpiresIn, ResOwner, Scope) -> response() when
+ AccessToken :: oauth2:token(),
+ ExpiresIn :: oaut2:lifetime(),
+ ResOwner :: term(),
+ Scope :: oauth2:scope().
new(AccessToken, ExpiresIn, ResOwner, Scope) ->
#response{access_token = AccessToken,
expires_in = ExpiresIn,
resource_owner = ResOwner,
scope = Scope}.
+-spec new(AccessToken, ExpiresIn, ResOwner, Scope, RefreshToken) -> response() when
+ AccessToken :: oauth2:token(),
+ ExpiresIn :: oaut2:lifetime(),
+ ResOwner :: term(),
+ Scope :: oauth2:scope(),
+ RefreshToken :: oauth2:token().
new(AccessToken, ExpiresIn, ResOwner, Scope, RefreshToken) ->
#response{access_token = AccessToken,
expires_in = ExpiresIn,
resource_owner = ResOwner,
scope = Scope,
refresh_token = RefreshToken}.
-new(_, ExpiresIn, ResOwner, Scope, _, AccessCode) ->
+-spec new(_AccessToken, ExpiresIn, ResOwner, Scope, _RefreshToken, AccessCode) -> response() when
+ _AccessToken :: oauth2:token(),
+ ExpiresIn :: oaut2:lifetime(),
+ ResOwner :: term(),
+ Scope :: oauth2:scope(),
+ _RefreshToken :: oauth2:token(),
+ AccessCode :: oauth2:token().
+new(_AccessToken, ExpiresIn, ResOwner, Scope, _RefreshToken, AccessCode) ->
#response{access_code = AccessCode,
expires_in = ExpiresIn,
resource_owner = ResOwner,
scope = Scope}.
+-spec access_token(response()) -> {ok, AccessToken} | {error, not_set} when
+ AccessToken :: oauth2:token().
access_token(#response{access_token = undefined}) ->
{error, not_set};
access_token(#response{access_token = AccessToken}) ->
{ok, AccessToken}.
+-spec access_token(response(), NewAccessToken) -> response() when
+ NewAccessToken :: oauth2:token().
access_token(Response, NewAccessToken) ->
Response#response{access_token = NewAccessToken}.
+-spec access_code(response()) -> {ok, AccessToken :: oauth2:token()}.
access_code(#response{access_code = AccessCode}) ->
{ok, AccessCode}.
+-spec access_code(response(), NewAccessCode) -> response() when
+ NewAccessCode :: oauth2:token().
access_code(Response, NewAccessCode) ->
Response#response{access_code = NewAccessCode}.
+-spec expires_in(response()) -> {ok, ExpiresIn} | {error, not_set} when
+ ExpiresIn :: oauth2:lifetime().
expires_in(#response{expires_in = undefined}) ->
{error, not_set};
expires_in(#response{expires_in = ExpiresIn}) ->
{ok, ExpiresIn}.
+-spec expires_in(response(), NewExpiresIn) -> response() when
+ NewExpiresIn :: oauth2:lifetime().
expires_in(Response, NewExpiresIn) ->
Response#response{expires_in = NewExpiresIn}.
+-spec scope(response()) -> {ok, Scope} | {error, not_set} when
+ Scope :: oauth2:scope().
scope(#response{scope = undefined}) ->
{error, not_set};
scope(#response{scope = Scope}) ->
{ok, Scope}.
+-spec scope(response(), NewScope) -> response() when
+ NewScope :: oauth2:scope().
scope(Response, NewScope) ->
Response#response{scope = NewScope}.
+-spec refresh_token(response()) -> {ok, RefreshToken} | {error, not_set} when
+ RefreshToken :: oauth2:token().
refresh_token(#response{refresh_token = undefined}) ->
{error, not_set};
refresh_token(#response{refresh_token = RefreshToken}) ->
{ok, RefreshToken}.
+-spec refresh_token(response(), NewRefreshToken) -> response() when
+ NewRefreshToken :: oauth2:token().
refresh_token(Response, NewRefreshToken) ->
Response#response{refresh_token = NewRefreshToken}.
+-spec resource_owner(response()) -> {ok, ResOwner} when
+ ResOwner :: term().
resource_owner(#response{resource_owner = ResOwner}) ->
{ok, ResOwner}.
+-spec resource_owner(response(), NewResOwner) -> response() when
+ NewResOwner :: term().
resource_owner(Response, NewResOwner) ->
Response#response{resource_owner = NewResOwner}.
+-spec to_proplist(response()) -> oauth2:proplist().
to_proplist(Response) ->
Keys = lists:map(fun to_binary/1, record_info(fields, response)),
Values = tl(tuple_to_list(Response)), %% Head is 'response'!
View
116 test/oauth2_tests.erl
@@ -98,6 +98,53 @@ bad_authorize_client_credentials_test_() ->
]
end}.
+bad_ttl_test_() ->
+ {setup,
+ fun () ->
+ meck:new(oauth2_backend),
+ meck:expect(oauth2_backend,
+ resolve_access_code,
+ fun(_) -> {ok, [{<<"identity">>, <<"123">>},
+ {<<"resource_owner">>, <<>>},
+ {<<"expiry_time">>, 123},
+ {<<"scope">>, <<>>}]}
+ end),
+ meck:expect(oauth2_backend, revoke_access_code, fun(_) -> ok end),
+ meck:expect(oauth2_backend,
+ resolve_access_token,
+ fun(_) -> {ok, [{<<"identity">>, <<"123">>},
+ {<<"resource_owner">>, <<>>},
+ {<<"expiry_time">>, 123},
+ {<<"scope">>, <<>>}]}
+ end),
+ meck:expect(oauth2_backend, revoke_access_token, fun(_) -> ok end),
+ meck:expect(oauth2_backend,
+ resolve_refresh_token,
+ fun(_) -> {ok, [{<<"identity">>, <<"123">>},
+ {<<"resource_owner">>, <<>>},
+ {<<"expiry_time">>, 123},
+ {<<"scope">>, <<>>}]}
+ end),
+ meck:expect(oauth2_backend, revoke_refresh_token, fun(_) -> ok end),
+ ok
+ end,
+ fun (_) ->
+ meck:unload(oauth2_backend)
+ end,
+ fun(_) ->
+ [
+ ?_assertMatch({error, invalid_grant},
+ oauth2:verify_access_code(
+ <<"XoaUdYODRCMyLkdaKkqlmhsl9QQJ4b">>)),
+ ?_assertMatch({error, access_denied},
+ oauth2:verify_access_token(
+ <<"TiaUdYODLOMyLkdaKkqlmdhsl9QJ94a">>)),
+ ?_assertMatch({error, access_denied},
+ oauth2:verify_refresh_issue_access_token(
+ <<"TiaUdYODLOMyLkdaKkqlmdhsl9QJ94a">>))
+ ]
+ end}.
+
verify_access_token_test_() ->
{setup,
fun start/0,
@@ -117,6 +164,31 @@ verify_access_token_test_() ->
]
end}.
+bad_access_code_test_() ->
+ {setup,
+ fun start/0,
+ fun stop/1,
+ fun(_) ->
+ [
+ fun() ->
+ {error, access_denied} = oauth2:issue_code_grant(
+ ?CLIENT_ID,
+ ?CLIENT_SECRET,
+ <<"http://in.val.id">>,
+ ?RESOURCE_OWNER,
+ ?CLIENT_SCOPE),
+ {error, unauthorized_client} = oauth2:issue_code_grant(
+ <<"XoaUdYODRCMyLkdaKkqlmhsl9QQJ4b">>,
+ ?CLIENT_SECRET,
+ ?CLIENT_URI,
+ ?RESOURCE_OWNER,
+ ?CLIENT_SCOPE),
+ ?_assertMatch({error, invalid_grant},
+ oauth2:verify_access_code(<<"nonexistent_token">>))
+ end
+ ]
+ end}.
+
verify_access_code_test_() ->
{setup,
fun start/0,
@@ -141,9 +213,34 @@ verify_access_code_test_() ->
?CLIENT_URI),
{ok, Token} = oauth2_response:access_token(Response2),
?assertMatch({ok, _}, oauth2:verify_access_token(Token))
- end,
- ?_assertMatch({error, invalid_grant},
- oauth2:verify_access_code(<<"nonexistent_token">>))
+ end
+ ]
+ end}.
+
+verify_refresh_token_test_() ->
+ {setup,
+ fun start/0,
+ fun stop/1,
+ fun(_) ->
+ [
+ fun() ->
+ {ok, _, Response} = oauth2:issue_code_grant(
+ ?CLIENT_ID,
+ ?CLIENT_SECRET,
+ ?CLIENT_URI,
+ ?RESOURCE_OWNER,
+ ?CLIENT_SCOPE),
+ {ok, Code} = oauth2_response:access_code(Response),
+ {ok, _, Response2} = oauth2:authorize_code_grant(
+ ?CLIENT_ID,
+ ?CLIENT_SECRET,
+ Code,
+ ?CLIENT_URI),
+ {ok, RefreshToken} = oauth2_response:refresh_token(Response2),
+ {ok, _, _Response3} = oauth2:verify_refresh_issue_access_token(RefreshToken),
+ {ok, Token} = oauth2_response:access_token(Response2),
+ ?assertMatch({ok, _}, oauth2:verify_access_token(Token))
+ end
]
end}.
@@ -186,12 +283,18 @@ start() ->
associate_access_token,
fun associate_access_token/2),
meck:expect(oauth2_backend,
+ associate_refresh_token,
+ fun associate_refresh_token/2),
+ meck:expect(oauth2_backend,
associate_access_code,
fun associate_access_code/2),
meck:expect(oauth2_backend,
resolve_access_token,
fun resolve_access_token/1),
meck:expect(oauth2_backend,
+ resolve_refresh_token,
+ fun resolve_refresh_token/1),
+ meck:expect(oauth2_backend,
revoke_access_token,
fun revoke_access_token/1),
meck:expect(oauth2_backend,
@@ -236,13 +339,20 @@ authenticate_client(_, _, _) ->
associate_access_code(AccessCode, Context) ->
associate_access_token(AccessCode, Context).
+associate_refresh_token(RefreshToken, Context) ->
+ ets:insert(?ETS_TABLE, {RefreshToken, Context}),
+ ok.
+
associate_access_token(AccessToken, Context) ->
ets:insert(?ETS_TABLE, {AccessToken, Context}),
ok.
resolve_access_code(AccessCode) ->
resolve_access_token(AccessCode).
+resolve_refresh_token(RefreshToken) ->
+ resolve_access_token(RefreshToken).
+
resolve_access_token(AccessToken) ->
case ets:lookup(?ETS_TABLE, AccessToken) of
[] ->

0 comments on commit 44bb4cd

Please sign in to comment.