├── .git-blame-ignore-revs ├── .github └── CODEOWNERS ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── rebar ├── rebar.config ├── rebar.lock ├── src ├── oauth2.app.src ├── oauth2.erl ├── oauth2_backend.erl ├── oauth2_config.erl ├── oauth2_priv_set.erl ├── oauth2_response.erl ├── oauth2_token.erl └── oauth2_token_generation.erl └── test ├── oauth2_mock_backend.erl ├── oauth2_priv_set_tests.erl ├── oauth2_response_tests.erl ├── oauth2_tests.erl └── oauth2_token_tests.erl /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Project-wide erlfmt formatting 2 | 4a9ce4ca05a052a2f81239694ff49b407d29bd8e 3 | 34665ce22d9439993b10bec853680176fe1835ff 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # the project is owned by the platform team 2 | * @kivra/platform-team 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | .eunit 3 | deps 4 | .rebar 5 | *.plt 6 | _build 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 18.0 4 | - 17.5 5 | - 17.4 6 | - 17.3 7 | - 17.1 8 | - 17.0 9 | - R16B03-1 10 | - R16B03 11 | - R16B02 12 | - R16B01 13 | - R16B 14 | - R15B03 15 | - R15B02 16 | - R15B01 17 | - R15B 18 | script: make ct 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Kivra 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = oauth2 2 | DIALYZER = dialyzer 3 | REBAR = ./rebar 4 | 5 | .PHONY: all deps compile clean test ct build-plt dialyze 6 | 7 | all: deps compile 8 | 9 | deps: 10 | $(REBAR) get-deps 11 | 12 | compile: 13 | $(REBAR) compile 14 | 15 | clean: 16 | $(REBAR) clean 17 | rm -f test/*.beam 18 | rm -f erl_crash.dump 19 | 20 | test: ct dialyze doc 21 | 22 | test-build: 23 | $(REBAR) compile 24 | 25 | ct: clean deps test-build 26 | $(REBAR) eunit skip_deps=true 27 | 28 | build-plt: 29 | $(DIALYZER) --build_plt --output_plt .$(PROJECT).plt \ 30 | --apps erts kernel stdlib sasl inets crypto public_key ssl 31 | 32 | dialyze: clean deps test-build 33 | $(DIALYZER) --plt .$(PROJECT).plt ebin 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 (v0.7.0) [![BuildStatus](https://travis-ci.org/kivra/oauth2.png?branch=master)](https://travis-ci.org/kivra/oauth2) 2 | This library is designed to simplify the implementation of the server side 3 | of OAuth2 (http://tools.ietf.org/html/rfc6749). It provides 4 | **no** support for developing clients. See 5 | [oauth2_client](https://github.com/kivra/oauth2_client) for support in 6 | accessing Oauth2 enabled services. 7 | 8 | oauth2 is released under the terms of the [MIT](http://en.wikipedia.org/wiki/MIT_License) license 9 | 10 | Current stable version: [0.6.1](https://github.com/kivra/oauth2/tree/0.6.1) 11 | 12 | Current α alpha version: [0.7.x](https://github.com/kivra/oauth2) 13 | 14 | copyright 2012-2015 Kivra 15 | 16 | ## tl;dr 17 | ### Examples 18 | Check out the [examples](https://github.com/kivra/oauth2_example). 19 | 20 | ### Related projects 21 | Webmachine server implementation by Oauth2 contributor 22 | Ivan Martinez: [oauth2_webmachine](https://github.com/IvanMartinez/oauth2_webmachine). 23 | 24 | Redis backed Oauth2 [backend](https://github.com/interline/oauth2_redis_backend). 25 | 26 | ## Concepts 27 | 28 | ### Tokens 29 | A token is a (randomly generated) string provided to the client by the server 30 | in response to some form of authorization request. 31 | There are several types of tokens: 32 | 33 | * *Access Token*: An access token identifies the origin of a request for a 34 | privileged resource. 35 | * *Refresh Token*: A refresh token can be used to replace an expired access token. 36 | 37 | #### Expiry 38 | Access tokens can (optionally) be set to expire after a certain amount of time. 39 | An expired token cannot be used to gain access to resources. 40 | 41 | ### Identities 42 | A token is associated with an *identity* -- a value that uniquely identifies 43 | a user, client or agent within your system. Typically, this is a user identifier. 44 | 45 | ### Scope 46 | The scope is handled by the backend implementation. The specification outlines 47 | that the scope is a space delimetered set of parameters. This library 48 | has been developed with the following in mind. 49 | 50 | Scope is implemented as a set and loosely modeled after the Solaris RBAC priviliges, i.e. 51 | `solaris.x.*` and implemented as a [MAC](http://en.wikipedia.org/wiki/Mandatory_access_control) 52 | with the ability to narrow the scope but not extend it beyond the predefined scope. 53 | 54 | But since the scope is opaque to this Oauth2 implementation you can use the 55 | scoping strategy that best suit your workflow. 56 | 57 | There is a utility module to work with scope. The recommendation is to pass 58 | a Scope as a list of binaries, i.e. `[<<"root.a.c.b">>, <<"root.x.y.z">>]` 59 | you can then validate these against another set like: 60 | 61 | ``` erlang 62 | > oauth2_priv_set:is_subset(oauth2_priv_set:new([<<"root.a.b">>, <<"root.x.y">>]), 63 | oauth2_priv_set:new([<<"root.*">>])). 64 | true 65 | > oauth2_priv_set:is_subset(oauth2_priv_set:new([<<"root.a.b">>, <<"root.x.y">>]), 66 | oauth2_priv_set:new([<<"root.x.y">>])). 67 | false 68 | > oauth2_priv_set:is_subset(oauth2_priv_set:new([<<"root.a.b">>, <<"root.x.y">>]), 69 | oauth2_priv_set:new([<<"root.a.*">>, <<"root.x.y">>])). 70 | true 71 | ``` 72 | 73 | ### Clients 74 | If you have many diverse clients connecting to your service -- for instance, 75 | a web client and an iPhone app -- it's desirable to be able to distinguish 76 | them from one another and to be able to grant or revoke privileges based 77 | on the type the client issuing a request. As described in the OAuth2 specification, 78 | clients come in two flavors: 79 | 80 | * *Confidential* clients, which can be expected to keep their credentials 81 | from being disclosed. For instance, a web site owned and operated by you 82 | could be regarded as confidential. 83 | * *Public* clients, whose credentials are assumed to be compromised the 84 | moment the client software is released to the public. 85 | 86 | Clients are distinguished by their identifiers, and can (optionally) be 87 | authenticated using a secret key shared between the client and server. 88 | 89 | ## Testing 90 | If you want to run the EUnit test cases, you can do so with: 91 | 92 | $ make ct 93 | 94 | ## Customization 95 | The library makes no assumptions as to how you want to implement 96 | authentication and persistence of users, clients and tokens. Instead, it 97 | provides a behavior (`oauth2_backend`) with functions that needs to be 98 | implemented. To direct calls to a different backend module, simply set 99 | `{backend, your_backend_module}` in the `oauth2` section of your app.config. 100 | 101 | Look at [oauth2_mock_backend](test/oauth2_mock_backend.erl) for how a backend 102 | can be implemented. 103 | 104 | The following example demonstrates a basic app.config section for oauth2. 105 | 106 | ``` erlang 107 | [ 108 | {oauth2, [ 109 | %% Default expiry_time for access_tokens unless 110 | %% overridden per flow 111 | {expiry_time, 3600} 112 | ,{backend, backend_goes_here} 113 | 114 | %% Optional expiry_time override per flow 115 | ,{password_credentials, [ 116 | {expiry_time, 7200} 117 | ]} 118 | ,{client_credentials, [ 119 | {expiry_time, 86400} 120 | ]} 121 | ,{refresh_token, [ 122 | {expiry_time, 2592000} %% 30 Days 123 | ]} 124 | ,{code_grant, [ 125 | %% Recommended absolute expiry time from the spec 126 | {expiry_time, 600} 127 | ]} 128 | ]} 129 | ]. 130 | ``` 131 | 132 | A complete list of functions that your backend must provide is available by looking 133 | at `oauth2_backend.erl`, which contains documentation and function specifications. 134 | 135 | To implement a custom token generation backend you can change your 136 | app.config as such: 137 | 138 | ``` erlang 139 | [ 140 | {oauth2, [ 141 | {token_generation, YOUR_TOKEN_GENERATOR} 142 | ]} 143 | ]. 144 | ``` 145 | 146 | The default token generator is called oauth2_token. To implement your 147 | own you should create your own module implementing the 148 | oauth2_token_generation behavior exporting one function 149 | generate/0. 150 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivra/oauth2/44b5728f8dc60fd16bf40caf7511cb5b2c52c903/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% oauth2: Erlang OAuth 2.0 implementation 4 | %% 5 | %% Copyright (c) 2012-2014 Kivra 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | {erl_opts, [ 28 | {platform_define, "^R", pre17}, 29 | {platform_define, "^(R|17)", pre18}, 30 | debug_info, 31 | warnings_as_errors, 32 | warn_export_vars, 33 | warn_unused_import, 34 | warn_keywords 35 | ]}. 36 | 37 | {shell, [{apps, [oauth2]}]}. 38 | 39 | {profiles, [ 40 | {test, [ 41 | {eunit_opts, [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}, 42 | {cover_enabled, true}, 43 | {cover_opts, [verbose]}, 44 | {clean_files, [".eunit", "ebin/*.beam", "test/*.beam"]}, 45 | {deps, 46 | [ {meck, "0.8.3"} 47 | , {proper, "1.4.0"} 48 | ] } 49 | ]} 50 | ]}. 51 | 52 | {dialyzer, [ 53 | {plt_apps, all_deps}, 54 | incremental, 55 | {warnings, [unmatched_returns]} 56 | ]}. 57 | 58 | {xref_checks, [ 59 | undefined_function_calls, 60 | undefined_functions, 61 | locals_not_used, 62 | deprecated_function_calls, 63 | deprecated_functions 64 | ]}. 65 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/oauth2.app.src: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | {application, oauth2, 21 | [ 22 | {description, "Erlang OAuth 2.0 implementation"}, 23 | {vsn, "0.7.0"}, 24 | {registered, []}, 25 | {applications, [ 26 | kernel, 27 | stdlib 28 | ]}, 29 | {env, []}, 30 | {pkg_name, "oauth2_erlang"}, 31 | {maintainers, ["kivra", "Heinz N. Gies"]}, 32 | {licenses,["MIT"]}, 33 | {links,[{"Github", 34 | "https://github.com/kivra/oauth2"}]} 35 | ]}. 36 | -------------------------------------------------------------------------------- /src/oauth2.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% 18 | %%% This library is designed to simplify the implementation of the 19 | %%% server side of OAuth2 (http://tools.ietf.org/html/rfc6749). 20 | %%% @end 21 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 22 | 23 | %%%_* Module declaration =============================================== 24 | -module(oauth2). 25 | -compile({no_auto_import, [get/2]}). 26 | 27 | %%%_* Exports ========================================================== 28 | %%%_ * API ------------------------------------------------------------- 29 | -export([authorize_password/3]). 30 | -export([authorize_password/4]). 31 | -export([authorize_password/5]). 32 | -export([authorize_client_credentials/3]). 33 | -export([authorize_code_grant/4]). 34 | -export([authorize_code_request/5]). 35 | -export([authorize_directly/4]). 36 | -export([issue_code/2]). 37 | -export([issue_token/2]). 38 | -export([issue_jwt/2]). 39 | -export([issue_token_and_refresh/2]). 40 | -export([issue_jwt_and_refresh/3]). 41 | -export([verify_access_token/2]). 42 | -export([verify_access_code/2]). 43 | -export([verify_access_code/3]). 44 | -export([verify_jwt/1]). 45 | -export([refresh_access_token/4]). 46 | -export([refresh_jwt/4]). 47 | 48 | -export_type([token/0]). 49 | -export_type([user/0]). 50 | -export_type([client/0]). 51 | -export_type([context/0]). 52 | -export_type([auth/0]). 53 | -export_type([lifetime/0]). 54 | -export_type([scope/0]). 55 | -export_type([appctx/0]). 56 | -export_type([error/0]). 57 | 58 | %%%_* Macros =========================================================== 59 | -define(BACKEND, (oauth2_config:backend())). 60 | -define(TOKEN, (oauth2_config:token_generation())). 61 | 62 | %%%_ * Types ----------------------------------------------------------- 63 | %% Opaque authentication record 64 | -record(a, { client = undefined :: undefined | term() 65 | , resowner = undefined :: undefined | term() 66 | , scope :: scope() 67 | , ttl = 0 :: non_neg_integer() 68 | , issuer = undefined :: undefined | binary() 69 | }). 70 | 71 | -type context() :: proplists:proplist(). 72 | -type auth() :: #a{}. 73 | -type user() :: any(). %% Opaque User Object 74 | -type client() :: any(). %% Opaque Client Object 75 | -type resowner() :: any(). %% Opaque Resource Owner Object 76 | -type rediruri() :: any(). %% Opaque Redirection URI 77 | -type token() :: binary(). 78 | -type response() :: oauth2_response:response(). 79 | -type lifetime() :: non_neg_integer(). 80 | -type scope() :: list(binary()) | binary(). 81 | -type appctx() :: term(). 82 | -type error() :: access_denied | invalid_client | invalid_grant | 83 | invalid_request | invalid_authorization | invalid_scope | 84 | unauthorized_client | unsupported_grant_type | 85 | unsupported_response_type | server_error | 86 | temporarily_unavailable | atom(). 87 | 88 | %%%_* Code ============================================================= 89 | %%%_ * API ------------------------------------------------------------- 90 | %% @doc Validates a request for an access token from resource owner's 91 | %% credentials. Use it to implement the following steps of RFC 6749: 92 | %% - 4.3.2. Resource Owner Password Credentials Grant > 93 | %% Access Token Request, when the client is public. 94 | -spec authorize_password(user(), scope(), appctx()) 95 | -> {ok, {appctx(), auth()}} | {error, error()}. 96 | authorize_password(User, Scope, Ctx0) -> 97 | case auth_user(User, Scope, Ctx0) of 98 | {error, _}=E -> E; 99 | {ok, _}=Auth -> Auth 100 | end. 101 | 102 | %% @doc Validates a request for an access token from client and resource 103 | %% owner's credentials. Use it to implement the following steps of 104 | %% RFC 6749: 105 | %% - 4.3.2. Resource Owner Password Credentials Grant > 106 | %% Access Token Request, when the client is confidential. 107 | -spec authorize_password(user(), client(), scope(), appctx()) 108 | -> {ok, {appctx(), auth()}} | {error, error()}. 109 | authorize_password(User, Client, Scope, Ctx0) -> 110 | case auth_client(Client, no_redir, Ctx0) of 111 | {error, _} -> {error, invalid_client}; 112 | {ok, {Ctx1, C}} -> 113 | case auth_user(User, Scope, Ctx1) of 114 | {error, _} = E -> E; 115 | {ok, {Ctx2, Auth}} -> {ok, {Ctx2, Auth#a{client=C}}} 116 | end 117 | end. 118 | 119 | %% @doc Validates a request for an access token from client and resource 120 | %% owner's credentials. Use it to implement the following steps of 121 | %% RFC 6749: 122 | %% - 4.2.1. Implicit Grant > Authorization Request, when the client 123 | %% is public. 124 | -spec authorize_password(user(), client(), rediruri(), scope(), appctx()) 125 | -> {ok, {appctx(), auth()}} | {error, error()}. 126 | authorize_password(User, Client, RedirUri, Scope, Ctx0) -> 127 | case ?BACKEND:get_client_identity(Client,Ctx0) of 128 | {error, _} ->{error, invalid_client}; 129 | {ok,{Ctx1,C}} -> 130 | case ?BACKEND:verify_redirection_uri(C, RedirUri, Ctx1) of 131 | {error, _} -> {error, invalid_client}; 132 | {ok, Ctx2} -> 133 | case auth_user(User, Scope, Ctx2) of 134 | {error, _} = E -> E; 135 | {ok, {Ctx3, Auth}} -> {ok, {Ctx3, Auth#a{client=C}}} 136 | end 137 | end 138 | end. 139 | 140 | %% @doc Validates a request for an access token from client's credentials. 141 | %% Use it to implement the following steps of RFC 6749: 142 | %% - 4.4.2. Client Credentials Grant > Access Token Request. 143 | -spec authorize_client_credentials(client(), scope(), appctx()) 144 | -> {ok, {appctx(), auth()}} | {error, error()}. 145 | authorize_client_credentials(Client, Scope0, Ctx0) -> 146 | case auth_client(Client, no_redir, Ctx0) of 147 | {error, _} -> {error, invalid_client}; 148 | {ok, {Ctx1, C}} -> 149 | case ?BACKEND:verify_client_scope(C, Scope0, Ctx1) of 150 | {error, _} -> {error, invalid_scope}; 151 | {ok, {Ctx2, Scope1}} -> 152 | {ok, {Ctx2, #a{ client=C 153 | , scope =Scope1 154 | , ttl =oauth2_config:expiry_time( 155 | client_credentials) 156 | }}} 157 | end 158 | 159 | end. 160 | 161 | %% @doc Validates a request for an access token from an authorization code. 162 | %% Use it to implement the following steps of RFC 6749: 163 | %% - 4.1.3. Authorization Code Grant > Access Token Request. 164 | -spec authorize_code_grant(client(), binary(), rediruri(), appctx()) 165 | -> {ok, {appctx(), auth()}} | {error, error()}. 166 | authorize_code_grant(Client, Code, RedirUri, Ctx0) -> 167 | case auth_client(Client, RedirUri, Ctx0) of 168 | {error, _} -> {error, invalid_client}; 169 | {ok, {Ctx1, C}} -> 170 | case verify_access_code(Code, C, Ctx1) of 171 | {error, _}=E -> E; 172 | {ok, {Ctx2, GrantCtx}} -> 173 | {ok, Ctx3} = ?BACKEND:revoke_access_code(Code, Ctx2), 174 | {ok, {Ctx3, #a{ client =C 175 | , resowner=get_(GrantCtx,<<"resource_owner">>) 176 | , scope =get_(GrantCtx, <<"scope">>) 177 | , ttl =oauth2_config:expiry_time( 178 | password_credentials) 179 | }}} 180 | end 181 | end. 182 | 183 | %% @doc Validates a request for an authorization code from client and resource 184 | %% owner's credentials. Use it to implement the following steps of 185 | %% RFC 6749: 186 | %% - 4.1.1. Authorization Code Grant > Authorization Request. 187 | -spec authorize_code_request(user(), client(), rediruri(), scope(), appctx()) -> 188 | {ok, {appctx(), auth()}} | {error, error()}. 189 | authorize_code_request(User, Client, RedirUri, Scope, Ctx0) -> 190 | case ?BACKEND:get_client_identity(Client, Ctx0) of 191 | {error, _} -> {error, unauthorized_client}; 192 | {ok, {Ctx1, C}} -> 193 | case ?BACKEND:verify_redirection_uri(C, RedirUri, Ctx1) of 194 | {error, _} -> {error, unauthorized_client}; 195 | {ok, Ctx2} -> 196 | case auth_user(User, Scope, Ctx2) of 197 | {error, _}=E -> E; 198 | {ok, {Ctx3, Auth}} -> 199 | {ok, { Ctx3 200 | , Auth#a{ client=C 201 | , ttl =oauth2_config:expiry_time( 202 | code_grant) 203 | } }} 204 | end 205 | end 206 | end. 207 | 208 | %% @doc Sometimes one wishes to authorize directly with a specific scope and/or 209 | %% a specific TTL, and this function is for that. 210 | -spec authorize_directly(client(), resowner(), scope(), non_neg_integer()) -> 211 | auth(). 212 | authorize_directly(Client, ResOwner, Scope, TTL) -> 213 | #a{ client = Client 214 | , resowner = ResOwner 215 | , scope = Scope 216 | , ttl = TTL 217 | }. 218 | 219 | %% @doc Issues an authorization code from an authorization. Use it to implement 220 | %% the following steps of RFC 6749: 221 | %% - 4.1.2. Authorization Code Grant > Authorization Response, with the 222 | %% result of authorize_code_request/6. 223 | -spec issue_code(auth(), appctx()) -> {ok, {appctx(), response()}}. 224 | issue_code(#a{client=Client, resowner=Owner, scope=Scope, ttl=TTL}, Ctx0) -> 225 | GrantContext = build_context(Client, seconds_since_epoch(TTL), Owner, Scope), 226 | AccessCode = ?TOKEN:generate(GrantContext), 227 | {ok, Ctx1} = ?BACKEND:associate_access_code(AccessCode,GrantContext,Ctx0), 228 | {ok, {Ctx1, oauth2_response:new(<<>>,TTL,Owner,Scope,<<>>,<<>>,AccessCode)}}. 229 | 230 | %% @doc Issues an access token without refresh token from an authorization. 231 | %% Use it to implement the following steps of RFC 6749: 232 | %% - 4.1.4. Authorization Code Grant > Authorization Response, with the 233 | %% result of authorize_code_grant/5 when no refresh token must be issued. 234 | %% - 4.2.2. Implicit Grant > Access Token Response, with the result of 235 | %% authorize_password/7. 236 | %% - 4.3.3. Resource Owner Password Credentials Grant > 237 | %% Access Token Response, with the result of authorize_password/4 or 238 | %% authorize_password/6 when the client is public or no refresh token 239 | %% must be issued. 240 | %% - 4.4.3. Client Credentials Grant > Access Token Response, with the 241 | %% result of authorize_client_credentials/4. 242 | -spec issue_token(auth(), appctx()) -> {ok, {appctx(), response()}}. 243 | issue_token(#a{client=Client, resowner=Owner, scope=Scope, ttl=TTL}, Ctx0) -> 244 | GrantContext = build_context(Client,seconds_since_epoch(TTL),Owner,Scope), 245 | AccessToken = ?TOKEN:generate(GrantContext), 246 | {ok, Ctx1} = ?BACKEND:associate_access_token( AccessToken 247 | , GrantContext 248 | , Ctx0 ), 249 | {ok, {Ctx1, oauth2_response:new(AccessToken, TTL, Owner, Scope)}}. 250 | 251 | %% @doc Issues an JWT without refresh token from an authorization. 252 | -spec issue_jwt(auth(), appctx()) -> {ok, {appctx(), context(), response()}}. 253 | issue_jwt(#a{ client = Client 254 | , resowner = ResOwner 255 | , scope = Scope 256 | , ttl = TTL 257 | , issuer = Issuer}, Ctx) -> 258 | ExpiryTime = seconds_since_epoch(TTL), 259 | IssuedAt = seconds_since_epoch(0), 260 | AccessCtx = build_jwt_context( Issuer, ResOwner, ExpiryTime, IssuedAt 261 | , Client, Scope), 262 | {ok, JWT} = ?BACKEND:jwt_sign(AccessCtx, Ctx), 263 | {ok, {Ctx, AccessCtx, oauth2_response:new(JWT, TTL)}}. 264 | 265 | %% @doc Issues access and refresh tokens from an authorization. 266 | %% Use it to implement the following steps of RFC 6749: 267 | %% - 4.1.4. Authorization Code Grant > Access Token Response, with the 268 | %% result of authorize_code_grant/5 when a refresh token must be issued. 269 | %% - 4.3.3. Resource Owner Password Credentials Grant > 270 | %% Access Token Response, with the result of authorize_password/6 when 271 | %% the client is confidential and a refresh token must be issued. 272 | -spec issue_token_and_refresh(auth(), appctx()) -> {ok, {appctx(), response()}} 273 | | {error, invalid_authorization}. 274 | issue_token_and_refresh(#a{client = undefined}, _Ctx) -> 275 | {error, invalid_authorization}; 276 | issue_token_and_refresh(#a{resowner = undefined}, _Ctx) -> 277 | {error, invalid_authorization}; 278 | issue_token_and_refresh( #a{client=Client, resowner=Owner, scope=Scope, ttl=TTL} 279 | , Ctx0 ) -> 280 | RTTL = oauth2_config:expiry_time(refresh_token), 281 | RefreshCtx = build_context(Client,seconds_since_epoch(RTTL),Owner,Scope), 282 | RefreshToken = ?TOKEN:generate(RefreshCtx), 283 | AccessCtx = build_context(Client,seconds_since_epoch(TTL),Owner,Scope,RefreshToken), 284 | AccessToken = ?TOKEN:generate(AccessCtx), 285 | {ok, Ctx1} = ?BACKEND:associate_access_token( AccessToken 286 | , AccessCtx 287 | , Ctx0), 288 | {ok, Ctx2} = ?BACKEND:associate_refresh_token( RefreshToken 289 | , RefreshCtx 290 | , Ctx1 ), 291 | {ok, {Ctx2, oauth2_response:new( AccessToken 292 | , TTL 293 | , Owner 294 | , Scope 295 | , RefreshToken 296 | , RTTL )}}. 297 | 298 | %% @doc Issues JWT and refresh token from an authorization. 299 | -spec issue_jwt_and_refresh(auth(), binary(), appctx()) -> 300 | {ok, {appctx(), context(), response()}} 301 | | {error, invalid_authorization}. 302 | issue_jwt_and_refresh(#a{client = undefined}, _, _) -> 303 | {error, invalid_authorization}; 304 | issue_jwt_and_refresh(#a{resowner = undefined}, _, _) -> 305 | {error, invalid_authorization}; 306 | issue_jwt_and_refresh( #a{ client = Client 307 | , resowner = ResOwner 308 | , scope = Scope 309 | , ttl = TTL 310 | , issuer = Issuer} 311 | , DeviceId 312 | , Ctx0) -> 313 | % access_token 314 | AccessExpiry = seconds_since_epoch(TTL), 315 | IssuedAt = seconds_since_epoch(0), 316 | AccessCtx = build_jwt_context( Issuer, ResOwner, AccessExpiry, IssuedAt 317 | , Client, Scope), 318 | {ok, JWT} = ?BACKEND:jwt_sign(AccessCtx, Ctx0), 319 | 320 | % refresh_token 321 | RefreshTTL = oauth2_config:expiry_time(jwt_refresh_token), 322 | RefreshExpiry = seconds_since_epoch(RefreshTTL), 323 | RefreshCtx = build_context(Client, RefreshExpiry, ResOwner, Scope), 324 | RefreshToken = ?TOKEN:generate(RefreshCtx), 325 | {ok, Ctx1} = ?BACKEND:associate_refresh_token( RefreshToken, RefreshCtx 326 | , DeviceId, Ctx0), 327 | {ok, {Ctx1, AccessCtx, oauth2_response:new(JWT, TTL, RefreshToken)}}. 328 | 329 | %% @doc Verifies an access code AccessCode, returning its associated 330 | %% context if successful. Otherwise, an OAuth2 error code is returned. 331 | -spec verify_access_code(token(), appctx()) -> {ok, {appctx(), context()}} 332 | | {error, error()}. 333 | verify_access_code(AccessCode, Ctx0) -> 334 | case ?BACKEND:resolve_access_code(AccessCode, Ctx0) of 335 | {error, _} -> {error, invalid_grant}; 336 | {ok, {Ctx1, GrantCtx}} -> 337 | case get_(GrantCtx, <<"expiry_time">>) > seconds_since_epoch(0) of 338 | true -> {ok, {Ctx1, GrantCtx}}; 339 | false -> 340 | ?BACKEND:revoke_access_code(AccessCode, Ctx1), 341 | {error, invalid_grant} 342 | end 343 | end. 344 | 345 | %% @doc Verifies an access code AccessCode and it's corresponding Identity, 346 | %% returning its associated context if successful. Otherwise, an OAuth2 347 | %% error code is returned. 348 | -spec verify_access_code(token(), client(), appctx()) -> 349 | {ok, {appctx(), context()}} | {error, error()}. 350 | verify_access_code(AccessCode, Client, Ctx0) -> 351 | case verify_access_code(AccessCode, Ctx0) of 352 | {error, _}=E -> E; 353 | {ok, {Ctx1, GrantCtx}} -> 354 | case get(GrantCtx, <<"client">>) of 355 | {ok, Client} -> {ok, {Ctx1, GrantCtx}}; 356 | _ -> {error, invalid_grant} 357 | end 358 | end. 359 | 360 | %% @doc Validates a request for an access token from a refresh token, issuing 361 | %% a new access token if valid. Use it to implement the following steps of 362 | %% RFC 6749: 363 | %% - 6. Refreshing an Access Token. 364 | -spec refresh_access_token(client(), token(), scope(), appctx()) 365 | -> {ok, {appctx(), response()}} | {error, error()}. 366 | refresh_access_token(Client, RefreshToken, Scope, Ctx0) -> 367 | case verify_refresh_token_basic(Client, RefreshToken, Scope, Ctx0) of 368 | {ok, {Ctx1, ClientId, ResOwner, VerifiedScope, TTL, _DeviceId}} -> 369 | issue_token(#a{ client = ClientId 370 | , resowner = ResOwner 371 | , scope = VerifiedScope 372 | , ttl = TTL 373 | }, Ctx1); 374 | {error, _} = E -> E 375 | end. 376 | 377 | %% @doc Validates a request for a JWT from a refresh token, issuing a new JWT 378 | %% if valid. 379 | -spec refresh_jwt(client(), token(), scope(), appctx()) -> 380 | {ok, {appctx(), context(), response()}} 381 | | {error, error()}. 382 | refresh_jwt(Client, RefreshToken, Scope, Ctx0) -> 383 | case verify_refresh_token_basic(Client, RefreshToken, Scope, Ctx0) of 384 | {ok, {Ctx1, ClientId, ResOwner, VerifiedScope, TTL, DeviceId}} -> 385 | % RFC 6749 Section 10.4 (Security Considerations for refresh_token) 386 | % 387 | % Authorization server could employ refresh token rotation in which 388 | % a new refresh token is issued with every access token refresh 389 | % response. The previous refresh token is invalidated but retained 390 | % by the authorization server. If a refresh token is compromised 391 | % and subsequently used by both the attacker and the legitimate 392 | % client, one of them will present an invalidated refresh token, 393 | % which will inform the authorization server of the breach. 394 | % 395 | % TODO: implement this 396 | ?BACKEND:revoke_refresh_token(RefreshToken, Ctx1), 397 | issue_jwt_and_refresh( #a{ client = ClientId 398 | , resowner = ResOwner 399 | , scope = VerifiedScope 400 | , ttl = TTL 401 | , issuer = ?BACKEND:jwt_issuer() 402 | } 403 | , DeviceId 404 | , Ctx1); 405 | {error, _} = E -> E 406 | end. 407 | 408 | %% @doc Verifies an access token AccessToken, returning its associated 409 | %% context if successful. Otherwise, an OAuth2 error code is returned. 410 | -spec verify_access_token(token(), appctx()) -> {ok, {appctx(), context()}} 411 | | {error, error()}. 412 | verify_access_token(AccessToken, Ctx0) -> 413 | case ?BACKEND:resolve_access_token(AccessToken, Ctx0) of 414 | {error, _} -> {error, access_denied}; 415 | {ok, {Ctx1, GrantCtx}} -> 416 | case get_(GrantCtx, <<"expiry_time">>) > seconds_since_epoch(0) of 417 | true -> {ok, {Ctx1, GrantCtx}}; 418 | false -> 419 | ?BACKEND:revoke_access_token(AccessToken, Ctx1), 420 | {error, access_denied} 421 | end 422 | end. 423 | 424 | %% @doc Verifies a JWT, returning its associated context if successful. 425 | %% Otherwise, an OAuth2 error code is returned. 426 | -spec verify_jwt(token()) -> {ok, context()} | {error, error()}. 427 | verify_jwt(JWT) -> 428 | case ?BACKEND:jwt_verify(JWT) of 429 | {error, _} -> {error, access_denied}; 430 | {ok, GrantCtx} -> 431 | case get_(GrantCtx, <<"exp">>) > seconds_since_epoch(0) of 432 | true -> {ok, GrantCtx}; 433 | false -> {error, access_denied} 434 | end 435 | end. 436 | 437 | %%%_* Private functions ================================================ 438 | auth_user(User, Scope0, Ctx0) -> 439 | case ?BACKEND:authenticate_user(User, Ctx0) of 440 | {error, _}=E -> E; 441 | {ok, {Ctx1, Owner}} -> 442 | case ?BACKEND:verify_resowner_scope(Owner, Scope0, Ctx1) of 443 | {error, _} -> {error, invalid_scope}; 444 | {ok, {Ctx2, Scope1}} -> 445 | {ok, {Ctx2, #a{ resowner = Owner 446 | , scope = Scope1 447 | , ttl = oauth2_config:expiry_time( 448 | password_credentials) 449 | , issuer = ?BACKEND:jwt_issuer() 450 | }}} 451 | end 452 | end. 453 | 454 | auth_client(Client, no_redir, Ctx0) -> 455 | ?BACKEND:authenticate_client(Client, Ctx0); 456 | auth_client(Client, RedirUri, Ctx0) -> 457 | case auth_client(Client, no_redir, Ctx0) of 458 | {error, _}=E -> E; 459 | {ok, {Ctx1, C}} -> 460 | case ?BACKEND:verify_redirection_uri(C, RedirUri, Ctx1) of 461 | {error, _} -> {error, invalid_grant}; 462 | {ok, Ctx2} -> {ok, {Ctx2, C}} 463 | end 464 | end. 465 | 466 | verify_refresh_token_basic(Client, RefreshToken, Scope, Ctx0) -> 467 | case auth_client(Client, no_redir, Ctx0) of 468 | {error, _} -> {error, invalid_client}; 469 | {ok, {Ctx1, ClientId}} -> 470 | case ?BACKEND:resolve_refresh_token(RefreshToken, Ctx1) of 471 | {error, _} -> {error, invalid_grant}; 472 | {ok, {Ctx2, GrantCtx}} -> 473 | {ok, ExpiryAbsolute} = get(GrantCtx, <<"expiry_time">>), 474 | case ExpiryAbsolute > seconds_since_epoch(0) of 475 | true -> 476 | {ok, ResOwner} = get(GrantCtx, <<"resource_owner">>), 477 | case ?BACKEND:verify_resowner_scope(ResOwner, Scope, Ctx2) of 478 | {error, _} -> {error, invalid_scope}; 479 | {ok, {Ctx3, VerifiedScope}} -> 480 | {ok, ClientId} = get(GrantCtx, <<"client">>), 481 | {ok, ResOwner} = get(GrantCtx, <<"resource_owner">>), 482 | {ok, DeviceId} = get(GrantCtx, <<"device_id">>), 483 | TTL = oauth2_config:expiry_time( 484 | password_credentials), 485 | {ok, { Ctx3, ClientId, ResOwner 486 | , VerifiedScope, TTL, DeviceId}} 487 | end; 488 | false -> 489 | ?BACKEND:revoke_refresh_token(RefreshToken, Ctx2), 490 | {error, invalid_grant} 491 | end 492 | end 493 | end. 494 | 495 | -spec build_context(term(), non_neg_integer(), term(), scope()) -> context(). 496 | build_context(Client, ExpiryTime, ResOwner, Scope) -> 497 | [ {<<"client">>, Client} 498 | , {<<"resource_owner">>, ResOwner} 499 | , {<<"expiry_time">>, ExpiryTime} 500 | , {<<"scope">>, Scope} 501 | ]. 502 | 503 | build_context(Client, ExpiryTime, ResOwner, Scope, RefreshToken) -> 504 | [{<<"refresh_token">>, RefreshToken} | build_context(Client, ExpiryTime, ResOwner, Scope)]. 505 | 506 | build_jwt_context(Issuer, ResOwner, ExpiryTime, IssuedAt, Client, Scope) -> 507 | [ {<<"iss">>, Issuer} 508 | , {<<"sub">>, ResOwner} 509 | , {<<"exp">>, ExpiryTime} 510 | , {<<"iat">>, IssuedAt} 511 | , {<<"client">>, Client} 512 | , {<<"scope">>, Scope} 513 | ]. 514 | 515 | -spec seconds_since_epoch(integer()) -> non_neg_integer(). 516 | seconds_since_epoch(Diff) -> 517 | {Mega, Secs, _} = os:timestamp(), 518 | Mega * 1000000 + Secs + Diff. 519 | 520 | get(O, K) -> 521 | case lists:keyfind(K, 1, O) of 522 | {K, V} -> {ok, V}; 523 | false -> {error, notfound} 524 | end. 525 | 526 | get_(O, K) -> 527 | {ok, V} = get(O, K), 528 | V. 529 | 530 | %%%_* Tests ============================================================ 531 | -ifdef(TEST). 532 | -include_lib("eunit/include/eunit.hrl"). 533 | 534 | -endif. 535 | 536 | %%%_* Emacs ============================================================ 537 | %%% Local Variables: 538 | %%% allout-layout: t 539 | %%% erlang-indent-level: 4 540 | %%% End: 541 | -------------------------------------------------------------------------------- /src/oauth2_backend.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | %%%_* Module declaration =============================================== 21 | -module(oauth2_backend). 22 | 23 | %%%_ * Types ----------------------------------------------------------- 24 | -type grantctx() :: oauth2:context(). 25 | -type appctx() :: oauth2:appctx(). 26 | -type token() :: oauth2:token(). 27 | -type scope() :: oauth2:scope(). 28 | -type user() :: oauth2:user(). 29 | -type client() :: oauth2:client(). 30 | 31 | %%%_* Behaviour ======================================================== 32 | %% @doc Authenticates a combination of username and password. 33 | %% Returns the resource owner identity if the credentials are valid. 34 | -callback authenticate_user(user(), appctx()) -> {ok, {appctx(), term()}} 35 | | {error, atom()}. 36 | 37 | %% @doc Authenticates a client's credentials for a given scope. 38 | -callback authenticate_client(client(), appctx()) -> {ok, {appctx(), client()}} 39 | | {error, notfound | badsecret}. 40 | 41 | %% @doc Stores a new access code token(), associating it with Context. 42 | %% The context is a proplist carrying information about the identity 43 | %% with which the code is associated, when it expires, etc. 44 | -callback associate_access_code(token(), grantctx(), appctx()) -> 45 | {ok, appctx()} | {error, notfound}. 46 | 47 | %% @doc Stores a new access token token(), associating it with Context. 48 | %% The context is a proplist carrying information about the identity 49 | %% with which the token is associated, when it expires, etc. 50 | -callback associate_access_token(token(), grantctx(), appctx()) -> 51 | {ok, appctx()} | {error, notfound}. 52 | 53 | %% @doc Stores a new refresh token token(), associating it with 54 | %% grantctx(). The context is a proplist carrying information about the 55 | %% identity with which the token is associated, when it expires, etc. 56 | -callback associate_refresh_token(token(), grantctx(), appctx()) -> 57 | {ok, appctx()} | {error, notfound}. 58 | 59 | %% @doc Stores a new refresh token token(), associating it with 60 | %% grantctx() and a device_id. 61 | -callback associate_refresh_token(token(), grantctx(), binary(), appctx()) -> 62 | {ok, appctx()} | {error, notfound}. 63 | 64 | %% @doc Looks up an access token token(), returning the corresponding 65 | %% context if a match is found. 66 | -callback resolve_access_token(token(), appctx()) -> 67 | {ok, {appctx(), grantctx()}} | {error, notfound}. 68 | 69 | %% @doc Looks up an access code token(), returning the corresponding 70 | %% context if a match is found. 71 | -callback resolve_access_code(token(), appctx()) -> 72 | {ok, {appctx(), grantctx()}} | {error, notfound}. 73 | 74 | %% @doc Looks up an refresh token token(), returning the corresponding 75 | %% context if a match is found. 76 | -callback resolve_refresh_token(token(), appctx()) -> 77 | {ok, {appctx(), grantctx()}} | {error, notfound}. 78 | 79 | %% @doc Revokes an access token token(), so that it cannot be used again. 80 | -callback revoke_access_token(token(), appctx()) -> 81 | {ok, appctx()} | {error, notfound}. 82 | 83 | %% @doc Revokes an access code token(), so that it cannot be used again. 84 | -callback revoke_access_code(token(), appctx()) -> 85 | {ok, appctx()} | {error, notfound}. 86 | 87 | %% @doc Revokes an refresh token token(), so that it cannot be used again. 88 | -callback revoke_refresh_token(token(), appctx()) -> 89 | {ok, appctx()} | {error, notfound}. 90 | 91 | %% @doc Returns a client identity for a given id. 92 | -callback get_client_identity(client(), appctx()) -> 93 | {ok, {appctx(), client()}} | {error, notfound | badsecret}. 94 | 95 | %% @doc Verifies that RedirectionUri is a valid redirection URI for the 96 | %% client identified by Identity. 97 | -callback verify_redirection_uri(client(), binary(), appctx()) -> 98 | {ok, appctx()} | {error, notfound | baduri}. 99 | 100 | %% @doc Verifies that scope() is a valid scope for the client identified 101 | %% by Identity. 102 | -callback verify_client_scope(client(), scope(), appctx()) -> 103 | {ok, {appctx(), scope()}} | {error, notfound | badscope}. 104 | 105 | %% @doc Verifies that scope() is a valid scope for the resource 106 | %% owner identified by Identity. 107 | -callback verify_resowner_scope(term(), scope(), appctx()) -> 108 | {ok, {appctx(), scope()}} | {error, notfound | badscope}. 109 | 110 | %% @doc Verifies that scope() is a valid scope of the set of scopes defined 111 | %% by Validscope()s. 112 | -callback verify_scope(scope(), scope(), appctx()) -> 113 | {ok, {appctx(), scope()}} | {error, notfound | badscope}. 114 | 115 | %% @doc Sign the grant context with a private key and produce a JWT. 116 | %% The grant context is a proplist carrying information about the identity 117 | %% with which the token is associated, when it expires, etc. 118 | -callback jwt_sign(grantctx(), appctx()) -> {ok, token()}. 119 | 120 | %% @doc Verifies a JWT, returning the corresponding grant context if 121 | %% verification succeeds. 122 | -callback jwt_verify(token()) -> {ok, grantctx()} | {error, badjwt}. 123 | 124 | %% @doc A case-sensitive string or URI that uniquely identifies the issuer. 125 | -callback jwt_issuer() -> binary(). 126 | 127 | %%%_* Tests ============================================================ 128 | -ifdef(TEST). 129 | -include_lib("eunit/include/eunit.hrl"). 130 | 131 | -endif. 132 | 133 | %%%_* Emacs ============================================================ 134 | %%% Local Variables: 135 | %%% allout-layout: t 136 | %%% erlang-indent-level: 4 137 | %%% End: 138 | -------------------------------------------------------------------------------- /src/oauth2_config.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | %%%_* Module declaration =============================================== 21 | -module(oauth2_config). 22 | 23 | %%%_* Exports ========================================================== 24 | %%%_ * API ------------------------------------------------------------- 25 | -export([backend/0]). 26 | -export([expiry_time/0]). 27 | -export([expiry_time/1]). 28 | -export([token_generation/0]). 29 | 30 | %%%_* Macros =========================================================== 31 | %% Default time in seconds before an authentication token expires. 32 | -define(DEFAULT_TOKEN_EXPIRY, 3600). 33 | 34 | %%%_* Code ============================================================= 35 | %%%_ * API ------------------------------------------------------------- 36 | %% @doc Gets the default expiry time for access tokens. 37 | -spec expiry_time() -> non_neg_integer(). 38 | expiry_time() -> get_optional(expiry_time, ?DEFAULT_TOKEN_EXPIRY). 39 | 40 | %% @doc Gets a specific expiry time for access tokens if available 41 | %% returns the default if non found 42 | -spec expiry_time(atom()) -> non_neg_integer(). 43 | expiry_time(Flow) -> 44 | case application:get_env(oauth2, Flow) of 45 | undefined -> expiry_time(); 46 | {ok, Value} -> 47 | case lists:keyfind(expiry_time, 1, Value) of 48 | false -> expiry_time(); 49 | {_Key, Val} -> Val 50 | end 51 | end. 52 | 53 | %% @doc Gets the backend for validating passwords, storing tokens, etc. 54 | -spec backend() -> atom(). 55 | backend() -> get_required(backend). 56 | 57 | %% @doc Gets the backend for generating tokens. 58 | -spec token_generation() -> atom(). 59 | token_generation() -> get_optional(token_generation, oauth2_token). 60 | 61 | %%%_* Private functions ================================================ 62 | get_optional(Key, Default) -> 63 | case application:get_env(oauth2, Key) of 64 | undefined -> Default; 65 | {ok, Value} -> Value 66 | end. 67 | 68 | get_required(Key) -> 69 | case application:get_env(oauth2, Key) of 70 | undefined -> throw({missing_config, Key}); 71 | {ok, Value} -> Value 72 | end. 73 | 74 | %%%_* Tests ============================================================ 75 | -ifdef(TEST). 76 | -include_lib("eunit/include/eunit.hrl"). 77 | -endif. 78 | 79 | %%%_* Emacs ============================================================ 80 | %%% Local Variables: 81 | %%% allout-layout: t 82 | %%% erlang-indent-level: 4 83 | %%% End: 84 | -------------------------------------------------------------------------------- /src/oauth2_priv_set.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | %%%_* Module declaration =============================================== 21 | -module(oauth2_priv_set). 22 | 23 | %%%_* Exports ========================================================== 24 | %%%_ * API ------------------------------------------------------------- 25 | -export([new/1]). 26 | -export([union/2]). 27 | -export([is_subset/2]). 28 | -export([is_member/2]). 29 | 30 | -export_type([priv_set/0]). 31 | 32 | %%%_ * Types ----------------------------------------------------------- 33 | %% Invariant: Children are sorted increasingly by name. 34 | -type priv_tree() :: {node, Name :: binary(), Children :: [priv_tree()]} | '*'. 35 | %% Invariant: 36 | %% The list of trees is sorted increasingly by the name of the root node. 37 | -type priv_set() :: [priv_tree()]. 38 | 39 | %%%_* Code ============================================================= 40 | %%%_ * API ------------------------------------------------------------- 41 | %% @doc Constructs a new priv_set from a single path or a list of paths. 42 | %% A path denotes a single privilege. 43 | -spec new(binary() | [binary()]) -> priv_set(). 44 | new(Paths) when is_list(Paths) -> 45 | lists:foldl(fun union/2, [], [make_forest(Path) || Path <- Paths]); 46 | new(Path) when is_binary(Path) -> 47 | make_forest(Path). 48 | 49 | %% @doc Returns the union of Set1 and Set2, i.e., a set such that 50 | %% any path present in either Set1 or Set2 is also present in the result. 51 | -spec union(priv_set(), priv_set()) -> priv_set(). 52 | union([H1={node, Name1, _}|T1], [H2={node, Name2, _}|T2]) when Name1 < Name2 -> 53 | [H1|union(T1, [H2|T2])]; 54 | union([H1={node, Name1, _}|T1], [H2={node, Name2, _}|T2]) when Name1 > Name2 -> 55 | [H2|union([H1|T1], T2)]; 56 | union([{node, Name, S1}|T1], [{node, Name, S2}|T2]) -> 57 | [{node, Name, union(S1, S2)}|union(T1, T2)]; 58 | union(['*'|_], _) -> ['*']; %% '*' in union with anything is still '*'. 59 | union(_, ['*'|_]) -> ['*']; 60 | union([], Set) -> Set; 61 | union(Set, []) -> Set. 62 | 63 | %% @doc Return true if Set1 is a subset of Set2, i.e., if 64 | %% every privilege held by Set1 is also held by Set2. 65 | -spec is_subset(priv_set(), priv_set()) -> boolean(). 66 | is_subset([{node, N1, _}|_], [{node, N2, _}|_]) when N1 < N2 -> 67 | false; %% This tree isn't present in Set2 as per the invariant. 68 | is_subset(Set1 = [{node, N1, _}|_], [{node, N2, _}|T2]) when N1 > N2 -> 69 | is_subset(Set1, T2); 70 | is_subset([{node, Name, S1}|T1], [{node, Name, S2}|T2]) -> 71 | case is_subset(S1, S2) of 72 | true -> is_subset(T1, T2); 73 | false -> false 74 | end; 75 | is_subset(['*'|_], ['*'|_]) -> true; %% '*' is only a subset of '*'. 76 | is_subset(_, ['*'|_]) -> true; %% Everything is a subset of '*'. 77 | is_subset([], _) -> true; %% The empty set is a subset of every set. 78 | is_subset(_, _) -> false. 79 | 80 | %% @doc Returns true if Path is present in Set, i.e, if 81 | %% the privilege denoted by Path is contained within Set. 82 | -spec is_member(binary(), priv_set()) -> boolean(). 83 | is_member(Path, Set) -> is_subset(make_forest(Path), Set). 84 | 85 | 86 | %%%_* Private functions ================================================ 87 | -spec make_forest(binary() | list()) -> priv_set(). 88 | make_forest(Path) when is_binary(Path) -> 89 | make_forest(binary:split(Path, <<".">>, [global])); 90 | make_forest(Path) when is_list(Path) -> 91 | [make_tree(Path)]. 92 | 93 | -spec make_tree([binary()]) -> priv_tree(). 94 | make_tree([<<"*">>|_]) -> '*'; 95 | make_tree([N]) -> make_node(N, []); 96 | make_tree([H|T]) -> make_node(H, [make_tree(T)]). 97 | 98 | -spec make_node(binary(), [priv_tree()]) -> priv_tree(). 99 | make_node(Name, Children) -> {node, Name, Children}. 100 | 101 | %%%_* Tests ============================================================ 102 | -ifdef(TEST). 103 | -include_lib("eunit/include/eunit.hrl"). 104 | -endif. 105 | 106 | %%%_* Emacs ============================================================ 107 | %%% Local Variables: 108 | %%% allout-layout: t 109 | %%% erlang-indent-level: 4 110 | %%% End: 111 | -------------------------------------------------------------------------------- /src/oauth2_response.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | %%%_* Module declaration =============================================== 21 | -module(oauth2_response). 22 | 23 | %%%_* Exports ========================================================== 24 | %%%_ * API ------------------------------------------------------------- 25 | -export([new/1]). 26 | -export([new/2]). 27 | -export([new/3]). 28 | -export([new/4]). 29 | -export([new/6]). 30 | -export([new/7]). 31 | -export([access_token/1]). 32 | -export([access_token/2]). 33 | -export([access_code/1]). 34 | -export([access_code/2]). 35 | -export([refresh_token/1]). 36 | -export([refresh_token/2]). 37 | -export([refresh_token_expires_in/1]). 38 | -export([refresh_token_expires_in/2]). 39 | -export([resource_owner/1]). 40 | -export([resource_owner/2]). 41 | -export([expires_in/1]). 42 | -export([expires_in/2]). 43 | -export([scope/1]). 44 | -export([scope/2]). 45 | -export([token_type/1]). 46 | -export([to_proplist/1]). 47 | -ifndef(pre17). 48 | -export([to_map/1]). 49 | -endif. 50 | 51 | -export_type([response/0]). 52 | 53 | %%%_* Macros =========================================================== 54 | -define(TOKEN_TYPE, <<"bearer">>). 55 | 56 | %%%_ * Types ----------------------------------------------------------- 57 | -record(response, { 58 | access_token :: undefined | oauth2:token() 59 | ,access_code :: undefined | oauth2:token() 60 | ,expires_in :: undefined | oauth2:lifetime() 61 | ,resource_owner :: undefined | term() 62 | ,scope :: undefined | oauth2:scope() 63 | ,refresh_token :: undefined | oauth2:token() 64 | ,refresh_token_expires_in :: undefined | oauth2:lifetime() 65 | ,token_type = ?TOKEN_TYPE :: binary() 66 | }). 67 | 68 | -type response() :: #response{}. 69 | -type token() :: oauth2:token(). 70 | -type lifetime() :: oauth2:lifetime(). 71 | -type scope() :: oauth2:scope(). 72 | 73 | %%%_* Code ============================================================= 74 | %%%_ * API ------------------------------------------------------------- 75 | -spec new(token()) -> response(). 76 | new(AccessToken) -> 77 | #response{access_token = AccessToken}. 78 | 79 | -spec new(token(), lifetime()) -> response(). 80 | new(AccessToken, ExpiresIn) -> 81 | #response{access_token = AccessToken, expires_in = ExpiresIn}. 82 | 83 | new(AccessToken, ExpiresIn, RefreshToken) -> 84 | #response{ access_token = AccessToken 85 | , expires_in = ExpiresIn 86 | , refresh_token = RefreshToken 87 | }. 88 | 89 | -spec new(token(), lifetime(), term(), scope()) -> response(). 90 | new(AccessToken, ExpiresIn, ResOwner, Scope) -> 91 | #response{ access_token = AccessToken 92 | , expires_in = ExpiresIn 93 | , resource_owner = ResOwner 94 | , scope = Scope 95 | }. 96 | 97 | -spec new(token(), lifetime(), term(), scope(), token(), lifetime()) -> response(). 98 | new(AccessToken, ExpiresIn, ResOwner, Scope, RefreshToken, RExpiresIn) -> 99 | #response{ access_token = AccessToken 100 | , expires_in = ExpiresIn 101 | , resource_owner = ResOwner 102 | , scope = Scope 103 | , refresh_token = RefreshToken 104 | , refresh_token_expires_in = RExpiresIn 105 | }. 106 | 107 | -spec new(_, lifetime(), term(), scope(), _, _, token()) -> response(). 108 | new(_, ExpiresIn, ResOwner, Scope, _, _, AccessCode) -> 109 | #response{ access_code = AccessCode 110 | , expires_in = ExpiresIn 111 | , resource_owner = ResOwner 112 | , scope = Scope 113 | }. 114 | 115 | -spec access_token(response()) -> {ok, token()} | {error, not_set}. 116 | access_token(#response{access_token = undefined}) -> {error, not_set}; 117 | access_token(#response{access_token = AccessToken}) -> {ok, AccessToken}. 118 | 119 | -spec access_token(response(), token()) -> response(). 120 | access_token(Response, NewAccessToken) -> 121 | Response#response{access_token = NewAccessToken}. 122 | 123 | -spec access_code(response()) -> {ok, token()} | {error, not_set}. 124 | access_code(#response{access_code = undefined}) -> {error, not_set}; 125 | access_code(#response{access_code = AccessCode}) -> {ok, AccessCode}. 126 | 127 | -spec access_code(response(), token()) -> response(). 128 | access_code(Response, NewAccessCode) -> 129 | Response#response{access_code = NewAccessCode}. 130 | 131 | -spec expires_in(response()) -> {ok, lifetime()} | {error, not_set}. 132 | expires_in(#response{expires_in = undefined}) -> {error, not_set}; 133 | expires_in(#response{expires_in = ExpiresIn}) -> {ok, ExpiresIn}. 134 | 135 | -spec expires_in(response(), lifetime()) -> response(). 136 | expires_in(Response, NewExpiresIn) -> 137 | Response#response{expires_in = NewExpiresIn}. 138 | 139 | -spec scope(response()) -> {ok, scope()} | {error, not_set}. 140 | scope(#response{scope = undefined}) -> {error, not_set}; 141 | scope(#response{scope = Scope}) -> {ok, Scope}. 142 | 143 | -spec scope(response(), scope()) -> response(). 144 | scope(Response, NewScope) -> Response#response{scope = NewScope}. 145 | 146 | -spec refresh_token(response()) -> {ok, token()} | {error, not_set}. 147 | refresh_token(#response{refresh_token = undefined}) -> {error, not_set}; 148 | refresh_token(#response{refresh_token = RefreshToken}) -> {ok, RefreshToken}. 149 | 150 | -spec refresh_token(response(), token()) -> response(). 151 | refresh_token(Response, NewRefreshToken) -> 152 | Response#response{refresh_token = NewRefreshToken}. 153 | 154 | -spec refresh_token_expires_in(response()) -> {ok, lifetime()} | {error, not_set}. 155 | refresh_token_expires_in(#response{refresh_token = undefined}) -> 156 | {error, not_set}; 157 | refresh_token_expires_in(#response{refresh_token_expires_in = RefreshTokenExpiresIn}) -> 158 | {ok, RefreshTokenExpiresIn}. 159 | 160 | -spec refresh_token_expires_in(response(), lifetime()) -> response(). 161 | refresh_token_expires_in(Response, NewRefreshTokenExpiresIn) -> 162 | Response#response{refresh_token_expires_in = NewRefreshTokenExpiresIn}. 163 | 164 | -spec resource_owner(response()) -> {ok, term()}. 165 | resource_owner(#response{resource_owner = ResOwner}) -> 166 | {ok, ResOwner}. 167 | 168 | -spec resource_owner(response(), term()) -> response(). 169 | resource_owner(Response, NewResOwner) -> 170 | Response#response{resource_owner = NewResOwner}. 171 | 172 | -spec token_type(response()) -> {ok, binary()}. 173 | token_type(#response{}) -> 174 | {ok, ?TOKEN_TYPE}. 175 | 176 | -spec to_proplist(response()) -> proplists:proplist(). 177 | to_proplist(Response) -> 178 | response_foldr(Response, fun(Key, Value, Acc) -> [{Key, Value} | Acc] end, []). 179 | 180 | -ifndef(pre17). 181 | -ifdef(pre18). 182 | -spec to_map(response()) -> map(binary(), any()). 183 | -else. 184 | -spec to_map(response()) -> #{binary() => any()}. 185 | -endif. 186 | to_map(Response) -> 187 | response_foldr(Response, fun(Key, Value, Acc) -> maps:put(Key, Value, Acc) end, maps:new()). 188 | -endif. 189 | 190 | %%%_* Private functions ================================================ 191 | -spec response_foldr(Response, Fun, Acc0) -> Return when 192 | Response :: response(), 193 | Fun :: fun((Key::binary(), Value::any(), Acc::any()) -> Acc::any()), 194 | Acc0 :: any(), 195 | Return :: any(). 196 | response_foldr(Record, Fun, Acc0) -> 197 | Keys = record_info(fields, response), 198 | Values = tl(tuple_to_list(Record)), %% Head is 'response'! 199 | response_foldr(Keys, Values, Fun, Acc0). 200 | 201 | response_foldr([], [], _Fun, Acc0) -> 202 | Acc0; 203 | response_foldr([_ | Ks], [undefined | Vs], Fun, Acc) -> 204 | response_foldr(Ks, Vs, Fun, Acc); 205 | response_foldr([refresh_token_expires_in | Ks], [V | Vs], Fun, Acc) -> 206 | Fun(<<"refresh_token_expires_in">>, V, response_foldr(Ks, Vs, Fun, Acc)); 207 | response_foldr([expires_in | Ks], [V | Vs], Fun, Acc) -> 208 | Fun(<<"expires_in">>, V, response_foldr(Ks, Vs, Fun, Acc)); 209 | response_foldr([K | Ks], [V | Vs], Fun, Acc) -> 210 | Key = atom_to_binary(K, latin1), 211 | Value = to_binary(V), 212 | Fun(Key, Value, response_foldr(Ks, Vs, Fun, Acc)). 213 | 214 | to_binary(Binary) when is_binary(Binary) -> 215 | Binary; 216 | to_binary([Binary]) when is_binary(Binary) -> 217 | Binary; 218 | to_binary([BinaryHead | Tail]) when is_binary(BinaryHead) -> 219 | <>; 220 | to_binary(List) when is_list(List) -> 221 | to_binary(list_to_binary(List)); 222 | to_binary(Atom) when is_atom(Atom) -> 223 | to_binary(atom_to_list(Atom)); 224 | to_binary(Float) when is_float(Float) -> 225 | to_binary(float_to_list(Float)); 226 | to_binary(Integer) when is_integer(Integer) -> 227 | to_binary(integer_to_list(Integer)); 228 | to_binary({Key, Value}) -> 229 | {to_binary(Key), to_binary(Value)}; 230 | to_binary(Term) -> 231 | to_binary(term_to_binary(Term)). 232 | 233 | %%%_* Tests ============================================================ 234 | -ifdef(TEST). 235 | -include_lib("eunit/include/eunit.hrl"). 236 | -endif. 237 | 238 | %%%_* Emacs ============================================================ 239 | %%% Local Variables: 240 | %%% allout-layout: t 241 | %%% erlang-indent-level: 4 242 | %%% End: 243 | -------------------------------------------------------------------------------- /src/oauth2_token.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | %%%_* Module declaration =============================================== 21 | -module(oauth2_token). 22 | 23 | -behaviour(oauth2_token_generation). 24 | 25 | %%%_* Exports ========================================================== 26 | %%%_ * API ------------------------------------------------------------- 27 | -export([generate/1]). 28 | 29 | %%%_* Macros =========================================================== 30 | -define(TOKEN_LENGTH, 32). 31 | 32 | %%%_* Code ============================================================= 33 | %%%_ * API ------------------------------------------------------------- 34 | %% @doc Generates a random OAuth2 token. 35 | -spec generate(oauth2:context()) -> oauth2:token(). 36 | generate(_Context) -> generate_fragment(?TOKEN_LENGTH). 37 | 38 | %%%_* Private functions ================================================ 39 | -spec generate_fragment(integer()) -> binary(). 40 | generate_fragment(0) -> <<>>; 41 | generate_fragment(N) -> 42 | Rand = base64:encode(crypto:strong_rand_bytes(N)), 43 | Frag = << <> || <> <= <>, is_alphanum(C) >>, 44 | <>. 45 | 46 | %% @doc Returns true for alphanumeric ASCII characters, false for all others. 47 | -spec is_alphanum(char()) -> boolean(). 48 | is_alphanum(C) when C >= 16#30 andalso C =< 16#39 -> true; 49 | is_alphanum(C) when C >= 16#41 andalso C =< 16#5A -> true; 50 | is_alphanum(C) when C >= 16#61 andalso C =< 16#7A -> true; 51 | is_alphanum(_) -> false. 52 | 53 | %%%_* Tests ============================================================ 54 | -ifdef(TEST). 55 | -include_lib("eunit/include/eunit.hrl"). 56 | -endif. 57 | 58 | %%%_* Emacs ============================================================ 59 | %%% Local Variables: 60 | %%% allout-layout: t 61 | %%% erlang-indent-level: 4 62 | %%% End: 63 | -------------------------------------------------------------------------------- /src/oauth2_token_generation.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2012-2015 Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Erlang OAuth 2.0 implementation 17 | %%% @end 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | 20 | %%%_* Module declaration =============================================== 21 | -module(oauth2_token_generation). 22 | 23 | %%%_* Behaviour ======================================================== 24 | %% @doc Generates a random OAuth2 token. 25 | -callback generate(oauth2:context()) -> oauth2:token(). 26 | 27 | %%%_* Tests ============================================================ 28 | -ifdef(TEST). 29 | -include_lib("eunit/include/eunit.hrl"). 30 | -endif. 31 | 32 | %%%_* Emacs ============================================================ 33 | %%% Local Variables: 34 | %%% allout-layout: t 35 | %%% erlang-indent-level: 4 36 | %%% End: 37 | -------------------------------------------------------------------------------- /test/oauth2_mock_backend.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% oauth2: Erlang OAuth 2.0 implementation 4 | %% 5 | %% Copyright (c) 2012-2014 Kivra 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(oauth2_mock_backend). 28 | 29 | -behavior(oauth2_backend). 30 | 31 | %%% Behavior API 32 | -export([authenticate_user/2]). 33 | -export([authenticate_client/2]). 34 | -export([get_client_identity/2]). 35 | -export([associate_access_code/3]). 36 | -export([associate_refresh_token/3]). 37 | -export([associate_access_token/3]). 38 | -export([resolve_access_code/2]). 39 | -export([resolve_refresh_token/2]). 40 | -export([resolve_access_token/2]). 41 | -export([revoke_access_code/2]). 42 | -export([revoke_access_token/2]). 43 | -export([revoke_refresh_token/2]). 44 | -export([get_redirection_uri/2]). 45 | -export([verify_redirection_uri/3]). 46 | -export([verify_client_scope/3]). 47 | -export([verify_resowner_scope/3]). 48 | -export([verify_scope/3]). 49 | 50 | %%% mock_backend-specifics 51 | -export([start/0]). 52 | -export([stop/0]). 53 | 54 | %%% Placeholder values that the mock backend will recognize. 55 | -define(UNAME, <<"herp">>). 56 | -define(PASSWORD, <<"derp">>). 57 | -define(USCOPE, [<<"xyz">>]). 58 | -define(RES_OWNER, <<"user">>). 59 | 60 | -define(CID, <<"TiaUdYODLOMyLkdaKkqlmhsl9QJ94a">>). 61 | -define(SECRET, <<"fvfDMAwjlruC9rv5FsLjmyrihCcIKJL">>). 62 | -define(CSCOPE, <<"abc">>). 63 | -define(CLIENT_URI, <<"https://no.where/cb">>). 64 | 65 | -define(ETS_TABLE, access_tokens). 66 | 67 | %%%=================================================================== 68 | %%% API 69 | %%%=================================================================== 70 | 71 | authenticate_user({?UNAME, ?PASSWORD}, Ctx) -> {ok, {Ctx, {user, 31337}}}; 72 | authenticate_user({?UNAME, _}, _) -> {error, badpass}; 73 | authenticate_user(_, _) -> {error, notfound}. 74 | 75 | authenticate_client({?CID, ?SECRET}, Ctx) -> {ok, {Ctx, {client, 4711}}}; 76 | authenticate_client({?CID, _}, _) -> {error, badsecret}; 77 | authenticate_client(_, _) -> {error, notfound}. 78 | 79 | get_client_identity(?CID, Ctx) -> {ok, {Ctx, {client, 4711}}}; 80 | get_client_identity(_, _) -> {error, notfound}. 81 | 82 | associate_access_code(AccessCode, Context, AppContext) -> 83 | associate_access_token(AccessCode, Context, AppContext). 84 | 85 | associate_refresh_token(RefreshToken, Context, AppContext) -> 86 | ets:insert(?ETS_TABLE, {RefreshToken, Context}), 87 | {ok, AppContext}. 88 | 89 | associate_access_token(AccessToken, Context, AppContext) -> 90 | ets:insert(?ETS_TABLE, {AccessToken, Context}), 91 | {ok, AppContext}. 92 | 93 | resolve_access_code(AccessCode, AppContext) -> 94 | resolve_access_token(AccessCode, AppContext). 95 | 96 | resolve_refresh_token(RefreshToken, AppContext) -> 97 | resolve_access_token(RefreshToken, AppContext). 98 | 99 | resolve_access_token(AccessToken, AppContext) -> 100 | case ets:lookup(?ETS_TABLE, AccessToken) of 101 | [] -> {error, notfound}; 102 | [{_, Context}] -> {ok, {AppContext, Context}} 103 | end. 104 | 105 | revoke_access_code(AccessCode, AppContext) -> 106 | revoke_access_token(AccessCode, AppContext). 107 | 108 | revoke_access_token(AccessToken, AppContext) -> 109 | ets:delete(?ETS_TABLE, AccessToken), 110 | {ok, AppContext}. 111 | 112 | revoke_refresh_token(_RefreshToken, AppContext) -> 113 | {ok, AppContext}. 114 | 115 | get_redirection_uri({?CID, ?SECRET}, Ctx) -> {ok, {Ctx, ?CLIENT_URI}}; 116 | get_redirection_uri(_, _) -> {error, notfound}. 117 | 118 | verify_redirection_uri({client, 4711}, ?CLIENT_URI, Ctx) -> {ok, Ctx}; 119 | verify_redirection_uri(_, _, _) -> {error, mismatch}. 120 | 121 | verify_client_scope({client, 4711}, [], Ctx) -> {ok, {Ctx, []}}; 122 | verify_client_scope({client, 4711}, ?CSCOPE, Ctx) -> {ok, {Ctx, ?CSCOPE}}; 123 | verify_client_scope(_, _, _) -> {error, invalid_scope}. 124 | 125 | verify_resowner_scope({user, 31337}, ?USCOPE, Ctx) -> {ok, {Ctx, ?USCOPE}}; 126 | verify_resowner_scope(_, _, _) -> {error, invalid_scope}. 127 | 128 | verify_scope(Scope, Scope, AppContext) -> {ok, {AppContext, Scope}}; 129 | verify_scope(_, _, _) -> {error, invalid_scope}. 130 | 131 | %% Set up the ETS table for holding access tokens. 132 | start() -> ets:new(?ETS_TABLE, [public, named_table, {read_concurrency, true}]). 133 | stop() -> ets:delete(?ETS_TABLE). 134 | 135 | %%%_* Tests ============================================================ 136 | -ifdef(TEST). 137 | -include_lib("eunit/include/eunit.hrl"). 138 | 139 | -endif. 140 | 141 | %%%_* Emacs ============================================================ 142 | %%% Local Variables: 143 | %%% allout-layout: t 144 | %%% erlang-indent-level: 4 145 | %%% End: 146 | -------------------------------------------------------------------------------- /test/oauth2_priv_set_tests.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% oauth2: Erlang OAuth 2.0 implementation 4 | %% 5 | %% Copyright (c) 2012-2014 Kivra 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(oauth2_priv_set_tests). 28 | 29 | -include_lib("proper/include/proper.hrl"). 30 | -include_lib("eunit/include/eunit.hrl"). 31 | 32 | %%%=================================================================== 33 | %%% Test cases 34 | %%%=================================================================== 35 | 36 | proper_type_spec_test_() -> 37 | {timeout, 1200, [{?LINE, 38 | fun() -> proper:check_specs(oauth2_priv_set, 39 | [{to_file, user}]) end}]}. 40 | 41 | new_test_() -> 42 | [ 43 | ?_assert(oauth2_priv_set:is_member( 44 | <<"x.y.z">>, 45 | oauth2_priv_set:new(<<"x.y.z">>))), 46 | ?_assert(oauth2_priv_set:is_member( 47 | <<"a.b.c">>, 48 | oauth2_priv_set:new([<<"a.b.c">>, <<"a.b.d">>]))), 49 | ?_assertNot(oauth2_priv_set:is_member( 50 | <<"a.b.c">>, 51 | oauth2_priv_set:new(<<"x.y.z">>))), 52 | ?_assertNot(oauth2_priv_set:is_member( 53 | <<"a.b.e">>, 54 | oauth2_priv_set:new([<<"a.b.c">>, <<"a.b.a">>]))) 55 | ]. 56 | 57 | is_subset_test_() -> 58 | [ 59 | ?_assert(oauth2_priv_set:is_subset( 60 | oauth2_priv_set:new(<<"a.b.c.d.e">>), 61 | oauth2_priv_set:new([ 62 | <<"a.b">>, 63 | <<"a.b.x.y">>, 64 | <<"a.b.z.x">>, 65 | <<"a.b.k.d.g.e">>, 66 | <<"a.b.m.n.p.q">>, 67 | <<"a.b.c.d.*">> 68 | ]))), 69 | ?_assert(oauth2_priv_set:is_subset( 70 | oauth2_priv_set:new(<<"a.b.c">>), 71 | oauth2_priv_set:new([<<"a.b.c">>, <<"a.s.d.f.g.h">>]))), 72 | ?_assert(oauth2_priv_set:is_subset( 73 | oauth2_priv_set:new(<<"x.y.z">>), 74 | oauth2_priv_set:new([<<"x.y">>, <<"x.*">>]))), 75 | ?_assert(oauth2_priv_set:is_subset( 76 | oauth2_priv_set:new(<<"x.y.z">>), 77 | oauth2_priv_set:new([<<"x.*">>, <<"x.y">>]))), 78 | ?_assert(oauth2_priv_set:is_subset( 79 | oauth2_priv_set:new(<<"x.*">>), 80 | oauth2_priv_set:new([<<"a.*">>, <<"x.*">>]))), 81 | ?_assertNot(oauth2_priv_set:is_subset( 82 | oauth2_priv_set:new(<<"a.b.c">>), 83 | oauth2_priv_set:new(<<"a.b">>))), 84 | ?_assertNot(oauth2_priv_set:is_subset( 85 | oauth2_priv_set:new(<<"x.y.z">>), 86 | oauth2_priv_set:new(<<"x.z.*">>))), 87 | ?_assertNot(oauth2_priv_set:is_subset( 88 | oauth2_priv_set:new(<<"a.*">>), 89 | oauth2_priv_set:new([<<"a.b.*">>, 90 | <<"a.c.*">>, 91 | <<"a.d.*">>, 92 | <<"a.e.*">>, 93 | <<"a.f.z">>]))) 94 | ]. 95 | -------------------------------------------------------------------------------- /test/oauth2_response_tests.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% oauth2: Erlang OAuth 2.0 implementation 4 | %% 5 | %% Copyright (c) 2012-2014 Kivra 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(oauth2_response_tests). 28 | 29 | -include_lib("proper/include/proper.hrl"). 30 | -include_lib("eunit/include/eunit.hrl"). 31 | 32 | -define(ACCESS, <<"9bX9iFUOsXbM12OOjfDW175IXXOELp6K">>). 33 | -define(REFRESH, <<"JVs3ZFQJBIdduJdhhWOoAt2B3qEKcHEo">>). 34 | -define(CODE, <<"Lz7Z24cKSQ28z8kem01ZP9c0aE3TEbGl">>). 35 | -define(RESOURCE_OWNER, <<"user">>). 36 | -define(EXPIRY, 3600). 37 | -define(SCOPE, <<"herp derp">>). 38 | -define(TOKEN_TYPE, <<"bearer">>). 39 | 40 | 41 | %%%=================================================================== 42 | %%% Test cases 43 | %%%=================================================================== 44 | 45 | proper_type_spec_test_() -> 46 | {timeout, 1200, [{?LINE, 47 | fun() -> proper:check_specs(oauth2_response, 48 | [{to_file, user}]) end}]}. 49 | 50 | new_1_test_() -> 51 | {setup, 52 | fun() -> oauth2_response:new(?ACCESS) end, 53 | fun(_) -> ok end, 54 | fun(Response) -> 55 | [ 56 | ?_assertEqual({ok, ?ACCESS}, oauth2_response:access_token(Response)), 57 | ?_assertMatch({error, not_set}, oauth2_response:expires_in(Response)), 58 | ?_assertMatch({error, not_set}, oauth2_response:scope(Response)), 59 | ?_assertMatch({error, not_set}, oauth2_response:refresh_token(Response)) 60 | ] 61 | end}. 62 | 63 | new_2_test_() -> 64 | {setup, 65 | fun() -> oauth2_response:new(?ACCESS, ?EXPIRY) end, 66 | fun(_) -> ok end, 67 | fun(Response) -> 68 | [ 69 | ?_assertEqual({ok, ?ACCESS}, oauth2_response:access_token(Response)), 70 | ?_assertEqual({ok, ?EXPIRY}, oauth2_response:expires_in(Response)), 71 | ?_assertMatch({error, not_set}, oauth2_response:scope(Response)), 72 | ?_assertMatch({error, not_set}, oauth2_response:refresh_token(Response)) 73 | ] 74 | end}. 75 | 76 | new_4_test_() -> 77 | {setup, 78 | fun() -> oauth2_response:new(?ACCESS, ?EXPIRY, ?RESOURCE_OWNER, ?SCOPE) end, 79 | fun(_) -> ok end, 80 | fun(Response) -> 81 | [ 82 | ?_assertEqual({ok, ?ACCESS}, oauth2_response:access_token(Response)), 83 | ?_assertEqual({ok, ?EXPIRY}, oauth2_response:expires_in(Response)), 84 | ?_assertEqual({ok, ?SCOPE}, oauth2_response:scope(Response)), 85 | ?_assertMatch({error, not_set}, oauth2_response:refresh_token(Response)) 86 | ] 87 | end}. 88 | 89 | new_5_test_() -> 90 | {setup, 91 | fun() -> oauth2_response:new(?ACCESS, ?EXPIRY, ?RESOURCE_OWNER, ?SCOPE, ?REFRESH, ?EXPIRY) end, 92 | fun(_) -> ok end, 93 | fun(Response) -> 94 | [ 95 | ?_assertEqual({ok, ?ACCESS}, oauth2_response:access_token(Response)), 96 | ?_assertEqual({ok, ?EXPIRY}, oauth2_response:expires_in(Response)), 97 | ?_assertEqual({ok, ?SCOPE}, oauth2_response:scope(Response)), 98 | ?_assertEqual({ok, ?REFRESH}, oauth2_response:refresh_token(Response)) 99 | ] 100 | end}. 101 | 102 | 103 | new_6_test_() -> 104 | {setup, 105 | fun() -> oauth2_response:new(?ACCESS, ?EXPIRY, ?RESOURCE_OWNER, ?SCOPE, ?REFRESH, ?EXPIRY, ?CODE) end, 106 | fun(_) -> ok end, 107 | fun(Response) -> 108 | [ 109 | ?_assertEqual({error, not_set}, oauth2_response:access_token(Response)), 110 | ?_assertEqual({ok, ?EXPIRY}, oauth2_response:expires_in(Response)), 111 | ?_assertEqual({ok, ?SCOPE}, oauth2_response:scope(Response)), 112 | ?_assertEqual({error, not_set}, oauth2_response:refresh_token(Response)), 113 | ?_assertEqual({ok, ?CODE}, oauth2_response:access_code(Response)) 114 | ] 115 | end}. 116 | 117 | 118 | access_token_test() -> 119 | ?assertEqual({ok, ?REFRESH}, 120 | oauth2_response:access_token( 121 | oauth2_response:access_token( 122 | oauth2_response:new(?ACCESS), 123 | ?REFRESH))). 124 | 125 | access_code_test() -> 126 | ?assertEqual({ok, ?CODE}, 127 | oauth2_response:access_code( 128 | oauth2_response:access_code( 129 | oauth2_response:new([], [], [], [], [], ?CODE), 130 | ?CODE))). 131 | 132 | expires_in_test() -> 133 | ?assertEqual({ok, ?EXPIRY}, 134 | oauth2_response:expires_in( 135 | oauth2_response:expires_in( 136 | oauth2_response:new(?ACCESS), 137 | ?EXPIRY))). 138 | 139 | scope_test() -> 140 | ?assertEqual({ok, ?SCOPE}, 141 | oauth2_response:scope( 142 | oauth2_response:scope( 143 | oauth2_response:new(?ACCESS), 144 | ?SCOPE))). 145 | 146 | refresh_token_test() -> 147 | ?assertEqual({ok, ?REFRESH}, 148 | oauth2_response:refresh_token( 149 | oauth2_response:refresh_token( 150 | oauth2_response:new(?ACCESS), 151 | ?REFRESH))). 152 | 153 | resource_owner_test() -> 154 | ?assertEqual({ok, ?RESOURCE_OWNER}, 155 | oauth2_response:resource_owner( 156 | oauth2_response:resource_owner( 157 | oauth2_response:new(?ACCESS), 158 | ?RESOURCE_OWNER))). 159 | 160 | token_type_test() -> 161 | ?assertEqual({ok, ?TOKEN_TYPE}, 162 | oauth2_response:token_type(oauth2_response:new(?ACCESS))). 163 | 164 | to_proplist_test() -> 165 | Property = ?FORALL( 166 | {AccessToken, Expiry, ResourceOwner, Scope, RefreshToken, RefreshExpiry}, 167 | {non_empty(oauth2:token()), oauth2:lifetime(), binary(), oauth2:scope(), non_empty(oauth2:token()), oauth2:lifetime()}, 168 | begin 169 | Response = oauth2_response:new(AccessToken, Expiry, ResourceOwner, Scope, RefreshToken, RefreshExpiry), 170 | [ 171 | {<<"access_token">>, AccessToken}, 172 | {<<"expires_in">>, Expiry}, 173 | {<<"resource_owner">>, ResourceOwner}, 174 | {<<"scope">>, scope_to_binary(Scope)}, 175 | {<<"refresh_token">>, RefreshToken}, 176 | {<<"refresh_token_expires_in">>, RefreshExpiry}, 177 | {<<"token_type">>, <<"bearer">>} 178 | ] =:= oauth2_response:to_proplist(Response) 179 | end), 180 | ?assert(proper:quickcheck(Property, [{to_file, user}])). 181 | 182 | -ifndef(pre17). 183 | to_map_test() -> 184 | Property = ?FORALL( 185 | {AccessToken, Expiry, ResourceOwner, Scope, RefreshToken, RefreshExpiry}, 186 | {non_empty(oauth2:token()), oauth2:lifetime(), binary(), oauth2:scope(), non_empty(oauth2:token()), oauth2:lifetime()}, 187 | begin 188 | Response = oauth2_response:new(AccessToken, Expiry, ResourceOwner, Scope, RefreshToken, RefreshExpiry), 189 | #{ 190 | <<"access_token">> => AccessToken, 191 | <<"expires_in">> => Expiry, 192 | <<"resource_owner">> => ResourceOwner, 193 | <<"scope">> => scope_to_binary(Scope), 194 | <<"refresh_token">> => RefreshToken, 195 | <<"refresh_token_expires_in">> => RefreshExpiry, 196 | <<"token_type">> => <<"bearer">> 197 | } =:= oauth2_response:to_map(Response) 198 | end), 199 | ?assert(proper:quickcheck(Property, [{to_file, user}])). 200 | -endif. 201 | 202 | %%%=================================================================== 203 | %%% Helpers 204 | %%%=================================================================== 205 | 206 | scope_to_binary(Binary) when is_binary(Binary) -> 207 | Binary; 208 | scope_to_binary([]) -> 209 | <<>>; 210 | scope_to_binary([Binary]) when is_binary(Binary) -> 211 | Binary; 212 | scope_to_binary([BinaryHead | Tail]) when is_binary(BinaryHead) -> 213 | <>. 214 | -------------------------------------------------------------------------------- /test/oauth2_tests.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% oauth2: Erlang OAuth 2.0 implementation 4 | %% 5 | %% Copyright (c) 2012-2014 Kivra 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(oauth2_tests). 28 | 29 | -include_lib("eunit/include/eunit.hrl"). 30 | 31 | %%% Placeholder values that the mock backend will recognize. 32 | -define(USER_NAME, <<"herp">>). 33 | -define(USER_PASSWORD, <<"derp">>). 34 | -define(USER_SCOPE, [<<"xyz">>]). 35 | -define(RESOURCE_OWNER, <<"user">>). 36 | 37 | -define(CLIENT_ID, <<"TiaUdYODLOMyLkdaKkqlmhsl9QJ94a">>). 38 | -define(CLIENT_SECRET, <<"fvfDMAwjlruC9rv5FsLjmyrihCcIKJL">>). 39 | -define(CLIENT_SCOPE, <<"abc">>). 40 | -define(CLIENT_URI, <<"https://no.where/cb">>). 41 | 42 | %%%=================================================================== 43 | %%% Test cases 44 | %%%=================================================================== 45 | 46 | bad_authorize_password_test_() -> 47 | {setup, 48 | fun start/0, 49 | fun stop/1, 50 | fun(_) -> 51 | [ 52 | ?_assertMatch({ok, _}, 53 | oauth2:authorize_password( 54 | {<<"herp">>, <<"derp">>}, 55 | [<<"xyz">>], 56 | foo_context)), 57 | ?_assertMatch({error, invalid_scope}, 58 | oauth2:authorize_password( 59 | {<<"herp">>, <<"derp">>}, 60 | <<"bad_scope">>, 61 | foo_context)), 62 | ?_assertMatch({error, badpass}, 63 | oauth2:authorize_password( 64 | {<<"herp">>, <<"herp">>}, 65 | <<"xyz">>, 66 | foo_context)), 67 | ?_assertMatch({error, notfound}, 68 | oauth2:authorize_password( 69 | {<<"derp">>,<<"derp">>}, 70 | <<"xyz">>, 71 | foo_context)), 72 | ?_assertMatch({ok, _}, 73 | oauth2:authorize_password( 74 | {<<"herp">>, <<"derp">>}, 75 | {?CLIENT_ID,?CLIENT_SECRET}, 76 | [<<"xyz">>], 77 | foo_context)), 78 | ?_assertMatch({error, invalid_scope}, 79 | oauth2:authorize_password( 80 | {<<"herp">>, <<"derp">>}, 81 | {?CLIENT_ID, ?CLIENT_SECRET}, 82 | <<"bad_scope">>, 83 | foo_context)), 84 | ?_assertMatch({error, badpass}, 85 | oauth2:authorize_password( 86 | {<<"herp">>, <<"herp">>}, 87 | {?CLIENT_ID, ?CLIENT_SECRET}, 88 | <<"xyz">>, 89 | foo_context)), 90 | ?_assertMatch({error, invalid_client}, 91 | oauth2:authorize_password( 92 | {<<"herp">>, <<"herp">>}, 93 | {?CLIENT_ID, <<"gggDMAwklAKc9kq5FsLjKrzi">>}, 94 | <<"xyz">>, 95 | foo_context)), 96 | ?_assertMatch({error, invalid_client}, 97 | oauth2:authorize_password( 98 | {<<"herp">>, <<"herp">>}, 99 | {<<"XoaUdYODRC">>, ?CLIENT_SECRET}, 100 | <<"xyz">>, 101 | foo_context)) 102 | ] 103 | end}. 104 | 105 | authorize_implicit_grant_test_() -> 106 | {setup, 107 | fun start/0, 108 | fun stop/1, 109 | fun(_) -> 110 | [ 111 | fun() -> 112 | {ok, {foo_context, Auth}} = 113 | oauth2:authorize_password( {?USER_NAME,?USER_PASSWORD} 114 | , ?CLIENT_ID 115 | , ?CLIENT_URI 116 | , ?USER_SCOPE 117 | , foo_context), 118 | {ok, {foo_context, Response}} = 119 | oauth2:issue_token(Auth, foo_context), 120 | {ok, Token} = oauth2_response:access_token(Response), 121 | ?assertMatch( {ok, _} 122 | , oauth2:verify_access_token( Token 123 | , foo_context )) 124 | end 125 | ] 126 | end}. 127 | 128 | bad_authorize_client_credentials_test_() -> 129 | {setup, 130 | fun start/0, 131 | fun stop/1, 132 | fun(_) -> 133 | [ 134 | ?_assertMatch({error, invalid_client}, 135 | oauth2:authorize_client_credentials( 136 | { <<"XoaUdYODRCMyLkdaKkqlmhsl9QQJ4b">> 137 | , <<"fvfDMAwjlruC9rv5FsLjmyrihCcIKJL">> }, 138 | <<"abc">>, 139 | foo_context)), 140 | ?_assertMatch({error, invalid_scope}, 141 | oauth2:authorize_client_credentials( 142 | {?CLIENT_ID, ?CLIENT_SECRET}, 143 | <<"bad_scope">>, 144 | foo_context)), 145 | ?_assertMatch({error, invalid_client}, 146 | oauth2:authorize_client_credentials( 147 | { <<"TiaUdYODLOMyLkdaKkqlmdhsl9QJ94a">> 148 | , <<"gggDMAwklAKc9kq5FsLjKrzihCcI123">> }, 149 | <<"abc">>, 150 | foo_context)), 151 | ?_assertMatch({error, invalid_client}, 152 | oauth2:authorize_client_credentials( 153 | { <<"TiaUdYODLOMyLkdaKkqlmdhsl9QJ94a">> 154 | , <<"fvfDMAwjlruC9rv5FsLjmyrihCcIKJL">> }, 155 | <<"cba">>, 156 | foo_context)) 157 | ] 158 | end}. 159 | 160 | bad_ttl_test_() -> 161 | {setup, 162 | fun start/0, 163 | fun stop/1, 164 | fun(_) -> 165 | [ 166 | fun() -> 167 | application:set_env(oauth2, expiry_time, 0), 168 | 169 | {ok, Response} = issue_access_token(foo_context), 170 | {ok, Token} = oauth2_response:access_token(Response), 171 | 172 | ?assertEqual({error, access_denied}, 173 | oauth2:verify_access_token(Token, foo_context)) 174 | end, 175 | fun() -> 176 | application:set_env(oauth2, expiry_time, 0), 177 | 178 | {ok, Response} = issue_access_code(foo_context), 179 | {ok, Code} = oauth2_response:access_code(Response), 180 | 181 | ?assertEqual({error, invalid_grant}, 182 | oauth2:verify_access_code(Code, foo_context)) 183 | end, 184 | fun() -> 185 | application:set_env(oauth2, expiry_time, 3600), 186 | 187 | {ok, Res1} = issue_access_code(foo_context), 188 | 189 | application:set_env(oauth2, expiry_time, 0), 190 | 191 | {ok, Res2} = issue_token_and_refresh(Res1, foo_context), 192 | 193 | {ok, RefreshToken} = oauth2_response:refresh_token(Res2), 194 | ?assertEqual({error, invalid_grant}, 195 | oauth2:refresh_access_token( 196 | {?CLIENT_ID, ?CLIENT_SECRET}, 197 | RefreshToken, 198 | ?USER_SCOPE, 199 | foo_context)) 200 | end 201 | ] 202 | end}. 203 | 204 | verify_access_token_test_() -> 205 | {setup, 206 | fun start/0, 207 | fun stop/1, 208 | fun(_) -> 209 | [ 210 | fun() -> 211 | {ok, Response} = issue_access_token(foo_context), 212 | {ok, Token} = oauth2_response:access_token(Response), 213 | 214 | ?assertMatch({ok, {foo_context, _}}, 215 | oauth2:verify_access_token(Token, foo_context)) 216 | end, 217 | ?_assertMatch({error, access_denied}, 218 | oauth2:verify_access_token(<<"nonexistent_token">>, 219 | foo_context)) 220 | ] 221 | end}. 222 | 223 | bad_access_code_test_() -> 224 | {setup, 225 | fun start/0, 226 | fun stop/1, 227 | fun(_) -> 228 | [ 229 | fun() -> 230 | {error, unauthorized_client} = 231 | oauth2:authorize_code_request( 232 | {?USER_NAME, ?USER_PASSWORD}, 233 | ?CLIENT_ID, 234 | <<"http://in.val.id">>, 235 | ?USER_SCOPE, 236 | foo_context), 237 | {error, unauthorized_client} = 238 | oauth2:authorize_code_request( 239 | {?USER_NAME, ?USER_PASSWORD}, 240 | <<"XoaUdYODRCMyLkdaKkqlmhsl9QQJ4b">>, 241 | ?CLIENT_URI, 242 | ?CLIENT_SCOPE, 243 | foo_context), 244 | {error, invalid_scope} = oauth2:authorize_code_request( 245 | {?USER_NAME, ?USER_PASSWORD}, 246 | ?CLIENT_ID, 247 | ?CLIENT_URI, 248 | <<"bad_scope">>, 249 | foo_context), 250 | {error, badpass} = oauth2:authorize_code_request( 251 | {<<"herp">>, <<"herp">>}, 252 | ?CLIENT_ID, 253 | ?CLIENT_URI, 254 | ?CLIENT_SCOPE, 255 | foo_context), 256 | ?_assertMatch({error, invalid_grant}, 257 | oauth2:verify_access_code(<<"nonexistent_token">>, foo_context)) 258 | end 259 | ] 260 | end}. 261 | 262 | verify_access_code_test_() -> 263 | {setup, 264 | fun start/0, 265 | fun stop/1, 266 | fun(_) -> 267 | [ 268 | fun() -> 269 | {ok, Response} = issue_access_code(foo_context), 270 | {ok, Code} = oauth2_response:access_code(Response), 271 | ?assertMatch({ok, {user, 31337}}, 272 | oauth2_response:resource_owner(Response)), 273 | ?assertMatch({ok, _}, oauth2:verify_access_code( 274 | Code, foo_context)), 275 | {ok, {foo_context, Auth2}} = 276 | oauth2:authorize_code_grant( 277 | {?CLIENT_ID, ?CLIENT_SECRET}, 278 | Code, 279 | ?CLIENT_URI, 280 | foo_context), 281 | {ok, {foo_context, Response2}} = 282 | oauth2:issue_token_and_refresh(Auth2, foo_context), 283 | {ok, Token} = oauth2_response:access_token(Response2), 284 | ?assertMatch({ok, _}, oauth2:verify_access_token( 285 | Token, 286 | foo_context)) 287 | end 288 | ] 289 | end}. 290 | 291 | bad_refresh_token_test_() -> 292 | {setup, 293 | fun start/0, 294 | fun stop/1, 295 | fun(_) -> 296 | [ 297 | fun() -> 298 | {ok, {foo_context, Auth}} = 299 | oauth2:authorize_code_request( 300 | {?USER_NAME, ?USER_PASSWORD}, 301 | ?CLIENT_ID, 302 | ?CLIENT_URI, 303 | ?USER_SCOPE, 304 | foo_context), 305 | {ok, {foo_context, Response}} = 306 | oauth2:issue_code(Auth, foo_context), 307 | {ok, Code} = oauth2_response:access_code(Response), 308 | {ok, {foo_context, Auth2}} = 309 | oauth2:authorize_code_grant( 310 | {?CLIENT_ID, ?CLIENT_SECRET}, 311 | Code, 312 | ?CLIENT_URI, 313 | foo_context), 314 | {ok, {foo_context, Res2}} = 315 | oauth2:issue_token_and_refresh(Auth2, foo_context), 316 | {ok, RefreshToken} = oauth2_response:refresh_token(Res2), 317 | ?assertMatch({error, invalid_client}, 318 | oauth2:refresh_access_token( 319 | {<<"foo">>, ?CLIENT_SECRET}, 320 | RefreshToken, 321 | ?CLIENT_SCOPE, 322 | foo_context)), 323 | ?assertMatch({error, invalid_client}, 324 | oauth2:refresh_access_token( 325 | {?CLIENT_ID, <<"foo">>}, 326 | RefreshToken, 327 | ?CLIENT_SCOPE, 328 | foo_context)), 329 | ?assertMatch({error, invalid_grant}, 330 | oauth2:refresh_access_token( 331 | {?CLIENT_ID, ?CLIENT_SECRET}, 332 | <<"foo">>, 333 | ?CLIENT_SCOPE, 334 | foo_context)), 335 | ?assertMatch({error, invalid_scope}, 336 | oauth2:refresh_access_token( 337 | {?CLIENT_ID, ?CLIENT_SECRET}, 338 | RefreshToken, 339 | <<"foo">>, 340 | foo_context)) 341 | end 342 | ] 343 | end}. 344 | 345 | verify_refresh_token_test_() -> 346 | {setup, 347 | fun start/0, 348 | fun stop/1, 349 | fun(_) -> 350 | [ 351 | fun() -> 352 | {ok, Res1} = issue_access_code(foo_context), 353 | {ok, Res2} = issue_token_and_refresh(Res1, foo_context), 354 | {ok, RefreshToken} = oauth2_response:refresh_token(Res2), 355 | {ok, _} = oauth2:refresh_access_token( 356 | {?CLIENT_ID, ?CLIENT_SECRET}, 357 | RefreshToken, 358 | ?USER_SCOPE, 359 | foo_context), 360 | {ok, Token} = oauth2_response:access_token(Res2), 361 | ?assertMatch({ok, _}, oauth2:verify_access_token( 362 | Token, 363 | foo_context)) 364 | end, 365 | fun() -> 366 | lists:foreach(fun(UserNameAndPasswordStrategyFun) -> 367 | {ok, Response} = 368 | issue_token_and_refresh_with_user_name_and_password( 369 | foo_context, 370 | UserNameAndPasswordStrategyFun), 371 | {ok, RefreshToken} = oauth2_response:refresh_token(Response), 372 | {ok, {foo_context, Response2}} = 373 | oauth2:refresh_access_token( 374 | {?CLIENT_ID, ?CLIENT_SECRET}, 375 | RefreshToken, 376 | ?USER_SCOPE, 377 | foo_context), 378 | {ok, NewAccessToken} = 379 | oauth2_response:access_token(Response2), 380 | ?assertMatch({ok, _}, oauth2:verify_access_token( 381 | NewAccessToken, 382 | foo_context)) 383 | end, [ 384 | fun(Context) -> 385 | oauth2:authorize_password( 386 | {?USER_NAME, ?USER_PASSWORD}, 387 | {?CLIENT_ID, ?CLIENT_SECRET}, 388 | ?USER_SCOPE, 389 | Context) 390 | end 391 | ]) 392 | end 393 | ] 394 | end}. 395 | 396 | %%%=================================================================== 397 | %%% Setup/teardown 398 | %%%=================================================================== 399 | 400 | start() -> 401 | application:set_env(oauth2, backend, oauth2_mock_backend), 402 | application:set_env(oauth2, expiry_time, 3600), 403 | oauth2_mock_backend:start(), 404 | ok. 405 | 406 | stop(_State) -> 407 | oauth2_mock_backend:stop(), 408 | ok. 409 | 410 | 411 | %%%=================================================================== 412 | %%% Helpers 413 | %%%=================================================================== 414 | 415 | issue_access_token(Context) -> 416 | {ok, {Context, Authorization}} = 417 | oauth2:authorize_client_credentials( 418 | {?CLIENT_ID, ?CLIENT_SECRET}, 419 | ?CLIENT_SCOPE, 420 | Context), 421 | {ok, {Context, Response}} = oauth2:issue_token(Authorization, Context), 422 | {ok, Response}. 423 | 424 | issue_access_code(Context) -> 425 | {ok, {Context, Auth}} = 426 | oauth2:authorize_code_request( 427 | {?USER_NAME, ?USER_PASSWORD}, 428 | ?CLIENT_ID, 429 | ?CLIENT_URI, 430 | ?USER_SCOPE, 431 | Context), 432 | {ok, {Context, Response}} = oauth2:issue_code(Auth, Context), 433 | {ok, Response}. 434 | 435 | issue_token_and_refresh(Response, Context) -> 436 | {ok, Code} = oauth2_response:access_code(Response), 437 | {ok, {Context, Auth2}} = 438 | oauth2:authorize_code_grant( 439 | {?CLIENT_ID, ?CLIENT_SECRET}, 440 | Code, 441 | ?CLIENT_URI, 442 | Context), 443 | {ok, {Context, Res2}} = oauth2:issue_token_and_refresh(Auth2, Context), 444 | {ok, Res2}. 445 | 446 | issue_token_and_refresh_with_user_name_and_password( 447 | Context, UserNameAndPasswordStrategyFun) -> 448 | {ok, {Context, Auth}} = UserNameAndPasswordStrategyFun(Context), 449 | {ok, {Context, Response}} = oauth2:issue_token_and_refresh(Auth, Context), 450 | {ok, Response}. 451 | -------------------------------------------------------------------------------- /test/oauth2_token_tests.erl: -------------------------------------------------------------------------------- 1 | %% ---------------------------------------------------------------------------- 2 | %% 3 | %% oauth2: Erlang OAuth 2.0 implementation 4 | %% 5 | %% Copyright (c) 2012-2014 Kivra 6 | %% 7 | %% Permission is hereby granted, free of charge, to any person obtaining a 8 | %% copy of this software and associated documentation files (the "Software"), 9 | %% to deal in the Software without restriction, including without limitation 10 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | %% and/or sell copies of the Software, and to permit persons to whom the 12 | %% Software is furnished to do so, subject to the following conditions: 13 | %% 14 | %% The above copyright notice and this permission notice shall be included in 15 | %% all copies or substantial portions of the Software. 16 | %% 17 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | %% DEALINGS IN THE SOFTWARE. 24 | %% 25 | %% ---------------------------------------------------------------------------- 26 | 27 | -module(oauth2_token_tests). 28 | 29 | -include_lib("proper/include/proper.hrl"). 30 | -include_lib("eunit/include/eunit.hrl"). 31 | 32 | %%%=================================================================== 33 | %%% Test cases 34 | %%%=================================================================== 35 | 36 | proper_type_spec_test_() -> 37 | {timeout, 1200, [{?LINE, 38 | fun() -> proper:check_specs(oauth2_token, 39 | [{to_file, user}]) end}]}. 40 | 41 | generate_test() -> 42 | Token = oauth2_token:generate([]), 43 | ?assertEqual(byte_size(Token), 32), 44 | ?assert(lists:all(fun is_alphanum/1, binary_to_list(Token))). 45 | 46 | %%%=================================================================== 47 | %%% Utility functions 48 | %%%=================================================================== 49 | 50 | %% @doc Returns true for alphanumeric ASCII characters, false for all others. 51 | -spec is_alphanum(Char :: char()) -> boolean(). 52 | is_alphanum(C) when C >= 16#30 andalso C =< 16#39 -> true; 53 | is_alphanum(C) when C >= 16#41 andalso C =< 16#5A -> true; 54 | is_alphanum(C) when C >= 16#61 andalso C =< 16#7A -> true; 55 | is_alphanum(_) -> false. 56 | --------------------------------------------------------------------------------