Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Major changes. Complete merge of @mtornwall's work

  • Loading branch information...
commit d48712146fd2b779906f652418fec100407de49e 1 parent b89e7c6
@bipthelin bipthelin authored
Showing with 2,125 additions and 731 deletions.
  1. +4 −175 LICENSE
  2. +14 −21 Makefile
  3. +76 −116 README.md
  4. +30 −0 examples/oauth2_example/Makefile
  5. +119 −0 examples/oauth2_example/README.md
  6. +5 −0 examples/oauth2_example/priv/app.config
  7. +30 −0 examples/oauth2_example/priv/static/auth_form.dtl
  8. BIN  examples/oauth2_example/rebar
  9. +6 −0 examples/oauth2_example/rebar.config
  10. +38 −0 examples/oauth2_example/src/oauth2_example.app.src
  11. +47 −0 examples/oauth2_example/src/oauth2_example.erl
  12. +54 −0 examples/oauth2_example/src/oauth2_example_app.erl
  13. +218 −0 examples/oauth2_example/src/oauth2_example_auth.erl
  14. +165 −0 examples/oauth2_example/src/oauth2_example_backend.erl
  15. +99 −0 examples/oauth2_example/src/oauth2_example_resource.erl
  16. +53 −0 examples/oauth2_example/src/oauth2_example_sup.erl
  17. +18 −16 include/oauth2.hrl
  18. +6 −0 priv/app.config
  19. BIN  rebar
  20. +6 −4 rebar.config
  21. +34 −5 src/oauth2.app.src
  22. +142 −147 src/oauth2.erl
  23. +101 −0 src/oauth2_backend.erl
  24. +70 −0 src/oauth2_config.erl
  25. +0 −31 src/oauth2_db.erl
  26. +0 −80 src/oauth2_mock_db.erl
  27. +122 −0 src/oauth2_priv_set.erl
  28. +120 −0 src/oauth2_response.erl
  29. +66 −0 src/oauth2_token.erl
  30. +88 −0 test/oauth2_priv_set_tests.erl
  31. +127 −0 test/oauth2_response_tests.erl
  32. +0 −136 test/oauth2_test.erl
  33. +213 −0 test/oauth2_tests.erl
  34. +54 −0 test/oauth2_token_tests.erl
