From 29c8e0142111e03237eb8f92a9470ba90d295e10 Mon Sep 17 00:00:00 2001 From: Benjamin Mako Hill Date: Thu, 22 Oct 2015 19:44:08 -0700 Subject: [PATCH] added updated version of oauthlib --- oauthlib/__init__.py | 2 +- oauthlib/common.py | 41 ++++++++++++--- oauthlib/oauth1/rfc5849/__init__.py | 17 +++++-- .../oauth1/rfc5849/endpoints/access_token.py | 7 +++ oauthlib/oauth1/rfc5849/endpoints/base.py | 2 +- .../oauth1/rfc5849/endpoints/request_token.py | 7 +++ oauthlib/oauth1/rfc5849/endpoints/resource.py | 7 +++ .../rfc5849/endpoints/signature_only.py | 5 ++ oauthlib/oauth1/rfc5849/request_validator.py | 2 +- oauthlib/oauth1/rfc5849/signature.py | 2 + oauthlib/oauth2/rfc6749/clients/base.py | 4 +- .../oauth2/rfc6749/endpoints/revocation.py | 7 +-- oauthlib/oauth2/rfc6749/errors.py | 34 +++++++++---- .../rfc6749/grant_types/authorization_code.py | 32 +++++++----- oauthlib/oauth2/rfc6749/grant_types/base.py | 3 +- .../rfc6749/grant_types/client_credentials.py | 2 +- .../oauth2/rfc6749/grant_types/implicit.py | 29 +++++++---- .../resource_owner_password_credentials.py | 2 +- oauthlib/oauth2/rfc6749/request_validator.py | 51 ++++++++++--------- oauthlib/oauth2/rfc6749/utils.py | 10 ++-- 20 files changed, 178 insertions(+), 88 deletions(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 300bdc7..50afc05 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'Idan Gazit ' -__version__ = '0.7.2' +__version__ = '1.0.3' import logging diff --git a/oauthlib/common.py b/oauthlib/common.py index 0179b8e..ed2b699 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -36,6 +36,8 @@ UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz' CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN' 'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}') +PASSWORD_PATTERN = re.compile(r'password=[^&]+') +INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]') always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' @@ -107,7 +109,7 @@ def decode_params_utf8(params): return decoded -urlencoded = set(always_safe) | set('=&;%+~,*@') +urlencoded = set(always_safe) | set('=&;%+~,*@!') def urldecode(query): @@ -132,8 +134,7 @@ def urldecode(query): # All encoded values begin with % followed by two hex characters # correct = %00, %A0, %0A, %FF # invalid = %G0, %5H, %PO - invalid_hex = '%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]' - if len(re.findall(invalid_hex, query)): + if INVALID_HEX_PATTERN.search(query): raise ValueError('Invalid hex encoding in query string.') # We encode to utf-8 prior to parsing because parse_qsl behaves @@ -378,20 +379,44 @@ class Request(object): self.http_method = encode(http_method) self.headers = CaseInsensitiveDict(encode(headers or {})) self.body = encode(body) - self.decoded_body = extract_params(encode(body)) + self.decoded_body = extract_params(self.body) self.oauth_params = [] - - self._params = {} + self.validator_log = {} + + self._params = { + "access_token": None, + "client": None, + "client_id": None, + "client_secret": None, + "code": None, + "extra_credentials": None, + "grant_type": None, + "redirect_uri": None, + "refresh_token": None, + "response_type": None, + "scope": None, + "scopes": None, + "state": None, + "token": None, + "user": None, + "token_type_hint": None, + } self._params.update(dict(urldecode(self.uri_query))) self._params.update(dict(self.decoded_body or [])) self._params.update(self.headers) def __getattr__(self, name): - return self._params.get(name, None) + if name in self._params: + return self._params[name] + else: + raise AttributeError(name) def __repr__(self): + body = self.body + if body and 'password=' in body: + body = PASSWORD_PATTERN.sub('password=***', body) return '' % ( - self.uri, self.http_method, self.headers, self.body) + self.uri, self.http_method, self.headers, body) @property def uri_query(self): diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index ad9713c..56b8c6f 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -7,7 +7,8 @@ This module is an implementation of various logic needed for signing and checking OAuth 1.0 RFC 5849 requests. """ from __future__ import absolute_import, unicode_literals - +import base64 +import hashlib import logging log = logging.getLogger(__name__) @@ -101,10 +102,6 @@ class Client(object): self.nonce = encode(nonce) self.timestamp = encode(timestamp) - if self.signature_method == SIGNATURE_RSA and self.rsa_key is None: - raise ValueError( - 'rsa_key is required when using RSA signature method.') - def __repr__(self): attrs = vars(self).copy() attrs['client_secret'] = '****' if attrs['client_secret'] else None @@ -176,6 +173,16 @@ class Client(object): if self.verifier: params.append(('oauth_verifier', self.verifier)) + # providing body hash for requests other than x-www-form-urlencoded + # as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html + # 4.1.1. When to include the body hash + # * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies + # * [...] SHOULD include the oauth_body_hash parameter on all other requests. + content_type = request.headers.get('Content-Type', None) + content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0 + if request.body is not None and content_type_eligible: + params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) + return params def _render(self, request, formencode=False, realm=None): diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py index 26db919..567965a 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/access_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/access_token.py @@ -192,6 +192,13 @@ class AccessTokenEndpoint(BaseEndpoint): valid_signature = self._check_signature(request, is_token_request=True) + # log the results to the validator_log + # this lets us handle internal reporting and analysis + request.validator_log['client'] = valid_client + request.validator_log['resource_owner'] = valid_resource_owner + request.validator_log['verifier'] = valid_verifier + request.validator_log['signature'] = valid_signature + # We delay checking validity until the very end, using dummy values for # calculations and fetching secrets/keys to ensure the flow of every # request remains almost identical regardless of whether valid values diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index 42006a1..42d9363 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -84,7 +84,7 @@ class BaseEndpoint(object): # receiving a request with duplicated protocol parameters. if len(dict(oauth_params)) != len(oauth_params): raise errors.InvalidRequestError( - description='Duplicate OAuth2 entries.') + description='Duplicate OAuth1 entries.') oauth_params = dict(oauth_params) request.signature = oauth_params.get('oauth_signature') diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py index e97c34b..4a76abc 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py @@ -187,6 +187,13 @@ class RequestTokenEndpoint(BaseEndpoint): valid_signature = self._check_signature(request) + # log the results to the validator_log + # this lets us handle internal reporting and analysis + request.validator_log['client'] = valid_client + request.validator_log['realm'] = valid_realm + request.validator_log['callback'] = valid_redirect + request.validator_log['signature'] = valid_signature + # We delay checking validity until the very end, using dummy values for # calculations and fetching secrets/keys to ensure the flow of every # request remains almost identical regardless of whether valid values diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py index 651a87c..6021627 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/resource.py +++ b/oauthlib/oauth1/rfc5849/endpoints/resource.py @@ -142,6 +142,13 @@ class ResourceEndpoint(BaseEndpoint): valid_signature = self._check_signature(request) + # log the results to the validator_log + # this lets us handle internal reporting and analysis + request.validator_log['client'] = valid_client + request.validator_log['resource_owner'] = valid_resource_owner + request.validator_log['realm'] = valid_realm + request.validator_log['signature'] = valid_signature + # We delay checking validity until the very end, using dummy values for # calculations and fetching secrets/keys to ensure the flow of every # request remains almost identical regardless of whether valid values diff --git a/oauthlib/oauth1/rfc5849/endpoints/signature_only.py b/oauthlib/oauth1/rfc5849/endpoints/signature_only.py index 2f8e7c9..d609b7c 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/signature_only.py +++ b/oauthlib/oauth1/rfc5849/endpoints/signature_only.py @@ -61,6 +61,11 @@ class SignatureOnlyEndpoint(BaseEndpoint): valid_signature = self._check_signature(request) + # log the results to the validator_log + # this lets us handle internal reporting and analysis + request.validator_log['client'] = valid_client + request.validator_log['signature'] = valid_signature + # We delay checking validity until the very end, using dummy values for # calculations and fetching secrets/keys to ensure the flow of every # request remains almost identical regardless of whether valid values diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py index e722029..30a3694 100644 --- a/oauthlib/oauth1/rfc5849/request_validator.py +++ b/oauthlib/oauth1/rfc5849/request_validator.py @@ -428,7 +428,7 @@ class RequestValidator(object): :param client_key: The client/consumer key. :param request_token: The request token string. :param request: An oauthlib.common.Request object. - :returns: The rsa public key as a string. + :returns: None Per `Section 2.3`__ of the spec: diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index f57d80a..8fa22ba 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -500,6 +500,8 @@ def sign_rsa_sha1(base_string, rsa_private_key): def sign_rsa_sha1_with_client(base_string, client): + if not client.rsa_key: + raise ValueError('rsa_key is required when using RSA signature method.') return sign_rsa_sha1(base_string, client.rsa_key) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index e53ccc1..4af9e76 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -229,7 +229,7 @@ class Client(object): self.redirect_url = redirect_url or self.redirect_url self.scope = scope or self.scope auth_url = self.prepare_request_uri( - authorization_url, redirect_uri=self.redirect_uri, + authorization_url, redirect_uri=self.redirect_url, scope=self.scope, state=self.state, **kwargs) return auth_url, FORM_ENC_HEADERS, '' @@ -297,7 +297,7 @@ class Client(object): raise InsecureTransportError() self.scope = scope or self.scope - body = self._client.prepare_refresh_body(body=body, + body = self.prepare_refresh_body(body=body, refresh_token=refresh_token, scope=self.scope, **kwargs) return token_url, FORM_ENC_HEADERS, body diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index b73131c..662c793 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -74,7 +74,7 @@ class RevocationEndpoint(BaseEndpoint): self.request_validator.revoke_token(request.token, request.token_type_hint, request) - response_body = None + response_body = '' if self.enable_jsonp and request.callback: response_body = request.callback + '();' return {}, response_body, 200 @@ -120,8 +120,9 @@ class RevocationEndpoint(BaseEndpoint): raise InvalidRequestError(request=request, description='Missing token parameter.') - if not self.request_validator.authenticate_client(request): - raise InvalidClientError(request=request) + if self.request_validator.client_authentication_required(request): + if not self.request_validator.authenticate_client(request): + raise InvalidClientError(request=request) if (request.token_type_hint and request.token_type_hint in self.valid_token_types and diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index a21d0bd..88f5375 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -3,7 +3,7 @@ oauthlib.oauth2.rfc6749.errors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Error used both by OAuth 2 clients and provicers to represent the spec +Error used both by OAuth 2 clients and providers to represent the spec defined error responses for all four core grant types. """ from __future__ import unicode_literals @@ -122,24 +122,32 @@ class FatalClientError(OAuth2Error): pass -class InvalidRedirectURIError(FatalClientError): - error = 'invalid_redirect_uri' +class InvalidRequestFatalError(FatalClientError): + """For fatal errors, the request is missing a required parameter, includes + an invalid parameter value, includes a parameter more than once, or is + otherwise malformed. + """ + error = 'invalid_request' + +class InvalidRedirectURIError(InvalidRequestFatalError): + description = 'Invalid redirect URI.' -class MissingRedirectURIError(FatalClientError): - error = 'missing_redirect_uri' +class MissingRedirectURIError(InvalidRequestFatalError): + description = 'Missing redirect URI.' -class MismatchingRedirectURIError(FatalClientError): - error = 'mismatching_redirect_uri' +class MismatchingRedirectURIError(InvalidRequestFatalError): + description = 'Mismatching redirect URI.' -class MissingClientIdError(FatalClientError): - error = 'invalid_client_id' +class InvalidClientIdError(InvalidRequestFatalError): + description = 'Invalid client_id parameter value.' -class InvalidClientIdError(FatalClientError): - error = 'invalid_client_id' + +class MissingClientIdError(InvalidRequestFatalError): + description = 'Missing client_id parameter.' class InvalidRequestError(OAuth2Error): @@ -151,6 +159,10 @@ class InvalidRequestError(OAuth2Error): error = 'invalid_request' +class MissingResponseTypeError(InvalidRequestError): + description = 'Missing response_type parameter.' + + class AccessDeniedError(OAuth2Error): """The resource owner or authorization server denied the request.""" diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index b6ff07c..658f5ad 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -264,6 +264,15 @@ class AuthorizationCodeGrant(GrantTypeBase): # error and MUST NOT automatically redirect the user-agent to the # invalid redirection URI. + # First check duplicate parameters + for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'): + try: + duplicate_params = request.duplicate_params + except ValueError: + raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request) + if param in duplicate_params: + raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) + # REQUIRED. The client identifier as described in Section 2.2. # http://tools.ietf.org/html/rfc6749#section-2.2 if not request.client_id: @@ -304,23 +313,22 @@ class AuthorizationCodeGrant(GrantTypeBase): # 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(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(description='Duplicate %s parameter.' % param, request=request) + # REQUIRED. + if request.response_type is None: + raise errors.MissingResponseTypeError(request=request) + # Value MUST be set to "code". + elif request.response_type != 'code': + raise errors.UnsupportedResponseTypeError(request=request) if not self.request_validator.validate_response_type(request.client_id, - request.response_type, request.client, request): + 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(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) @@ -379,8 +387,8 @@ class AuthorizationCodeGrant(GrantTypeBase): request.client_id, request.client, request.scopes) raise errors.InvalidGrantError(request=request) - for attr in ('user', 'state', 'scopes'): - if getattr(request, attr) is None: + for attr in ('user', 'scopes'): + if getattr(request, attr, None) is None: log.debug('request.%s was not set on code validation.', attr) # REQUIRED, if the "redirect_uri" parameter was included in the diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index 4a8017f..9fc632f 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -23,7 +23,8 @@ class GrantTypeBase(object): raise NotImplementedError('Subclasses must implement this method.') def validate_grant_type(self, request): - if not self.request_validator.validate_grant_type(request.client_id, + client_id = getattr(request, 'client_id', None) + if not self.request_validator.validate_grant_type(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) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index 30df247..49173cc 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -83,7 +83,7 @@ class ClientCredentialsGrant(GrantTypeBase): return headers, json.dumps(token), 200 def validate_token_request(self, request): - if not getattr(request, 'grant_type'): + if not getattr(request, 'grant_type', None): raise errors.InvalidRequestError('Request is missing grant type.', request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 27bcb24..2a92a02 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -260,6 +260,15 @@ class ImplicitGrant(GrantTypeBase): # error and MUST NOT automatically redirect the user-agent to the # invalid redirection URI. + # First check duplicate parameters + for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'): + try: + duplicate_params = request.duplicate_params + except ValueError: + raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request) + if param in duplicate_params: + raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) + # REQUIRED. The client identifier as described in Section 2.2. # http://tools.ietf.org/html/rfc6749#section-2.2 if not request.client_id: @@ -304,23 +313,21 @@ class ImplicitGrant(GrantTypeBase): # 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(description='Missing response_type parameter.', - request=request) + # populated through the use of specific exceptions - for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'): - if param in request.duplicate_params: - raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request) - - # REQUIRED. Value MUST be set to "token". - if request.response_type != 'token': + # REQUIRED. + if request.response_type is None: + raise errors.MissingResponseTypeError(request=request) + # Value MUST be set to "token". + elif request.response_type != 'token': raise errors.UnsupportedResponseTypeError(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): + 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) diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index c19e6cf..51b7374 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -160,7 +160,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): .. _`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): + if not getattr(request, param, None): raise errors.InvalidRequestError( 'Request is missing %s parameter.' % param, request=request) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index e622ff1..442b2a8 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -51,12 +51,6 @@ class RequestValidator(object): both body and query can be obtained by direct attribute access, i.e. request.client_id for client_id in the URL query. - OBS! Certain grant types rely on this authentication, possibly with - other fallbacks, and for them to recognize this authorization please - set the client attribute on the request (request.client). Note that - preferably this client object should have a client_id attribute of - unicode type (request.client.client_id). - :param request: oauthlib.common.Request :rtype: True or False @@ -90,14 +84,14 @@ class RequestValidator(object): def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): - """Ensure client is authorized to redirect to the redirect_uri requested. - - If the client specifies a redirect_uri when obtaining code then - that redirect URI must be bound to the code and verified equal - in this method. + """Ensure that the authorization process represented by this authorization + code began with this 'redirect_uri'. - All clients should register the absolute URIs of all URIs they intend - to redirect to. The registration is outside of the scope of oauthlib. + If the client specifies a redirect_uri when obtaining code then that + redirect URI must be bound to the code and verified equal in this + method, according to RFC 6749 section 4.1.3. Do not compare against + the client's allowed redirect URIs, but against the URI used when the + code was saved. :param client_id: Unicode client identifier :param code: Unicode authorization_code. @@ -214,21 +208,25 @@ class RequestValidator(object): def save_authorization_code(self, client_id, code, request, *args, **kwargs): """Persist the authorization_code. - The code should at minimum be associated with: - - a client and it's client_id + The code should at minimum be stored with: + - the client_id (client_id) - the redirect URI used (request.redirect_uri) - - whether the redirect URI used is the client default or not - a resource owner / user (request.user) - - authorized scopes (request.scopes) + - the authorized scopes (request.scopes) + - the client state, if given (code.get('state')) - The authorization code grant dict (code) holds at least the key 'code':: + The 'code' argument is actually a dictionary, containing at least a + 'code' key with the actual authorization code: {'code': 'sdf345jsdf0934f'} + It may also have a 'state' key containing a nonce for the client, if it + chose to send one. That value should be saved and used in + 'validate_code'. + :param client_id: Unicode client identifier - :param code: A dict of the authorization code grant. + :param code: A dict of the authorization code grant and, optionally, state. :param request: The HTTP Request (oauthlib.common.Request) - :rtype: The default redirect URI for the client Method is used by: - Authorization Code Grant @@ -339,11 +337,18 @@ class RequestValidator(object): raise NotImplementedError('Subclasses must implement this method.') def validate_code(self, client_id, code, client, request, *args, **kwargs): - """Ensure the authorization_code is valid and assigned to client. + """Verify that the authorization_code is valid and assigned to the given + client. + + Before returning true, set the following based on the information stored + with the code in 'save_authorization_code': + - request.user + - request.state (if given) + - request.scopes OBS! The request.user attribute should be set to the resource owner - associated with this authorization code. Similarly request.scopes and - request.state must also be set. + associated with this authorization code. Similarly request.scopes + must also be set. :param client_id: Unicode client identifier :param code: Unicode authorization code diff --git a/oauthlib/oauth2/rfc6749/utils.py b/oauthlib/oauth2/rfc6749/utils.py index 6a8e24b..870ac32 100644 --- a/oauthlib/oauth2/rfc6749/utils.py +++ b/oauthlib/oauth2/rfc6749/utils.py @@ -24,20 +24,16 @@ def list_to_scope(scope): """Convert a list of scopes to a space separated string.""" if isinstance(scope, unicode_type) or scope is None: return scope - elif isinstance(scope, (tuple, list)): + elif isinstance(scope, (set, tuple, list)): return " ".join([unicode_type(s) for s in scope]) - elif isinstance(scope, set): - return list_to_scope(list(scope)) else: - raise ValueError("Invalid scope, must be string or list.") + raise ValueError("Invalid scope (%s), must be string, tuple, set, or list." % scope) def scope_to_list(scope): """Convert a space separated string to a list of scopes.""" - if isinstance(scope, list): + if isinstance(scope, (tuple, list, set)): return [unicode_type(s) for s in scope] - if isinstance(scope, set): - scope_to_list(list(scope)) elif scope is None: return None else: -- 2.30.2