From 9b95e4e8f094d78abe577203ad1ef53aecfdb270 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 8 Nov 2017 09:55:03 +0100 Subject: [PATCH 1/7] Added initial introspect support --- docs/feature_matrix.rst | 1 + docs/oauth2/endpoints/endpoints.rst | 6 +- docs/oauth2/endpoints/introspect.rst | 26 ++++ oauthlib/oauth2/__init__.py | 1 + oauthlib/oauth2/rfc6749/endpoints/__init__.py | 1 + .../oauth2/rfc6749/endpoints/introspect.py | 135 ++++++++++++++++++ .../rfc6749/endpoints/pre_configured.py | 27 ++-- oauthlib/oauth2/rfc6749/errors.py | 2 +- oauthlib/oauth2/rfc6749/request_validator.py | 20 +++ .../endpoints/test_introspect_endpoint.py | 132 +++++++++++++++++ 10 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 docs/oauth2/endpoints/introspect.rst create mode 100644 oauthlib/oauth2/rfc6749/endpoints/introspect.py create mode 100644 tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index 0f9021da..59f3f3ab 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -17,6 +17,7 @@ OAuth 2 client and provider support for - Bearer Tokens - Draft MAC tokens - Token Revocation +- Token Introspection - OpenID Connect Authentication with support for SAML2 and JWT tokens, dynamic client registration and more to diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 0e707986..5f7ae8c6 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -14,11 +14,12 @@ client attempts to access the user resources on their behalf. :maxdepth: 2 authorization + introspect token resource revocation -There are three different endpoints, the authorization endpoint which mainly +There are three main endpoints, the authorization endpoint which mainly handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This @@ -27,3 +28,6 @@ later. The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. + +Then, you can extend your OAuth implementation by proposing introspect or +revocation endpoints. diff --git a/docs/oauth2/endpoints/introspect.rst b/docs/oauth2/endpoints/introspect.rst new file mode 100644 index 00000000..53ade8b2 --- /dev/null +++ b/docs/oauth2/endpoints/introspect.rst @@ -0,0 +1,26 @@ +=================== +Token introspection +=================== + +Introspect endpoints read opaque access and/or refresh tokens upon client +request. Also known as tokeninfo. + +.. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Token revocation + uri = 'https://example.com/introspect' + headers, body, http_method = {}, 'token=sldafh309sdf', 'POST' + + headers, body, status = server.create_introspect_response(uri, + headers=headers, body=body, http_method=http_method) + + from your_framework import http_response + http_response(body, status=status, headers=headers) + + +.. autoclass:: oauthlib.oauth2.IntrospectEndpoint + :members: diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index c8d934e9..dc7b431c 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -15,6 +15,7 @@ from .rfc6749.clients import BackendApplicationClient from .rfc6749.clients import ServiceApplicationClient from .rfc6749.endpoints import AuthorizationEndpoint +from .rfc6749.endpoints import IntrospectEndpoint from .rfc6749.endpoints import TokenEndpoint from .rfc6749.endpoints import ResourceEndpoint from .rfc6749.endpoints import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py index 848bec6b..9557f92a 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py @@ -9,6 +9,7 @@ from __future__ import absolute_import, unicode_literals from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .token import TokenEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py new file mode 100644 index 00000000..7613acce --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.endpoint.introspect +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An implementation of the OAuth 2.0 `Token Introspection`. + +.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662 +""" +from __future__ import absolute_import, unicode_literals + +import json +import logging + +from oauthlib.common import Request + +from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error, + UnsupportedTokenTypeError) +from .base import BaseEndpoint, catch_errors_and_unavailability + +log = logging.getLogger(__name__) + + +class IntrospectEndpoint(BaseEndpoint): + + """Introspect token endpoint. + + This endpoint defines a method to query an OAuth 2.0 authorization + server to determine the active state of an OAuth 2.0 token and to + determine meta-information about this token. OAuth 2.0 deployments + can use this method to convey information about the authorization + context of the token from the authorization server to the protected + resource. + + To prevent the values of access tokens from leaking into + server-side logs via query parameters, an authorization server + offering token introspection MAY disallow the use of HTTP GET on + the introspection endpoint and instead require the HTTP POST method + to be used at the introspection endpoint. + """ + + valid_token_types = ('access_token', 'refresh_token') + + def __init__(self, request_validator, supported_token_types=None): + BaseEndpoint.__init__(self) + self.request_validator = request_validator + self.supported_token_types = ( + supported_token_types or self.valid_token_types) + + @catch_errors_and_unavailability + def create_introspect_response(self, uri, http_method='POST', body=None, + headers=None): + """Create introspect valid or invalid response + + If the authorization server is unable to determine the state + of the token without additional information, it SHOULD return + an introspection response indicating the token is not active + as described in Section 2.2. + """ + request = Request(uri, http_method, body, headers) + try: + self.validate_introspect_request(request) + log.debug('Token introspect valid for %r.', request) + except OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + return {}, e.json, e.status_code + + claims = self.request_validator.introspect_token( + request.token, + request.token_type_hint, + request + ) + headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + if claims is None: + return headers, json.dumps(dict(active=False)), 200 + if "active" in claims: + claims.pop("active") + return headers, json.dumps(dict(active=True, **claims)), 200 + + def validate_introspect_request(self, request): + """Ensure the request is valid. + + The protected resource calls the introspection endpoint using + an HTTP POST request with parameters sent as + "application/x-www-form-urlencoded". + + token REQUIRED. The string value of the token. + + token_type_hint OPTIONAL. + A hint about the type of the token submitted for + introspection. The protected resource MAY pass this parameter to + help the authorization server optimize the token lookup. If the + server is unable to locate the token using the given hint, it MUST + extend its search across all of its supported token types. An + authorization server MAY ignore this parameter, particularly if it + is able to detect the token type automatically. + * access_token: An Access Token as defined in [`RFC6749`], + `section 1.4`_ + + * refresh_token: A Refresh Token as defined in [`RFC6749`], + `section 1.5`_ + + The introspection endpoint MAY accept other OPTIONAL + parameters to provide further context to the query. For + instance, an authorization server may desire to know the IP + address of the client accessing the protected resource to + determine if the correct client is likely to be presenting the + token. The definition of this or any other parameters are + outside the scope of this specification, to be defined by + service documentation or extensions to this specification. + + .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 + .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 + .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 + """ + if not request.token: + raise InvalidRequestError(request=request, + description='Missing token parameter.') + + if self.request_validator.client_authentication_required(request): + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + elif not self.request_validator.authenticate_client_id(request.client_id, request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + + if (request.token_type_hint and + request.token_type_hint in self.valid_token_types and + request.token_type_hint not in self.supported_token_types): + raise UnsupportedTokenTypeError(request=request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 07c37158..f1dfead9 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -18,13 +18,14 @@ ResourceOwnerPasswordCredentialsGrant) from ..tokens import BearerToken from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint from .token import TokenEndpoint -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -88,10 +89,11 @@ def __init__(self, request_validator, token_expires_in=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" @@ -126,10 +128,11 @@ def __init__(self, request_validator, token_generator=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint, - RevocationEndpoint): +class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" @@ -159,10 +162,11 @@ def __init__(self, request_validator, token_generator=None, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator) -class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" @@ -195,10 +199,11 @@ def __init__(self, request_validator, token_generator=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class BackendApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" @@ -228,3 +233,5 @@ def __init__(self, request_validator, token_generator=None, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 180f6363..1d5e98d5 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -267,7 +267,7 @@ class UnsupportedGrantTypeError(OAuth2Error): class UnsupportedTokenTypeError(OAuth2Error): """ - The authorization server does not support the revocation of the + The authorization server does not support the hint of the presented token type. I.e. the client tried to revoke an access token on a server not supporting this feature. """ diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index ba129d5e..525ba33d 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -166,6 +166,26 @@ def is_within_original_scope(self, request_scopes, refresh_token, request, *args """ return False + def introspect_token(self, token, token_type_hint, request, *args, **kwargs): + """Introspect an access or refresh token. + + Called once introspect token request is validated. This method + should return a dictionary with any desired claims associated + with the *token*. The implementation can use *token_type_hint* + to lookup this type first, but then it must fallback to other + types known, to be compliant with RFC. + + The dict of claims is added to request.token after this method. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + + Method is used by: + - Introspect Endpoint (all grants are compatible) + """ + raise NotImplementedError('Subclasses must implement this method.') + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """Invalidate an authorization code after use. diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py new file mode 100644 index 00000000..7ec81905 --- /dev/null +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from json import loads + +from mock import MagicMock + +from oauthlib.common import urlencode +from oauthlib.oauth2 import RequestValidator, IntrospectEndpoint + +from ....unittest import TestCase + + +class IntrospectEndpointTest(TestCase): + + def setUp(self): + self.validator = MagicMock(wraps=RequestValidator()) + self.validator.client_authentication_required.return_value = True + self.validator.authenticate_client.return_value = True + self.validator.validate_bearer_token.return_value = True + self.validator.introspect_token.return_value = {} + self.endpoint = IntrospectEndpoint(self.validator) + + self.uri = 'should_not_matter' + self.headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + self.resp_h = { + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + 'Pragma': 'no-cache' + } + self.resp_b = { + "active": True + } + + def test_introspect_token(self): + for token_type in ('access_token', 'refresh_token', 'invalid'): + body = urlencode([('token', 'foo'), + ('token_type_hint', token_type)]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_nohint(self): + # don't specify token_type_hint + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_false(self): + self.validator.introspect_token.return_value = None + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": False}) + self.assertEqual(s, 200) + + def test_introspect_token_claims(self): + self.validator.introspect_token.return_value = {"foo": "bar"} + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": True, "foo": "bar"}) + self.assertEqual(s, 200) + + def test_introspect_token_claims_spoof_active(self): + self.validator.introspect_token.return_value = {"foo": "bar", "active": False} + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": True, "foo": "bar"}) + self.assertEqual(s, 200) + + def test_introspect_token_client_authentication_failed(self): + self.validator.authenticate_client.return_value = False + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_client') + self.assertEqual(s, 401) + + def test_introspect_token_public_client_authentication(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = True + for token_type in ('access_token', 'refresh_token', 'invalid'): + body = urlencode([('token', 'foo'), + ('token_type_hint', token_type)]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_public_client_authentication_failed(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = False + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_client') + self.assertEqual(s, 401) + + + def test_introspect_unsupported_token(self): + endpoint = IntrospectEndpoint(self.validator, + supported_token_types=['access_token']) + body = urlencode([('token', 'foo'), + ('token_type_hint', 'refresh_token')]) + h, b, s = endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'unsupported_token_type') + self.assertEqual(s, 400) + + h, b, s = endpoint.create_introspect_response(self.uri, + headers=self.headers, body='') + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertEqual(s, 400) From ef8a3b47305b23b278310c1f21106c677a748434 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 19 Dec 2017 15:18:48 +0100 Subject: [PATCH 2/7] Added default supported_token_types for Mobile --- oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index f1dfead9..378339ac 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -162,7 +162,8 @@ def __init__(self, request_validator, token_generator=None, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, From 296c6bc5931c95f631c1a496dacc523959fc50e9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 19 Dec 2017 15:19:09 +0100 Subject: [PATCH 3/7] Improved doc by adding links to RFC and list of claims. --- oauthlib/oauth2/rfc6749/request_validator.py | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 525ba33d..4b76b7a9 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -169,11 +169,28 @@ def is_within_original_scope(self, request_scopes, refresh_token, request, *args def introspect_token(self, token, token_type_hint, request, *args, **kwargs): """Introspect an access or refresh token. - Called once introspect token request is validated. This method - should return a dictionary with any desired claims associated - with the *token*. The implementation can use *token_type_hint* - to lookup this type first, but then it must fallback to other - types known, to be compliant with RFC. + Called once the introspect request is validated. This method should + verify the *token* and either return a dictionary with the list of + claims associated, or `None` in case the token is unknown. + + Below the list of registered claims you should be interested in: + - scope : space-separated list of scopes + - client_id : client identifier + - username : human-readable identifier for the resource owner + - token_type : type of the token + - exp : integer timestamp indicating when this token will expire + - iat : integer timestamp indicating when this token was issued + - nbf : integer timestamp indicating when it can be "not-before" used + - sub : subject of the token - identifier of the resource owner + - aud : list of string identifiers representing the intended audience + - iss : string representing issuer of this token + - jti : string identifier for the token + + Note that most of them are coming directly from JWT RFC. More details + can be found in `Introspect Claims`_ or `_JWT Claims`_. + + The implementation can use *token_type_hint* to improve lookup + efficency, but must fallback to other types to be compliant with RFC. The dict of claims is added to request.token after this method. @@ -183,6 +200,9 @@ def introspect_token(self, token, token_type_hint, request, *args, **kwargs): Method is used by: - Introspect Endpoint (all grants are compatible) + + .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2 + .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4 """ raise NotImplementedError('Subclasses must implement this method.') From 6da09f284593546daac545d625f68014d7464c39 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 23 May 2018 16:59:22 +0200 Subject: [PATCH 4/7] Deploy only when building python36 with tox Avoid multiple deploy steps and lead to failures (e.g. errors "already deployed") --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0a7d8ad3..dd72d5cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ deploy: on: tags: true all_branches: true + condition: $TOXENV = py36 repo: oauthlib/oauthlib - provider: releases api_key: @@ -42,4 +43,5 @@ deploy: on: tags: true all_branches: true + condition: $TOXENV = py36 repo: oauthlib/oauthlib From 789220fc5b450ed72899d87961eef155fbd22fc6 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 23 Apr 2018 21:47:51 +0100 Subject: [PATCH 5/7] Prepare 2.1.0 release. (cherry picked from commit 5c76855) --- CHANGELOG.rst | 9 +++++++++ oauthlib/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7389af0f..a8e1941d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +2.1.0 (2018-05-21) +------------------ + +* 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) +* Make populate attributes API public (#546) + 2.0.7 (2018-03-19) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 36450108..3393efeb 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'The OAuthlib Community' -__version__ = '2.0.7' +__version__ = '2.1.0' import logging From 27702f40753f88fc5bbf15128dac15758d4bc29a Mon Sep 17 00:00:00 2001 From: Mattia Procopio Date: Sat, 26 May 2018 21:33:41 +0200 Subject: [PATCH 6/7] Check that the Bearer header is properly formatted (#491) --- oauthlib/oauth2/rfc6749/tokens.py | 40 ++++++++------ tests/oauth2/rfc6749/test_tokens.py | 81 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 4ae20e0e..a7491f47 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -220,6 +220,24 @@ def signed_token_generator(request): return signed_token_generator +def get_token_from_header(request): + """ + Helper function to extract a token from the request header. + :param request: The request object + :return: Return the token or None if the Authorization header is malformed. + """ + token = None + + if 'Authorization' in request.headers: + split_header = request.headers.get('Authorization').split() + if len(split_header) == 2 and split_header[0] == 'Bearer': + token = split_header[1] + else: + token = request.access_token + + return token + + class TokenBase(object): def __call__(self, request, refresh_token=False): @@ -286,16 +304,12 @@ def create_token(self, request, refresh_token=False, save_token=True): return token def validate_request(self, request): - token = None - if 'Authorization' in request.headers: - token = request.headers.get('Authorization')[7:] - else: - token = request.access_token + token = get_token_from_header(request) return self.request_validator.validate_bearer_token( token, request.scopes, request) def estimate_type(self, request): - if request.headers.get('Authorization', '').startswith('Bearer'): + if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer': return 9 elif request.access_token is not None: return 5 @@ -331,17 +345,13 @@ def create_token(self, request, refresh_token=False, save_token=False): return self.request_validator.get_jwt_bearer_token(None, None, request) def validate_request(self, request): - token = None - if 'Authorization' in request.headers: - token = request.headers.get('Authorization')[7:] - else: - token = request.access_token + token = get_token_from_header(request) return self.request_validator.validate_jwt_bearer_token( token, request.scopes, request) def estimate_type(self, request): - token = request.headers.get('Authorization', '')[7:] - if token.startswith('ey') and token.count('.') in (2, 4): + split_header = request.headers.get('Authorization', '').split() + + if len(split_header) == 2 and split_header[0] == 'Bearer' and split_header[1].startswith('ey') and split_header[1].count('.') in (2, 4): return 10 - else: - return 0 + return 0 diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index 570afb01..ecac03e2 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -2,6 +2,7 @@ import mock +from oauthlib.common import Request from oauthlib.oauth2.rfc6749.tokens import * from ...unittest import TestCase @@ -61,9 +62,22 @@ class TokenTest(TestCase): bearer_headers = { 'Authorization': 'Bearer vF9dft4qmT' } + fake_bearer_headers = [ + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BeavervF9dft4qmT'}, + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BearerF9dft4qmT'}, + {'Authorization': 'Bearer vF9d ft4qmT'}, + ] + valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} bearer_body = 'access_token=vF9dft4qmT' bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT' + def _mocked_validate_bearer_token(self, token, scopes, request): + if not token: + return False + return True + def test_prepare_mac_header(self): """Verify mac signatures correctness @@ -83,8 +97,57 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = BearerToken(request_validator=request_validator).validate_request(request) + + self.assertFalse(result) + + def test_header_with_multispaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + request = Request('/', headers=self.valid_header_with_multiple_spaces) + result = BearerToken(request_validator=request_validator).validate_request(request) + + self.assertTrue(result) + + def test_estimate_type_with_fake_header_returns_type_0(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = BearerToken(request_validator=request_validator).estimate_type(request) + + if fake_header['Authorization'].count(' ') == 2 and \ + fake_header['Authorization'].split()[0] == 'Bearer': + # If we're dealing with the header containing 2 spaces, it will be recognized + # as a Bearer valid header, the token itself will be invalid by the way. + self.assertEqual(result, 9) + else: + self.assertEqual(result, 0) + class JWTTokenTestCase(TestCase): + fake_bearer_headers = [ + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BeavervF9dft4qmT'}, + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BearerF9dft4qmT'}, + {'Authorization': 'Bearer vF9df t4qmT'}, + ] + + valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} + + def _mocked_validate_bearer_token(self, token, scopes, request): + if not token: + return False + return True def test_create_token_callable_expires_in(self): """ @@ -180,6 +243,24 @@ def test_validate_token_from_request(self): request.scopes, request) + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = JWTToken(request_validator=request_validator).validate_request(request) + + self.assertFalse(result) + + def test_header_with_multiple_spaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token + request = Request('/', headers=self.valid_header_with_multiple_spaces) + result = JWTToken(request_validator=request_validator).validate_request(request) + + self.assertTrue(result) + def test_estimate_type(self): """ Estimate type results for a jwt token From fedc1d1b740a0407ec59152750bbbd9dc736b51d Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sun, 27 May 2018 03:38:05 +0800 Subject: [PATCH 7/7] Add missing NotImplementedError (#499) --- oauthlib/oauth2/rfc6749/clients/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 07ef8949..406832d7 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -143,6 +143,7 @@ def prepare_request_body(self, *args, **kwargs): def parse_request_uri_response(self, *args, **kwargs): """Abstract method used to parse redirection responses.""" + raise NotImplementedError("Must be implemented by inheriting classes.") def add_token(self, uri, http_method='GET', body=None, headers=None, token_placement=None, **kwargs):