View
179 LICENSE
@@ -1,178 +1,7 @@
+Copyright (c) 2012 KIVRA
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+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:
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+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.
View
35 Makefile
@@ -1,31 +1,24 @@
-ERL ?= erl
-APP := oauth2
+REBAR = ./rebar
-.PHONY: deps test
+.PHONY: all compile deps test doc clean distclean
-all: deps compile
+all: compile
-compile:
- @./rebar compile
-
-debug:
- @./rebar debug_info=1 compile
+compile: deps
+ @$(REBAR) compile
deps:
- @./rebar get-deps
+ @$(REBAR) get-deps
+
+test: compile
+ @$(REBAR) eunit skip_deps=true
-app:
- @./rebar compile skip_deps=true
+doc:
+ @$(REBAR) doc skip_deps=true
clean:
- @./rebar clean
+ @$(REBAR) clean
distclean: clean
- @./rebar delete-deps
-
-test:
- @./rebar compile skip_deps=true eunit
-
-docs:
- @erl -noshell -run edoc_run application '$(APP)' '"."' '[]'
-
+ @$(REBAR) delete-deps
+ -@rmdir deps
View
192 README.md
@@ -1,117 +1,77 @@
-Oauth2 -- An erlang Oauth2 library
-====================================
-
-## DESCRIPTION
-
-Oauth2 is a library to build Oauth2 aware servers. This library tries to adhere to the spec as close as possible: <http://tools.ietf.org/html/draft-ietf-oauth-v2-23>.
-
-Currently tested and supported flows are:
-
-* Authentication Code Flow (Web Server Flow)
-* Implicit Flow (Client Side Flow)
-
-Other flows should work but hasn't been tested yet.
-
-## USAGE
-
-Include Oauth2 as a rebar dependency with:
-
- {deps, [{oauth2, ".*", {git, "git://github.com/bipthelin/oauth2.git", "master"}}]}.
-
-Then you will have to write a DB module to handle various tasks such as verifying client_id, redirect_uri, etc.
-
-There's a mock db adapter included which is a good reference for implementing your own. It should be no more than a couple of lines of code to implement your own.
-
- -module(my_oauth2_db).
-
- -export([get/2, set/3, delete/2]).
- -export([verify_redirect_uri/2]).
-
- -include_lib("include/oauth2.hrl").
-
- %%
- %% Oauth2 DB Behavior
- -behavior(oauth2_db).
-
- %%
- %% Get Oauth2 record from the Auth DB
- get(auth, Key) ->
- get_from_table(?TAB_AUTH, Key);
- %%
- %% Get Oauth2 record from the Access DB
- get(access, Key) ->
- get_from_table(?TAB_ACC, Key).
-
- %%
- %% Put Oauth2 record to the Auth DB
- set(auth, Key, Value) ->
- CliendId = Value#oauth2.client_id,
- Expires = Value#oauth2.expires,
- Scope = Value#oauth2.scope,
-
- put_to_table(?TAB_AUTH, Key, Value);
- %%
- %% Put Oauth2 record to the Access DB
- set(access, Key, Value) ->
- CliendId = Value#oauth2.client_id,
- Expires = Value#oauth2.expires,
- Scope = Value#oauth2.scope,
-
- put_to_table(?TAB_ACC, Key, Value).
-
- %%
- %% Delete Oauth2 record from the Auth DB
- delete(auth, Key) ->
- delete_from_table(?TAB_AUTH, Key);
- %%
- %% Delete Oauth2 record from the Access DB
- delete(access, Key) ->
- delete_from_table(?TAB_ACC, Key).
-
- %%
- %% Verify if a given Client ID and RedirectUri match
- verify_redirect_uri(CliendId, RedirectUri) ->
- true.
-
-Here's a step by step to the various flows:
-
-## Implicit Flow
-
- 1> RedirectUri = "http://REDIRECT.URL/here?this=that".
- "http://REDIRECT.URL/here?this=that"
- 2> Scope = "This That".
- "This That"
- 3> State = "Just a little state".
- "Just a little state"
- 4> ClientId = "123abcABC".
- "123abcABC"
- 5> oauth2:authorize(token, my_oauth2_db, ClientId, RedirectUri, Scope, State).
- {ok,"226a4OHh8NgasQv.1330703188.Qegej3cFVewKHr7",
- "http://REDIRECT.URL/here?state=Just+a+little+state&this=that#code=226a4OHh8NgasQv.1330703188.Qegej3cFVewKHr7",
- 7200}
-
-## Authentication Code Flow
-
- 1> RedirectUri = "http://REDIRECT.URL/here?this=that".
- "http://REDIRECT.URL/here?this=that"
- 2> Scope = "This That".
- "This That"
- 3> State = [].
- []
- 4> ClientId = "123abcABC".
- "123abcABC"
- 5> oauth2:authorize(code, my_oauth2_db, ClientId, RedirectUri, Scope, State).
- {ok,"n2HqNFz3QhZ_EjcXP8QuWgpCrbZCJx",
- "http://REDIRECT.URL/here?code=n2HqNFz3QhZ_EjcXP8QuWgpCrbZCJx&this=that",
- 30}
- 6> oauth2:verify_token(authorization_code,oauth2_mock_db,"n2HqNFz3QhZ_EjcXP8QuWgpCrbZCJx", ClientId, RedirectUri).
- {ok,[{access_token,"aTjJHonW0nsHzUp.1330937706.xS__1bdSYTYcZlB"},
- {token_type,"Bearer"},
- {expires_in,7200}]}
- 7> oauth2:verify_token(access_token,oauth2_mock_db,"aTjJHonW0nsHzUp.1330937706.xS__1bdSYTYcZlB", ClientId).
- {ok,[{audience,"123abcABC"},
- {scope,"This That"},
- {expires_in,7046}]}
-
-xoxo
+# OAuth2
+This library is designed to simplify the implementation of the server side
+of OAuth2 (http://tools.ietf.org/html/draft-ietf-oauth-v2-30). It provides
+**no** support for developing clients.
+
+The library is currently in a highly experimental state and should not
+be relied upon for production use. No guarantees are made as to whether
+this library implements the OAuth2 specification correctly.
+
+## tl;dr
+Check out the [examples](examples/)
+
+## Concepts
+
+### Tokens
+A token is a (randomly generated) string provided to the client by the server
+in response to some form of authorization request.
+There are several types of tokens:
+
+* *Access Token*: An access token identifies the origin of a request for a
+privileged resource.
+* *Refresh Token*: A refresh token can be used to replace an expired access token.
+
+#### Expiry
+Access tokens can (optionally) be set to expire after a certain amount of time.
+An expired token cannot be used to gain access to resources.
+
+### Identities
+A token is associated with an *identity* -- a value that uniquely identifies
+a user, client or agent within your system. Typically, this is a user identifier.
+
+### Clients
+If you have many diverse clients connecting to your service -- for instance,
+a web client and an iPhone app -- it's desirable to be able to distinguish
+them from one another and to be able to grant or revoke privileges based
+on the type the client issuing a request. As described in the OAuth2 specification,
+clients come in two flavors:
+
+* *Confidential* clients, which can be expected to keep their credentials
+from being disclosed. For instance, a web site owned and operated by you
+could be regarded as confidential.
+* *Public* clients, whose credentials are assumed to be compromised the
+moment the client software is released to the public.
+
+Clients are distinguished by their identifiers, and can (optionally) be
+authenticated using a secret key shared between the client and server.
+
+## Building
+This library is built using rebar wrapped with make. It has been developed
+and tested under Erlang R15B01; nothing's stopping you from trying it with another
+version, but your mileage may vary.
+
+Build with:
+
+ $ make
+
+If you want to run the EUnit test cases, you can do so with:
+
+ $ make test
+
+To generate documentation, run:
+
+ $ make doc
+
+## Customization
+The library makes no assumptions as to how you want to implement authentication and persistence of
+users, clients and tokens. Instead, it provides a proxy module (`oauth2_backend`) for directing
+calls to a backend plugin supplied by you. To direct calls to a different backend module,
+simply set `{backend, your_backend_module}` in the `oauth2` section of your app.config.
+
+A complete list of functions that your backend must provide is available by looking
+at `oauth2_backend.erl`, which contains documentation and function specifications.
+
+## License
+The KIVRA oauth2 library uses an [MIT license](http://en.wikipedia.org/wiki/MIT_License). So go ahead and do what
+you want!
View
30 examples/oauth2_example/Makefile
@@ -0,0 +1,30 @@
+REBAR = ./rebar
+
+.PHONY: all compile deps test doc clean distclean start
+
+all: compile
+
+compile: deps
+ @$(REBAR) compile
+
+deps:
+ @$(REBAR) get-deps
+
+test: compile
+ @$(REBAR) eunit skip_deps=true
+
+doc:
+ @$(REBAR) doc skip_deps=true
+
+clean:
+ @$(REBAR) clean
+
+distclean: clean
+ @$(REBAR) delete-deps
+ -@rmdir deps
+
+start: compile
+ erl \
+ -pa ebin -pa deps/*/ebin \
+ -config priv/app.config \
+ -s oauth2_example
View
119 examples/oauth2_example/README.md
@@ -0,0 +1,119 @@
+# OAuth2 Example
+This is an example application intended to demonstrate how the Kivra OAuth2
+library can be used. As with the library itself, it probably doesn't conform
+to the standard very well right now.
+
+# Walkthrough
+
+## Getting started
+To build and start the example:
+
+ $ make start
+
+Provided all goes well you now have a running Cowboy listener on port 8000.
+
+## Getting authorization
+
+We'll start off by setting up a user account. Switch to the Erlang shell
+you just started and do:
+
+ 1> oauth2_example_backend:add_user(<<"bob">>, <<"luv_alice">>).
+ ok
+
+Now that we have a user set up, we can authenticate through the
+*Resource Owner Password Credentials* grant type. Release your inner cURL:
+
+ $ curl -v -X POST http://127.0.0.1:8000/auth \
+ -d "grant_type=password&username=bob&password=luv_alice&scope=yourbase"
+ < HTTP/1.1 200 OK
+ < Content-Type: application/json
+ < Content-Length: 112
+ < Date: Fri, 03 Aug 2012 08:28:00 GMT
+ < Server: Cowboy
+ < Connection: keep-alive
+ <
+ {
+ "access_token": "1t4VDlrqpgzih1OEd6lkoMkSndeA9p1y",
+ "expires_in": "3600",
+ "scope": "yourbase",
+ "token_type": "bearer"
+ }
+
+The `access_token` field is the critical part. When issuing a request for
+a resource, we have two ways of providing the token to the server:
+via the `Authorization` header, or via the `access_token` query string
+parameter. The latter should be quite self-explanatory, so we'll do the former
+instead:
+
+ $ curl -v -H "Authorization: Bearer 1t4VDlrqpgzih1OEd6lkoMkSndeA9p1y" \
+ http://127.0.0.1:8000/resource
+ < HTTP/1.1 204 No Content
+ < Content-Type: application/json
+ < Content-Length: 0
+ < Date: Fri, 03 Aug 2012 08:32:13 GMT
+ < Server: Cowboy
+ < Connection: keep-alive
+
+Seems to have worked well enough; the 204 response indicates that no content
+is available at this URL, which is all well, since no content is served.
+Had we omitted the access token, or provided an invalid or expired token,
+we would have been rejected with an HTTP 401 instead. Observe:
+
+ $ curl -v http://127.0.0.1:8000/resource
+ < HTTP/1.1 401 Unauthorized
+ < Www-Authenticate: Bearer
+ < Content-Length: 0
+ < Date: Fri, 03 Aug 2012 08:33:58 GMT
+ < Server: Cowboy
+ < Connection: keep-alive
+
+# Other grant types
+
+## Client Credentials
+
+The OAuth2 library also supports the *Client Credentials* grant type,
+in which an access token is issued for a *client* rather than a *user*;
+no username or password is needed; the client simply authenticates
+with its own identifier and secret. It should, for obvious reasons, not be
+used with clients that are distributed to end users, since it's probably
+easy enough to find the client secret in the distributed binary, thus
+compromising your security.
+
+The *Client Credentials* grant works in a fashion quite similar to that
+of the *Resource Owner Password Credentials* grant, with the key difference
+being that the client supplies its credentials as an HTTP Basic Auth
+header of the form
+
+ Authorization: Basic base64(client_id + ":" + client_secret)
+
+For instance, a client with the identifier `my_client` and secret `souper_sekr3t`
+would identify with:
+
+ Authorization: Basic bXlfY2xpZW50OnNvdXBlcl9zZWtyM3Q=
+
+Start off by registering the client the same way we registered the user:
+
+ 2> oauth2_example_backend:add_client(<<"my_client">>, <<"souper_sekr3t">>).
+ ok
+
+However, as with the *Resource Owner Password Credentials* grant type,
+you also need to provide a scope and a grant type parameter.
+This is done in an `application/x-www-form-urlencoded` body as before:
+
+ $ curl -v -X POST \
+ -H "Authorization: Basic bXlfY2xpZW50OnNvdXBlcl9zZWtyM3Q=" \
+ -d "grant_type=client_credentials&scope=yourbase"
+ http://127.0.0.1:8000/auth
+ < HTTP/1.1 200 OK
+ < Content-Type: application/json
+ < Content-Length: 112
+ < Date: Fri, 03 Aug 2012 08:43:30 GMT
+ < Server: Cowboy
+ < Connection: keep-alive
+ <
+ {
+ "access_token": "a4QJhx31xCJI6kaqsg4WPNWJkxYBYyEh",
+ "expires_in": "3600",
+ "scope": "yourbase",
+ "token_type": "bearer"
+ }
View
5 examples/oauth2_example/priv/app.config
@@ -0,0 +1,5 @@
+[
+ {oauth2, [
+ {backend, oauth2_example_backend}
+ ]}
+].
View
30 examples/oauth2_example/priv/static/auth_form.dtl
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ ul li {
+ float: left;
+ }
+ </style>
+ </head>
+ <body>
+ <form action="/auth" method="post">
+ <input type="hidden" name="response_type" value="token" />
+ <input type="hidden" name="client_id" value="{{ client_id }}" />
+ <input type="hidden" name="redirect_uri" value="{{ redirect_uri }}" />
+ <input type="hidden" name="state" value="{{ state }}" />
+ <input type="hidden" name="scope" value="{{ scope }}" />
+ <ul>
+ <li><input placeholder="Username" type="text" name="username" /></li>
+ <li><input placeholder="Password" type="password" name="password" /></li>
+ <li><button type="submit">Login</button></li>
+ </ul>
+ </form>
+ </body>
+</html>
View
BIN  examples/oauth2_example/rebar
Binary file not shown
View
6 examples/oauth2_example/rebar.config
@@ -0,0 +1,6 @@
+{deps, [
+ {cowboy, ".*", {git, "https://github.com/extend/cowboy.git", "master"}},
+ {jsx, ".*", {git, "https://github.com/talentdeficit/jsx.git", "master"}},
+ {oauth2, ".*", {git, "git@github.com:mtornwall/oauth2.git", "master"}},
+ {erlydtl, ".*", {git, "https://github.com/evanmiller/erlydtl.git", "master"}}
+]}.
View
38 examples/oauth2_example/src/oauth2_example.app.src
@@ -0,0 +1,38 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+{application, oauth2_example,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {mod, { oauth2_example_app, []}},
+ {env, []}
+ ]}.
View
47 examples/oauth2_example/src/oauth2_example.erl
@@ -0,0 +1,47 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_example).
+
+%%% API
+-export([
+ start/0
+ ,stop/0
+ ]).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+start() ->
+ application:start(cowboy),
+ application:start(oauth2),
+ application:start(oauth2_example).
+
+stop() ->
+ application:stop(oauth2_example),
+ application:stop(oauth2),
+ application:stop(cowboy).
View
54 examples/oauth2_example/src/oauth2_example_app.erl
@@ -0,0 +1,54 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_example_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([
+ start/2
+ ,stop/1
+ ]).
+
+%%%===================================================================
+%%% Application callbacks
+%%%===================================================================
+
+start(_StartType, _StartArgs) ->
+ oauth2_example_backend:start(),
+ Dispatch = [{'_', [
+ {[<<"auth">>], oauth2_example_auth, []},
+ {[<<"resource">>], oauth2_example_resource, []}
+ ]}],
+ cowboy:start_listener(oauth2_example_listener, 100,
+ cowboy_tcp_transport, [{port, 8000}],
+ cowboy_http_protocol, [{dispatch, Dispatch}]),
+ oauth2_example_sup:start_link().
+
+stop(_State) ->
+ oauth2_example_backend:stop(),
+ ok.
View
218 examples/oauth2_example/src/oauth2_example_auth.erl
@@ -0,0 +1,218 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_example_auth).
+
+-export([
+ init/3
+ ,rest_init/2
+ ,allowed_methods/2
+ ]).
+
+-export([
+ content_types_provided/2
+ ,content_types_accepted/2
+ ]).
+
+-export([
+ process_post/2
+ ,process_get/2
+ ]).
+
+%%%===================================================================
+%%% Cowboy callbacks
+%%%===================================================================
+
+init(_Transport, _Req, _Opts) ->
+ %% Compile the DTL template used for the authentication
+ %% form in the implicit grant flow.
+ ok = erlydtl:compile(filename:join(["priv", "static", "auth_form.dtl"]),
+ auth_form),
+ {upgrade, protocol, cowboy_http_rest}.
+
+rest_init(Req, _Opts) ->
+ {ok, Req, undefined_state}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"html">>, []}, process_get}], Req, State}.
+
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, process_post},
+ {{<<"application">>, <<"x-www-form-urlencoded">>, []}, process_post}],
+ Req, State}.
+
+allowed_methods(Req, State) ->
+ {['POST', 'GET'], Req, State}.
+
+process_post(Req, State) ->
+ {ok, Body, Req2} = cowboy_http_req:body(Req),
+ Params = decode_form(Body),
+ {ok, Reply} =
+ case lists:max([proplists:get_value(K, Params)
+ || K <- [<<"grant_type">>, <<"response_type">>]]) of
+ <<"password">> ->
+ process_password_grant(Req2, Params);
+ <<"client_credentials">> ->
+ process_client_credentials_grant(Req2, Params);
+ <<"token">> ->
+ process_implicit_grant_stage2(Req2, Params);
+ _ ->
+ cowboy_http_req:reply(400, [], <<"Bad Request.">>, Req2)
+ end,
+ {halt, Reply, State}.
+
+process_get(Req, State) ->
+ {ResponseType, Req2} = cowboy_http_req:qs_val(<<"response_type">>, Req),
+ {ok, Reply} =
+ case ResponseType of
+ <<"token">> ->
+ {Req3, Params} =
+ lists:foldl(fun(Name, {R, Acc}) ->
+ {Val, R2} =
+ cowboy_http_req:qs_val(Name, R),
+ {R2, [{Name, Val}|Acc]}
+ end,
+ {Req2, []},
+ [<<"client_id">>,
+ <<"redirect_uri">>,
+ <<"scope">>,
+ <<"state">>]),
+ process_implicit_grant(Req3, Params);
+ _ ->
+ JSON = jsx:encode([{error, <<"unsupported_respose_type">>}]),
+ cowboy_http_req:reply(400, [], JSON, Req2)
+ end,
+ {halt, Reply, State}.
+
+%%%===================================================================
+%%% Grant type handlers
+%%%===================================================================
+
+process_password_grant(Req, Params) ->
+ Username = proplists:get_value(<<"username">>, Params),
+ Password = proplists:get_value(<<"password">>, Params),
+ Scope = proplists:get_value(<<"scope">>, Params, <<"">>),
+ emit_response(oauth2:authorize_password(Username, Password, Scope), Req).
+
+process_client_credentials_grant(Req, Params) ->
+ {<<"Basic ", Credentials/binary>>, Req2} =
+ cowboy_http_req:header('Authorization', Req),
+ [Id, Secret] = binary:split(base64:decode(Credentials), <<":">>),
+ Scope = proplists:get_value(<<"scope">>, Params),
+ emit_response(oauth2:authorize_client_credentials(Id, Secret, Scope), Req2).
+
+process_implicit_grant(Req, Params) ->
+ State = proplists:get_value(<<"state">>, Params),
+ Scope = proplists:get_value(<<"scope">>, Params, <<>>),
+ ClientId = proplists:get_value(<<"client_id">>, Params),
+ RedirectUri = proplists:get_value(<<"redirect_uri">>, Params),
+ case oauth2:verify_redirection_uri(ClientId, RedirectUri) of
+ ok ->
+ %% Pass the scope, state and redirect URI to the browser
+ %% as hidden form parameters, allowing them to "propagate"
+ %% to the next stage.
+ {ok, Html} = auth_form:render([{redirect_uri, RedirectUri},
+ {client_id, ClientId},
+ {state, State},
+ {scope, Scope}]),
+ cowboy_http_req:reply(200, [], Html, Req);
+ %% TODO: Return an OAuth2 response code here.
+ %% The returned Reason might not be valid in an OAuth2 context.
+ {error, Reason} ->
+ redirect_resp(RedirectUri,
+ [{<<"error">>, to_binary(Reason)},
+ {<<"state">>, State}],
+ Req)
+ end.
+
+process_implicit_grant_stage2(Req, Params) ->
+ ClientId = proplists:get_value(<<"client_id">>, Params),
+ RedirectUri = proplists:get_value(<<"redirect_uri">>, Params),
+ Username = proplists:get_value(<<"username">>, Params),
+ Password = proplists:get_value(<<"password">>, Params),
+ State = proplists:get_value(<<"state">>, Params),
+ Scope = proplists:get_value(<<"scope">>, Params),
+ case oauth2:verify_redirection_uri(ClientId, RedirectUri) of
+ ok ->
+ case oauth2:authorize_password(Username, Password, Scope) of
+ {ok, Response} ->
+ Props = [{<<"state">>, State}
+ | oauth2_response:to_proplist(Response)],
+ redirect_resp(RedirectUri, Props, Req);
+ {error, Reason} ->
+ redirect_resp(RedirectUri,
+ [{<<"error">>, to_binary(Reason)},
+ {<<"state">>, State}],
+ Req)
+ end;
+ {error, _} ->
+ %% This should not happen. Redirection URI was
+ %% supposedly verified in the previous step, so
+ %% someone must have been tampering with the
+ %% hidden form values.
+ cowboy_http_req:reply(400, Req)
+ end.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+emit_response(AuthResult, Req) ->
+ {Code, JSON} =
+ case AuthResult of
+ {ok, Response} ->
+ {200, jsx:encode(oauth2_response:to_proplist(Response))};
+ {error, Reason} ->
+ {400, jsx:encode([{error, to_binary(Reason)}])}
+ end,
+ cowboy_http_req:reply(Code, [], JSON, Req).
+
+decode_form(Form) ->
+ RawForm = cowboy_http:urldecode(Form),
+ Pairs = binary:split(RawForm, <<"&">>, [global]),
+ lists:map(fun(Pair) ->
+ [K, V] = binary:split(Pair, <<"=">>),
+ {K, V}
+ end,
+ Pairs).
+
+to_binary(Atom) when is_atom(Atom) ->
+ list_to_binary(atom_to_list(Atom)).
+
+redirect_resp(RedirectUri, FragParams, Req) ->
+ Frag = binary_join([<<(cowboy_http:urlencode(K))/binary, "=",
+ (cowboy_http:urlencode(V))/binary>>
+ || {K, V} <- FragParams],
+ <<"&">>),
+ Header = [{'Location', <<RedirectUri/binary, "#", Frag/binary>>}],
+ cowboy_http_req:reply(302, Header, <<>>, Req).
+
+binary_join([H], _Sep) ->
+ <<H/binary>>;
+binary_join([H|T], Sep) ->
+ <<H/binary, Sep/binary, (binary_join(T, Sep))/binary>>;
+binary_join([], _Sep) ->
+ <<>>.
View
165 examples/oauth2_example/src/oauth2_example_backend.erl
@@ -0,0 +1,165 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_example_backend).
+
+%%% API
+-export([
+ start/0
+ ,stop/0
+ ,add_user/2
+ ,delete_user/1
+ ,add_client/2, add_client/3
+ ,delete_client/1
+ ]).
+
+%%% OAuth2 backend functionality
+-export([
+ authenticate_username_password/3
+ ,authenticate_client/3
+ ,associate_access_token/2
+ ,resolve_access_token/1
+ ,get_redirection_uri/1
+ ]).
+
+-define(ACCESS_TOKEN_TABLE, access_tokens).
+-define(USER_TABLE, users).
+-define(CLIENT_TABLE, clients).
+
+-define(TABLES, [?ACCESS_TOKEN_TABLE,
+ ?USER_TABLE,
+ ?CLIENT_TABLE]).
+
+-record(client, {
+ client_id :: binary(),
+ client_secret :: binary(),
+ redirect_uri :: binary()
+ }).
+
+-record(user, {
+ username :: binary(),
+ password :: binary()
+ }).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+start() ->
+ lists:foreach(fun(Table) ->
+ ets:new(Table, [named_table, public])
+ end,
+ ?TABLES),
+ oauth2_example_backend:add_client(<<"my_client">>,<<"ohai">>,<<"https://kivra.com">>),
+ oauth2_example_backend:add_user(<<"martin">>,<<"ohai">>).
+
+stop() ->
+ lists:foreach(fun ets:delete/1, ?TABLES).
+
+add_user(Username, Password) ->
+ put(?USER_TABLE, Username, #user{username = Username, password = Password}).
+
+delete_user(Username) ->
+ delete(?USER_TABLE, Username).
+
+add_client(Id, Secret, RedirectUri) ->
+ put(?CLIENT_TABLE, Id, #client{client_id = Id,
+ client_secret = Secret,
+ redirect_uri = RedirectUri
+ }).
+
+add_client(Id, Secret) ->
+ add_client(Id, Secret, undefined).
+
+delete_client(Id) ->
+ delete(?CLIENT_TABLE, Id).
+
+%%%===================================================================
+%%% OAuth2 backend functions
+%%%===================================================================
+
+authenticate_username_password(Username, Password, _Scope) ->
+ case get(?USER_TABLE, Username) of
+ {ok, #user{password = UserPw}} ->
+ case Password of
+ UserPw ->
+ {ok, {user, Username}};
+ _ ->
+ {error, badpass}
+ end;
+ Error = {error, notfound} ->
+ Error
+ end.
+
+authenticate_client(ClientId, ClientSecret, _Scope) ->
+ case get(?CLIENT_TABLE, ClientId) of
+ {ok, #client{client_secret = ClientSecret}} ->
+ {ok, {client, ClientId}};
+ {ok, #client{client_secret = _WrongSecret}} ->
+ {error, badsecret};
+ _ ->
+ {error, notfound}
+ end.
+
+associate_access_token(AccessToken, Context) ->
+ put(?ACCESS_TOKEN_TABLE, AccessToken, Context).
+
+resolve_access_token(AccessToken) ->
+ %% The case trickery is just here to make sure that
+ %% we don't propagate errors that cannot be legally
+ %% returned from this function according to the spec.
+ case get(?ACCESS_TOKEN_TABLE, AccessToken) of
+ Value = {ok, _} ->
+ Value;
+ Error = {error, notfound} ->
+ Error
+ end.
+
+get_redirection_uri(ClientId) ->
+ case get(?CLIENT_TABLE, ClientId) of
+ {ok, #client{redirect_uri = RedirectUri}} ->
+ {ok, RedirectUri};
+ Error = {error, notfound} ->
+ Error
+ end.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+get(Table, Key) ->
+ case ets:lookup(Table, Key) of
+ [] ->
+ {error, notfound};
+ [{_Key, Value}] ->
+ {ok, Value}
+ end.
+
+put(Table, Key, Value) ->
+ ets:insert(Table, {Key, Value}).
+
+delete(Table, Key) ->
+ ets:delete(Table, Key).
View
99 examples/oauth2_example/src/oauth2_example_resource.erl
@@ -0,0 +1,99 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_example_resource).
+
+-export([
+ init/3
+ ,rest_init/2
+ ,allowed_methods/2
+ ,is_authorized/2
+ ]).
+
+-export([
+ content_types_provided/2
+ ,content_types_accepted/2
+ ]).
+
+-export([
+ process_get/2
+ ,process_put/2
+ ]).
+
+%%%===================================================================
+%%% Cowboy callbacks
+%%%===================================================================
+
+init(_Transport, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_http_rest}.
+
+rest_init(Req, _Opts) ->
+ {ok, Req, undefined_state}.
+
+allowed_methods(Req, State) ->
+ {['GET', 'PUT'], Req, State}.
+
+is_authorized(Req, State) ->
+ case get_access_token(Req) of
+ {ok, Token} ->
+ case oauth2:verify_access_token(Token) of
+ {ok, _Identity} ->
+ {true, Req, State};
+ {error, access_denied} ->
+ {{false, <<"Bearer">>}, Req, State}
+ end;
+ {error, _} ->
+ {{false, <<"Bearer">>}, Req, State}
+ end.
+
+content_types_provided(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, process_get}], Req, State}.
+
+content_types_accepted(Req, State) ->
+ {[{{<<"application">>, <<"json">>, []}, process_put}], Req, State}.
+
+process_put(Req, State) ->
+ {ok, cowboy_http_req:reply(201, Req), State}.
+
+process_get(Req, State) ->
+ {ok, cowboy_http_req:reply(204, Req), State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+get_access_token(Req) ->
+ case cowboy_http_req:header('Authorization', Req) of
+ {<<"Bearer ", Token/binary>>, _Req} ->
+ {ok, Token};
+ _ ->
+ case cowboy_http_req:qs_val(<<"access_token">>, Req) of
+ {Token, _Req} ->
+ {ok, Token};
+ _ ->
+ {error, missing}
+ end
+ end.
View
53 examples/oauth2_example/src/oauth2_example_sup.erl
@@ -0,0 +1,53 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_example_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ {ok, { {one_for_one, 5, 10}, []} }.
+
View
34 include/oauth2.hrl
@@ -2,25 +2,27 @@
%%
%% oauth2: Erlang OAuth 2.0 implementation
%%
-%% Copyright 2012 (c) KIVRA. All Rights Reserved.
-%% http://developer.kivra.com dev@kivra.com
+%% Copyright (c) 2012 KIVRA
%%
-%% This file is provided to you under the Apache License, Version 2.0 (the
-%% "License"); you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
+%% 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:
%%
-%% http://www.apache.org/licenses/LICENSE-2.0
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-%% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-%% License for the specific language governing permissions and limitations
-%% under the License.
+%% 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.
%%
%% ----------------------------------------------------------------------------
--record(oauth2, {client_id :: string(),
- expires :: non_neg_integer(),
- scope :: list(string())
- }).
-
+%% Length of binary data to use for token generation.
+-define(TOKEN_LENGTH, 32).
View
6 priv/app.config
@@ -0,0 +1,6 @@
+[
+ {oauth2, [
+ {expiry_time, 3600},
+ {backend, backend_goes_here}
+ ]}
+].
View
BIN  rebar
Binary file not shown
View
10 rebar.config
@@ -1,5 +1,7 @@
-{erl_opts, [warnings_as_errors]}.
-{cover_enabled, true}.
-
-{deps, [ {mochiweb_util, ".*", {git, "git://github.com/bipthelin/mochiweb_util.git", "master"}}]}.
+{deps, [{meck, ".*", {git, "https://github.com/eproxus/meck.git", "master"}}]}.
+{clean_files, [".eunit", "ebin/*.beam", "test/*.beam"]}.
+%% EUnit options
+{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}.
+{cover_enabled, true}.
+{covertool_eunit, "eunit.coverage.xml"}. % Output report file name
View
39 src/oauth2.app.src
@@ -1,8 +1,37 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
{application, oauth2,
- [{description, "Erlang OAuth2"},
+ [
+ {description, ""},
{vsn, "0.1.0"},
- {modules, []},
{registered, []},
- {applications, [stdlib]},
- {env, []}]}.
-
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {env, []}
+ ]}.
View
289 src/oauth2.erl
@@ -2,170 +2,165 @@
%%
%% oauth2: Erlang OAuth 2.0 implementation
%%
-%% Copyright 2012 (c) KIVRA. All Rights Reserved.
-%% http://developer.kivra.com dev@kivra.com
+%% Copyright (c) 2012 KIVRA
%%
-%% This file is provided to you under the Apache License, Version 2.0 (the
-%% "License"); you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
+%% 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:
%%
-%% http://www.apache.org/licenses/LICENSE-2.0
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-%% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-%% License for the specific language governing permissions and limitations
-%% under the License.
+%% 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.
%%
%% ----------------------------------------------------------------------------
-module(oauth2).
--export([authorize/6]).
--export([verify_token/4, verify_token/5]).
-
--include_lib("include/oauth2.hrl").
-
--define(DEF_AUTH_CODE_EXPIRE, 30).
--define(DEF_ACCESS_TOKEN_EXPIRE, 60 * 60 *2).
-
-authorize(ResponseType, Db, ClientId, RedirectUri, Scope, State) ->
- case Db:verify_redirect_uri(ClientId, RedirectUri) of
- false ->
- {error, redirect_uri_mismatch};
- true ->
- {Code, Expires} = case ResponseType of
- token ->
- Data = #oauth2{client_id=ClientId,
- expires=seconds_since_epoch(?DEF_ACCESS_TOKEN_EXPIRE),
- scope=Scope},
- AccessToken = generate_access_token(Data#oauth2.expires),
- Key = generate_key(ClientId, AccessToken),
- Db:set(access, Key, Data),
- {AccessToken, Data#oauth2.expires};
- code ->
- Data = #oauth2{client_id=ClientId,
- expires=seconds_since_epoch(?DEF_AUTH_CODE_EXPIRE),
- scope=Scope},
- AuthCode = generate_auth_code(),
- Key = generate_key(ClientId, AuthCode),
- Db:set(auth, Key, Data),
- {AuthCode, Data#oauth2.expires}
- end,
- NewRedirectUri = get_redirect_uri(ResponseType, Code, RedirectUri, State),
- {ok, Code, NewRedirectUri, calculate_expires_in(Expires)}
+%%% API
+-export([
+ authorize_password/3
+ ,authorize_client_credentials/3
+ ,verify_access_token/1
+ ,verify_redirection_uri/2
+ ]).
+
+%%% Internal types
+-type proplist(TyKey, TyVal) :: [{TyKey, TyVal}].
+
+%%% Exported types
+-type token() :: binary().
+-type lifetime() :: non_neg_integer().
+-type scope() :: binary().
+-type error() :: invalid_request | unauthorized_client
+ | access_denied | unsupported_response_type
@dvv
dvv added a note

unsupported_response_type never reported

@dvv
dvv added a note

Response type check could easily be done outside this library but the problem is that for authorization code grant flow it's unknown how to report this error -- via validated redirect_uri or just by 400 Invalid Request, because of no means exposed to validate redirect_uri. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ | invalid_scope | server_error
+ | temporarily_unavailable.
+
+-export_type([
+ token/0
+ ,lifetime/0
+ ,scope/0
+ ,error/0
+ ]).
+
+%%%===================================================================
+%%% API functions
+%%%===================================================================
+
+%% @doc Authorizes a client via Resource Owner Password Credentials.
+-spec authorize_password(Username, Password, Scope)
+ -> {ok, Identity} | {error, Reason} when
+ Username :: binary(),
+ Password :: binary(),
+ Scope :: scope(),
+ Identity :: term(),
+ Reason :: error().
+authorize_password(Username, Password, Scope) ->
+ case oauth2_backend:authenticate_username_password(Username, Password, Scope) of
+ {ok, Identity} ->
+ Response = issue_token(Identity, Scope),
+ {ok, Response};
+ {error, _Reason} ->
+ {error, access_denied}
end.
-verify_token(access_token, Db, Token, ClientId) ->
- case Db:get(access, generate_key(ClientId, Token)) of
- {ok, Data} ->
- ClientId = Data#oauth2.client_id,
- Expires = Data#oauth2.expires,
- Scope = Data#oauth2.scope,
+%% @doc Authorize client via its own credentials, i.e., a combination
+%% of a public client identifier and a shared client secret.
+%% Should only be used for confidential clients; see the OAuth2 draft
+%% for clarification.
+%% @end
+-spec authorize_client_credentials(ClientId, ClientSecret, Scope)
+ -> {ok, Identity} | {error, Reason} when
+ ClientId :: binary(),
+ ClientSecret :: binary(),
+ Scope :: scope(),
+ Identity :: term(),
+ Reason :: error().
+authorize_client_credentials(ClientId, ClientSecret, Scope) ->
+ case oauth2_backend:authenticate_client(ClientId, ClientSecret, Scope) of
+ {ok, Identity} ->
+ %% NOTE: The OAuth2 draft dictates that no refresh token be issued here.
+ Response = issue_token(Identity, Scope),
+ {ok, Response};
+ {error, _Reason} ->
+ {error, access_denied}
+ end.
- case calculate_expires_in(Expires) > 0 of
- false ->
- Db:delete(access, generate_key(ClientId, Token)),
- {error, invalid_token};
+%% @doc Verifies an access token AccessToken, returning its associated
+%% context if successful. Otherwise, an OAuth2 error code is returned.
+%% @end
+-spec verify_access_token(AccessToken) -> {ok, Context} | {error, Reason} when
+ AccessToken :: token(),
+ Context :: proplist(atom(), term()),
+ Reason :: error().
+verify_access_token(AccessToken) ->
+ case oauth2_backend:resolve_access_token(AccessToken) of
+ {ok, Context} ->
+ ExpiryAbsolute = proplists:get_value(expiry_time, Context),
+ case ExpiryAbsolute > seconds_since_epoch(0) of
true ->
- {ok, [{audience, ClientId},
- {scope, Scope},
- {expires_in, calculate_expires_in(Expires)}
- ]}
+ {ok, Context};
+ false ->
+ oauth2_backend:revoke_access_token(AccessToken),
+ {error, access_denied}
end;
_ ->
- {error, invalid_token}
- end;
-verify_token(_, _Db, _Token, _ClientId) ->
- {error, invalid_token}.
-
-verify_token(authorization_code, Db, Token, ClientId, RedirectUri) ->
- case Db:verify_redirect_uri(ClientId, RedirectUri) of
- false ->
- {error, redirect_uri_mismatch};
- true ->
- case Db:get(auth, generate_key(ClientId, Token)) of
- {ok, Data} ->
- ClientId = Data#oauth2.client_id,
- Expires = Data#oauth2.expires,
- Scope = Data#oauth2.scope,
- Db:delete(auth, generate_key(ClientId, Token)),
-
- case calculate_expires_in(Expires) > 0 of
- false ->
- {error, invalid_grant};
- true ->
- AccessToken = generate_access_token(Expires),
- AccessData = #oauth2{client_id=ClientId,
- expires=seconds_since_epoch(?DEF_ACCESS_TOKEN_EXPIRE),
- scope=Scope},
- Key = generate_key(ClientId, AccessToken),
- Db:set(access, Key, AccessData),
-
- {ok, [{access_token, AccessToken},
- {token_type, "Bearer"},
- {expires_in, calculate_expires_in(AccessData#oauth2.expires)}
- ]}
- end;
- _ ->
- {error, invalid_grant}
- end
- end;
-verify_token(_, _Db, _Token, _ClientId, _RedirectUri) ->
- {error, invalid_token}.
-
-%% Internal API
-%%
-get_redirect_uri(Type, Code, Uri, State) ->
- get_redirect_uri(Type, Code, Uri, State, []).
-
-get_redirect_uri(Type, Code, Uri, State, _ExtraQuery) ->
- {S, N, P, Q, _} = mochiweb_util:urlsplit(Uri),
- State2 = case State of
- "" -> [];
- StateVal -> [{state, StateVal}]
- end,
- Q2 = mochiweb_util:parse_qs(Q),
- CF = [{code, Code}],
- case Type of
- token ->
- Q3 = lists:append([State2, Q2]),
- CF2 = mochiweb_util:urlencode(CF),
- Query = mochiweb_util:urlencode(Q3),
- mochiweb_util:urlunsplit({S, N, P, Query, CF2});
- code ->
- Q3 = lists:append([CF, State2, Q2]),
- Query = mochiweb_util:urlencode(Q3),
- mochiweb_util:urlunsplit({S, N, P, Query, ""})
+ {error, access_denied}
end.
-generate_key(ClientId, AuthCode) ->
- lists:flatten([ClientId, "#", AuthCode]).
-
-generate_access_token(Expires) ->
- S1 = generate_rnd_chars(15),
- S2 = generate_rnd_chars(15),
- S1++"."++integer_to_list(Expires)++"."++S2.
-
-generate_auth_code() ->
- generate_rnd_chars(30).
-
-generate_rnd_chars(N) ->
- Chars = list_to_tuple("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"),
- random:seed(now()),
- rnd_auth(N, Chars).
-
-rnd_auth(0, _) ->
- [];
-rnd_auth(Len, C) ->
- [rnd_auth(C)|rnd_auth(Len-1, C)].
-rnd_auth(C) ->
- element(random:uniform(tuple_size(C)), C).
-
-calculate_expires_in(Expire) ->
- Expire - seconds_since_epoch(0).
+%% @doc Verifies that RedirectionUri matches the redirection URI registered
+%% for the client identified by ClientId.
+%% @end
+-spec verify_redirection_uri(ClientId, RedirectionUri) -> Result when
+ ClientId :: binary(),
+ RedirectionUri :: binary(),
+ Result :: ok | {error, Reason :: term()}.
+verify_redirection_uri(ClientId, RedirectionUri) ->
+ case oauth2_backend:get_redirection_uri(ClientId) of
+ {ok, RedirectionUri} ->
+ ok;
+ {ok, _OtherUri} ->
+ {error, mismatch};
+ Error = {error, _} ->
+ Error
+ end.
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+-spec issue_token(Identity, Scope) -> oauth2_response:response() when
+ Identity :: term(),
+ Scope :: scope().
+issue_token(Identity, Scope) ->
+ AccessToken = oauth2_token:generate(),
+ ExpiryRelative = oauth2_config:expiry_time(),
+ ExpiryAbsolute = seconds_since_epoch(ExpiryRelative),
+ Context = build_context(Identity, ExpiryAbsolute, Scope),
+ oauth2_backend:associate_access_token(AccessToken, Context),
+ oauth2_response:new(AccessToken, ExpiryRelative, Scope).
+
+-spec build_context(Identity, ExpiryTime, Scope) -> Context when
+ Identity :: term(),
+ ExpiryTime :: non_neg_integer(),
+ Scope :: scope(),
+ Context :: proplist(atom(),term()).
+build_context(Identity, ExpiryTime, Scope) ->
+ [{identity, Identity},
+ {expiry_time, ExpiryTime},
+ {scope, Scope}].
+
+-spec seconds_since_epoch(Diff :: integer()) -> non_neg_integer().
seconds_since_epoch(Diff) ->
{Mega, Secs, _Micro} = now(),
Mega * 1000000 + Secs + Diff.
-
View
101 src/oauth2_backend.erl
@@ -0,0 +1,101 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_backend).
+
+%%% API
+-export([
+ authenticate_username_password/3
+ ,authenticate_client/3
+ ,associate_access_token/2
+ ,resolve_access_token/1
+ ,revoke_access_token/1
+ ,get_redirection_uri/1
+ ]).
+
+-type proplist(Key, Val) :: [{Key, Val}].
+
+-define(BACKEND, (oauth2_config:backend())).
+
+%%%===================================================================
+%%% API functions
+%%%===================================================================
+
+%% @doc Authenticates a combination of username, password and scope.
+%% Returns true if the user's credentials are valid and all items
+%% in scope are within the user's privileges to grant.
+%% @end
+-spec authenticate_username_password(Username, Password, Scope)
+ -> {ok, Identity} | {error, Reason} when
+ Username :: binary(),
+ Password :: binary(),
+ Scope :: oauth2:scope(),
+ Identity :: term(),
+ Reason :: notfound | badpass | badscope.
+authenticate_username_password(Username, Password, Scope) ->
+ ?BACKEND:authenticate_username_password(Username, Password, Scope).
+
+%% @doc Authenticates a client's credentials for a given scope.
+-spec authenticate_client(ClientId, ClientSecret, Scope) -> {ok, Identity} |
+ {error, Reason} when
+ ClientId :: binary(),
+ ClientSecret :: binary(),
+ Scope :: oauth2:scope(),
+ Identity :: term(),
+ Reason :: notfound | badsecret | badscope.
+authenticate_client(ClientId, ClientSecret, Scope) ->
+ ?BACKEND:authenticate_client(ClientId, ClientSecret, Scope).
+
+%% @doc Stores a new access token AccessToken, associating it with Context.
+%% The context is a proplist carrying information about the identity
+%% with which the token is associated, when it expires, etc.
+%% @end
+associate_access_token(AccessToken, Context) ->
+ ?BACKEND:associate_access_token(AccessToken, Context).
+
+%% @doc Looks up an access token AccessToken, returning the corresponding
+%% context if a match is found.
+%% @end
+-spec resolve_access_token(AccessToken) -> {ok, Context} | {error, Reason} when
+ AccessToken :: oauth2:token(),
+ Context :: proplist(atom(), term()),
+ Reason :: notfound.
+resolve_access_token(AccessToken) ->
+ ?BACKEND:resolve_access_token(AccessToken).
+
+%% @doc Revokes an access token AccessToken, so that it cannot be used again.
+-spec revoke_access_token(AccessToken) -> ok | {error, Reason} when
+ AccessToken :: oauth2:token(),
+ Reason :: notfound.
+revoke_access_token(AccessToken) ->
+ ?BACKEND:revoke_access_token(AccessToken).
+
+%% @doc Returns the redirection URI associated with the client ClientId.
+-spec get_redirection_uri(ClientId) -> Result when
+ ClientId :: binary(),
+ Result :: {error, Reason :: term()} | {ok, RedirectionUri :: binary()}.
+get_redirection_uri(ClientId) ->
+ ?BACKEND:get_redirection_uri(ClientId).
View
70 src/oauth2_config.erl
@@ -0,0 +1,70 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_config).
+
+%%% API
+-export([
+ expiry_time/0,
+ backend/0
+ ]).
+
+%% Default time in seconds before an authentication token expires.
+-define(DEFAULT_TOKEN_EXPIRY, 3600).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+%% @doc Gets the default expiry time for access tokens.
+-spec expiry_time() -> ExpiryTime :: non_neg_integer().
+expiry_time() ->
+ get_optional(expiry_time, ?DEFAULT_TOKEN_EXPIRY).
+
+%% @doc Gets the backend for validating passwords, storing tokens, etc.
+-spec backend() -> Module :: atom().
+backend() ->
+ get_required(backend).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+get_optional(Key, Default) ->
+ case application:get_env(oauth2, Key) of
+ undefined ->
+ Default;
+ {ok, Value} ->
+ Value
+ end.
+
+get_required(Key) ->
+ case application:get_env(oauth2, Key) of
+ undefined ->
+ throw({missing_config, Key});
+ {ok, Value} ->
+ Value
+ end.
View
31 src/oauth2_db.erl
@@ -1,31 +0,0 @@
-%% ----------------------------------------------------------------------------
-%%
-%% oauth2: Erlang OAuth 2.0 implementation
-%%
-%% Copyright 2012 (c) KIVRA. All Rights Reserved.
-%% http://developer.kivra.com dev@kivra.com
-%%
-%% This file is provided to you under the Apache License, Version 2.0 (the
-%% "License"); you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%% http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-%% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-%% License for the specific language governing permissions and limitations
-%% under the License.
-%%
-%% ----------------------------------------------------------------------------
-
--module(oauth2_db).
-
--export([behaviour_info/1]).
-
-behaviour_info(callbacks) ->
- [{get, 2},
- {set, 3},
- {delete, 2},
- {verify_redirect_uri, 2}].
-
View
80 src/oauth2_mock_db.erl
@@ -1,80 +0,0 @@
-%% ----------------------------------------------------------------------------
-%%
-%% oauth2: Erlang OAuth 2.0 implementation
-%%
-%% Copyright 2012 (c) KIVRA. All Rights Reserved.
-%% http://developer.kivra.com dev@kivra.com
-%%
-%% This file is provided to you under the Apache License, Version 2.0 (the
-%% "License"); you may not use this file except in compliance with the License.
-%% You may obtain a copy of the License at
-%%
-%% http://www.apache.org/licenses/LICENSE-2.0
-%%
-%% Unless required by applicable law or agreed to in writing, software
-%% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-%% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-%% License for the specific language governing permissions and limitations
-%% under the License.
-%%
-%% ----------------------------------------------------------------------------
-
--module(oauth2_mock_db).
-
--export([get/2, set/3, delete/2]).
--export([verify_redirect_uri/2]).
-
--include_lib("include/oauth2.hrl").
-
-%%
-%% Non behavioral functions
--export([init/0, delete_table/0]).
-
--define(TAB_AUTH, auth).
--define(TAB_ACC, acc).
-
--behavior(oauth2_db).
-
-get(auth, Key) ->
- get_tab(?TAB_AUTH, Key);
-get(access, Key) ->
- get_tab(?TAB_ACC, Key).
-
-set(auth, Key, Value) ->
- set_tab(?TAB_AUTH, Key, Value);
-set(access, Key, Value) ->
- set_tab(?TAB_ACC, Key, Value).
-
-delete(auth, Key) ->
- delete_tab(?TAB_AUTH, Key);
-delete(access, Key) ->
- delete_tab(?TAB_ACC, Key).
-
-verify_redirect_uri(_, _) ->
- true.
-
-%%
-%% Non behavioral functions
-delete_tab(Table, Key) ->
- ets:delete(Table, Key).
-
-set_tab(Table, Key, Value) ->
- ets:insert(Table, {Key, Value}).
-
-get_tab(Table, Key) ->
- case ets:lookup(Table, Key) of
- [] ->
- undefined;
- [{_Key, Value}] ->
- {ok, Value}
- end.
-
-init() ->
- ?TAB_AUTH = ets:new(?TAB_AUTH, [named_table, {read_concurrency, true}]),
- ?TAB_ACC = ets:new(?TAB_ACC, [named_table, {read_concurrency, true}]),
- ok.
-
-delete_table() ->
- ets:delete(?TAB_AUTH),
- ets:delete(?TAB_ACC).
-
View
122 src/oauth2_priv_set.erl
@@ -0,0 +1,122 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_priv_set).
+
+%%% API
+-export([
+ new/1
+ ,union/2
+ ,is_subset/2
+ ,is_member/2
+ ]).
+
+%% Invariant: Children are sorted increasingly by name.
+-type priv_tree() :: {node, Name :: binary(), Children :: [priv_tree()]} | '*'.
+%% Invariant:
+%% The list of trees is sorted increasingly by the name of the root node.
+-type priv_set() :: [priv_tree()].
+
+%% @doc Constructs a new priv_set from a single path or a list of paths.
+%% A path denotes a single privilege.
+%% @end
+-spec new(Paths :: binary() | [binary()]) -> priv_set().
+new(Paths) when is_list(Paths) ->
+ lists:foldl(fun union/2, [], [make_forest(Path) || Path <- Paths]);
+new(Path) when is_binary(Path) ->
+ make_forest(Path).
+
+%% @doc Returns the union of Set1 and Set2, i.e., a set such that
+%% any path present in either Set1 or Set2 is also present in the result.
+%% @end
+-spec union(Set1 :: priv_set(), Set2 :: priv_set) -> Union :: priv_set().
+union([H1={node, Name1, _}|T1], [H2={node, Name2, _}|T2]) when Name1 < Name2 ->
+ [H1|union(T1, [H2|T2])];
+union([H1={node, Name1, _}|T1], [H2={node, Name2, _}|T2]) when Name1 > Name2 ->
+ [H2|union([H1|T1], T2)];
+union([{node, Name, S1}|T1], [{node, Name, S2}|T2]) ->
+ [{node, Name, union(S1, S2)}|union(T1, T2)];
+union(['*'|_], _) -> %% '*' in union with anything is still '*'.
+ ['*'];
+union(_, ['*'|_]) ->
+ ['*'];
+union([], Set) ->
+ Set;
+union(Set, []) ->
+ Set.
+
+%% @doc Return true if Set1 is a subset of Set2, i.e., if
+%% every privilege held by Set1 is also held by Set2.
+%% @end
+-spec is_subset(Set1 :: priv_set(), Set2 :: priv_set()) -> boolean().
+is_subset([{node, Name1, _}|_], [{node, Name2, _}|_]) when Name1 < Name2 ->
+ false; %% This tree isn't present in Set2 as per the invariant.
+is_subset(Set1 = [{node, Name1, _}|_], [{node, Name2, _}|T2]) when Name1 > Name2 ->
+ is_subset(Set1, T2);
+is_subset([{node, Name, S1}|T1], [{node, Name, S2}|T2]) ->
+ case is_subset(S1, S2) of
+ true ->
+ is_subset(T1, T2);
+ false ->
+ false
+ end;
+is_subset(['*'|_], ['*'|_]) -> %% '*' is only a subset of '*'.
+ true;
+is_subset(_, ['*'|_]) -> %% Everything is a subset of '*'.
+ true;
+is_subset([], _) -> %% The empty set is a subset of every set.
+ true;
+is_subset(_, _) ->
+ false.
+
+%% @doc Returns true if Path is present in Set, i.e, if
+%% the privilege denoted by Path is contained within Set.
+%% @end
+-spec is_member(Path :: binary() | [binary()], Set :: priv_set()) -> boolean().
+is_member(Path, Set) ->
+ is_subset(make_forest(Path), Set).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+-spec make_forest(Path :: binary() | list()) -> priv_set().
+make_forest(Path) when is_binary(Path) ->
+ make_forest(binary:split(Path, <<".">>, [global]));
+make_forest(Path) when is_list(Path) ->
+ [make_tree(Path)].
+
+-spec make_tree(Path :: [binary()]) -> priv_tree().
+make_tree([<<"*">>|_]) ->
+ '*';
+make_tree([N]) ->
+ make_node(N, []);
+make_tree([H|T]) ->
+ make_node(H, [make_tree(T)]).
+
+-spec make_node(Name :: binary(), Children :: [priv_tree()]) -> priv_tree().
+make_node(Name, Children) ->
+ {node, Name, Children}.
View
120 src/oauth2_response.erl
@@ -0,0 +1,120 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_response).
+
+%% Length of binary data to use for token generation.
+-define(TOKEN_LENGTH, 32).
+
+%%% API
+-export([
+ new/1, new/2, new/3, new/4,
+ access_token/1, access_token/2,
+ refresh_token/1, refresh_token/2,
+ expires_in/1, expires_in/2,
+ scope/1, scope/2,
+ to_proplist/1
+ ]).
+
+-record(response, {
+ access_token :: oauth2:token(),
+ expires_in :: oauth2:lifetime(),
+ scope :: oauth2:scope(),
+ refresh_token :: oauth2:token(),
+ token_type = <<"bearer">> :: binary()
+ }).
+
+-type response() :: #response{}.
+-export_type([
+ response/0
+ ]).
+
+%%%===================================================================
+%%% API functions
+%%%===================================================================
+
+new(AccessToken) ->
+ #response{access_token = AccessToken}.
+
+new(AccessToken, ExpiresIn) ->
+ #response{access_token = AccessToken, expires_in = ExpiresIn}.
+
+new(AccessToken, ExpiresIn, Scope) ->
+ #response{access_token = AccessToken,
+ expires_in = ExpiresIn,
+ scope = Scope}.
+
+new(AccessToken, ExpiresIn, Scope, RefreshToken) ->
+ #response{access_token = AccessToken,
+ expires_in = ExpiresIn,
+ scope = Scope,
+ refresh_token = RefreshToken}.
+
+access_token(#response{access_token = AccessToken}) ->
+ {ok, AccessToken}.
+
+access_token(Response, NewAccessToken) ->
+ Response#response{access_token = NewAccessToken}.
+
+expires_in(#response{expires_in = undefined}) ->
+ {error, not_set};
+expires_in(#response{expires_in = ExpiresIn}) ->
+ {ok, ExpiresIn}.
+
+expires_in(Response, NewExpiresIn) ->
+ Response#response{expires_in = NewExpiresIn}.
+
+scope(#response{scope = undefined}) ->
+ {error, not_set};
+scope(#response{scope = Scope}) ->
+ {ok, Scope}.
+
+scope(Response, NewScope) ->
+ Response#response{scope = NewScope}.
+
+refresh_token(#response{refresh_token = undefined}) ->
+ {error, not_set};
+refresh_token(#response{refresh_token = RefreshToken}) ->
+ {ok, RefreshToken}.
+
+refresh_token(Response, NewRefreshToken) ->
+ Response#response{refresh_token = NewRefreshToken}.
+
+to_proplist(Response) ->
+ Keys = lists:map(fun to_binary/1, record_info(fields, response)),
+ Values = tl(tuple_to_list(Response)), %% Head is 'response'!
+ [{K, to_binary(V)} || {K , V} <- lists:zip(Keys, Values), V =/= undefined].
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+to_binary(Atom) when is_atom(Atom) ->
+ list_to_binary(atom_to_list(Atom));
+to_binary(Integer) when is_integer(Integer) ->
+ list_to_binary(integer_to_list(Integer));
+to_binary(Binary) when is_binary(Binary) ->
+ Binary.
View
66 src/oauth2_token.erl
@@ -0,0 +1,66 @@
+%% ----------------------------------------------------------------------------
+%%
+%% oauth2: Erlang OAuth 2.0 implementation
+%%
+%% Copyright (c) 2012 KIVRA
+%%
+%% 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:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% 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.
+%%
+%% ----------------------------------------------------------------------------
+
+-module(oauth2_token).
+
+-include("oauth2.hrl").
+