"""
__author__ = 'Idan Gazit <idan@gazit.me>'
-__version__ = '0.7.2'
+__version__ = '1.0.3'
import logging
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'
return decoded
-urlencoded = set(always_safe) | set('=&;%+~,*@')
+urlencoded = set(always_safe) | set('=&;%+~,*@!')
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
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 '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
- self.uri, self.http_method, self.headers, self.body)
+ self.uri, self.http_method, self.headers, body)
@property
def uri_query(self):
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__)
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
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):
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
# 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')
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
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
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
: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:
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)
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, ''
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
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
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
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
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):
error = 'invalid_request'
+class MissingResponseTypeError(InvalidRequestError):
+ description = 'Missing response_type parameter.'
+
+
class AccessDeniedError(OAuth2Error):
"""The resource owner or authorization server denied the request."""
# 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:
# 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)
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
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)
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)
# 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:
# 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)
.. _`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)
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
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.
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
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
"""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: