diff options
33 files changed, 852 insertions, 341 deletions
diff --git a/docs/index.rst b/docs/index.rst index eb81b2c..c44333a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,15 +15,18 @@ For news and discussions please check out our `G+ OAuthLib community`_. .. _`G+ OAuthLib community`: https://plus.google.com/communities/101889017375384052571 .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + feature_matrix + faq + contributing oauth_1_versus_oauth_2 + +.. toctree:: + :maxdepth: 3 + oauth1/oauth1 oauth2/oauth2 - contributing - faq - feature_matrix - Indices and tables ================== diff --git a/docs/oauth1/client.rst b/docs/oauth1/client.rst index 8d0f121..868a7ad 100644 --- a/docs/oauth1/client.rst +++ b/docs/oauth1/client.rst @@ -1,6 +1,6 @@ -========================= -OAuth 1: Using the Client -========================= +================ +Using the Client +================ **Are you using requests?** diff --git a/docs/oauth1/endpoints.rst b/docs/oauth1/endpoints.rst index 0b76212..7634554 100644 --- a/docs/oauth1/endpoints.rst +++ b/docs/oauth1/endpoints.rst @@ -10,6 +10,12 @@ used either independently or in a combination. They depend on the use of a See :doc:`preconfigured_servers` for available composite endpoints/servers. +RequestTokenEndpoint +-------------------- + +.. autoclass:: oauthlib.oauth1.RequestTokenEndpoint + :members: + AuthorizationEndpoint --------------------- @@ -22,12 +28,6 @@ AccessTokenEndpoint .. autoclass:: oauthlib.oauth1.AccessTokenEndpoint :members: -RequestTokenEndpoint --------------------- - -.. autoclass:: oauthlib.oauth1.RequestTokenEndpoint - :members: - ResourceEndpoint ---------------- diff --git a/docs/oauth1/endpoints/access_token.rst b/docs/oauth1/endpoints/access_token.rst new file mode 100644 index 0000000..d2ceba1 --- /dev/null +++ b/docs/oauth1/endpoints/access_token.rst @@ -0,0 +1,5 @@ +Access Token +------------ + +.. autoclass:: oauthlib.oauth1.AccessTokenEndpoint + :members: diff --git a/docs/oauth1/endpoints/authorization.rst b/docs/oauth1/endpoints/authorization.rst new file mode 100644 index 0000000..98f788c --- /dev/null +++ b/docs/oauth1/endpoints/authorization.rst @@ -0,0 +1,5 @@ +Authorization +------------- + +.. autoclass:: oauthlib.oauth1.AuthorizationEndpoint + :members: diff --git a/docs/oauth1/endpoints/endpoints.rst b/docs/oauth1/endpoints/endpoints.rst new file mode 100644 index 0000000..d869824 --- /dev/null +++ b/docs/oauth1/endpoints/endpoints.rst @@ -0,0 +1,16 @@ +Provider endpoints +================== + +Each endpoint is responsible for one step in the OAuth 1 workflow. They can be +used either independently or in a combination. They depend on the use of a +:doc:`../validator`. + +See :doc:`../preconfigured_servers` for available composite endpoints/servers. + +.. toctree:: + :maxdepth: 2 + + request_token + authorization + access_token + resource diff --git a/docs/oauth1/endpoints/request_token.rst b/docs/oauth1/endpoints/request_token.rst new file mode 100644 index 0000000..48be472 --- /dev/null +++ b/docs/oauth1/endpoints/request_token.rst @@ -0,0 +1,5 @@ +Request Token +------------- + +.. autoclass:: oauthlib.oauth1.RequestTokenEndpoint + :members: diff --git a/docs/oauth1/endpoints/resource.rst b/docs/oauth1/endpoints/resource.rst new file mode 100644 index 0000000..95fccf6 --- /dev/null +++ b/docs/oauth1/endpoints/resource.rst @@ -0,0 +1,5 @@ +Resource authorization +---------------------- + +.. autoclass:: oauthlib.oauth1.ResourceEndpoint + :members: diff --git a/docs/oauth1/oauth1.rst b/docs/oauth1/oauth1.rst index 64d8a3e..263328d 100644 --- a/docs/oauth1/oauth1.rst +++ b/docs/oauth1/oauth1.rst @@ -6,4 +6,4 @@ OAuth 1.0 client server - + endpoints/endpoints diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index 8fcfd2d..fec8e40 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -1,5 +1,6 @@ -OAuth 1: Creating a Provider -============================ +=================== +Creating a Provider +=================== OAuthLib is a dependency free library that may be used with any web framework. That said, there are framework specific helper libraries diff --git a/docs/oauth2/clients/backendapplicationclient.rst b/docs/oauth2/clients/backendapplicationclient.rst index e0f4774..11bfaed 100644 --- a/docs/oauth2/clients/backendapplicationclient.rst +++ b/docs/oauth2/clients/backendapplicationclient.rst @@ -1,5 +1,5 @@ -Client Credentials Grant flow (BackendApplicationClient) --------------------------------------------------------- +BackendApplicationClient +------------------------ .. autoclass:: oauthlib.oauth2.BackendApplicationClient :members: diff --git a/docs/oauth2/clients/baseclient.rst b/docs/oauth2/clients/baseclient.rst index 122af7f..48dcb48 100644 --- a/docs/oauth2/clients/baseclient.rst +++ b/docs/oauth2/clients/baseclient.rst @@ -1,5 +1,5 @@ -Base Client (Client) --------------------- +Base Client +----------- .. autoclass:: oauthlib.oauth2.Client :members: diff --git a/docs/oauth2/clients/client.rst b/docs/oauth2/clients/client.rst index 8486f3d..9b9d289 100644 --- a/docs/oauth2/clients/client.rst +++ b/docs/oauth2/clients/client.rst @@ -1,6 +1,6 @@ -====================== -OAuth 2: Using Clients -====================== +============= +Using Clients +============= OAuthLib supports all four core grant types defined in the OAuth 2 RFC and will continue to add more as they are defined. For more information on how diff --git a/docs/oauth2/clients/legacyapplicationclient.rst b/docs/oauth2/clients/legacyapplicationclient.rst index 8b85608..062a6a7 100644 --- a/docs/oauth2/clients/legacyapplicationclient.rst +++ b/docs/oauth2/clients/legacyapplicationclient.rst @@ -1,5 +1,5 @@ -Resource Owner Password Credentials Grant flow (LegacyApplicationClient) ------------------------------------------------------------------------- +LegacyApplicationClient +----------------------- .. autoclass:: oauthlib.oauth2.LegacyApplicationClient :members: diff --git a/docs/oauth2/clients/mobileapplicationclient.rst b/docs/oauth2/clients/mobileapplicationclient.rst index cd96377..66394d9 100644 --- a/docs/oauth2/clients/mobileapplicationclient.rst +++ b/docs/oauth2/clients/mobileapplicationclient.rst @@ -1,5 +1,5 @@ -Implicit Grant flow (MobileApplicationClient) ---------------------------------------------- +MobileApplicationClient +----------------------- .. autoclass:: oauthlib.oauth2.MobileApplicationClient :members: diff --git a/docs/oauth2/clients/webapplicationclient.rst b/docs/oauth2/clients/webapplicationclient.rst index a29081b..ecbf994 100644 --- a/docs/oauth2/clients/webapplicationclient.rst +++ b/docs/oauth2/clients/webapplicationclient.rst @@ -1,5 +1,5 @@ -Authorization Code Grant flow (WebApplicationClient) ----------------------------------------------------- +WebApplicationClient +-------------------- .. autoclass:: oauthlib.oauth2.WebApplicationClient :members: diff --git a/docs/oauth2/endpoints/authorization.rst b/docs/oauth2/endpoints/authorization.rst new file mode 100644 index 0000000..3c42387 --- /dev/null +++ b/docs/oauth2/endpoints/authorization.rst @@ -0,0 +1,123 @@ +============= +Authorization +============= + +Authorization can be either explicit or implicit. The former require the user to +actively authorize the client by being redirected to the authorization endpoint. +There he/she is usually presented by a form and asked to either accept or deny +access to certain scopes. These scopes can be thought of as Access Control Lists +that are tied to certain privileges and categories of resources, such as write +access to their status feed or read access to their profile. It is vital that +the implications of granting access to a certain scope is very clear in the +authorization form presented to the user. It is up to the provider to allow the +user agree to all, a few or none of the scopes. Being flexible here is a great +benefit to the user at the cost of added complexity in both the provider and +clients. + +Implicit authorization happens when the authorization happens before the OAuth +flow, such as the user giving the client his/her password and username, or if +there is a very high level of trust between the user, client and provider and no +explicit authorization is necessary. + +Examples of explicit authorization is the Authorization Code Grant and the +Implicit Grant. + +Examples of implicit authorization is the Resource Owner Password Credentials +Grant and the Client Credentials Grant. + +**Pre Authorization Request** + OAuth is known for it's authorization page where the user accepts or denies + access to a certain client and set of scopes. Before presenting the user + with such a form you need to ensure the credentials the client supplied in + the redirection to this page are valid. + + .. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Validate request + uri = 'https://example.com/authorize?client_id=foo&state=xyz + headers, body, http_method = {}, '', 'GET' + + from oauthlib.oauth2 import FatalClientError + from your_framework import redirect + try: + scopes, credentials = server.validate_authorization_request( + uri, http_method, body, headers) + # scopes will hold default scopes for client, i.e. + ['https://example.com/userProfile', 'https://example.com/pictures'] + + # credentials is a dictionary of + { + 'client_id': 'foo', + 'redirect_uri': 'https://foo.com/welcome_back', + 'response_type': 'code', + 'state': 'randomstring', + } + # these credentials will be needed in the post authorization view and + # should be persisted between. None of them are secret but take care + # to ensure their integrety if embedding them in the form or cookies. + from your_datastore import persist_credentials + persist_credentials(credentials) + + # Present user with a nice form where client (id foo) request access to + # his default scopes (omitted from request), after which you will + # redirect to his default redirect uri (omitted from request). + + except FatalClientError as e: + # this is your custom error page + from your_view_helpers import error_to_response + return error_to_response(e) + + +**Post Authorization Request** + Generally, this is where you handle the submitted form. Rather than using + ``validate_authorization_request`` we use ``create_authorization_response`` + which in the case of Authorization Code Grant embed an authorization code in + the client provided redirect uri. + + .. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Validate request + uri = 'https://example.com/post_authorize?client_id=foo + headers, body, http_method = {}, '', 'GET' + + # Fetch the credentials saved in the pre authorization phase + from your_datastore import fetch_credentials + credentials = fetch_credentials() + + # Fetch authorized scopes from the request + from your_framework import request + scopes = request.POST.get('scopes') + + from oauthlib.oauth2 import FatalClientError, OAuth2Error + from your_framework import http_response + http_response(body, status=status, headers=headers) + try: + headers, body, status = server.create_authorization_response( + uri, http_method, body, headers, scopes, credentials) + # headers = {'Location': 'https://foo.com/welcome_back?code=somerandomstring&state=xyz'}, this might change to include suggested headers related + # to cache best practices etc. + # body = '', this might be set in future custom grant types + # status = 302, suggested HTTP status code + + return http_response(body, status=status, headers=headers) + + except FatalClientError as e: + # this is your custom error page + from your_view_helpers import error_to_response + return error_to_response(e) + + except OAuth2Error as e: + # Less grave errors will be reported back to client + client_redirect_uri = credentials.get('redirect_uri') + redirect(e.in_uri(client_redirect_uri)) + +.. autoclass:: oauthlib.oauth2.AuthorizationEndpoint + :members: diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst new file mode 100644 index 0000000..42b1deb --- /dev/null +++ b/docs/oauth2/endpoints/endpoints.rst @@ -0,0 +1,29 @@ +Provider Endpoints +================== + +Endpoints in OAuth 2 are targets with a specific responsibility and often +associated with a particular URL. Because of this the word endpoint might be +used interchangably from the endpoint url. + +There main three responsibilities in an OAuth 2 flow is to authorize access to a +certain users resources to a client, to supply said client with a token +embodying this authorization and to verify that the token is valid when the +client attempts to access thee user resources on their behalf. + +.. toctree:: + :maxdepth: 2 + + authorization + token + resource + revocation + +There are three different endpoints, the authorization endpoint which mainly +handles user authorization, the token endpoint which provides tokens and the +resource endpoint which provides access to protected resources. It is to the +endpoints you will feed requests and get back an almost complete response. This +process is simplified for you using a decorator such as the django one described +later. + +The main purpose of the endpoint in OAuthLib is to figure out which grant type +or token to dispatch the request to. diff --git a/docs/oauth2/endpoints/resource.rst b/docs/oauth2/endpoints/resource.rst new file mode 100644 index 0000000..a5ff885 --- /dev/null +++ b/docs/oauth2/endpoints/resource.rst @@ -0,0 +1,41 @@ +====================== +Resource authorization +====================== + +Resource endpoints verify that the token presented is valid and granted access +to the scopes associated with the resource in question. + +**Request Verfication** + Each view may set certain scopes under which it is bound. Only requests + that present an access token bound to the correct scopes may access the + view. Access tokens are commonly embedded in the authorization header but + may appear in the query or the body as well. + + .. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Per view scopes + required_scopes = ['https://example.com/userProfile'] + + # Validate request + uri = 'https://example.com/userProfile?access_token=sldafh309sdf' + headers, body, http_method = {}, '', 'GET' + + valid, oauthlib_request = server.verify_request( + uri, http_method, body, headers, required_scopes) + + # oauthlib_request has a few convenient attributes set such as + # oauthlib_request.client = the client associated with the token + # oauthlib_request.user = the user associated with the token + # oauthlib_request.scopes = the scopes bound to this token + + if valid: + # return the protected resource / view + else: + # return an http forbidden 403 + +.. autoclass:: oauthlib.oauth2.ResourceEndpoint + :members: diff --git a/docs/oauth2/endpoints/revocation.rst b/docs/oauth2/endpoints/revocation.rst new file mode 100644 index 0000000..a5ca9ba --- /dev/null +++ b/docs/oauth2/endpoints/revocation.rst @@ -0,0 +1,26 @@ +================ +Token revocation +================ + +Revocation endpoints invalidate access and refresh tokens upon client request. +They are commonly part of the authorization endpoint. + +.. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Token revocation + uri = 'https://example.com/revoke_token' + headers, body, http_method = {}, 'token=sldafh309sdf', 'POST' + + headers, body, status = server.create_revocation_response(uri, + headers=headers, body=body, http_method=http_method) + + from your_framework import http_response + http_response(body, status=status, headers=headers) + + +.. autoclass:: oauthlib.oauth2.RevocationEndpoint + :members: diff --git a/docs/oauth2/endpoints/token.rst b/docs/oauth2/endpoints/token.rst new file mode 100644 index 0000000..bf1a256 --- /dev/null +++ b/docs/oauth2/endpoints/token.rst @@ -0,0 +1,86 @@ +============== +Token creation +============== + +Token endpoints issue tokens to clients who have already been authorized access, +be it by explicit actions from the user or implicitely. The token response is +well defined and typically consist of an unguessable access token, the token +type, its expiration from now in seconds and depending on the scenario, a +refresh token to be used to fetch new access tokens without authorization. + +One argument for OAuth 2 being more scalable than OAuth 1 is that tokens may +contain hidden information. A provider may embed information such as client +identifier, user identifier, expiration times, etc. in the token by encrypting +it. This trades a slight increase in work required to decrypt the token but +frees the necessary database lookups otherwise required, thus improving latency +substantially. OAuthlib currently does not provide a method for creating +crypto-tokens but may do in the future. + +The standard token type, Bearer, does not require that the provider bind a +specific client to the token. Not binding clients to tokens allow for anonymized +tokens which unless you are certain you need them, are a bad idea. + +**Token Request** + A POST request used in most grant types but with a varied setup of + credentials. If you wish to embed extra credentials in the request, i.e. for + later use in validation or when creating the token, you can use the + ``credentials`` argument in ``create_token_response``. + + All responses are in json format and the headers argument returned by + ``create_token_response`` will contain a few suggested headers related to + content type and caching. + + .. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Validate request + uri = 'https://example.com/token' + http_method = 'POST' + body = 'authorization_code=somerandomstring&' + 'grant_type=authorization_code&' + # Clients authenticate through a method of your choosing, for example + # using HTTP Basic Authentication + headers = { 'Authorization': 'Basic ksjdhf923sf' } + + # Extra credentials you wish to include + credentials = {'client_ip': '1.2.3.4'} + + headers, body, status = server.create_token_response( + uri, http_method, body, headers, credentials) + + # headers will contain some suggested headers to add to your response + { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + # body will contain the token in json format and expiration from now + # in seconds. + { + 'access_token': 'sldafh309sdf', + 'refresh_token': 'alsounguessablerandomstring', + 'expires_in': 3600, + 'scopes': [ + 'https://example.com/userProfile', + 'https://example.com/pictures' + ], + 'token_type': 'Bearer' + } + # body will contain an error code and possibly an error description if + # the request failed, also in json format. + { + 'error': 'invalid_grant_type', + 'description': 'athorizatoin_coed is not a valid grant type' + } + # status will be a suggested status code, 200 on ok, 400 on bad request + # and 401 if client is trying to use an invalid authorization code, + # fail to authenticate etc. + + from your_framework import http_response + http_response(body, status=status, headers=headers) + +.. autoclass:: oauthlib.oauth2.TokenEndpoint + :members: diff --git a/docs/oauth2/grants/grants.rst b/docs/oauth2/grants/grants.rst new file mode 100644 index 0000000..1018d55 --- /dev/null +++ b/docs/oauth2/grants/grants.rst @@ -0,0 +1,33 @@ +=========== +Grant types +=========== + +.. toctree:: + :maxdepth: 2 + + authcode + implicit + password + credentials + +Grant types are what make OAuth 2 so flexible. The Authorization Code grant is +very similar to OAuth 1 (with less crypto), the Implicit grant serves less +secure applications such as mobile applications, the Resource Owner Password +Credentials grant allows for legacy applications to incrementally transition to +OAuth 2, the Client Credentials grant is excellent for embedded services and +backend applications. + +The main purpose of the grant types is to authorize access to protected +resources in various ways with different security credentials. + +Naturally, OAuth 2 allows for extension grant types to be defined and OAuthLib +attempts to cater for easy inclusion of this as much as possible. + +Certain grant types allow the issuing of refresh tokens which will allow a +client to request new tokens for as long as you as provider allow them too. In +general, OAuth 2 tokens should expire quickly and rather than annoying the user +by require them to go through the authorization redirect loop you may use the +refresh token to get a new access token. Refresh tokens, contrary to what their +name suggest, are components of a grant type rather than token types (like +Bearer tokens), much like the authorization code in the authorization code +grant. diff --git a/docs/oauth2/oauth2.rst b/docs/oauth2/oauth2.rst index 4f56c82..2c92182 100644 --- a/docs/oauth2/oauth2.rst +++ b/docs/oauth2/oauth2.rst @@ -4,6 +4,8 @@ OAuth 2.0 .. toctree:: :maxdepth: 2 - overview clients/client server + endpoints/endpoints + grants/grants + tokens/tokens diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index cba56d1..fca6c3c 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -1,6 +1,6 @@ -============================ -OAuth 2: Creating a Provider -============================ +=================== +Creating a Provider +=================== OAuthLib is a dependency free library that may be used with any web framework. That said, there are framework specific helper libraries @@ -18,322 +18,392 @@ as well as provide an interface for a backend to store tokens, clients, etc. .. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib -**1. Create your datastore models** +.. contents:: Tutorial Contents + :depth: 3 - These models will represent various OAuth specific concepts. There are a few - important links between them that the security of OAuth is based on. Below - is a suggestion for models and why you need certain properties. There is - also example Django model fields which should be straightforward to - translate to other ORMs such as SQLAlchemy and the Appengine Datastore. +1. Create your datastore models +------------------------------- - **User (or Resource Owner)** - The user of your site which resources might be access by clients upon - authorization from the user. In our example we will re-use the User - model provided in django.contrib.auth.models. How the user authenticates - is orthogonal from OAuth and may be any way you prefer:: +These models will represent various OAuth specific concepts. There are a few +important links between them that the security of OAuth is based on. Below +is a suggestion for models and why you need certain properties. There is +also example Django model fields which should be straightforward to +translate to other ORMs such as SQLAlchemy and the Appengine Datastore. - from django.contrib.auth.models import User +User (or Resource Owner) +^^^^^^^^^^^^^^^^^^^^^^^^ - **Client (or Consumer)** - The client interested in accessing protected resources. +The user of your site which resources might be access by clients upon +authorization from the user. In our example we will re-use the User +model provided in django.contrib.auth.models. How the user authenticates +is orthogonal from OAuth and may be any way you prefer:: - **Client Identifier**: - Required. The identifier the client will use during the OAuth - workflow. Structure is up to you and may be a simple UUID:: + from django.contrib.auth.models import User - client_id = django.db.models.CharField(max_length=100, unique=True) +Client (or Consumer) +^^^^^^^^^^^^^^^^^^^^ - **User**: - Recommended. It is common practice to link each client with one of - your existing users. Whether you do associate clients and users or - not, ensure you are able to protect yourself against malicious - clients:: +The client interested in accessing protected resources. - user = django.db.models.ForeignKey(User) +**Client Identifier**: - **Grant Type**: - Required. The grant type the client may utilize. This should only be - one per client as each grant type has different security properties - and it is best to keep them separate to avoid mistakes:: + Required. The identifier the client will use during the OAuth + workflow. Structure is up to you and may be a simple UUID. - # max_length and choices depend on which grants you support - grant_type = django.db.models.CharField(max_length=18, - choices=[('authorization_code', 'Authorization code')]) + .. code-block:: python + + client_id = django.db.models.CharField(max_length=100, unique=True) + +**User**: + + Recommended. It is common practice to link each client with one of + your existing users. Whether you do associate clients and users or + not, ensure you are able to protect yourself against malicious + clients. + + .. code-block:: python - **Response Type**: - Required, if using a grant type with an associated response type - (eg. Authorization Code Grant) or using a grant which only utilizes - response types (eg. Implicit Grant):: + user = django.db.models.ForeignKey(User) - # max_length and choices depend on which response types you support - response_type = django.db.models.CharField(max_length=4, - choices=[('code', 'Authorization code')]) +**Grant Type**: - **Scopes**: - Required. The list of scopes the client may request access to. If - you allow multiple types of grants this will vary related to their - different security properties. For example, the Implicit Grant might - only allow read-only scopes but the Authorization Grant also allow - writes:: + Required. The grant type the client may utilize. This should only be + one per client as each grant type has different security properties + and it is best to keep them separate to avoid mistakes. - # You could represent it either as a list of keys or by serializing - # the scopes into a string. - scopes = django.db.models.TextField() + .. code-block:: python - # You might also want to mark a certain set of scopes as default - # scopes in case the client does not specify any in the authorization - default_scopes = django.db.models.TextField() + # max_length and choices depend on which response types you support + grant_type = django.db.models.CharField(max_length=18, + choices=[('authorization_code', 'Authorization code')]) - **Redirect URIs**: - These are the absolute URIs that a client may use to redirect to after - authorization. You should never allow a client to redirect to a URI - that has not previously been registered:: +**Response Type**: - # You could represent the URIs either as a list of keys or by - # serializing them into a string. - redirect_uris = django.db.models.TextField() + Required, if using a grant type with an associated response type + (eg. Authorization Code Grant) or using a grant which only utilizes + response types (eg. Implicit Grant). - # You might also want to mark a certain URI as default in case the - # client does not specify any in the authorization - default_redirect_uri = django.db.models.TextField() + .. code-block:: python - **Bearer Token (OAuth 2 Standard Token)** - The most common type of OAuth 2 token. Through the documentation this - will be considered an object with several properties, such as token type - and expiration date, and distinct from the access token it contains. - Think of OAuth 2 tokens as containers and access tokens and refresh - tokens as text. + # max_length and choices depend on which response types you support + response_type = django.db.models.CharField(max_length=4, + choices=[('code', 'Authorization code')]) - **Client**: - Association with the client to whom the token was given:: +**Scopes**: - client = django.db.models.ForeignKey(Client) + Required. The list of scopes the client may request access to. If + you allow multiple types of grants this will vary related to their + different security properties. For example, the Implicit Grant might + only allow read-only scopes but the Authorization Grant also allow + writes. - **User**: - Association with the user to which protected resources this token - grants access:: + .. code-block:: python - user = django.db.models.ForeignKey(User) + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + scopes = django.db.models.TextField() - **Scopes**: - Scopes to which the token is bound. Attempt to access protected - resources outside these scopes will be denied:: + # You might also want to mark a certain set of scopes as default + # scopes in case the client does not specify any in the authorization + default_scopes = django.db.models.TextField() - # You could represent it either as a list of keys or by serializing - # the scopes into a string. - scopes = django.db.models.TextField() +**Redirect URIs**: - **Access Token**: - An unguessable unique string of characters:: + These are the absolute URIs that a client may use to redirect to after + authorization. You should never allow a client to redirect to a URI + that has not previously been registered. - access_token = django.db.models.CharField(max_length=100, unique=True) + .. code-block:: python - **Refresh Token**: - An unguessable unique string of characters. This token is only - supplied to confidential clients. For example the Authorization Code - Grant or the Resource Owner Password Credentials Grant:: + # You could represent the URIs either as a list of keys or by + # serializing them into a string. + redirect_uris = django.db.models.TextField() - refresh_token = django.db.models.CharField(max_length=100, unique=True) + # You might also want to mark a certain URI as default in case the + # client does not specify any in the authorization + default_redirect_uri = django.db.models.TextField() - **Expiration time**: - Exact time of expiration. Commonly this is one hour after creation:: +Bearer Token (OAuth 2 Standard Token) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - expires_at = django.db.models.DateTimeField() +The most common type of OAuth 2 token. Through the documentation this +will be considered an object with several properties, such as token type +and expiration date, and distinct from the access token it contains. +Think of OAuth 2 tokens as containers and access tokens and refresh +tokens as text. - **Authorization Code** - This is specific to the Authorization Code grant and represent the - temporary credential granted to the client upon successful - authorization. It will later be exchanged for an access token, when that - is done it should cease to exist. It should have a limited life time, - less than ten minutes. This model is similar to the Bearer Token as it - mainly acts a temporary storage of properties to later be transferred to - the token. +**Client**: - **Client**: - Association with the client to whom the token was given:: + Association with the client to whom the token was given. - client = django.db.models.ForeignKey(Client) + .. code-block:: python - **User**: - Association with the user to which protected resources this token - grants access:: + client = django.db.models.ForeignKey(Client) - user = django.db.models.ForeignKey(User) +**User**: - **Scopes**: - Scopes to which the token is bound. Attempt to access protected - resources outside these scopes will be denied:: + Association with the user to which protected resources this token + grants access. - # You could represent it either as a list of keys or by serializing - # the scopes into a string. - scopes = django.db.models.TextField() + .. code-block:: python - **Authorization Code**: - An unguessable unique string of characters:: + user = django.db.models.ForeignKey(User) - code = django.db.models.CharField(max_length=100, unique=True) +**Scopes**: - **Expiration time**: - Exact time of expiration. Commonly this is under ten minutes after - creation:: + Scopes to which the token is bound. Attempt to access protected + resources outside these scopes will be denied. - expires_at = django.db.models.DateTimeField() + .. code-block:: python -**2. Implement a validator** + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + scopes = django.db.models.TextField() - The majority of the work involved in implementing an OAuth 2 provider - relates to mapping various validation and persistence methods to a storage - backend. The not very accurately named interface you will need to implement - is called a :doc:`RequestValidator <validator>` (name suggestions welcome). +**Access Token**: - An example of a very basic implementation of the validate_client_id method - can be seen below:: + An unguessable unique string of characters. - from oauthlib.oauth2 import RequestValidator + .. code-block:: python - # From the previous section on models - from my_models import Client + access_token = django.db.models.CharField(max_length=100, unique=True) - class MyRequestValidator(RequestValidator): +**Refresh Token**: - def validate_client_id(self, client_id, request): - try: - Client.objects.get(client_id=client_id) - return True - except Client.DoesNotExist: - return False + An unguessable unique string of characters. This token is only + supplied to confidential clients. For example the Authorization Code + Grant or the Resource Owner Password Credentials Grant. - The full API you will need to implement is available in the - :doc:`RequestValidator <validator>` section. You might not need to implement - all methods depending on which grant types you wish to support. A skeleton - validator listing the methods required for the WebApplicationServer is - available in the `examples`_ folder on GitHub. + .. code-block:: python - .. _`examples`: https://github.com/idan/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py + refresh_token = django.db.models.CharField(max_length=100, unique=True) - Relevant sections include: +**Expiration time**: - .. toctree:: - :maxdepth: 1 + Exact time of expiration. Commonly this is one hour after creation. - validator - security + .. code-block:: python + expires_at = django.db.models.DateTimeField() -**3. Create your composite endpoint** +Authorization Code +^^^^^^^^^^^^^^^^^^ - Each of the endpoints can function independently from each other, however - for this example it is easier to consider them as one unit. An example of a - pre-configured all-in-one Authorization Code Grant endpoint is given below:: +This is specific to the Authorization Code grant and represent the +temporary credential granted to the client upon successful +authorization. It will later be exchanged for an access token, when that +is done it should cease to exist. It should have a limited life time, +less than ten minutes. This model is similar to the Bearer Token as it +mainly acts a temporary storage of properties to later be transferred to +the token. - # From the previous section on validators - from my_validator import MyRequestValidator +**Client**: - from oauthlib.oauth2 import WebApplicationServer + Association with the client to whom the token was given. - validator = MyRequestValidator() - server = WebApplicationServer(validator) + .. code-block:: python - Relevant sections include: + client = django.db.models.ForeignKey(Client) - .. toctree:: - :maxdepth: 1 +**User**: - preconfigured_servers + Association with the user to which protected resources this token + grants access. + .. code-block:: python -**4. Create your endpoint views** + user = django.db.models.ForeignKey(User) - We are implementing support for the Authorization Code Grant and will - therefore need two views for the authorization, pre- and post-authorization - together with the token view. We also include an error page to redirect - users to if the client supplied invalid credentials in their redirection, - for example an invalid redirect URI. +**Scopes**: - The example using Django but should be transferable to any framework. + Scopes to which the token is bound. Attempt to access protected + resources outside these scopes will be denied. .. code-block:: python - # Handles GET and POST requests to /authorize - class AuthorizationView(View): + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + scopes = django.db.models.TextField() + +**Authorization Code**: + + An unguessable unique string of characters. + + .. code-block:: python + + code = django.db.models.CharField(max_length=100, unique=True) + +**Expiration time**: + + Exact time of expiration. Commonly this is under ten minutes after + creation. + + .. code-block:: python + + expires_at = django.db.models.DateTimeField() + +2. Implement a validator +------------------------ + +The majority of the work involved in implementing an OAuth 2 provider +relates to mapping various validation and persistence methods to a storage +backend. The not very accurately named interface you will need to implement +is called a :doc:`RequestValidator <validator>` (name suggestions welcome). + +An example of a very basic implementation of the validate_client_id method +can be seen below. + +.. code-block:: python + + from oauthlib.oauth2 import RequestValidator + + # From the previous section on models + from my_models import Client + + class MyRequestValidator(RequestValidator): + + def validate_client_id(self, client_id, request): + try: + Client.objects.get(client_id=client_id) + return True + except Client.DoesNotExist: + return False + +The full API you will need to implement is available in the +:doc:`RequestValidator <validator>` section. You might not need to implement +all methods depending on which grant types you wish to support. A skeleton +validator listing the methods required for the WebApplicationServer is +available in the `examples`_ folder on GitHub. + +.. _`examples`: https://github.com/idan/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py + +Relevant sections include: - def __init__(self): - # Using the server from previous section - self._authorization_endpoint = server +.. toctree:: + :maxdepth: 1 - def get(self, request): - # You need to define extract_params and make sure it does not - # include file like objects waiting for input. In Django this - # is request.META['wsgi.input'] and request.META['wsgi.errors'] - uri, http_method, body, headers = extract_params(request) + validator + security - try: - scopes, credentials = self._authorization_endpoint.validate_authorization_request( - uri, http_method, body, headers) - # Not necessarily in session but they need to be - # accessible in the POST view after form submit. - request.session['oauth2_credentials'] = credentials +3. Create your composite endpoint +--------------------------------- - # You probably want to render a template instead. - response = HttpResponse() - response.write('<h1> Authorize access to %s </h1>' % client_id) - response.write('<form method="POST" action="/authorize">') - for scope in scopes or []: - response.write('<input type="checkbox" name="scopes" ' + - 'value="%s"/> %s' % (scope, scope)) - response.write('<input type="submit" value="Authorize"/>') - return response +Each of the endpoints can function independently from each other, however +for this example it is easier to consider them as one unit. An example of a +pre-configured all-in-one Authorization Code Grant endpoint is given below. - # Errors that should be shown to the user on the provider website - except errors.FatalClientError as e: - return response_from_error(e) +.. code-block:: python - # Errors embedded in the redirect URI back to the client - except errors.OAuth2Error as e: - return HttpResponseRedirect(e.in_uri(e.redirect_uri)) + # From the previous section on validators + from my_validator import MyRequestValidator - @csrf_exempt - def post(self, request): - uri, http_method, body, headers = extract_params(request) + from oauthlib.oauth2 import WebApplicationServer - # The scopes the user actually authorized, i.e. checkboxes - # that were selected. - scopes = request.POST.getlist(['scopes']) + validator = MyRequestValidator() + server = WebApplicationServer(validator) - # Extra credentials we need in the validator - credentials = {'user': request.user} +Relevant sections include: - # The previously stored (in authorization GET view) credentials - credentials.update(request.session.get('oauth2_credentials', {})) +.. toctree:: + :maxdepth: 1 - try: - headers, body, status = self._authorization_endpoint.create_authorization_response( - uri, http_method, body, headers, scopes, credentials) - return response_from_return(headers, body, status) + preconfigured_servers - except errors.FatalClientError as e: - return response_from_error(e) - # Handles requests to /token - class TokenView(View): +4. Create your endpoint views +----------------------------- - def __init__(self): - # Using the server from previous section - self._token_endpoint = server +We are implementing support for the Authorization Code Grant and will +therefore need two views for the authorization, pre- and post-authorization +together with the token view. We also include an error page to redirect +users to if the client supplied invalid credentials in their redirection, +for example an invalid redirect URI. - def post(self, request): - uri, http_method, body, headers = extract_params(request) +The example using Django but should be transferable to any framework. - # If you wish to include request specific extra credentials for - # use in the validator, do so here. - credentials = {'foo': 'bar'} +.. code-block:: python - headers, body, status = self._token_endpoint.create_token_response( - uri, http_method, body, headers, credentials) + # Handles GET and POST requests to /authorize + class AuthorizationView(View): - # All requests to /token will return a json response, no redirection. - return response_from_return(headers, body, status) + def __init__(self): + # Using the server from previous section + self._authorization_endpoint = server + + def get(self, request): + # You need to define extract_params and make sure it does not + # include file like objects waiting for input. In Django this + # is request.META['wsgi.input'] and request.META['wsgi.errors'] + uri, http_method, body, headers = extract_params(request) + + try: + scopes, credentials = self._authorization_endpoint.validate_authorization_request( + uri, http_method, body, headers) + + # Not necessarily in session but they need to be + # accessible in the POST view after form submit. + request.session['oauth2_credentials'] = credentials + + # You probably want to render a template instead. + response = HttpResponse() + response.write('<h1> Authorize access to %s </h1>' % client_id) + response.write('<form method="POST" action="/authorize">') + for scope in scopes or []: + response.write('<input type="checkbox" name="scopes" ' + + 'value="%s"/> %s' % (scope, scope)) + response.write('<input type="submit" value="Authorize"/>') + return response + + # Errors that should be shown to the user on the provider website + except errors.FatalClientError as e: + return response_from_error(e) + + # Errors embedded in the redirect URI back to the client + except errors.OAuth2Error as e: + return HttpResponseRedirect(e.in_uri(e.redirect_uri)) + + @csrf_exempt + def post(self, request): + uri, http_method, body, headers = extract_params(request) + + # The scopes the user actually authorized, i.e. checkboxes + # that were selected. + scopes = request.POST.getlist(['scopes']) + + # Extra credentials we need in the validator + credentials = {'user': request.user} + + # The previously stored (in authorization GET view) credentials + credentials.update(request.session.get('oauth2_credentials', {})) + + try: + headers, body, status = self._authorization_endpoint.create_authorization_response( + uri, http_method, body, headers, scopes, credentials) + return response_from_return(headers, body, status) + + except errors.FatalClientError as e: + return response_from_error(e) + + # Handles requests to /token + class TokenView(View): + + def __init__(self): + # Using the server from previous section + self._token_endpoint = server + + def post(self, request): + uri, http_method, body, headers = extract_params(request) + + # If you wish to include request specific extra credentials for + # use in the validator, do so here. + credentials = {'foo': 'bar'} + + headers, body, status = self._token_endpoint.create_token_response( + uri, http_method, body, headers, credentials) + + # All requests to /token will return a json response, no redirection. + return response_from_return(headers, body, status) def response_from_return(headers, body, status): response = HttpResponse(content=body, status=status) @@ -345,83 +415,91 @@ as well as provide an interface for a backend to store tokens, clients, etc. return HttpResponseBadRequest('Evil client is unable to send a proper request. Error is: ' + e.description) +5. Protect your APIs using scopes +--------------------------------- -**5. Protect your APIs using scopes** +Let's define a decorator we can use to protect the views. - Let's define a decorator we can use to protect the views. +.. code-block:: python - .. code-block:: python + class OAuth2ProviderDecorator(object): + + def __init__(self, resource_endpoint): + self._resource_endpoint = resource_endpoint + + def protected_resource_view(self, scopes=None): + def decorator(f): + @functools.wraps(f) + def wrapper(request): + # Get the list of scopes + try: + scopes_list = scopes(request) + except TypeError: + scopes_list = scopes + + uri, http_method, body, headers = extract_params(request) + + valid, r = self._resource_endpoint.verify_request( + uri, http_method, body, headers, scopes_list) + + # For convenient parameter access in the view + add_params(request, { + 'client': r.client, + 'user': r.user, + 'scopes': r.scopes + }) + if valid: + return f(request) + else: + # Framework specific HTTP 403 + return HttpResponseForbidden() + return wrapper + return decorator + + provider = OAuth2ProviderDecorator(server) + +At this point you are ready to protect your API views with OAuth. Take some +time to come up with a good set of scopes as they can be very powerful in +controlling access. + +.. code-block:: python + + @provider.protected_resource_view(scopes=['images']) + def i_am_protected(request, client, resource_owner): + # One of your many OAuth 2 protected resource views + # Returns whatever you fancy + # May be bound to various scopes of your choosing + return HttpResponse('pictures of cats') + +The set of scopes that protects a view may also be dynamically configured +at runtime by a function, rather then by a list. + +.. code-block:: python + + def dynamic_scopes(request): + # Place code here to dynamically determine the scopes + # and return as a list + return ['images'] + + @provider.protected_resource_view(scopes=dynamic_scopes) + def i_am_also_protected(request, client, resource_owner, **kwargs) + # A view that has its views functionally set. + return HttpResponse('pictures of cats') + +6. Let us know how it went! +--------------------------- + +Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) + +.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new + +If you run into issues it can be helpful to enable debug logging. + +.. code-block:: python - class OAuth2ProviderDecorator(object): - - def __init__(self, resource_endpoint): - self._resource_endpoint = resource_endpoint - - def protected_resource_view(self, scopes=None): - def decorator(f): - @functools.wraps(f) - def wrapper(request): - # Get the list of scopes - try: - scopes_list = scopes(request) - except TypeError: - scopes_list = scopes - - uri, http_method, body, headers = extract_params(request) - - valid, r = self._resource_endpoint.verify_request( - uri, http_method, body, headers, scopes_list) - - # For convenient parameter access in the view - add_params(request, { - 'client': r.client, - 'user': r.user, - 'scopes': r.scopes - }) - if valid: - return f(request) - else: - # Framework specific HTTP 403 - return HttpResponseForbidden() - return wrapper - return decorator - - provider = OAuth2ProviderDecorator(server) - - At this point you are ready to protect your API views with OAuth. Take some - time to come up with a good set of scopes as they can be very powerful in - controlling access:: - - @provider.protected_resource_view(scopes=['images']) - def i_am_protected(request, client, resource_owner, **kwargs): - # One of your many OAuth 2 protected resource views - # Returns whatever you fancy - # May be bound to various scopes of your choosing - return HttpResponse('pictures of cats') - - The set of scopes that protects a view may also be dynamically configured - at runtime by a function, rather then by a list:: - - def dynamic_scopes(request): - # Place code here to dynamically determine the scopes - # and return as a list - return ['images'] - - @provider.protected_resource_view(scopes=dynamic_scopes) - def i_am_also_protected(request, client, resource_owner, **kwargs) - # A view that has its views functionally set. - return HttpResponse('pictures of cats') - -**6. Let us know how it went!** - - Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) - - .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 - .. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new - - If you run into issues it can be helpful to enable debug logging:: - - import logging - log = logging.getLogger('oauthlib') - log.addHandler(logging.StreamHandler(sys.stdout)) - log.setLevel(logging.DEBUG) + import logging + import sys + log = logging.getLogger('oauthlib') + log.addHandler(logging.StreamHandler(sys.stdout)) + log.setLevel(logging.DEBUG) diff --git a/docs/oauth2/tokens/bearer.rst b/docs/oauth2/tokens/bearer.rst new file mode 100644 index 0000000..8c6270d --- /dev/null +++ b/docs/oauth2/tokens/bearer.rst @@ -0,0 +1,13 @@ +============= +Bearer Tokens +============= + +The most common OAuth 2 token type. It provides very little in terms of security +and relies heavily upon the ability of the client to keep the token secret. + +Bearer tokens are the default setting with all configured endpoints. Generally +you will not need to ever construct a token yourself as the provided servers +will do so for you. + +.. autoclass:: oauthlib.oauth2.BearerToken + :members: diff --git a/docs/oauth2/tokens/jwt.rst b/docs/oauth2/tokens/jwt.rst new file mode 100644 index 0000000..87aed11 --- /dev/null +++ b/docs/oauth2/tokens/jwt.rst @@ -0,0 +1,7 @@ +========== +JWT Tokens +========== + +Not yet implemented. Track progress in `GitHub issue 50`_. + +.. _`GitHub issue 50`: https://github.com/idan/oauthlib/issues/50 diff --git a/docs/oauth2/tokens/mac.rst b/docs/oauth2/tokens/mac.rst new file mode 100644 index 0000000..4986819 --- /dev/null +++ b/docs/oauth2/tokens/mac.rst @@ -0,0 +1,8 @@ +========== +MAC tokens +========== + +Not yet implemented. Track progress in `GitHub issue 29`_. Might never be +supported depending on whether the work on the specification is resumed or not. + +.. _`GitHub issue 29`: https://github.com/idan/oauthlib/issues/29 diff --git a/docs/oauth2/tokens/saml.rst b/docs/oauth2/tokens/saml.rst new file mode 100644 index 0000000..9a00937 --- /dev/null +++ b/docs/oauth2/tokens/saml.rst @@ -0,0 +1,7 @@ +=========== +SAML Tokens +=========== + +Not yet implemented. Track progress in `GitHub issue 49`_. + +.. _`GitHub issue 49`: https://github.com/idan/oauthlib/issues/49 diff --git a/docs/oauth2/tokens/tokens.rst b/docs/oauth2/tokens/tokens.rst new file mode 100644 index 0000000..3273f77 --- /dev/null +++ b/docs/oauth2/tokens/tokens.rst @@ -0,0 +1,18 @@ +====== +Tokens +====== + +The main token type of OAuth 2 is Bearer tokens and that is what OAuthLib +currently supports. Other tokens, such as JWT, SAML and possibly MAC (if the +spec matures) can easily be added (and will be in due time). + +The purpose of a token is to authorize access to protected resources to a client +(i.e. your G+ feed). + +.. toctree:: + :maxdepth: 2 + + bearer + saml + jwt + mac diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py index ab953a4..870a675 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/access_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/access_token.py @@ -22,7 +22,7 @@ class AccessTokenEndpoint(BaseEndpoint): Typical use is to instantiate with a request validator and invoke the ``create_access_token_response`` from a view function. The tuple returned has all information necessary (body, status, headers) to quickly form - and return a proper response. See :doc:`validator` for details on which + and return a proper response. See :doc:`/oauth1/validator` for details on which validator methods to implement for this endpoint. """ diff --git a/oauthlib/oauth1/rfc5849/endpoints/authorization.py b/oauthlib/oauth1/rfc5849/endpoints/authorization.py index 5b78299..a7bbe29 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/authorization.py +++ b/oauthlib/oauth1/rfc5849/endpoints/authorization.py @@ -33,7 +33,7 @@ class AuthorizationEndpoint(BaseEndpoint): validate the request, create a verifier as well as prepare the final redirection URI used to send the user back to the client. - See :doc:`validator` for details on which validator methods to implement + See :doc:`/oauth1/validator` for details on which validator methods to implement for this endpoint. """ diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py index 424e33f..5c598e1 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py @@ -22,7 +22,7 @@ class RequestTokenEndpoint(BaseEndpoint): Typical use is to instantiate with a request validator and invoke the ``create_request_token_response`` from a view function. The tuple returned has all information necessary (body, status, headers) to quickly form - and return a proper response. See :doc:`validator` for details on which + and return a proper response. See :doc:`/oauth1/validator` for details on which validator methods to implement for this endpoint. """ diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py index fe3295c..7718852 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/resource.py +++ b/oauthlib/oauth1/rfc5849/endpoints/resource.py @@ -23,7 +23,7 @@ class ResourceEndpoint(BaseEndpoint): view. If invalid create and return an error response directly from the decorator. - See :doc:`validator` for details on which validator methods to implement + See :doc:`/oauth1/validator` for details on which validator methods to implement for this endpoint. An example decorator:: |