diff options
author | Ib Lundgren <ib.lundgren@gmail.com> | 2013-05-30 12:13:12 +0100 |
---|---|---|
committer | Ib Lundgren <ib.lundgren@gmail.com> | 2013-05-30 12:13:12 +0100 |
commit | 07b2c2bca9b7c0deb24a801841b373198b8a99bf (patch) | |
tree | 3dda951d7bdb7c8327d352b3a956c301accbf420 /oauthlib/oauth2 | |
parent | cd6da5ab2522e283d2fd7f89c74d77cab83b5eb8 (diff) | |
download | oauthlib-07b2c2bca9b7c0deb24a801841b373198b8a99bf.tar.gz |
Split OAuth2 large modules into smaller ones. #168.
Diffstat (limited to 'oauthlib/oauth2')
22 files changed, 2792 insertions, 1309 deletions
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index b1eb97a..c02a11f 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -9,13 +9,24 @@ This module is a wrapper for the most recent implementation of OAuth 2.0 Client and Server classes. """ -from .rfc6749 import Client, Server, WebApplicationClient -from .rfc6749 import UserAgentClient as MobileApplicationClient -from .rfc6749 import PasswordCredentialsClient as LegacyApplicationClient -from .rfc6749 import ClientCredentialsClient as BackendApplicationClient -from .rfc6749 import AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint -from .rfc6749 import WebApplicationServer, MobileApplicationServer -from .rfc6749 import LegacyApplicationServer, BackendApplicationServer -from .rfc6749.grant_types import * -from .rfc6749.tokens import BearerToken +from .rfc6749.clients import Client +from .rfc6749.clients import WebApplicationClient +from .rfc6749.clients import MobileApplicationClient +from .rfc6749.clients import LegacyApplicationClient +from .rfc6749.clients import BackendApplicationClient +from .rfc6749.endpoints import AuthorizationEndpoint +from .rfc6749.endpoints import TokenEndpoint +from .rfc6749.endpoints import ResourceEndpoint +from .rfc6749.endpoints import Server +from .rfc6749.endpoints import WebApplicationServer +from .rfc6749.endpoints import MobileApplicationServer +from .rfc6749.endpoints import LegacyApplicationServer +from .rfc6749.endpoints import BackendApplicationServer from .rfc6749.errors import * +from .rfc6749.grant_types import AuthorizationCodeGrant +from .rfc6749.grant_types import ImplicitGrant +from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant +from .rfc6749.grant_types import ClientCredentialsGrant +from .rfc6749.grant_types import RefreshTokenGrant +from .rfc6749.request_validator import RequestValidator +from .rfc6749.tokens import BearerToken diff --git a/oauthlib/oauth2/rfc6749/__init__.py b/oauthlib/oauth2/rfc6749/__init__.py index f8ee488..c82c06a 100644 --- a/oauthlib/oauth2/rfc6749/__init__.py +++ b/oauthlib/oauth2/rfc6749/__init__.py @@ -8,962 +8,11 @@ oauthlib.oauth2.rfc6749 This module is an implementation of various logic needed for consuming and providing OAuth 2.0 RFC6749. """ -import datetime import functools -import logging -from oauthlib.common import Request -from . import tokens, grant_types -from .errors import TokenExpiredError, InsecureTransportError +from oauthlib.common import log from .errors import TemporarilyUnavailableError, ServerError from .errors import FatalClientError, OAuth2Error -from .parameters import prepare_grant_uri, prepare_token_request -from .parameters import parse_authorization_code_response -from .parameters import parse_implicit_response, parse_token_response - - -AUTH_HEADER = 'auth_header' -URI_QUERY = 'query' -BODY = 'body' - -log = logging.getLogger('oauthlib') - -# Add a NullHandler to prevent warnings for users who don't wish -# to configure logging. -try: - log.addHandler(logging.NullHandler()) -# NullHandler gracefully backported to 2.6 -except AttributeError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - log.addHandler(NullHandler()) - - -class Client(object): - """Base OAuth2 client responsible for access tokens. - - While this class can be used to simply append tokens onto requests - it is often more useful to use a client targeted at a specific workflow. - """ - - def __init__(self, client_id, - default_token_placement=AUTH_HEADER, - token_type='Bearer', - access_token=None, - refresh_token=None, - mac_key=None, - mac_algorithm=None, - token=None, - **kwargs): - """Initialize a client with commonly used attributes.""" - - self.client_id = client_id - self.default_token_placement = default_token_placement - self.token_type = token_type - self.access_token = access_token - self.refresh_token = refresh_token - self.mac_key = mac_key - self.mac_algorithm = mac_algorithm - self.token = token or {} - self._expires_at = None - self._populate_attributes(self.token) - - @property - def token_types(self): - """Supported token types and their respective methods - - Additional tokens can be supported by extending this dictionary. - - The Bearer token spec is stable and safe to use. - - The MAC token spec is not yet stable and support for MAC tokens - is experimental and currently matching version 00 of the spec. - """ - return { - 'Bearer': self._add_bearer_token, - 'MAC': self._add_mac_token - } - - def add_token(self, uri, http_method='GET', body=None, headers=None, - token_placement=None, **kwargs): - """Add token to the request uri, body or authorization header. - - The access token type provides the client with the information - required to successfully utilize the access token to make a protected - resource request (along with type-specific attributes). The client - MUST NOT use an access token if it does not understand the token - type. - - For example, the "bearer" token type defined in - [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access - token string in the request: - - .. code-block:: http - - GET /resource/1 HTTP/1.1 - Host: example.com - Authorization: Bearer mF_9.B5f-4.1JqM - - while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is - utilized by issuing a MAC key together with the access token which is - used to sign certain components of the HTTP requests: - - .. code-block:: http - - GET /resource/1 HTTP/1.1 - Host: example.com - Authorization: MAC id="h480djs93hd8", - nonce="274312:dj83hs9s", - mac="kDZvddkndxvhGRXZhvuDjEWhGeE=" - - .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2 - .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2 - """ - if not uri.lower().startswith('https://'): - raise InsecureTransportError() - - token_placement = token_placement or self.default_token_placement - - case_insensitive_token_types = dict((k.lower(), v) for k, v in self.token_types.items()) - if not self.token_type.lower() in case_insensitive_token_types: - raise ValueError("Unsupported token type: %s" % self.token_type) - - if not self.access_token: - raise ValueError("Missing access token.") - - if self._expires_at and self._expires_at < datetime.datetime.now(): - raise TokenExpiredError() - - return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body, - headers, token_placement, **kwargs) - - def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): - """Prepare an access token request, using a refresh token. - - If the authorization server issued a refresh token to the client, the - client makes a refresh request to the token endpoint by adding the - following parameters using the "application/x-www-form-urlencoded" - format in the HTTP request entity-body: - - grant_type - REQUIRED. Value MUST be set to "refresh_token". - refresh_token - REQUIRED. The refresh token issued to the client. - scope - OPTIONAL. The scope of the access request as described by - Section 3.3. The requested scope MUST NOT include any scope - not originally granted by the resource owner, and if omitted is - treated as equal to the scope originally granted by the - resource owner. - """ - refresh_token = refresh_token or self.refresh_token - return prepare_token_request('refresh_token', body=body, scope=scope, - refresh_token=refresh_token, **kwargs) - - def _add_bearer_token(self, uri, http_method='GET', body=None, - headers=None, token_placement=None): - """Add a bearer token to the request uri, body or authorization header.""" - if token_placement == AUTH_HEADER: - headers = tokens.prepare_bearer_headers(self.access_token, headers) - - elif token_placement == URI_QUERY: - uri = tokens.prepare_bearer_uri(self.access_token, uri) - - elif token_placement == BODY: - body = tokens.prepare_bearer_body(self.access_token, body) - - else: - raise ValueError("Invalid token placement.") - return uri, headers, body - - def _add_mac_token(self, uri, http_method='GET', body=None, - headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs): - """Add a MAC token to the request authorization header. - - Warning: MAC token support is experimental as the spec is not yet stable. - """ - headers = tokens.prepare_mac_header(self.access_token, uri, - self.mac_key, http_method, headers=headers, body=body, ext=ext, - hash_algorithm=self.mac_algorithm, **kwargs) - return uri, headers, body - - def _populate_attributes(self, response): - """Add commonly used values such as access_token to self.""" - - if 'access_token' in response: - self.access_token = response.get('access_token') - - if 'refresh_token' in response: - self.refresh_token = response.get('refresh_token') - - if 'token_type' in response: - self.token_type = response.get('token_type') - - if 'expires_in' in response: - self.expires_in = response.get('expires_in') - self._expires_at = datetime.datetime.now() + datetime.timedelta( - seconds=int(self.expires_in)) - - if 'code' in response: - self.code = response.get('code') - - if 'mac_key' in response: - self.mac_key = response.get('mac_key') - - if 'mac_algorithm' in response: - self.mac_algorithm = response.get('mac_algorithm') - - def prepare_request_uri(self, *args, **kwargs): - """Abstract method used to create request URIs.""" - raise NotImplementedError("Must be implemented by inheriting classes.") - - def prepare_request_body(self, *args, **kwargs): - """Abstract method used to create request bodies.""" - raise NotImplementedError("Must be implemented by inheriting classes.") - - def parse_request_uri_response(self, *args, **kwargs): - """Abstract method used to parse redirection responses.""" - - def parse_request_body_response(self, *args, **kwargs): - """Abstract method used to parse JSON responses.""" - - -class WebApplicationClient(Client): - """A client utilizing the authorization code grant workflow. - - A web application is a confidential client running on a web - server. Resource owners access the client via an HTML user - interface rendered in a user-agent on the device used by the - resource owner. The client credentials as well as any access - token issued to the client are stored on the web server and are - not exposed to or accessible by the resource owner. - - The authorization code grant type is used to obtain both access - tokens and refresh tokens and is optimized for confidential clients. - As a redirection-based flow, the client must be capable of - interacting with the resource owner's user-agent (typically a web - browser) and capable of receiving incoming requests (via redirection) - from the authorization server. - """ - - def __init__(self, client_id, code=None, **kwargs): - super(WebApplicationClient, self).__init__(client_id, **kwargs) - self.code = code - - def prepare_request_uri(self, uri, redirect_uri=None, scope=None, - state=None, **kwargs): - """Prepare the authorization code request URI - - The client constructs the request URI by adding the following - parameters to the query component of the authorization endpoint URI - using the "application/x-www-form-urlencoded" format, per `Appendix B`_: - - :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI - and it should have been registerd with the OAuth - provider prior to use. As described in `Section 3.1.2`_. - - :param scope: OPTIONAL. The scope of the access request as described by - Section 3.3`_. These may be any string but are commonly - URIs or various categories such as ``videos`` or ``documents``. - - :param state: RECOMMENDED. An opaque value used by the client to maintain - state between the request and callback. The authorization - server includes this value when redirecting the user-agent back - to the client. The parameter SHOULD be used for preventing - cross-site request forgery as described in `Section 10.12`_. - - :param kwargs: Extra arguments to include in the request URI. - - In addition to supplied parameters, OAuthLib will append the ``client_id`` - that was provided in the constructor as well as the mandatory ``response_type`` - argument, set to ``code``:: - - >>> from oauthlib.oauth2 import WebApplicationClient - >>> client = WebApplicationClient('your_id') - >>> client.prepare_request_uri('https://example.com') - 'https://example.com?client_id=your_id&response_type=code' - >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback') - 'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback' - >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures']) - 'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures' - >>> client.prepare_request_uri('https://example.com', foo='bar') - 'https://example.com?client_id=your_id&response_type=code&foo=bar' - - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 - """ - return prepare_grant_uri(uri, self.client_id, 'code', - redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) - - def prepare_request_body(self, client_id=None, code=None, body='', - redirect_uri=None, **kwargs): - """Prepare the access token request body. - - The client makes a request to the token endpoint by adding the - following parameters using the "application/x-www-form-urlencoded" - format in the HTTP request entity-body: - - :param client_id: REQUIRED, if the client is not authenticating with the - authorization server as described in `Section 3.2.1`_. - - :param code: REQUIRED. The authorization code received from the - authorization server. - - :param redirect_uri: REQUIRED, if the "redirect_uri" parameter was included in the - authorization request as described in `Section 4.1.1`_, and their - values MUST be identical. - - :param kwargs: Extra parameters to include in the token request. - - In addition OAuthLib will add the ``grant_type`` parameter set to - ``authorization_code``. - - If the client type is confidential or the client was issued client - credentials (or assigned other authentication requirements), the - client MUST authenticate with the authorization server as described - in `Section 3.2.1`_:: - - >>> from oauthlib.oauth2 import WebApplicationClient - >>> client = WebApplicationClient('your_id') - >>> client.prepare_request_body(code='sh35ksdf09sf') - 'grant_type=authorization_code&code=sh35ksdf09sf' - >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') - 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' - - .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 - """ - code = code or self.code - return prepare_token_request('authorization_code', code=code, body=body, - client_id=self.client_id, redirect_uri=redirect_uri, **kwargs) - - def parse_request_uri_response(self, uri, state=None): - """Parse the URI query for code and state. - - If the resource owner grants the access request, the authorization - server issues an authorization code and delivers it to the client by - adding the following parameters to the query component of the - redirection URI using the "application/x-www-form-urlencoded" format: - - :param uri: The callback URI that resulted from the user being redirected - back from the provider to you, the client. - :param state: The state provided in the authorization request. - - **code** - The authorization code generated by the authorization server. - The authorization code MUST expire shortly after it is issued - to mitigate the risk of leaks. A maximum authorization code - lifetime of 10 minutes is RECOMMENDED. The client MUST NOT - use the authorization code more than once. If an authorization - code is used more than once, the authorization server MUST deny - the request and SHOULD revoke (when possible) all tokens - previously issued based on that authorization code. - The authorization code is bound to the client identifier and - redirection URI. - - **state** - If the "state" parameter was present in the authorization request. - - This method is mainly intended to enforce strict state checking with - the added benefit of easily extracting parameters from the URI:: - - >>> from oauthlib.oauth2 import WebApplicationClient - >>> client = WebApplicationClient('your_id') - >>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45' - >>> client.parse_request_uri_response(uri, state='sfetw45') - {'state': 'sfetw45', 'code': 'sdfkjh345'} - >>> client.parse_request_uri_response(uri, state='other') - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response - back from the provider to you, the client. - File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response - raise MismatchingStateError() - oauthlib.oauth2.rfc6749.errors.MismatchingStateError - """ - response = parse_authorization_code_response(uri, state=state) - self._populate_attributes(response) - return response - - def parse_request_body_response(self, body, scope=None): - """Parse the JSON response body. - - If the access token request is valid and authorized, the - authorization server issues an access token and optional refresh - token as described in `Section 5.1`_. If the request client - authentication failed or is invalid, the authorization server returns - an error response as described in `Section 5.2`_. - - :param body: The response body from the token request. - :param scope: Scopes originally requested. - :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. - - These response are json encoded and could easily be parsed without - the assistance of OAuthLib. However, there are a few subtle issues - to be aware of regarding the response which are helpfully addressed - through the raising of various errors. - - A successful response should always contain - - **access_token** - The access token issued by the authorization server. Often - a random string. - - **token_type** - The type of the token issued as described in `Section 7.1`_. - Commonly ``Bearer``. - - While it is not mandated it is recommended that the provider include - - **expires_in** - The lifetime in seconds of the access token. For - example, the value "3600" denotes that the access token will - expire in one hour from the time the response was generated. - If omitted, the authorization server SHOULD provide the - expiration time via other means or document the default value. - - **scope** - Providers may supply this in all responses but are required to only - if it has changed since the authorization request. - - A normal response might look like:: - - >>> json.loads(response_body) - { - 'access_token': 'sdfkjh345', - 'token_type': 'Bearer', - 'expires_in': '3600', - 'refresh_token': 'x345dgasd', - 'scope': 'hello world', - } - >>> from oauthlib.oauth2 import WebApplicationClient - >>> client = WebApplicationClient('your_id') - >>> client.parse_request_body_response(response_body) - { - 'access_token': 'sdfkjh345', - 'token_type': 'Bearer', - 'expires_in': '3600', - 'refresh_token': 'x345dgasd', - 'scope': ['hello', 'world'], # note the list - } - - If there was a scope change you will be notified with a warning:: - - >>> client.parse_request_body_response(response_body, scope=['images']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. - - If there was an error on the providers side you will be notified with - an error. For example, if there was no ``token_type`` provided:: - - >>> client.parse_request_body_response(response_body) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 276, in validate_token_parameters - raise MissingTokenTypeError() - oauthlib.oauth2.rfc6749.errors.MissingTokenTypeError - - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - """ - self.token = parse_token_response(body, scope=scope) - self._populate_attributes(self.token) - return self.token - - -class MobileApplicationClient(Client): - """A public client utilizing the implicit code grant workflow. - - A user-agent-based application is a public client in which the - client code is downloaded from a web server and executes within a - user-agent (e.g. web browser) on the device used by the resource - owner. Protocol data and credentials are easily accessible (and - often visible) to the resource owner. Since such applications - reside within the user-agent, they can make seamless use of the - user-agent capabilities when requesting authorization. - - The implicit grant type is used to obtain access tokens (it does not - support the issuance of refresh tokens) and is optimized for public - clients known to operate a particular redirection URI. These clients - are typically implemented in a browser using a scripting language - such as JavaScript. - - As a redirection-based flow, the client must be capable of - interacting with the resource owner's user-agent (typically a web - browser) and capable of receiving incoming requests (via redirection) - from the authorization server. - - Unlike the authorization code grant type in which the client makes - separate requests for authorization and access token, the client - receives the access token as the result of the authorization request. - - The implicit grant type does not include client authentication, and - relies on the presence of the resource owner and the registration of - the redirection URI. Because the access token is encoded into the - redirection URI, it may be exposed to the resource owner and other - applications residing on the same device. - """ - - def prepare_request_uri(self, uri, redirect_uri=None, scope=None, - state=None, **kwargs): - """Prepare the implicit grant request URI. - - The client constructs the request URI by adding the following - parameters to the query component of the authorization endpoint URI - using the "application/x-www-form-urlencoded" format, per `Appendix B`_: - - :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI - and it should have been registerd with the OAuth - provider prior to use. As described in `Section 3.1.2`_. - - :param scope: OPTIONAL. The scope of the access request as described by - Section 3.3`_. These may be any string but are commonly - URIs or various categories such as ``videos`` or ``documents``. - - :param state: RECOMMENDED. An opaque value used by the client to maintain - state between the request and callback. The authorization - server includes this value when redirecting the user-agent back - to the client. The parameter SHOULD be used for preventing - cross-site request forgery as described in `Section 10.12`_. - - :param kwargs: Extra arguments to include in the request URI. - - In addition to supplied parameters, OAuthLib will append the ``client_id`` - that was provided in the constructor as well as the mandatory ``response_type`` - argument, set to ``token``:: - - >>> from oauthlib.oauth2 import MobileApplicationClient - >>> client = MobileApplicationClient('your_id') - >>> client.prepare_request_uri('https://example.com') - 'https://example.com?client_id=your_id&response_type=token' - >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback') - 'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback' - >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures']) - 'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures' - >>> client.prepare_request_uri('https://example.com', foo='bar') - 'https://example.com?client_id=your_id&response_type=token&foo=bar' - - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 - """ - return prepare_grant_uri(uri, self.client_id, 'token', - redirect_uri=redirect_uri, state=state, scope=scope, **kwargs) - - def parse_request_uri_response(self, uri, state=None, scope=None): - """Parse the response URI fragment. - - If the resource owner grants the access request, the authorization - server issues an access token and delivers it to the client by adding - the following parameters to the fragment component of the redirection - URI using the "application/x-www-form-urlencoded" format: - - :param uri: The callback URI that resulted from the user being redirected - back from the provider to you, the client. - :param state: The state provided in the authorization request. - :param scope: The scopes provided in the authorization request. - :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. - - A successful response should always contain - - **access_token** - The access token issued by the authorization server. Often - a random string. - - **token_type** - The type of the token issued as described in `Section 7.1`_. - Commonly ``Bearer``. - - **state** - If you provided the state parameter in the authorization phase, then - the provider is required to include that exact state value in the - response. - - While it is not mandated it is recommended that the provider include - - **expires_in** - The lifetime in seconds of the access token. For - example, the value "3600" denotes that the access token will - expire in one hour from the time the response was generated. - If omitted, the authorization server SHOULD provide the - expiration time via other means or document the default value. - - **scope** - Providers may supply this in all responses but are required to only - if it has changed since the authorization request. - - A few example responses can be seen below:: - - >>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world' - >>> from oauthlib.oauth2 import MobileApplicationClient - >>> client = MobileApplicationClient('your_id') - >>> client.parse_request_uri_response(response_uri) - { - 'access_token': 'sdlfkj452', - 'token_type': 'Bearer', - 'state': 'ss345asyht', - 'scope': [u'hello', u'world'] - } - >>> client.parse_request_uri_response(response_uri, state='other') - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response - **scope** - File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response - raise ValueError("Mismatching or missing state in params.") - ValueError: Mismatching or missing state in params. - >>> client.parse_request_uri_response(response_uri, scope=['other']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response - **scope** - File "oauthlib/oauth2/rfc6749/parameters.py", line 199, in parse_implicit_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. - - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - """ - self.token = parse_implicit_response(uri, state=state, scope=scope) - self._populate_attributes(self.token) - return self.token - - -class BackendApplicationClient(Client): - """A public client utilizing the client credentials grant workflow. - - The client can request an access token using only its client - credentials (or other supported means of authentication) when the - client is requesting access to the protected resources under its - control, or those of another resource owner which has been previously - arranged with the authorization server (the method of which is beyond - the scope of this specification). - - The client credentials grant type MUST only be used by confidential - clients. - - Since the client authentication is used as the authorization grant, - no additional authorization request is needed. - """ - - def prepare_request_body(self, body='', scope=None, **kwargs): - """Add the client credentials to the request body. - - The client makes a request to the token endpoint by adding the - following parameters using the "application/x-www-form-urlencoded" - format per `Appendix B`_ in the HTTP request entity-body: - - :param scope: The scope of the access request as described by - `Section 3.3`_. - :param kwargs: Extra credentials to include in the token request. - - The client MUST authenticate with the authorization server as - described in `Section 3.2.1`_. - - The prepared body will include all provided credentials as well as - the ``grant_type`` parameter set to ``client_credentials``:: - - >>> from oauthlib.oauth2 import BackendApplicationClient - >>> client = BackendApplicationClient('your_id') - >>> client.prepare_request_body(scope=['hello', 'world']) - 'grant_type=client_credentials&scope=hello+world' - - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 - """ - return prepare_token_request('client_credentials', body=body, - scope=scope, **kwargs) - - def parse_request_body_response(self, body, scope=None): - """Parse the JSON response body. - - If the access token request is valid and authorized, the - authorization server issues an access token as described in - `Section 5.1`_. A refresh token SHOULD NOT be included. If the request - failed client authentication or is invalid, the authorization server - returns an error response as described in `Section 5.2`_. - - :param body: The response body from the token request. - :param scope: Scopes originally requested. - :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. - - These response are json encoded and could easily be parsed without - the assistance of OAuthLib. However, there are a few subtle issues - to be aware of regarding the response which are helpfully addressed - through the raising of various errors. - - A successful response should always contain - - **access_token** - The access token issued by the authorization server. Often - a random string. - - **token_type** - The type of the token issued as described in `Section 7.1`_. - Commonly ``Bearer``. - - While it is not mandated it is recommended that the provider include - - **expires_in** - The lifetime in seconds of the access token. For - example, the value "3600" denotes that the access token will - expire in one hour from the time the response was generated. - If omitted, the authorization server SHOULD provide the - expiration time via other means or document the default value. - - **scope** - Providers may supply this in all responses but are required to only - if it has changed since the authorization request. - - A normal response might look like:: - - >>> json.loads(response_body) - { - 'access_token': 'sdfkjh345', - 'token_type': 'Bearer', - 'expires_in': '3600', - 'refresh_token': 'x345dgasd', - 'scope': 'hello world', - } - >>> from oauthlib.oauth2 import BackendApplicationClient - >>> client = BackendApplicationClient('your_id') - >>> client.parse_request_body_response(response_body) - { - 'access_token': 'sdfkjh345', - 'token_type': 'Bearer', - 'expires_in': '3600', - 'refresh_token': 'x345dgasd', - 'scope': ['hello', 'world'], # note the list - } - - If there was a scope change you will be notified with a warning:: - - >>> client.parse_request_body_response(response_body, scope=['images']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. - - If there was an error on the providers side you will be notified with - an error. For example, if there was no ``token_type`` provided:: - - >>> client.parse_request_body_response(response_body) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 276, in validate_token_parameters - raise MissingTokenTypeError() - oauthlib.oauth2.rfc6749.errors.MissingTokenTypeError - - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - """ - self.token = parse_token_response(body, scope=scope) - self._populate_attributes(self.token) - return self.token - - -class LegacyApplicationClient(Client): - """A public client using the resource owner password and username directly. - - The resource owner password credentials grant type is suitable in - cases where the resource owner has a trust relationship with the - client, such as the device operating system or a highly privileged - application. The authorization server should take special care when - enabling this grant type, and only allow it when other flows are not - viable. - - The grant type is suitable for clients capable of obtaining the - resource owner's credentials (username and password, typically using - an interactive form). It is also used to migrate existing clients - using direct authentication schemes such as HTTP Basic or Digest - authentication to OAuth by converting the stored credentials to an - access token. - - The method through which the client obtains the resource owner - credentials is beyond the scope of this specification. The client - MUST discard the credentials once an access token has been obtained. - """ - - def __init__(self, client_id, **kwargs): - super(LegacyApplicationClient, self).__init__(client_id, **kwargs) - - def prepare_request_body(self, username, password, body='', scope=None, **kwargs): - """Add the resource owner password and username to the request body. - - The client makes a request to the token endpoint by adding the - following parameters using the "application/x-www-form-urlencoded" - format per `Appendix B`_ in the HTTP request entity-body: - - :param username: The resource owner username. - :param password: The resource owner password. - :param scope: The scope of the access request as described by - `Section 3.3`_. - :param kwargs: Extra credentials to include in the token request. - - If the client type is confidential or the client was issued client - credentials (or assigned other authentication requirements), the - client MUST authenticate with the authorization server as described - in `Section 3.2.1`_. - - The prepared body will include all provided credentials as well as - the ``grant_type`` parameter set to ``password``:: - - >>> from oauthlib.oauth2 import LegacyApplicationClient - >>> client = LegacyApplicationClient('your_id') - >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world']) - 'grant_type=password&username=foo&scope=hello+world&password=bar' - - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 - """ - return prepare_token_request('password', body=body, username=username, - password=password, scope=scope, **kwargs) - - def parse_request_body_response(self, body, scope=None): - """Parse the JSON response body. - - If the access token request is valid and authorized, the - authorization server issues an access token and optional refresh - token as described in `Section 5.1`_. If the request failed client - authentication or is invalid, the authorization server returns an - error response as described in `Section 5.2`_. - - :param body: The response body from the token request. - :param scope: Scopes originally requested. - :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. - - These response are json encoded and could easily be parsed without - the assistance of OAuthLib. However, there are a few subtle issues - to be aware of regarding the response which are helpfully addressed - through the raising of various errors. - - A successful response should always contain - - **access_token** - The access token issued by the authorization server. Often - a random string. - - **token_type** - The type of the token issued as described in `Section 7.1`_. - Commonly ``Bearer``. - - While it is not mandated it is recommended that the provider include - - **expires_in** - The lifetime in seconds of the access token. For - example, the value "3600" denotes that the access token will - expire in one hour from the time the response was generated. - If omitted, the authorization server SHOULD provide the - expiration time via other means or document the default value. - - **scope** - Providers may supply this in all responses but are required to only - if it has changed since the authorization request. - - A normal response might look like:: - - >>> json.loads(response_body) - { - 'access_token': 'sdfkjh345', - 'token_type': 'Bearer', - 'expires_in': '3600', - 'refresh_token': 'x345dgasd', - 'scope': 'hello world', - } - >>> from oauthlib.oauth2 import LegacyApplicationClient - >>> client = LegacyApplicationClient('your_id') - >>> client.parse_request_body_response(response_body) - { - 'access_token': 'sdfkjh345', - 'token_type': 'Bearer', - 'expires_in': '3600', - 'refresh_token': 'x345dgasd', - 'scope': ['hello', 'world'], # note the list - } - - If there was a scope change you will be notified with a warning:: - - >>> client.parse_request_body_response(response_body, scope=['images']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. - - If there was an error on the providers side you will be notified with - an error. For example, if there was no ``token_type`` provided:: - - >>> client.parse_request_body_response(response_body) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 276, in validate_token_parameters - raise MissingTokenTypeError() - oauthlib.oauth2.rfc6749.errors.MissingTokenTypeError - - .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - """ - self.token = parse_token_response(body, scope=scope) - self._populate_attributes(self.token) - return self.token - - -# TODO(ib-lundgren): Deprecate these names -class UserAgentClient(MobileApplicationClient): - pass - - -class PasswordCredentialsClient(LegacyApplicationClient): - pass - - -class ClientCredentialsClient(BackendApplicationClient): - pass class BaseEndpoint(object): @@ -1010,351 +59,3 @@ def catch_errors_and_unavailability(f): else: return f(endpoint, uri, *args, **kwargs) return wrapper - - -class AuthorizationEndpoint(BaseEndpoint): - """Authorization endpoint - used by the client to obtain authorization - from the resource owner via user-agent redirection. - - The authorization endpoint is used to interact with the resource - owner and obtain an authorization grant. The authorization server - MUST first verify the identity of the resource owner. The way in - which the authorization server authenticates the resource owner (e.g. - username and password login, session cookies) is beyond the scope of - this specification. - - The endpoint URI MAY include an "application/x-www-form-urlencoded" - formatted (per `Appendix B`_) query component, - which MUST be retained when adding additional query parameters. The - endpoint URI MUST NOT include a fragment component:: - - https://example.com/path?query=component # OK - https://example.com/path?query=component#fragment # Not OK - - Since requests to the authorization endpoint result in user - authentication and the transmission of clear-text credentials (in the - HTTP response), the authorization server MUST require the use of TLS - as described in Section 1.6 when sending requests to the - authorization endpoint:: - - # We will deny any request which URI schema is not with https - - The authorization server MUST support the use of the HTTP "GET" - method [RFC2616] for the authorization endpoint, and MAY support the - use of the "POST" method as well:: - - # HTTP method is currently not enforced - - Parameters sent without a value MUST be treated as if they were - omitted from the request. The authorization server MUST ignore - unrecognized request parameters. Request and response parameters - MUST NOT be included more than once:: - - # Enforced through the design of oauthlib.common.Request - - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - """ - - def __init__(self, default_response_type, default_token_type, - response_types): - BaseEndpoint.__init__(self) - self._response_types = response_types - self._default_response_type = default_response_type - self._default_token_type = default_token_type - - @property - def response_types(self): - return self._response_types - - @property - def default_response_type(self): - return self._default_response_type - - @property - def default_response_type_handler(self): - return self.response_types.get(self.default_response_type) - - @property - def default_token_type(self): - return self._default_token_type - - @catch_errors_and_unavailability - def create_authorization_response(self, uri, http_method='GET', body=None, - headers=None, scopes=None, credentials=None): - """Extract response_type and route to the designated handler.""" - request = Request(uri, http_method=http_method, body=body, headers=headers) - request.scopes = scopes - # TODO: decide whether this should be a required argument - request.user = None # TODO: explain this in docs - for k, v in (credentials or {}).items(): - setattr(request, k, v) - response_type_handler = self.response_types.get( - request.response_type, self.default_response_type_handler) - log.debug('Dispatching response_type %s request to %r.', - request.response_type, response_type_handler) - return response_type_handler.create_authorization_response( - request, self.default_token_type) - - @catch_errors_and_unavailability - def validate_authorization_request(self, uri, http_method='GET', body=None, - headers=None): - """Extract response_type and route to the designated handler.""" - request = Request(uri, http_method=http_method, body=body, headers=headers) - request.scopes = None - response_type_handler = self.response_types.get( - request.response_type, self.default_response_type_handler) - return response_type_handler.validate_authorization_request(request) - - -class TokenEndpoint(BaseEndpoint): - """Token issuing endpoint. - - The token endpoint is used by the client to obtain an access token by - presenting its authorization grant or refresh token. The token - endpoint is used with every authorization grant except for the - implicit grant type (since an access token is issued directly). - - The means through which the client obtains the location of the token - endpoint are beyond the scope of this specification, but the location - is typically provided in the service documentation. - - The endpoint URI MAY include an "application/x-www-form-urlencoded" - formatted (per `Appendix B`_) query component, - which MUST be retained when adding additional query parameters. The - endpoint URI MUST NOT include a fragment component:: - - https://example.com/path?query=component # OK - https://example.com/path?query=component#fragment # Not OK - - Since requests to the authorization endpoint result in user - Since requests to the token endpoint result in the transmission of - clear-text credentials (in the HTTP request and response), the - authorization server MUST require the use of TLS as described in - Section 1.6 when sending requests to the token endpoint:: - - # We will deny any request which URI schema is not with https - - The client MUST use the HTTP "POST" method when making access token - requests:: - - # HTTP method is currently not enforced - - Parameters sent without a value MUST be treated as if they were - omitted from the request. The authorization server MUST ignore - unrecognized request parameters. Request and response parameters - MUST NOT be included more than once:: - - # Delegated to each grant type. - - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - """ - - def __init__(self, default_grant_type, default_token_type, grant_types): - BaseEndpoint.__init__(self) - self._grant_types = grant_types - self._default_token_type = default_token_type - self._default_grant_type = default_grant_type - - @property - def grant_types(self): - return self._grant_types - - @property - def default_grant_type(self): - return self._default_grant_type - - @property - def default_grant_type_handler(self): - return self.grant_types.get(self.default_grant_type) - - @property - def default_token_type(self): - return self._default_token_type - - @catch_errors_and_unavailability - def create_token_response(self, uri, http_method='GET', body=None, - headers=None, credentials=None): - """Extract grant_type and route to the designated handler.""" - request = Request(uri, http_method=http_method, body=body, headers=headers) - request.extra_credentials = credentials - grant_type_handler = self.grant_types.get(request.grant_type, - self.default_grant_type_handler) - log.debug('Dispatching grant_type %s request to %r.', - request.grant_type, grant_type_handler) - return grant_type_handler.create_token_response( - request, self.default_token_type) - - -class ResourceEndpoint(BaseEndpoint): - """Authorizes access to protected resources. - - The client accesses protected resources by presenting the access - token to the resource server. The resource server MUST validate the - access token and ensure that it has not expired and that its scope - covers the requested resource. The methods used by the resource - server to validate the access token (as well as any error responses) - are beyond the scope of this specification but generally involve an - interaction or coordination between the resource server and the - authorization server:: - - # For most cases, returning a 403 should suffice. - - The method in which the client utilizes the access token to - authenticate with the resource server depends on the type of access - token issued by the authorization server. Typically, it involves - using the HTTP "Authorization" request header field [RFC2617] with an - authentication scheme defined by the specification of the access - token type used, such as [RFC6750]:: - - # Access tokens may also be provided in query and body - https://example.com/protected?access_token=kjfch2345sdf # Query - access_token=sdf23409df # Body - """ - def __init__(self, default_token, token_types): - BaseEndpoint.__init__(self) - self._tokens = token_types - self._default_token = default_token - - @property - def default_token(self): - return self._default_token - - @property - def default_token_type_handler(self): - return self.tokens.get(self.default_token) - - @property - def tokens(self): - return self._tokens - - @catch_errors_and_unavailability - def verify_request(self, uri, http_method='GET', body=None, headers=None, - scopes=None): - """Validate client, code etc, return body + headers""" - request = Request(uri, http_method, body, headers) - request.token_type = self.find_token_type(request) - request.scopes = scopes - token_type_handler = self.tokens.get(request.token_type, - self.default_token_type_handler) - log.debug('Dispatching token_type %s request to %r.', - request.token_type, token_type_handler) - return token_type_handler.validate_request(request), request - - def find_token_type(self, request): - """Token type identification. - - RFC 6749 does not provide a method for easily differentiating between - different token types during protected resource access. We estimate - the most likely token type (if any) by asking each known token type - to give an estimation based on the request. - """ - estimates = sorted(((t.estimate_type(request), n) for n, t in self.tokens.items())) - return estimates[0][1] if len(estimates) else None - - -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint): - """An all-in-one endpoint featuring all four major grant types.""" - - def __init__(self, request_validator, token_expires_in=None, - *args, **kwargs): - auth_grant = grant_types.AuthorizationCodeGrant(request_validator) - implicit_grant = grant_types.ImplicitGrant(request_validator) - password_grant = grant_types.ResourceOwnerPasswordCredentialsGrant(request_validator) - credentials_grant = grant_types.ClientCredentialsGrant(request_validator) - refresh_grant = grant_types.RefreshTokenGrant(request_validator) - bearer = tokens.BearerToken(request_validator, - expires_in=token_expires_in) - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={ - 'code': auth_grant, - 'token': implicit_grant, - }, - default_token_type=bearer) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': auth_grant, - 'password': password_grant, - 'client_credentials': credentials_grant, - 'refresh_token': refresh_grant, - }, - default_token_type=bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) - - -class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint): - """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" - - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, **kwargs): - """Construct a new web application server. - - :param request_validator: An implementation of oauthlib.oauth2.RequestValidator. - :param token_generator: A function to generate a token from a request. - :param kwargs: Extra parameters to pass to authorization endpoint, - token endpoint and resource endpoint constructors. - """ - auth_grant = grant_types.AuthorizationCodeGrant(request_validator) - refresh_grant = grant_types.RefreshTokenGrant(request_validator) - bearer = tokens.BearerToken(request_validator, token_generator, - expires_in=token_expires_in) - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={'code': auth_grant}, - default_token_type=bearer) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': auth_grant, - 'refresh_token': refresh_grant, - }, - default_token_type=bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) - - -class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint): - """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" - - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, **kwargs): - implicit_grant = grant_types.ImplicitGrant(request_validator) - bearer = tokens.BearerToken(request_validator, token_generator, - expires_in=token_expires_in) - AuthorizationEndpoint.__init__(self, default_response_type='token', - response_types={'token': implicit_grant}, - default_token_type=bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) - - -class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint): - """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" - - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, **kwargs): - password_grant = grant_types.ResourceOwnerPasswordCredentialsGrant(request_validator) - refresh_grant = grant_types.RefreshTokenGrant(request_validator) - bearer = tokens.BearerToken(request_validator, token_generator, - expires_in=token_expires_in) - TokenEndpoint.__init__(self, default_grant_type='password', - grant_types={ - 'password': password_grant, - 'refresh_token': refresh_grant, - }, - default_token_type=bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) - - -class BackendApplicationServer(TokenEndpoint, ResourceEndpoint): - """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" - - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, **kwargs): - credentials_grant = grant_types.ClientCredentialsGrant(request_validator) - bearer = tokens.BearerToken(request_validator, token_generator, - expires_in=token_expires_in) - TokenEndpoint.__init__(self, default_grant_type='client_credentials', - grant_types={'client_credentials': credentials_grant}, - default_token_type=bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) diff --git a/oauthlib/oauth2/rfc6749/clients/__init__.py b/oauthlib/oauth2/rfc6749/clients/__init__.py new file mode 100644 index 0000000..b9f633f --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming OAuth 2.0 RFC6749. +""" + +from .base import * +from .web_application import WebApplicationClient +from .mobile_application import MobileApplicationClient +from .legacy_application import LegacyApplicationClient +from .backend_application import BackendApplicationClient diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py new file mode 100644 index 0000000..e281128 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from .base import Client +from ..parameters import prepare_token_request +from ..parameters import parse_token_response + + +class BackendApplicationClient(Client): + """A public client utilizing the client credentials grant workflow. + + The client can request an access token using only its client + credentials (or other supported means of authentication) when the + client is requesting access to the protected resources under its + control, or those of another resource owner which has been previously + arranged with the authorization server (the method of which is beyond + the scope of this specification). + + The client credentials grant type MUST only be used by confidential + clients. + + Since the client authentication is used as the authorization grant, + no additional authorization request is needed. + """ + + def prepare_request_body(self, body='', scope=None, **kwargs): + """Add the client credentials to the request body. + + The client makes a request to the token endpoint by adding the + following parameters using the "application/x-www-form-urlencoded" + format per `Appendix B`_ in the HTTP request entity-body: + + :param scope: The scope of the access request as described by + `Section 3.3`_. + :param kwargs: Extra credentials to include in the token request. + + The client MUST authenticate with the authorization server as + described in `Section 3.2.1`_. + + The prepared body will include all provided credentials as well as + the ``grant_type`` parameter set to ``client_credentials``:: + + >>> from oauthlib.oauth2 import BackendApplicationClient + >>> client = BackendApplicationClient('your_id') + >>> client.prepare_request_body(scope=['hello', 'world']) + 'grant_type=client_credentials&scope=hello+world' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ + return prepare_token_request('client_credentials', body=body, + scope=scope, **kwargs) + + def parse_request_body_response(self, body, scope=None): + """Parse the JSON response body. + + If the access token request is valid and authorized, the + authorization server issues an access token as described in + `Section 5.1`_. A refresh token SHOULD NOT be included. If the request + failed client authentication or is invalid, the authorization server + returns an error response as described in `Section 5.2`_. + + :param body: The response body from the token request. + :param scope: Scopes originally requested. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + These response are json encoded and could easily be parsed without + the assistance of OAuthLib. However, there are a few subtle issues + to be aware of regarding the response which are helpfully addressed + through the raising of various errors. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A normal response might look like:: + + >>> json.loads(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': 'hello world', + } + >>> from oauthlib.oauth2 import BackendApplicationClient + >>> client = BackendApplicationClient('your_id') + >>> client.parse_request_body_response(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': ['hello', 'world'], # note the list + } + + If there was a scope change you will be notified with a warning:: + + >>> client.parse_request_body_response(response_body, scope=['images']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + If there was an error on the providers side you will be notified with + an error. For example, if there was no ``token_type`` provided:: + + >>> client.parse_request_body_response(response_body) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 276, in validate_token_parameters + raise MissingTokenTypeError() + oauthlib.oauth2.rfc6749.errors.MissingTokenTypeError + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + """ + self.token = parse_token_response(body, scope=scope) + self._populate_attributes(self.token) + return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py new file mode 100644 index 0000000..ac78ab3 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming OAuth 2.0 RFC6749. +""" +import datetime + +from oauthlib.oauth2.rfc6749 import tokens +from oauthlib.oauth2.rfc6749.parameters import prepare_token_request +from oauthlib.oauth2.rfc6749.errors import TokenExpiredError +from oauthlib.oauth2.rfc6749.errors import InsecureTransportError + + +AUTH_HEADER = 'auth_header' +URI_QUERY = 'query' +BODY = 'body' + + +class Client(object): + """Base OAuth2 client responsible for access tokens. + + While this class can be used to simply append tokens onto requests + it is often more useful to use a client targeted at a specific workflow. + """ + + def __init__(self, client_id, + default_token_placement=AUTH_HEADER, + token_type='Bearer', + access_token=None, + refresh_token=None, + mac_key=None, + mac_algorithm=None, + token=None, + **kwargs): + """Initialize a client with commonly used attributes.""" + + self.client_id = client_id + self.default_token_placement = default_token_placement + self.token_type = token_type + self.access_token = access_token + self.refresh_token = refresh_token + self.mac_key = mac_key + self.mac_algorithm = mac_algorithm + self.token = token or {} + self._expires_at = None + self._populate_attributes(self.token) + + @property + def token_types(self): + """Supported token types and their respective methods + + Additional tokens can be supported by extending this dictionary. + + The Bearer token spec is stable and safe to use. + + The MAC token spec is not yet stable and support for MAC tokens + is experimental and currently matching version 00 of the spec. + """ + return { + 'Bearer': self._add_bearer_token, + 'MAC': self._add_mac_token + } + + def add_token(self, uri, http_method='GET', body=None, headers=None, + token_placement=None, **kwargs): + """Add token to the request uri, body or authorization header. + + The access token type provides the client with the information + required to successfully utilize the access token to make a protected + resource request (along with type-specific attributes). The client + MUST NOT use an access token if it does not understand the token + type. + + For example, the "bearer" token type defined in + [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access + token string in the request: + + .. code-block:: http + + GET /resource/1 HTTP/1.1 + Host: example.com + Authorization: Bearer mF_9.B5f-4.1JqM + + while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is + utilized by issuing a MAC key together with the access token which is + used to sign certain components of the HTTP requests: + + .. code-block:: http + + GET /resource/1 HTTP/1.1 + Host: example.com + Authorization: MAC id="h480djs93hd8", + nonce="274312:dj83hs9s", + mac="kDZvddkndxvhGRXZhvuDjEWhGeE=" + + .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2 + .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2 + """ + if not uri.lower().startswith('https://'): + raise InsecureTransportError() + + token_placement = token_placement or self.default_token_placement + + case_insensitive_token_types = dict((k.lower(), v) for k, v in self.token_types.items()) + if not self.token_type.lower() in case_insensitive_token_types: + raise ValueError("Unsupported token type: %s" % self.token_type) + + if not self.access_token: + raise ValueError("Missing access token.") + + if self._expires_at and self._expires_at < datetime.datetime.now(): + raise TokenExpiredError() + + return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body, + headers, token_placement, **kwargs) + + def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): + """Prepare an access token request, using a refresh token. + + If the authorization server issued a refresh token to the client, the + client makes a refresh request to the token endpoint by adding the + following parameters using the "application/x-www-form-urlencoded" + format in the HTTP request entity-body: + + grant_type + REQUIRED. Value MUST be set to "refresh_token". + refresh_token + REQUIRED. The refresh token issued to the client. + scope + OPTIONAL. The scope of the access request as described by + Section 3.3. The requested scope MUST NOT include any scope + not originally granted by the resource owner, and if omitted is + treated as equal to the scope originally granted by the + resource owner. + """ + refresh_token = refresh_token or self.refresh_token + return prepare_token_request('refresh_token', body=body, scope=scope, + refresh_token=refresh_token, **kwargs) + + def _add_bearer_token(self, uri, http_method='GET', body=None, + headers=None, token_placement=None): + """Add a bearer token to the request uri, body or authorization header.""" + if token_placement == AUTH_HEADER: + headers = tokens.prepare_bearer_headers(self.access_token, headers) + + elif token_placement == URI_QUERY: + uri = tokens.prepare_bearer_uri(self.access_token, uri) + + elif token_placement == BODY: + body = tokens.prepare_bearer_body(self.access_token, body) + + else: + raise ValueError("Invalid token placement.") + return uri, headers, body + + def _add_mac_token(self, uri, http_method='GET', body=None, + headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs): + """Add a MAC token to the request authorization header. + + Warning: MAC token support is experimental as the spec is not yet stable. + """ + headers = tokens.prepare_mac_header(self.access_token, uri, + self.mac_key, http_method, headers=headers, body=body, ext=ext, + hash_algorithm=self.mac_algorithm, **kwargs) + return uri, headers, body + + def _populate_attributes(self, response): + """Add commonly used values such as access_token to self.""" + + if 'access_token' in response: + self.access_token = response.get('access_token') + + if 'refresh_token' in response: + self.refresh_token = response.get('refresh_token') + + if 'token_type' in response: + self.token_type = response.get('token_type') + + if 'expires_in' in response: + self.expires_in = response.get('expires_in') + self._expires_at = datetime.datetime.now() + datetime.timedelta( + seconds=int(self.expires_in)) + + if 'code' in response: + self.code = response.get('code') + + if 'mac_key' in response: + self.mac_key = response.get('mac_key') + + if 'mac_algorithm' in response: + self.mac_algorithm = response.get('mac_algorithm') + + def prepare_request_uri(self, *args, **kwargs): + """Abstract method used to create request URIs.""" + raise NotImplementedError("Must be implemented by inheriting classes.") + + def prepare_request_body(self, *args, **kwargs): + """Abstract method used to create request bodies.""" + raise NotImplementedError("Must be implemented by inheriting classes.") + + def parse_request_uri_response(self, *args, **kwargs): + """Abstract method used to parse redirection responses.""" + + def parse_request_body_response(self, *args, **kwargs): + """Abstract method used to parse JSON responses.""" diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py new file mode 100644 index 0000000..7164d23 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from .base import Client +from ..parameters import prepare_token_request +from ..parameters import parse_token_response + + +class LegacyApplicationClient(Client): + """A public client using the resource owner password and username directly. + + The resource owner password credentials grant type is suitable in + cases where the resource owner has a trust relationship with the + client, such as the device operating system or a highly privileged + application. The authorization server should take special care when + enabling this grant type, and only allow it when other flows are not + viable. + + The grant type is suitable for clients capable of obtaining the + resource owner's credentials (username and password, typically using + an interactive form). It is also used to migrate existing clients + using direct authentication schemes such as HTTP Basic or Digest + authentication to OAuth by converting the stored credentials to an + access token. + + The method through which the client obtains the resource owner + credentials is beyond the scope of this specification. The client + MUST discard the credentials once an access token has been obtained. + """ + + def __init__(self, client_id, **kwargs): + super(LegacyApplicationClient, self).__init__(client_id, **kwargs) + + def prepare_request_body(self, username, password, body='', scope=None, **kwargs): + """Add the resource owner password and username to the request body. + + The client makes a request to the token endpoint by adding the + following parameters using the "application/x-www-form-urlencoded" + format per `Appendix B`_ in the HTTP request entity-body: + + :param username: The resource owner username. + :param password: The resource owner password. + :param scope: The scope of the access request as described by + `Section 3.3`_. + :param kwargs: Extra credentials to include in the token request. + + If the client type is confidential or the client was issued client + credentials (or assigned other authentication requirements), the + client MUST authenticate with the authorization server as described + in `Section 3.2.1`_. + + The prepared body will include all provided credentials as well as + the ``grant_type`` parameter set to ``password``:: + + >>> from oauthlib.oauth2 import LegacyApplicationClient + >>> client = LegacyApplicationClient('your_id') + >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world']) + 'grant_type=password&username=foo&scope=hello+world&password=bar' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ + return prepare_token_request('password', body=body, username=username, + password=password, scope=scope, **kwargs) + + def parse_request_body_response(self, body, scope=None): + """Parse the JSON response body. + + If the access token request is valid and authorized, the + authorization server issues an access token and optional refresh + token as described in `Section 5.1`_. If the request failed client + authentication or is invalid, the authorization server returns an + error response as described in `Section 5.2`_. + + :param body: The response body from the token request. + :param scope: Scopes originally requested. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + These response are json encoded and could easily be parsed without + the assistance of OAuthLib. However, there are a few subtle issues + to be aware of regarding the response which are helpfully addressed + through the raising of various errors. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A normal response might look like:: + + >>> json.loads(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': 'hello world', + } + >>> from oauthlib.oauth2 import LegacyApplicationClient + >>> client = LegacyApplicationClient('your_id') + >>> client.parse_request_body_response(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': ['hello', 'world'], # note the list + } + + If there was a scope change you will be notified with a warning:: + + >>> client.parse_request_body_response(response_body, scope=['images']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + If there was an error on the providers side you will be notified with + an error. For example, if there was no ``token_type`` provided:: + + >>> client.parse_request_body_response(response_body) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 276, in validate_token_parameters + raise MissingTokenTypeError() + oauthlib.oauth2.rfc6749.errors.MissingTokenTypeError + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + """ + self.token = parse_token_response(body, scope=scope) + self._populate_attributes(self.token) + return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py new file mode 100644 index 0000000..e6c23c8 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from .base import Client +from ..parameters import prepare_grant_uri +from ..parameters import parse_implicit_response + + +class MobileApplicationClient(Client): + """A public client utilizing the implicit code grant workflow. + + A user-agent-based application is a public client in which the + client code is downloaded from a web server and executes within a + user-agent (e.g. web browser) on the device used by the resource + owner. Protocol data and credentials are easily accessible (and + often visible) to the resource owner. Since such applications + reside within the user-agent, they can make seamless use of the + user-agent capabilities when requesting authorization. + + The implicit grant type is used to obtain access tokens (it does not + support the issuance of refresh tokens) and is optimized for public + clients known to operate a particular redirection URI. These clients + are typically implemented in a browser using a scripting language + such as JavaScript. + + As a redirection-based flow, the client must be capable of + interacting with the resource owner's user-agent (typically a web + browser) and capable of receiving incoming requests (via redirection) + from the authorization server. + + Unlike the authorization code grant type in which the client makes + separate requests for authorization and access token, the client + receives the access token as the result of the authorization request. + + The implicit grant type does not include client authentication, and + relies on the presence of the resource owner and the registration of + the redirection URI. Because the access token is encoded into the + redirection URI, it may be exposed to the resource owner and other + applications residing on the same device. + """ + + def prepare_request_uri(self, uri, redirect_uri=None, scope=None, + state=None, **kwargs): + """Prepare the implicit grant request URI. + + The client constructs the request URI by adding the following + parameters to the query component of the authorization endpoint URI + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI + and it should have been registerd with the OAuth + provider prior to use. As described in `Section 3.1.2`_. + + :param scope: OPTIONAL. The scope of the access request as described by + Section 3.3`_. These may be any string but are commonly + URIs or various categories such as ``videos`` or ``documents``. + + :param state: RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + :param kwargs: Extra arguments to include in the request URI. + + In addition to supplied parameters, OAuthLib will append the ``client_id`` + that was provided in the constructor as well as the mandatory ``response_type`` + argument, set to ``token``:: + + >>> from oauthlib.oauth2 import MobileApplicationClient + >>> client = MobileApplicationClient('your_id') + >>> client.prepare_request_uri('https://example.com') + 'https://example.com?client_id=your_id&response_type=token' + >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback') + 'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback' + >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures']) + 'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures' + >>> client.prepare_request_uri('https://example.com', foo='bar') + 'https://example.com?client_id=your_id&response_type=token&foo=bar' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + """ + return prepare_grant_uri(uri, self.client_id, 'token', + redirect_uri=redirect_uri, state=state, scope=scope, **kwargs) + + def parse_request_uri_response(self, uri, state=None, scope=None): + """Parse the response URI fragment. + + If the resource owner grants the access request, the authorization + server issues an access token and delivers it to the client by adding + the following parameters to the fragment component of the redirection + URI using the "application/x-www-form-urlencoded" format: + + :param uri: The callback URI that resulted from the user being redirected + back from the provider to you, the client. + :param state: The state provided in the authorization request. + :param scope: The scopes provided in the authorization request. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + **state** + If you provided the state parameter in the authorization phase, then + the provider is required to include that exact state value in the + response. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A few example responses can be seen below:: + + >>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world' + >>> from oauthlib.oauth2 import MobileApplicationClient + >>> client = MobileApplicationClient('your_id') + >>> client.parse_request_uri_response(response_uri) + { + 'access_token': 'sdlfkj452', + 'token_type': 'Bearer', + 'state': 'ss345asyht', + 'scope': [u'hello', u'world'] + } + >>> client.parse_request_uri_response(response_uri, state='other') + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response + **scope** + File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response + raise ValueError("Mismatching or missing state in params.") + ValueError: Mismatching or missing state in params. + >>> client.parse_request_uri_response(response_uri, scope=['other']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response + **scope** + File "oauthlib/oauth2/rfc6749/parameters.py", line 199, in parse_implicit_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + """ + self.token = parse_implicit_response(uri, state=state, scope=scope) + self._populate_attributes(self.token) + return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py new file mode 100644 index 0000000..b2e4417 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from .base import Client +from ..parameters import prepare_grant_uri, prepare_token_request +from ..parameters import parse_authorization_code_response +from ..parameters import parse_token_response + + +class WebApplicationClient(Client): + """A client utilizing the authorization code grant workflow. + + A web application is a confidential client running on a web + server. Resource owners access the client via an HTML user + interface rendered in a user-agent on the device used by the + resource owner. The client credentials as well as any access + token issued to the client are stored on the web server and are + not exposed to or accessible by the resource owner. + + The authorization code grant type is used to obtain both access + tokens and refresh tokens and is optimized for confidential clients. + As a redirection-based flow, the client must be capable of + interacting with the resource owner's user-agent (typically a web + browser) and capable of receiving incoming requests (via redirection) + from the authorization server. + """ + + def __init__(self, client_id, code=None, **kwargs): + super(WebApplicationClient, self).__init__(client_id, **kwargs) + self.code = code + + def prepare_request_uri(self, uri, redirect_uri=None, scope=None, + state=None, **kwargs): + """Prepare the authorization code request URI + + The client constructs the request URI by adding the following + parameters to the query component of the authorization endpoint URI + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI + and it should have been registerd with the OAuth + provider prior to use. As described in `Section 3.1.2`_. + + :param scope: OPTIONAL. The scope of the access request as described by + Section 3.3`_. These may be any string but are commonly + URIs or various categories such as ``videos`` or ``documents``. + + :param state: RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + :param kwargs: Extra arguments to include in the request URI. + + In addition to supplied parameters, OAuthLib will append the ``client_id`` + that was provided in the constructor as well as the mandatory ``response_type`` + argument, set to ``code``:: + + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> client.prepare_request_uri('https://example.com') + 'https://example.com?client_id=your_id&response_type=code' + >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback') + 'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback' + >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures']) + 'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures' + >>> client.prepare_request_uri('https://example.com', foo='bar') + 'https://example.com?client_id=your_id&response_type=code&foo=bar' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + """ + return prepare_grant_uri(uri, self.client_id, 'code', + redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) + + def prepare_request_body(self, client_id=None, code=None, body='', + redirect_uri=None, **kwargs): + """Prepare the access token request body. + + The client makes a request to the token endpoint by adding the + following parameters using the "application/x-www-form-urlencoded" + format in the HTTP request entity-body: + + :param client_id: REQUIRED, if the client is not authenticating with the + authorization server as described in `Section 3.2.1`_. + + :param code: REQUIRED. The authorization code received from the + authorization server. + + :param redirect_uri: REQUIRED, if the "redirect_uri" parameter was included in the + authorization request as described in `Section 4.1.1`_, and their + values MUST be identical. + + :param kwargs: Extra parameters to include in the token request. + + In addition OAuthLib will add the ``grant_type`` parameter set to + ``authorization_code``. + + If the client type is confidential or the client was issued client + credentials (or assigned other authentication requirements), the + client MUST authenticate with the authorization server as described + in `Section 3.2.1`_:: + + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> client.prepare_request_body(code='sh35ksdf09sf') + 'grant_type=authorization_code&code=sh35ksdf09sf' + >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') + 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' + + .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ + code = code or self.code + return prepare_token_request('authorization_code', code=code, body=body, + client_id=self.client_id, redirect_uri=redirect_uri, **kwargs) + + def parse_request_uri_response(self, uri, state=None): + """Parse the URI query for code and state. + + If the resource owner grants the access request, the authorization + server issues an authorization code and delivers it to the client by + adding the following parameters to the query component of the + redirection URI using the "application/x-www-form-urlencoded" format: + + :param uri: The callback URI that resulted from the user being redirected + back from the provider to you, the client. + :param state: The state provided in the authorization request. + + **code** + The authorization code generated by the authorization server. + The authorization code MUST expire shortly after it is issued + to mitigate the risk of leaks. A maximum authorization code + lifetime of 10 minutes is RECOMMENDED. The client MUST NOT + use the authorization code more than once. If an authorization + code is used more than once, the authorization server MUST deny + the request and SHOULD revoke (when possible) all tokens + previously issued based on that authorization code. + The authorization code is bound to the client identifier and + redirection URI. + + **state** + If the "state" parameter was present in the authorization request. + + This method is mainly intended to enforce strict state checking with + the added benefit of easily extracting parameters from the URI:: + + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45' + >>> client.parse_request_uri_response(uri, state='sfetw45') + {'state': 'sfetw45', 'code': 'sdfkjh345'} + >>> client.parse_request_uri_response(uri, state='other') + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response + back from the provider to you, the client. + File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response + raise MismatchingStateError() + oauthlib.oauth2.rfc6749.errors.MismatchingStateError + """ + response = parse_authorization_code_response(uri, state=state) + self._populate_attributes(response) + return response + + def parse_request_body_response(self, body, scope=None): + """Parse the JSON response body. + + If the access token request is valid and authorized, the + authorization server issues an access token and optional refresh + token as described in `Section 5.1`_. If the request client + authentication failed or is invalid, the authorization server returns + an error response as described in `Section 5.2`_. + + :param body: The response body from the token request. + :param scope: Scopes originally requested. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + These response are json encoded and could easily be parsed without + the assistance of OAuthLib. However, there are a few subtle issues + to be aware of regarding the response which are helpfully addressed + through the raising of various errors. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A normal response might look like:: + + >>> json.loads(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': 'hello world', + } + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> client.parse_request_body_response(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': ['hello', 'world'], # note the list + } + + If there was a scope change you will be notified with a warning:: + + >>> client.parse_request_body_response(response_body, scope=['images']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + If there was an error on the providers side you will be notified with + an error. For example, if there was no ``token_type`` provided:: + + >>> client.parse_request_body_response(response_body) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/rfc6749/parameters.py", line 276, in validate_token_parameters + raise MissingTokenTypeError() + oauthlib.oauth2.rfc6749.errors.MissingTokenTypeError + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + """ + self.token = parse_token_response(body, scope=scope) + self._populate_attributes(self.token) + return self.token diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py new file mode 100644 index 0000000..f0a6f7c --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" + +from .authorization import AuthorizationEndpoint +from .token import TokenEndpoint +from .resource import ResourceEndpoint +from .pre_configured import Server +from .pre_configured import WebApplicationServer +from .pre_configured import MobileApplicationServer +from .pre_configured import LegacyApplicationServer +from .pre_configured import BackendApplicationServer diff --git a/oauthlib/oauth2/rfc6749/endpoints/authorization.py b/oauthlib/oauth2/rfc6749/endpoints/authorization.py new file mode 100644 index 0000000..d8606ea --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/authorization.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" + +from oauthlib.common import Request, log + +from .base import BaseEndpoint, catch_errors_and_unavailability + + +class AuthorizationEndpoint(BaseEndpoint): + """Authorization endpoint - used by the client to obtain authorization + from the resource owner via user-agent redirection. + + The authorization endpoint is used to interact with the resource + owner and obtain an authorization grant. The authorization server + MUST first verify the identity of the resource owner. The way in + which the authorization server authenticates the resource owner (e.g. + username and password login, session cookies) is beyond the scope of + this specification. + + The endpoint URI MAY include an "application/x-www-form-urlencoded" + formatted (per `Appendix B`_) query component, + which MUST be retained when adding additional query parameters. The + endpoint URI MUST NOT include a fragment component:: + + https://example.com/path?query=component # OK + https://example.com/path?query=component#fragment # Not OK + + Since requests to the authorization endpoint result in user + authentication and the transmission of clear-text credentials (in the + HTTP response), the authorization server MUST require the use of TLS + as described in Section 1.6 when sending requests to the + authorization endpoint:: + + # We will deny any request which URI schema is not with https + + The authorization server MUST support the use of the HTTP "GET" + method [RFC2616] for the authorization endpoint, and MAY support the + use of the "POST" method as well:: + + # HTTP method is currently not enforced + + Parameters sent without a value MUST be treated as if they were + omitted from the request. The authorization server MUST ignore + unrecognized request parameters. Request and response parameters + MUST NOT be included more than once:: + + # Enforced through the design of oauthlib.common.Request + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + """ + + def __init__(self, default_response_type, default_token_type, + response_types): + BaseEndpoint.__init__(self) + self._response_types = response_types + self._default_response_type = default_response_type + self._default_token_type = default_token_type + + @property + def response_types(self): + return self._response_types + + @property + def default_response_type(self): + return self._default_response_type + + @property + def default_response_type_handler(self): + return self.response_types.get(self.default_response_type) + + @property + def default_token_type(self): + return self._default_token_type + + @catch_errors_and_unavailability + def create_authorization_response(self, uri, http_method='GET', body=None, + headers=None, scopes=None, credentials=None): + """Extract response_type and route to the designated handler.""" + request = Request(uri, http_method=http_method, body=body, headers=headers) + request.scopes = scopes + # TODO: decide whether this should be a required argument + request.user = None # TODO: explain this in docs + for k, v in (credentials or {}).items(): + setattr(request, k, v) + response_type_handler = self.response_types.get( + request.response_type, self.default_response_type_handler) + log.debug('Dispatching response_type %s request to %r.', + request.response_type, response_type_handler) + return response_type_handler.create_authorization_response( + request, self.default_token_type) + + @catch_errors_and_unavailability + def validate_authorization_request(self, uri, http_method='GET', body=None, + headers=None): + """Extract response_type and route to the designated handler.""" + request = Request(uri, http_method=http_method, body=body, headers=headers) + request.scopes = None + response_type_handler = self.response_types.get( + request.response_type, self.default_response_type_handler) + return response_type_handler.validate_authorization_request(request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py new file mode 100644 index 0000000..41b3aef --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +import functools + +from oauthlib.common import log + +from ..errors import TemporarilyUnavailableError, ServerError +from ..errors import FatalClientError, OAuth2Error + + +class BaseEndpoint(object): + def __init__(self): + self._available = True + self._catch_errors = False + + @property + def available(self): + return self._available + + @available.setter + def available(self, available): + self._available = available + + @property + def catch_errors(self): + return self._catch_errors + + @catch_errors.setter + def catch_errors(self, catch_errors): + self._catch_errors = catch_errors + + +def catch_errors_and_unavailability(f): + @functools.wraps(f) + def wrapper(endpoint, uri, *args, **kwargs): + if not endpoint.available: + e = TemporarilyUnavailableError() + log.info('Endpoint unavailable, ignoring request %s.' % uri) + return None, {}, e.json, 503 + + if endpoint.catch_errors: + try: + return f(endpoint, uri, *args, **kwargs) + except OAuth2Error: + raise + except FatalClientError: + raise + except Exception as e: + error = ServerError() + log.warning('Exception caught while processing request, %s.' % e) + return None, {}, error.json, 500 + else: + return f(endpoint, uri, *args, **kwargs) + return wrapper diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py new file mode 100644 index 0000000..539879d --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from ..tokens import BearerToken +from ..grant_types import AuthorizationCodeGrant +from ..grant_types import ImplicitGrant +from ..grant_types import ResourceOwnerPasswordCredentialsGrant +from ..grant_types import ClientCredentialsGrant +from ..grant_types import RefreshTokenGrant + +from .authorization import AuthorizationEndpoint +from .token import TokenEndpoint +from .resource import ResourceEndpoint + + +class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring all four major grant types.""" + + def __init__(self, request_validator, token_expires_in=None, + *args, **kwargs): + auth_grant = AuthorizationCodeGrant(request_validator) + implicit_grant = ImplicitGrant(request_validator) + password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) + credentials_grant = ClientCredentialsGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + bearer = BearerToken(request_validator, + expires_in=token_expires_in) + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={ + 'code': auth_grant, + 'token': implicit_grant, + }, + default_token_type=bearer) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': auth_grant, + 'password': password_grant, + 'client_credentials': credentials_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" + + def __init__(self, request_validator, token_generator=None, + token_expires_in=None, **kwargs): + """Construct a new web application server. + + :param request_validator: An implementation of oauthlib.oauth2.RequestValidator. + :param token_generator: A function to generate a token from a request. + :param kwargs: Extra parameters to pass to authorization endpoint, + token endpoint and resource endpoint constructors. + """ + auth_grant = AuthorizationCodeGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + bearer = BearerToken(request_validator, token_generator, + expires_in=token_expires_in) + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={'code': auth_grant}, + default_token_type=bearer) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': auth_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" + + def __init__(self, request_validator, token_generator=None, + token_expires_in=None, **kwargs): + implicit_grant = ImplicitGrant(request_validator) + bearer = BearerToken(request_validator, token_generator, + expires_in=token_expires_in) + AuthorizationEndpoint.__init__(self, default_response_type='token', + response_types={'token': implicit_grant}, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" + + def __init__(self, request_validator, token_generator=None, + token_expires_in=None, **kwargs): + password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + bearer = BearerToken(request_validator, token_generator, + expires_in=token_expires_in) + TokenEndpoint.__init__(self, default_grant_type='password', + grant_types={ + 'password': password_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) + + +class BackendApplicationServer(TokenEndpoint, ResourceEndpoint): + """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" + + def __init__(self, request_validator, token_generator=None, + token_expires_in=None, **kwargs): + credentials_grant = ClientCredentialsGrant(request_validator) + bearer = BearerToken(request_validator, token_generator, + expires_in=token_expires_in) + TokenEndpoint.__init__(self, default_grant_type='client_credentials', + grant_types={'client_credentials': credentials_grant}, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer}) diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py new file mode 100644 index 0000000..c1f6b18 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/resource.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from oauthlib.common import Request, log + +from .base import BaseEndpoint, catch_errors_and_unavailability + + +class ResourceEndpoint(BaseEndpoint): + """Authorizes access to protected resources. + + The client accesses protected resources by presenting the access + token to the resource server. The resource server MUST validate the + access token and ensure that it has not expired and that its scope + covers the requested resource. The methods used by the resource + server to validate the access token (as well as any error responses) + are beyond the scope of this specification but generally involve an + interaction or coordination between the resource server and the + authorization server:: + + # For most cases, returning a 403 should suffice. + + The method in which the client utilizes the access token to + authenticate with the resource server depends on the type of access + token issued by the authorization server. Typically, it involves + using the HTTP "Authorization" request header field [RFC2617] with an + authentication scheme defined by the specification of the access + token type used, such as [RFC6750]:: + + # Access tokens may also be provided in query and body + https://example.com/protected?access_token=kjfch2345sdf # Query + access_token=sdf23409df # Body + """ + def __init__(self, default_token, token_types): + BaseEndpoint.__init__(self) + self._tokens = token_types + self._default_token = default_token + + @property + def default_token(self): + return self._default_token + + @property + def default_token_type_handler(self): + return self.tokens.get(self.default_token) + + @property + def tokens(self): + return self._tokens + + @catch_errors_and_unavailability + def verify_request(self, uri, http_method='GET', body=None, headers=None, + scopes=None): + """Validate client, code etc, return body + headers""" + request = Request(uri, http_method, body, headers) + request.token_type = self.find_token_type(request) + request.scopes = scopes + token_type_handler = self.tokens.get(request.token_type, + self.default_token_type_handler) + log.debug('Dispatching token_type %s request to %r.', + request.token_type, token_type_handler) + return token_type_handler.validate_request(request), request + + def find_token_type(self, request): + """Token type identification. + + RFC 6749 does not provide a method for easily differentiating between + different token types during protected resource access. We estimate + the most likely token type (if any) by asking each known token type + to give an estimation based on the request. + """ + estimates = sorted(((t.estimate_type(request), n) for n, t in self.tokens.items())) + return estimates[0][1] if len(estimates) else None diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py new file mode 100644 index 0000000..9fac254 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/token.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from oauthlib.common import Request, log + +from .base import BaseEndpoint, catch_errors_and_unavailability + + +class TokenEndpoint(BaseEndpoint): + """Token issuing endpoint. + + The token endpoint is used by the client to obtain an access token by + presenting its authorization grant or refresh token. The token + endpoint is used with every authorization grant except for the + implicit grant type (since an access token is issued directly). + + The means through which the client obtains the location of the token + endpoint are beyond the scope of this specification, but the location + is typically provided in the service documentation. + + The endpoint URI MAY include an "application/x-www-form-urlencoded" + formatted (per `Appendix B`_) query component, + which MUST be retained when adding additional query parameters. The + endpoint URI MUST NOT include a fragment component:: + + https://example.com/path?query=component # OK + https://example.com/path?query=component#fragment # Not OK + + Since requests to the authorization endpoint result in user + Since requests to the token endpoint result in the transmission of + clear-text credentials (in the HTTP request and response), the + authorization server MUST require the use of TLS as described in + Section 1.6 when sending requests to the token endpoint:: + + # We will deny any request which URI schema is not with https + + The client MUST use the HTTP "POST" method when making access token + requests:: + + # HTTP method is currently not enforced + + Parameters sent without a value MUST be treated as if they were + omitted from the request. The authorization server MUST ignore + unrecognized request parameters. Request and response parameters + MUST NOT be included more than once:: + + # Delegated to each grant type. + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + """ + + def __init__(self, default_grant_type, default_token_type, grant_types): + BaseEndpoint.__init__(self) + self._grant_types = grant_types + self._default_token_type = default_token_type + self._default_grant_type = default_grant_type + + @property + def grant_types(self): + return self._grant_types + + @property + def default_grant_type(self): + return self._default_grant_type + + @property + def default_grant_type_handler(self): + return self.grant_types.get(self.default_grant_type) + + @property + def default_token_type(self): + return self._default_token_type + + @catch_errors_and_unavailability + def create_token_response(self, uri, http_method='GET', body=None, + headers=None, credentials=None): + """Extract grant_type and route to the designated handler.""" + request = Request(uri, http_method=http_method, body=body, headers=headers) + request.extra_credentials = credentials + grant_type_handler = self.grant_types.get(request.grant_type, + self.default_grant_type_handler) + log.debug('Dispatching grant_type %s request to %r.', + request.grant_type, grant_type_handler) + return grant_type_handler.create_token_response( + request, self.default_token_type) diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py new file mode 100644 index 0000000..2ec8e4f --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import + +from .authorization_code import AuthorizationCodeGrant +from .implicit import ImplicitGrant +from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant +from .client_credentials import ClientCredentialsGrant +from .refresh_token import RefreshTokenGrant diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py new file mode 100644 index 0000000..55f70f3 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -0,0 +1,387 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import +import json + +from oauthlib import common +from oauthlib.common import log +from oauthlib.uri_validate import is_absolute_uri + +from .base import GrantTypeBase +from .. import errors +from ..request_validator import RequestValidator + + +class AuthorizationCodeGrant(GrantTypeBase): + """`Authorization Code Grant`_ + + The authorization code grant type is used to obtain both access + tokens and refresh tokens and is optimized for confidential clients. + Since this is a redirection-based flow, the client must be capable of + interacting with the resource owner's user-agent (typically a web + browser) and capable of receiving incoming requests (via redirection) + from the authorization server:: + + +----------+ + | Resource | + | Owner | + | | + +----------+ + ^ + | + (B) + +----|-----+ Client Identifier +---------------+ + | -+----(A)-- & Redirection URI ---->| | + | User- | | Authorization | + | Agent -+----(B)-- User authenticates --->| Server | + | | | | + | -+----(C)-- Authorization Code ---<| | + +-|----|---+ +---------------+ + | | ^ v + (A) (C) | | + | | | | + ^ v | | + +---------+ | | + | |>---(D)-- Authorization Code ---------' | + | Client | & Redirection URI | + | | | + | |<---(E)----- Access Token -------------------' + +---------+ (w/ Optional Refresh Token) + + Note: The lines illustrating steps (A), (B), and (C) are broken into + two parts as they pass through the user-agent. + + Figure 3: Authorization Code Flow + + The flow illustrated in Figure 3 includes the following steps: + + (A) The client initiates the flow by directing the resource owner's + user-agent to the authorization endpoint. The client includes + its client identifier, requested scope, local state, and a + redirection URI to which the authorization server will send the + user-agent back once access is granted (or denied). + + (B) The authorization server authenticates the resource owner (via + the user-agent) and establishes whether the resource owner + grants or denies the client's access request. + + (C) Assuming the resource owner grants access, the authorization + server redirects the user-agent back to the client using the + redirection URI provided earlier (in the request or during + client registration). The redirection URI includes an + authorization code and any local state provided by the client + earlier. + + (D) The client requests an access token from the authorization + server's token endpoint by including the authorization code + received in the previous step. When making the request, the + client authenticates with the authorization server. The client + includes the redirection URI used to obtain the authorization + code for verification. + + (E) The authorization server authenticates the client, validates the + authorization code, and ensures that the redirection URI + received matches the URI used to redirect the client in + step (C). If valid, the authorization server responds back with + an access token and, optionally, a refresh token. + + .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1 + """ + def __init__(self, request_validator=None): + self.request_validator = request_validator or RequestValidator() + + def create_authorization_code(self, request): + """Generates an authorization grant represented as a dictionary.""" + grant = {'code': common.generate_token()} + if hasattr(request, 'state') and request.state: + grant['state'] = request.state + log.debug('Created authorization code grant %r for request %r.', + grant, request) + return grant + + def create_authorization_response(self, request, token_handler): + """ + The client constructs the request URI by adding the following + parameters to the query component of the authorization endpoint URI + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + response_type + REQUIRED. Value MUST be set to "code". + client_id + REQUIRED. The client identifier as described in `Section 2.2`_. + redirect_uri + OPTIONAL. As described in `Section 3.1.2`_. + scope + OPTIONAL. The scope of the access request as described by + `Section 3.3`_. + state + RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + The client directs the resource owner to the constructed URI using an + HTTP redirection response, or by other means available to it via the + user-agent. + + :param request: oauthlib.commong.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + :returns: uri, headers, body, status + :raises: FatalClientError on invalid redirect URI or client id. + ValueError if scopes are not set on the request object. + + A few examples:: + + >>> from your_validator import your_validator + >>> request = Request('https://example.com/authorize?client_id=valid' + ... '&redirect_uri=http%3A%2F%2Fclient.com%2F') + >>> from oauthlib.common import Request + >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken + >>> token = BearerToken(your_validator) + >>> grant = AuthorizationCodeGrant(your_validator) + >>> grant.create_authorization_response(request, token) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response + raise ValueError('Scopes must be set on post auth.') + ValueError: Scopes must be set on post auth. + >>> request.scopes = ['authorized', 'in', 'some', 'form'] + >>> grant.create_authorization_response(request, token) + (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400) + >>> request = Request('https://example.com/authorize?client_id=valid' + ... '&redirect_uri=http%3A%2F%2Fclient.com%2F' + ... '&response_type=code') + >>> request.scopes = ['authorized', 'in', 'some', 'form'] + >>> grant.create_authorization_response(request, token) + (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200) + >>> # If the client id or redirect uri fails validation + >>> grant.create_authorization_response(request, token) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response + >>> grant.create_authorization_response(request, token) + File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request + oauthlib.oauth2.rfc6749.errors.InvalidClientIdError + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + """ + try: + # request.scopes is only mandated in post auth and both pre and + # post auth use validate_authorization_request + if not request.scopes: + raise ValueError('Scopes must be set on post auth.') + + self.validate_authorization_request(request) + log.debug('Pre resource owner authorization validation ok for %r.', + request) + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + except errors.FatalClientError as e: + log.debug('Fatal client error during validation of %r. %r.', + request, e) + raise + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the query component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B: + # http://tools.ietf.org/html/rfc6749#appendix-B + except errors.OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + request.redirect_uri = request.redirect_uri or self.error_uri + return common.add_params_to_uri(request.redirect_uri, e.twotuples), None, None, e.status_code + + grant = self.create_authorization_code(request) + log.debug('Saving grant %r for %r.', grant, request) + self.request_validator.save_authorization_code(request.client_id, grant, request) + return common.add_params_to_uri(request.redirect_uri, grant.items()), None, None, 302 + + def create_token_response(self, request, token_handler): + """Validate the authorization code. + + The client MUST NOT use the authorization code more than once. If an + authorization code is used more than once, the authorization server + MUST deny the request and SHOULD revoke (when possible) all tokens + previously issued based on that authorization code. The authorization + code is bound to the client identifier and redirection URI. + """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + try: + self.validate_token_request(request) + log.debug('Token request validation ok for %r.', request) + except errors.OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + return None, headers, e.json, e.status_code + + token = token_handler.create_token(request, refresh_token=True) + self.request_validator.invalidate_authorization_code( + request.client_id, request.code, request) + return None, headers, json.dumps(token), 200 + + def validate_authorization_request(self, request): + """Check the authorization request for normal and fatal errors. + + A normal error could be a missing response_type parameter or the client + attempting to access scope it is not allowed to ask authorization for. + Normal errors can safely be included in the redirection URI and + sent back to the client. + + Fatal errors occur when the client_id or redirect_uri is invalid or + missing. These must be caught by the provider and handled, how this + is done is outside of the scope of OAuthLib but showing an error + page describing the issue is a good idea. + """ + + # First check for fatal errors + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + + # REQUIRED. The client identifier as described in Section 2.2. + # http://tools.ietf.org/html/rfc6749#section-2.2 + if not request.client_id: + raise errors.MissingClientIdError(state=request.state, request=request) + + if not self.request_validator.validate_client_id(request.client_id, request): + raise errors.InvalidClientIdError(state=request.state, request=request) + + # OPTIONAL. As described in Section 3.1.2. + # http://tools.ietf.org/html/rfc6749#section-3.1.2 + log.debug('Validating redirection uri %s for client %s.', + request.redirect_uri, request.client_id) + if request.redirect_uri is not None: + request.using_default_redirect_uri = False + log.debug('Using provided redirect_uri %s', request.redirect_uri) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(state=request.state, request=request) + + if not self.request_validator.validate_redirect_uri( + request.client_id, request.redirect_uri, request): + raise errors.MismatchingRedirectURIError(state=request.state, request=request) + else: + request.redirect_uri = self.request_validator.get_default_redirect_uri( + request.client_id, request) + request.using_default_redirect_uri = True + log.debug('Using default redirect_uri %s.', request.redirect_uri) + if not request.redirect_uri: + raise errors.MissingRedirectURIError(state=request.state, request=request) + + # Then check for normal errors. + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the query component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B. + # http://tools.ietf.org/html/rfc6749#appendix-B + + # Note that the correct parameters to be added are automatically + # populated through the use of specific exceptions. + if request.response_type is None: + raise errors.InvalidRequestError(state=request.state, + description='Missing response_type parameter.', request=request) + + for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'): + if param in request.duplicate_params: + raise errors.InvalidRequestError(state=request.state, + description='Duplicate %s parameter.' % param, request=request) + + if not self.request_validator.validate_response_type(request.client_id, + request.response_type, request.client, request): + log.debug('Client %s is not authorized to use response_type %s.', + request.client_id, request.response_type) + raise errors.UnauthorizedClientError(request=request) + + # REQUIRED. Value MUST be set to "code". + if request.response_type != 'code': + raise errors.UnsupportedResponseTypeError(state=request.state, request=request) + + # OPTIONAL. The scope of the access request as described by Section 3.3 + # http://tools.ietf.org/html/rfc6749#section-3.3 + self.validate_scopes(request) + + return request.scopes, { + 'client_id': request.client_id, + 'redirect_uri': request.redirect_uri, + 'response_type': request.response_type, + 'state': request.state, + } + + def validate_token_request(self, request): + # REQUIRED. Value MUST be set to "authorization_code". + if request.grant_type != 'authorization_code': + raise errors.UnsupportedGrantTypeError(request=request) + + if request.code is None: + raise errors.InvalidRequestError( + description='Missing code parameter.', request=request) + + for param in ('client_id', 'grant_type', 'redirect_uri'): + if param in request.duplicate_params: + raise errors.InvalidRequestError(state=request.state, + description='Duplicate %s parameter.' % param, + request=request) + + # If the client type is confidential or the client was issued client + # credentials (or assigned other authentication requirements), the + # client MUST authenticate with the authorization server as described + # in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + if not self.request_validator.authenticate_client(request): + # REQUIRED, if the client is not authenticating with the + # authorization server as described in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + if not self.request_validator.authenticate_client_id( + request.client_id, request): + log.debug('Client authentication failed, %r.', request) + raise errors.InvalidClientError(request=request) + else: + if not hasattr(request.client, 'client_id'): + raise NotImplementedError('Authenticate client must set the ' + 'request.client.client_id attribute ' + 'in authenticate_client.') + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + # REQUIRED. The authorization code received from the + # authorization server. + if not self.request_validator.validate_code(request.client_id, + request.code, request.client, request): + log.debug('Client, %r (%r), is not allowed access to scopes %r.', + request.client_id, request.client, request.scopes) + raise errors.InvalidGrantError(request=request) + + for attr in ('user', 'state', 'scopes'): + if getattr(request, attr) is None: + log.debug('request.%s was not set on code validation.', attr) + + # REQUIRED, if the "redirect_uri" parameter was included in the + # authorization request as described in Section 4.1.1, and their + # values MUST be identical. + if not self.request_validator.confirm_redirect_uri(request.client_id, + request.code, request.redirect_uri, request.client): + log.debug('Redirect_uri (%r) invalid for client %r (%r).', + request.redirect_uri, request.client_id, request.client) + raise errors.AccessDeniedError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py new file mode 100644 index 0000000..a3dd87c --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import + +from oauthlib.common import log +from oauthlib.oauth2.rfc6749 import errors, utils + + +class GrantTypeBase(object): + error_uri = None + request_validator = None + + def create_authorization_response(self, request, token_handler): + raise NotImplementedError('Subclasses must implement this method.') + + def create_token_response(self, request, token_handler): + raise NotImplementedError('Subclasses must implement this method.') + + def validate_grant_type(self, request): + if not self.request_validator.validate_grant_type(request.client_id, + request.grant_type, request.client, request): + log.debug('Unauthorized from %r (%r) access to grant type %s.', + request.client_id, request.client, request.grant_type) + raise errors.UnauthorizedClientError(request=request) + + def validate_scopes(self, request): + if not request.scopes: + request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( + self.request_validator.get_default_scopes(request.client_id, request)) + log.debug('Validating access to scopes %r for client %r (%r).', + request.scopes, request.client_id, request.client) + if not self.request_validator.validate_scopes(request.client_id, + request.scopes, request.client, request): + raise errors.InvalidScopeError(state=request.state, request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py new file mode 100644 index 0000000..146eac8 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import +import json +from oauthlib.common import log + +from .base import GrantTypeBase +from .. import errors +from ..request_validator import RequestValidator + + +class ClientCredentialsGrant(GrantTypeBase): + """`Client Credentials Grant`_ + + The client can request an access token using only its client + credentials (or other supported means of authentication) when the + client is requesting access to the protected resources under its + control, or those of another resource owner that have been previously + arranged with the authorization server (the method of which is beyond + the scope of this specification). + + The client credentials grant type MUST only be used by confidential + clients:: + + +---------+ +---------------+ + : : : : + : :>-- A - Client Authentication --->: Authorization : + : Client : : Server : + : :<-- B ---- Access Token ---------<: : + : : : : + +---------+ +---------------+ + + Figure 6: Client Credentials Flow + + The flow illustrated in Figure 6 includes the following steps: + + (A) The client authenticates with the authorization server and + requests an access token from the token endpoint. + + (B) The authorization server authenticates the client, and if valid, + issues an access token. + + .. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4 + """ + + def __init__(self, request_validator=None): + self.request_validator = request_validator or RequestValidator() + + def create_token_response(self, request, token_handler): + """Return token or error in JSON format. + + If the access token request is valid and authorized, the + authorization server issues an access token as described in + `Section 5.1`_. A refresh token SHOULD NOT be included. If the request + failed client authentication or is invalid, the authorization server + returns an error response as described in `Section 5.2`_. + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + """ + try: + log.debug('Validating access token request, %r.', request) + self.validate_token_request(request) + except errors.OAuth2Error as e: + log.debug('Client error in token request. %s.', e) + return None, {}, e.json, e.status_code + + token = token_handler.create_token(request, refresh_token=False) + log.debug('Issuing token to client id %r (%r), %r.', + request.client_id, request.client, token) + return None, {}, json.dumps(token), 200 + + def validate_token_request(self, request): + if not getattr(request, 'grant_type'): + raise errors.InvalidRequestError('Request is missing grant type.', + request=request) + + if not request.grant_type == 'client_credentials': + raise errors.UnsupportedGrantTypeError(request=request) + + for param in ('grant_type', 'scope'): + if param in request.duplicate_params: + raise errors.InvalidRequestError(state=request.state, + description='Duplicate %s parameter.' % param, + request=request) + + log.debug('Authenticating client, %r.', request) + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise errors.InvalidClientError(request=request) + else: + if not hasattr(request.client, 'client_id'): + raise NotImplementedError('Authenticate client must set the ' + 'request.client.client_id attribute ' + 'in authenticate_client.') + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + log.debug('Authorizing access to user %r.', request.user) + request.client_id = request.client_id or request.client.client_id + self.validate_scopes(request) + + +class RefreshTokenGrant(GrantTypeBase): + """`Refresh token grant`_ + + .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6 + """ + + @property + def issue_new_refresh_tokens(self): + return True + + def __init__(self, request_validator=None, issue_new_refresh_tokens=True): + self.request_validator = request_validator or RequestValidator() + + def create_token_response(self, request, token_handler): + """Create a new access token from a refresh_token. + + If valid and authorized, the authorization server issues an access + token as described in `Section 5.1`_. If the request failed + verification or is invalid, the authorization server returns an error + response as described in `Section 5.2`_. + + The authorization server MAY issue a new refresh token, in which case + the client MUST discard the old refresh token and replace it with the + new refresh token. The authorization server MAY revoke the old + refresh token after issuing a new refresh token to the client. If a + new refresh token is issued, the refresh token scope MUST be + identical to that of the refresh token included by the client in the + request. + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + try: + log.debug('Validating refresh token request, %r.', request) + self.validate_token_request(request) + except errors.OAuth2Error as e: + return None, headers, e.json, 400 + + token = token_handler.create_token(request, + refresh_token=self.issue_new_refresh_tokens) + log.debug('Issuing new token to client id %r (%r), %r.', + request.client_id, request.client, token) + return None, headers, json.dumps(token), 200 + + def validate_token_request(self, request): + # REQUIRED. Value MUST be set to "refresh_token". + if request.grant_type != 'refresh_token': + raise errors.UnsupportedGrantTypeError(request=request) + + if request.refresh_token is None: + raise errors.InvalidRequestError( + description='Missing refresh token parameter.', + request=request) + + # Because refresh tokens are typically long-lasting credentials used to + # request additional access tokens, the refresh token is bound to the + # client to which it was issued. If the client type is confidential or + # the client was issued client credentials (or assigned other + # authentication requirements), the client MUST authenticate with the + # authorization server as described in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + log.debug('Authenticating client, %r.', request) + if not self.request_validator.authenticate_client(request): + log.debug('Invalid client (%r), denying access.', request) + raise errors.AccessDeniedError(request=request) + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + # OPTIONAL. The scope of the access request as described by + # Section 3.3. The requested scope MUST NOT include any scope + # not originally granted by the resource owner, and if omitted is + # treated as equal to the scope originally granted by the + # resource owner. + if request.scopes: + log.debug('Ensuring refresh token %s has access to scopes %r.', + request.refresh_token, request.scopes) + else: + log.debug('Reusing scopes from previous access token.') + if not self.request_validator.confirm_scopes(request.refresh_token, + request.scopes, request): + log.debug('Refresh token %s lack requested scopes, %r.', + request.refresh_token, request.scopes) + raise errors.InvalidScopeError(state=request.state, request=request) + + # REQUIRED. The refresh token issued to the client. + log.debug('Validating refresh token %s for client %r.', + request.refresh_token, request.client) + if not self.request_validator.validate_refresh_token( + request.refresh_token, request.client, request): + log.debug('Invalid refresh token, %s, for client %r.', + request.refresh_token, request.client) + raise errors.InvalidRequestError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py new file mode 100644 index 0000000..7e2cc54 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import +from oauthlib import common +from oauthlib.common import log +from oauthlib.uri_validate import is_absolute_uri + +from .base import GrantTypeBase +from .. import errors +from ..request_validator import RequestValidator + + +class ImplicitGrant(GrantTypeBase): + """`Implicit Grant`_ + + The implicit grant type is used to obtain access tokens (it does not + support the issuance of refresh tokens) and is optimized for public + clients known to operate a particular redirection URI. These clients + are typically implemented in a browser using a scripting language + such as JavaScript. + + Unlike the authorization code grant type, in which the client makes + separate requests for authorization and for an access token, the + client receives the access token as the result of the authorization + request. + + The implicit grant type does not include client authentication, and + relies on the presence of the resource owner and the registration of + the redirection URI. Because the access token is encoded into the + redirection URI, it may be exposed to the resource owner and other + applications residing on the same device:: + + +----------+ + | Resource | + | Owner | + | | + +----------+ + ^ + | + (B) + +----|-----+ Client Identifier +---------------+ + | -+----(A)-- & Redirection URI --->| | + | User- | | Authorization | + | Agent -|----(B)-- User authenticates -->| Server | + | | | | + | |<---(C)--- Redirection URI ----<| | + | | with Access Token +---------------+ + | | in Fragment + | | +---------------+ + | |----(D)--- Redirection URI ---->| Web-Hosted | + | | without Fragment | Client | + | | | Resource | + | (F) |<---(E)------- Script ---------<| | + | | +---------------+ + +-|--------+ + | | + (A) (G) Access Token + | | + ^ v + +---------+ + | | + | Client | + | | + +---------+ + + Note: The lines illustrating steps (A) and (B) are broken into two + parts as they pass through the user-agent. + + Figure 4: Implicit Grant Flow + + The flow illustrated in Figure 4 includes the following steps: + + (A) The client initiates the flow by directing the resource owner's + user-agent to the authorization endpoint. The client includes + its client identifier, requested scope, local state, and a + redirection URI to which the authorization server will send the + user-agent back once access is granted (or denied). + + (B) The authorization server authenticates the resource owner (via + the user-agent) and establishes whether the resource owner + grants or denies the client's access request. + + (C) Assuming the resource owner grants access, the authorization + server redirects the user-agent back to the client using the + redirection URI provided earlier. The redirection URI includes + the access token in the URI fragment. + + (D) The user-agent follows the redirection instructions by making a + request to the web-hosted client resource (which does not + include the fragment per [RFC2616]). The user-agent retains the + fragment information locally. + + (E) The web-hosted client resource returns a web page (typically an + HTML document with an embedded script) capable of accessing the + full redirection URI including the fragment retained by the + user-agent, and extracting the access token (and other + parameters) contained in the fragment. + + (F) The user-agent executes the script provided by the web-hosted + client resource locally, which extracts the access token. + + (G) The user-agent passes the access token to the client. + + See `Section 10.3`_ and `Section 10.16`_ for important security considerations + when using the implicit grant. + + .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2 + .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3 + .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16 + """ + + def __init__(self, request_validator=None): + self.request_validator = request_validator or RequestValidator() + + def create_authorization_response(self, request, token_handler): + """Create an authorization response. + The client constructs the request URI by adding the following + parameters to the query component of the authorization endpoint URI + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + response_type + REQUIRED. Value MUST be set to "token". + + client_id + REQUIRED. The client identifier as described in `Section 2.2`_. + + redirect_uri + OPTIONAL. As described in `Section 3.1.2`_. + + scope + OPTIONAL. The scope of the access request as described by + `Section 3.3`_. + + state + RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + The authorization server validates the request to ensure that all + required parameters are present and valid. The authorization server + MUST verify that the redirection URI to which it will redirect the + access token matches a redirection URI registered by the client as + described in `Section 3.1.2`_. + + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + """ + return self.create_token_response(request, token_handler) + + def create_token_response(self, request, token_handler): + """Return token or error embedded in the URI fragment. + + If the resource owner grants the access request, the authorization + server issues an access token and delivers it to the client by adding + the following parameters to the fragment component of the redirection + URI using the "application/x-www-form-urlencoded" format, per + `Appendix B`_: + + access_token + REQUIRED. The access token issued by the authorization server. + + token_type + REQUIRED. The type of the token issued as described in + `Section 7.1`_. Value is case insensitive. + + expires_in + RECOMMENDED. The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + scope + OPTIONAL, if identical to the scope requested by the client; + otherwise, REQUIRED. The scope of the access token as + described by `Section 3.3`_. + + state + REQUIRED if the "state" parameter was present in the client + authorization request. The exact value received from the + client. + + The authorization server MUST NOT issue a refresh token. + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + """ + try: + # request.scopes is only mandated in post auth and both pre and + # post auth use validate_authorization_request + if not request.scopes: + raise ValueError('Scopes must be set on post auth.') + + self.validate_token_request(request) + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + except errors.FatalClientError as e: + log.debug('Fatal client error during validation of %r. %r.', + request, e) + raise + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the fragment component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B: + # http://tools.ietf.org/html/rfc6749#appendix-B + except errors.OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + return common.add_params_to_uri(request.redirect_uri, e.twotuples, + fragment=True), {}, None, e.status_code + + token = token_handler.create_token(request, refresh_token=False) + return common.add_params_to_uri(request.redirect_uri, token.items(), + fragment=True), {}, None, 302 + + def validate_authorization_request(self, request): + return self.validate_token_request(request) + + def validate_token_request(self, request): + """Check the token request for normal and fatal errors. + + This method is very similar to validate_authorization_request in + the AuthorizationCodeGrant but differ in a few subtle areas. + + A normal error could be a missing response_type parameter or the client + attempting to access scope it is not allowed to ask authorization for. + Normal errors can safely be included in the redirection URI and + sent back to the client. + + Fatal errors occur when the client_id or redirect_uri is invalid or + missing. These must be caught by the provider and handled, how this + is done is outside of the scope of OAuthLib but showing an error + page describing the issue is a good idea. + """ + + # First check for fatal errors + + # If the request fails due to a missing, invalid, or mismatching + # redirection URI, or if the client identifier is missing or invalid, + # the authorization server SHOULD inform the resource owner of the + # error and MUST NOT automatically redirect the user-agent to the + # invalid redirection URI. + + # REQUIRED. The client identifier as described in Section 2.2. + # http://tools.ietf.org/html/rfc6749#section-2.2 + if not request.client_id: + raise errors.MissingClientIdError(state=request.state, request=request) + + if not self.request_validator.validate_client_id(request.client_id, request): + raise errors.InvalidClientIdError(state=request.state, request=request) + + # OPTIONAL. As described in Section 3.1.2. + # http://tools.ietf.org/html/rfc6749#section-3.1.2 + if request.redirect_uri is not None: + request.using_default_redirect_uri = False + log.debug('Using provided redirect_uri %s', request.redirect_uri) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(state=request.state, request=request) + + # The authorization server MUST verify that the redirection URI + # to which it will redirect the access token matches a + # redirection URI registered by the client as described in + # Section 3.1.2. + # http://tools.ietf.org/html/rfc6749#section-3.1.2 + if not self.request_validator.validate_redirect_uri( + request.client_id, request.redirect_uri, request): + raise errors.MismatchingRedirectURIError(state=request.state, request=request) + else: + request.redirect_uri = self.request_validator.get_default_redirect_uri( + request.client_id, request) + request.using_default_redirect_uri = True + log.debug('Using default redirect_uri %s.', request.redirect_uri) + if not request.redirect_uri: + raise errors.MissingRedirectURIError(state=request.state, request=request) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(state=request.state, request=request) + + # Then check for normal errors. + + # If the resource owner denies the access request or if the request + # fails for reasons other than a missing or invalid redirection URI, + # the authorization server informs the client by adding the following + # parameters to the fragment component of the redirection URI using the + # "application/x-www-form-urlencoded" format, per Appendix B. + # http://tools.ietf.org/html/rfc6749#appendix-B + + # Note that the correct parameters to be added are automatically + # populated through the use of specific exceptions. + if request.response_type is None: + raise errors.InvalidRequestError(state=request.state, + description='Missing response_type parameter.', + request=request) + + for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'): + if param in request.duplicate_params: + raise errors.InvalidRequestError(state=request.state, + description='Duplicate %s parameter.' % param, request=request) + + # REQUIRED. Value MUST be set to "token". + if request.response_type != 'token': + raise errors.UnsupportedResponseTypeError(state=request.state, request=request) + + log.debug('Validating use of response_type token for client %r (%r).', + request.client_id, request.client) + if not self.request_validator.validate_response_type(request.client_id, + request.response_type, request.client, request): + log.debug('Client %s is not authorized to use response_type %s.', + request.client_id, request.response_type) + raise errors.UnauthorizedClientError(request=request) + + # OPTIONAL. The scope of the access request as described by Section 3.3 + # http://tools.ietf.org/html/rfc6749#section-3.3 + self.validate_scopes(request) + + return request.scopes, { + 'client_id': request.client_id, + 'redirect_uri': request.redirect_uri, + 'response_type': request.response_type, + 'state': request.state, + } diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py new file mode 100644 index 0000000..b63013b --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import +import json + +from oauthlib.common import log + +from .base import GrantTypeBase +from .. import errors +from ..request_validator import RequestValidator + + +class RefreshTokenGrant(GrantTypeBase): + """`Refresh token grant`_ + + .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6 + """ + + @property + def issue_new_refresh_tokens(self): + return True + + def __init__(self, request_validator=None, issue_new_refresh_tokens=True): + self.request_validator = request_validator or RequestValidator() + + def create_token_response(self, request, token_handler): + """Create a new access token from a refresh_token. + + If valid and authorized, the authorization server issues an access + token as described in `Section 5.1`_. If the request failed + verification or is invalid, the authorization server returns an error + response as described in `Section 5.2`_. + + The authorization server MAY issue a new refresh token, in which case + the client MUST discard the old refresh token and replace it with the + new refresh token. The authorization server MAY revoke the old + refresh token after issuing a new refresh token to the client. If a + new refresh token is issued, the refresh token scope MUST be + identical to that of the refresh token included by the client in the + request. + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + try: + log.debug('Validating refresh token request, %r.', request) + self.validate_token_request(request) + except errors.OAuth2Error as e: + return None, headers, e.json, 400 + + token = token_handler.create_token(request, + refresh_token=self.issue_new_refresh_tokens) + log.debug('Issuing new token to client id %r (%r), %r.', + request.client_id, request.client, token) + return None, headers, json.dumps(token), 200 + + def validate_token_request(self, request): + # REQUIRED. Value MUST be set to "refresh_token". + if request.grant_type != 'refresh_token': + raise errors.UnsupportedGrantTypeError(request=request) + + if request.refresh_token is None: + raise errors.InvalidRequestError( + description='Missing refresh token parameter.', + request=request) + + # Because refresh tokens are typically long-lasting credentials used to + # request additional access tokens, the refresh token is bound to the + # client to which it was issued. If the client type is confidential or + # the client was issued client credentials (or assigned other + # authentication requirements), the client MUST authenticate with the + # authorization server as described in Section 3.2.1. + # http://tools.ietf.org/html/rfc6749#section-3.2.1 + log.debug('Authenticating client, %r.', request) + if not self.request_validator.authenticate_client(request): + log.debug('Invalid client (%r), denying access.', request) + raise errors.AccessDeniedError(request=request) + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + # OPTIONAL. The scope of the access request as described by + # Section 3.3. The requested scope MUST NOT include any scope + # not originally granted by the resource owner, and if omitted is + # treated as equal to the scope originally granted by the + # resource owner. + if request.scopes: + log.debug('Ensuring refresh token %s has access to scopes %r.', + request.refresh_token, request.scopes) + else: + log.debug('Reusing scopes from previous access token.') + if not self.request_validator.confirm_scopes(request.refresh_token, + request.scopes, request): + log.debug('Refresh token %s lack requested scopes, %r.', + request.refresh_token, request.scopes) + raise errors.InvalidScopeError(state=request.state, request=request) + + # REQUIRED. The refresh token issued to the client. + log.debug('Validating refresh token %s for client %r.', + request.refresh_token, request.client) + if not self.request_validator.validate_refresh_token( + request.refresh_token, request.client, request): + log.debug('Invalid refresh token, %s, for client %r.', + request.refresh_token, request.client) + raise errors.InvalidRequestError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py new file mode 100644 index 0000000..5b991c6 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import +import json +from oauthlib.common import log + +from .base import GrantTypeBase +from .. import errors +from ..request_validator import RequestValidator + + +class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): + """`Resource Owner Password Credentials Grant`_ + + The resource owner password credentials grant type is suitable in + cases where the resource owner has a trust relationship with the + client, such as the device operating system or a highly privileged + application. The authorization server should take special care when + enabling this grant type and only allow it when other flows are not + viable. + + This grant type is suitable for clients capable of obtaining the + resource owner's credentials (username and password, typically using + an interactive form). It is also used to migrate existing clients + using direct authentication schemes such as HTTP Basic or Digest + authentication to OAuth by converting the stored credentials to an + access token:: + + +----------+ + | Resource | + | Owner | + | | + +----------+ + v + | Resource Owner + (A) Password Credentials + | + v + +---------+ +---------------+ + | |>--(B)---- Resource Owner ------->| | + | | Password Credentials | Authorization | + | Client | | Server | + | |<--(C)---- Access Token ---------<| | + | | (w/ Optional Refresh Token) | | + +---------+ +---------------+ + + Figure 5: Resource Owner Password Credentials Flow + + The flow illustrated in Figure 5 includes the following steps: + + (A) The resource owner provides the client with its username and + password. + + (B) The client requests an access token from the authorization + server's token endpoint by including the credentials received + from the resource owner. When making the request, the client + authenticates with the authorization server. + + (C) The authorization server authenticates the client and validates + the resource owner credentials, and if valid, issues an access + token. + + .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3 + """ + + def __init__(self, request_validator=None): + self.request_validator = request_validator or RequestValidator() + + def create_token_response(self, request, token_handler, + require_authentication=True): + """Return token or error in json format. + + If the access token request is valid and authorized, the + authorization server issues an access token and optional refresh + token as described in `Section 5.1`_. If the request failed client + authentication or is invalid, the authorization server returns an + error response as described in `Section 5.2`_. + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + try: + if require_authentication: + log.debug('Authenticating client, %r.', request) + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise errors.InvalidClientError(request=request) + else: + if not hasattr(request.client, 'client_id'): + raise NotImplementedError( + 'Authenticate client must set the ' + 'request.client.client_id attribute ' + 'in authenticate_client.') + else: + log.debug('Client authentication disabled, %r.', request) + log.debug('Validating access token request, %r.', request) + self.validate_token_request(request) + except errors.OAuth2Error as e: + log.debug('Client error in token request, %s.', e) + return None, headers, e.json, e.status_code + + token = token_handler.create_token(request, refresh_token=True) + log.debug('Issuing token %r to client id %r (%r) and username %s.', + token, request.client_id, request.client, request.username) + return None, headers, json.dumps(token), 200 + + def validate_token_request(self, request): + """ + The client makes a request to the token endpoint by adding the + following parameters using the "application/x-www-form-urlencoded" + format per Appendix B with a character encoding of UTF-8 in the HTTP + request entity-body: + + grant_type + REQUIRED. Value MUST be set to "password". + + username + REQUIRED. The resource owner username. + + password + REQUIRED. The resource owner password. + + scope + OPTIONAL. The scope of the access request as described by + `Section 3.3`_. + + If the client type is confidential or the client was issued client + credentials (or assigned other authentication requirements), the + client MUST authenticate with the authorization server as described + in `Section 3.2.1`_. + + The authorization server MUST: + + o require client authentication for confidential clients or for any + client that was issued client credentials (or with other + authentication requirements), + + o authenticate the client if client authentication is included, and + + o validate the resource owner password credentials using its + existing password validation algorithm. + + Since this access token request utilizes the resource owner's + password, the authorization server MUST protect the endpoint against + brute force attacks (e.g., using rate-limitation or generating + alerts). + + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ + for param in ('grant_type', 'username', 'password'): + if not getattr(request, param): + raise errors.InvalidRequestError( + 'Request is missing %s parameter.' % param, request=request) + + for param in ('grant_type', 'username', 'password', 'scope'): + if param in request.duplicate_params: + raise errors.InvalidRequestError(state=request.state, + description='Duplicate %s parameter.' % param, request=request) + + # This error should rarely (if ever) occur if requests are routed to + # grant type handlers based on the grant_type parameter. + if not request.grant_type == 'password': + raise errors.UnsupportedGrantTypeError(request=request) + + log.debug('Validating username %s and password %s.', + request.username, request.password) + if not self.request_validator.validate_user(request.username, + request.password, request.client, request): + raise errors.InvalidGrantError('Invalid credentials given.', request=request) + else: + if not hasattr(request.client, 'client_id'): + raise NotImplementedError( + 'Validate user must set the ' + 'request.client.client_id attribute ' + 'in authenticate_client.') + log.debug('Authorizing access to user %r.', request.user) + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + if request.client: + request.client_id = request.client_id or request.client.client_id + self.validate_scopes(request) diff --git a/oauthlib/oauth2/rfc6749/grant_types.py b/oauthlib/oauth2/rfc6749/request_validator.py index 25edcc8..25edcc8 100644 --- a/oauthlib/oauth2/rfc6749/grant_types.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py |