diff --git a/AUTHORS b/AUTHORS index 7d5d9ada..f52ce9ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ Joel Stevenson Brendan McCollam Jonathan Huot Pieter Ennes +Olaf Conradi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7389af0f..d7bb76e9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +2.0.8 (2018-05-18) +------------------ + +* Fixed some copy and paste typos (#535) +* Use secrets module in Python 3.6 and later (#533) +* Add request argument to confirm_redirect_uri (#504) +* Avoid populating spurious token credentials (#542, #546) + 2.0.7 (2018-03-19) ------------------ diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst index a1432a92..df1e2a0e 100644 --- a/docs/oauth1/security.rst +++ b/docs/oauth1/security.rst @@ -16,11 +16,13 @@ A few important facts regarding OAuth security * **Tokens must be random**, OAuthLib provides a method for generating secure tokens and it's packed into ``oauthlib.common.generate_token``, - use it. If you decide to roll your own, use ``random.SystemRandom`` - which is based on ``os.urandom`` rather than the default ``random`` - based on the effecient but not truly random Mersenne Twister. - Predictable tokens allow attackers to bypass virtually all defences - OAuth provides. + use it. If you decide to roll your own, use ``secrets.SystemRandom`` + for Python 3.6 and later. The ``secrets`` module is designed for + generating cryptographically strong random numbers. For earlier versions + of Python, use ``random.SystemRandom`` which is based on ``os.urandom`` + rather than the default ``random`` based on the effecient but not truly + random Mersenne Twister. Predictable tokens allow attackers to bypass + virtually all defences OAuth provides. * **Timing attacks are real** and more than possible if you host your application inside a shared datacenter. Ensure all ``validate_`` methods diff --git a/examples/skeleton_oauth2_web_application_server.py b/examples/skeleton_oauth2_web_application_server.py index 8bfd9369..e53232f3 100644 --- a/examples/skeleton_oauth2_web_application_server.py +++ b/examples/skeleton_oauth2_web_application_server.py @@ -67,7 +67,7 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): # state and user to request.scopes and request.user. pass - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): # You did save the redirect uri with the authorization code right? pass diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 36450108..039efb89 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'The OAuthlib Community' -__version__ = '2.0.7' +__version__ = '2.0.8' import logging diff --git a/oauthlib/common.py b/oauthlib/common.py index afcc09ca..f25656ff 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -11,11 +11,16 @@ import collections import datetime import logging -import random import re import sys import time +try: + from secrets import randbits + from secrets import SystemRandom +except ImportError: + from random import getrandbits as randbits + from random import SystemRandom try: from urllib import quote as _quote from urllib import unquote as _unquote @@ -202,7 +207,7 @@ def generate_nonce(): .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ - return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp()) + return unicode_type(unicode_type(randbits(64)) + generate_timestamp()) def generate_timestamp(): @@ -225,7 +230,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): and entropy when generating the random characters is important. Which is why SystemRandom is used instead of the default random.choice method. """ - rand = random.SystemRandom() + rand = SystemRandom() return ''.join(rand.choice(chars) for x in range(length)) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index a07a5c99..07ef8949 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -9,6 +9,7 @@ from __future__ import absolute_import, unicode_literals import time +import warnings from oauthlib.common import generate_token from oauthlib.oauth2.rfc6749 import tokens @@ -111,8 +112,10 @@ def __init__(self, client_id, self.state_generator = state_generator self.state = state self.redirect_url = redirect_url + self.code = None + self.expires_in = None self._expires_at = None - self._populate_attributes(self.token) + self.populate_token_attributes(self.token) @property def token_types(self): @@ -406,7 +409,7 @@ def parse_request_body_response(self, body, scope=None, **kwargs): .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) - self._populate_attributes(self.token) + self.populate_token_attributes(self.token) return self.token def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): @@ -460,7 +463,18 @@ def _add_mac_token(self, uri, http_method='GET', body=None, return uri, headers, body def _populate_attributes(self, response): - """Add commonly used values such as access_token to self.""" + warnings.warn("Please switch to the public method " + "populate_token_attributes.", DeprecationWarning) + return self.populate_token_attributes(response) + + def populate_code_attributes(self, response): + """Add attributes from an auth code response to self.""" + + if 'code' in response: + self.code = response.get('code') + + def populate_token_attributes(self, response): + """Add attributes from a token exchange response to self.""" if 'access_token' in response: self.access_token = response.get('access_token') @@ -478,9 +492,6 @@ def _populate_attributes(self, response): if 'expires_at' in response: self._expires_at = int(response.get('expires_at')) - if 'code' in response: - self.code = response.get('code') - if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 311aacf8..aa20daab 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -168,5 +168,5 @@ def parse_request_uri_response(self, uri, state=None, scope=None): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) - self._populate_attributes(self.token) + self.populate_token_attributes(self.token) return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 84ea0e99..7f336bbc 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -146,8 +146,8 @@ def prepare_request_body(self, ' token requests.') claim = { 'iss': issuer or self.issuer, - 'aud': audience or self.issuer, - 'sub': subject or self.issuer, + 'aud': audience or self.audience, + 'sub': subject or self.subject, 'exp': int(expires_at or time.time() + 3600), 'iat': int(issued_at or time.time()), } diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 0dd5f6ef..25280bf8 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -172,5 +172,5 @@ def parse_request_uri_response(self, uri, state=None): oauthlib.oauth2.rfc6749.errors.MismatchingStateError """ response = parse_authorization_code_response(uri, state=state) - self._populate_attributes(response) + self.populate_code_attributes(response) return response diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 7bea6507..06602634 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -421,7 +421,8 @@ def validate_token_request(self, request): # 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): + request.redirect_uri, request.client, + request): log.debug('Redirect_uri (%r) invalid for client %r (%r).', request.redirect_uri, request.client_id, request.client) raise errors.MismatchingRedirectURIError(request=request) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 26b0041c..fee7b8cb 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -82,7 +82,7 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): """Ensure that the authorization process represented by this authorization code began with this 'redirect_uri'. diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py index 309220ba..51e4dab3 100644 --- a/tests/oauth2/rfc6749/clients/test_mobile_application.py +++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py @@ -69,6 +69,18 @@ def test_implicit_token_uri(self): uri = client.prepare_request_uri(self.uri, **self.kwargs) self.assertURLEqual(uri, self.uri_kwargs) + def test_populate_attributes(self): + + client = MobileApplicationClient(self.client_id) + + response_uri = (self.response_uri + "&code=EVIL-CODE") + + client.parse_request_uri_response(response_uri, scope=self.scope) + + # We must not accidentally pick up any further security + # credentials at this point. + self.assertIsNone(client.code) + def test_parse_token_response(self): client = MobileApplicationClient(self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 85b247d1..fa6643eb 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -117,6 +117,25 @@ def test_parse_grant_uri_response(self): self.response_uri, state="invalid") + def test_populate_attributes(self): + + client = WebApplicationClient(self.client_id) + + response_uri = (self.response_uri + + "&access_token=EVIL-TOKEN" + "&refresh_token=EVIL-TOKEN" + "&mac_key=EVIL-KEY") + + client.parse_request_uri_response(response_uri, self.state) + + self.assertEqual(client.code, self.code) + + # We must not accidentally pick up any further security + # credentials at this point. + self.assertIsNone(client.access_token) + self.assertIsNone(client.refresh_token) + self.assertIsNone(client.mac_key) + def test_parse_token_response(self): client = WebApplicationClient(self.client_id)