From 18e6dafe67d2421468e59efd40dad87805877c13 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:32:41 -0300 Subject: [PATCH 01/14] Add command to clean up builds to makefile --- Makefile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8571a911..f9cc4abf 100644 --- a/Makefile +++ b/Makefile @@ -12,13 +12,27 @@ # Since these contacts will be addressed with Github mentions they # need to be Github users (for now)(sorry Bitbucket). # -clean: +clean: clean-eggs clean-build + @find . -iname '*.pyc' -delete + @find . -iname '*.pyo' -delete + @find . -iname '*~' -delete + @find . -iname '*.swp' -delete + @find . -iname '__pycache__' -delete rm -rf .tox rm -rf bottle-oauthlib rm -rf django-oauth-toolkit rm -rf flask-oauthlib rm -rf requests-oauthlib +clean-eggs: + @find . -name '*.egg' -print0|xargs -0 rm -rf -- + @rm -rf .eggs/ + +clean-build: + @rm -fr build/ + @rm -fr dist/ + @rm -fr *.egg-info + test: tox @@ -51,7 +65,6 @@ requests: cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox - .DEFAULT_GOAL := all .PHONY: clean test bottle django flask requests all: clean test bottle django flask requests From ea31c644fe859487d9a9c5d27175e325234fac83 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:33:28 -0300 Subject: [PATCH 02/14] Fix docs strings for endpoints pre_configured --- oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 66af5161..7758f581 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749 -~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.oauth2.rfc6749.endpoints.pre_configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This module is an implementation of various logic needed -for consuming and providing OAuth 2.0 RFC6749. +This module is an implementation of various endpoints needed +for providing OAuth 2.0 RFC6749 servers. """ from __future__ import absolute_import, unicode_literals From 6fe228d159f33200b93eaf8e86b45351ce088544 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:35:25 -0300 Subject: [PATCH 03/14] Chnage grant_types.openid_connect to include a deprecation warning be a backward compatible --- .../rfc6749/grant_types/openid_connect.py | 474 ++---------------- 1 file changed, 34 insertions(+), 440 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py index 4371b286..63c56f3b 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py @@ -5,447 +5,41 @@ """ from __future__ import absolute_import, unicode_literals -import datetime import logging -from json import loads - -from ..errors import ConsentRequired, InvalidRequestError, LoginRequired -from ..request_validator import RequestValidator -from .authorization_code import AuthorizationCodeGrant -from .implicit import ImplicitGrant +import warnings + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.base import GrantTypeBase +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + ImplicitTokenGrantDispatcher as NewImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher, + AuthorizationCodeGrantDispatcher +) +from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt as NewOIDCNoPrompt log = logging.getLogger(__name__) - -class OIDCNoPrompt(Exception): - """Exception used to inform users that no explicit authorization is needed. - - Normally users authorize requests after validation of the request is done. - Then post-authorization validation is again made and a response containing - an auth code or token is created. However, when OIDC clients request - no prompting of user authorization the final response is created directly. - - Example (without the shortcut for no prompt) - - scopes, req_info = endpoint.validate_authorization_request(url, ...) - authorization_view = create_fancy_auth_form(scopes, req_info) - return authorization_view - - Example (with the no prompt shortcut) - try: - scopes, req_info = endpoint.validate_authorization_request(url, ...) - authorization_view = create_fancy_auth_form(scopes, req_info) - return authorization_view - except OIDCNoPrompt: - # Note: Location will be set for you - headers, body, status = endpoint.create_authorization_response(url, ...) - redirect_view = create_redirect(headers, body, status) - return redirect_view - """ - - def __init__(self): - msg = ("OIDC request for no user interaction received. Do not ask user " - "for authorization, it should been done using silent " - "authentication through create_authorization_response. " - "See OIDCNoPrompt.__doc__ for more details.") - super(OIDCNoPrompt, self).__init__(msg) - - -class AuthCodeGrantDispatcher(object): - """ - This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope - including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. - """ - def __init__(self, default_auth_grant=None, oidc_auth_grant=None): - self.default_auth_grant = default_auth_grant - self.oidc_auth_grant = oidc_auth_grant - - def _handler_for_request(self, request): - handler = self.default_auth_grant - - if request.scopes and "openid" in request.scopes: - handler = self.oidc_auth_grant - - log.debug('Selecting handler for request %r.', handler) - return handler - - def create_authorization_response(self, request, token_handler): - return self._handler_for_request(request).create_authorization_response(request, token_handler) - - def validate_authorization_request(self, request): - return self._handler_for_request(request).validate_authorization_request(request) - - -class ImplicitTokenGrantDispatcher(object): - """ - This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope - including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. - """ - def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): - self.default_implicit_grant = default_implicit_grant - self.oidc_implicit_grant = oidc_implicit_grant - - def _handler_for_request(self, request): - handler = self.default_implicit_grant - - if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: - handler = self.oidc_implicit_grant - - log.debug('Selecting handler for request %r.', handler) - return handler - - def create_authorization_response(self, request, token_handler): - return self._handler_for_request(request).create_authorization_response(request, token_handler) - - def validate_authorization_request(self, request): - return self._handler_for_request(request).validate_authorization_request(request) - - -class AuthTokenGrantDispatcher(object): - """ - This is an adapter class that will route simple Token requests, those that authorization_code have a scope - including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. - """ - def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): - self.default_token_grant = default_token_grant - self.oidc_token_grant = oidc_token_grant - self.request_validator = request_validator - - def _handler_for_request(self, request): - handler = self.default_token_grant - scopes = () - parameters = dict(request.decoded_body) - client_id = parameters.get('client_id', None) - code = parameters.get('code', None) - redirect_uri = parameters.get('redirect_uri', None) - - # If code is not pressent fallback to `default_token_grant` wich will - # raise an error for the missing `code` in `create_token_response` step. - if code: - scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) - - if 'openid' in scopes: - handler = self.oidc_token_grant - - log.debug('Selecting handler for request %r.', handler) - return handler - - def create_token_response(self, request, token_handler): - handler = self._handler_for_request(request) - return handler.create_token_response(request, token_handler) - - -class OpenIDConnectBase(object): - - # Just proxy the majority of method calls through to the - # proxy_target grant type handler, which will usually be either - # the standard OAuth2 AuthCode or Implicit grant types. - def __getattr__(self, attr): - return getattr(self.proxy_target, attr) - - def __setattr__(self, attr, value): - proxied_attrs = set(('refresh_token', 'response_types')) - if attr in proxied_attrs: - setattr(self.proxy_target, attr, value) - else: - super(OpenIDConnectBase, self).__setattr__(attr, value) - - def validate_authorization_request(self, request): - """Validates the OpenID Connect authorization request parameters. - - :returns: (list of scopes, dict of request info) - """ - # If request.prompt is 'none' then no login/authorization form should - # be presented to the user. Instead, a silent login/authorization - # should be performed. - if request.prompt == 'none': - raise OIDCNoPrompt() - else: - return self.proxy_target.validate_authorization_request(request) - - def _inflate_claims(self, request): - # this may be called multiple times in a single request so make sure we only de-serialize the claims once - if request.claims and not isinstance(request.claims, dict): - # specific claims are requested during the Authorization Request and may be requested for inclusion - # in either the id_token or the UserInfo endpoint response - # see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - try: - request.claims = loads(request.claims) - except Exception as ex: - raise InvalidRequestError(description="Malformed claims parameter", - uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter") - - def add_id_token(self, token, token_handler, request): - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return token - - # Only add an id token on auth/token step if asked for. - if request.response_type and 'id_token' not in request.response_type: - return token - - if 'state' not in token: - token['state'] = request.state - - if request.max_age: - d = datetime.datetime.utcnow() - token['auth_time'] = d.isoformat("T") + "Z" - - # TODO: acr claims (probably better handled by server code using oauthlib in get_id_token) - - token['id_token'] = self.request_validator.get_id_token(token, token_handler, request) - - return token - - def openid_authorization_validator(self, request): - """Perform OpenID Connect specific authorization request validation. - - nonce - OPTIONAL. String value used to associate a Client session with - an ID Token, and to mitigate replay attacks. The value is - passed through unmodified from the Authentication Request to - the ID Token. Sufficient entropy MUST be present in the nonce - values used to prevent attackers from guessing values - - display - OPTIONAL. ASCII string value that specifies how the - Authorization Server displays the authentication and consent - user interface pages to the End-User. The defined values are: - - page - The Authorization Server SHOULD display the - authentication and consent UI consistent with a full User - Agent page view. If the display parameter is not specified, - this is the default display mode. - - popup - The Authorization Server SHOULD display the - authentication and consent UI consistent with a popup User - Agent window. The popup User Agent window should be of an - appropriate size for a login-focused dialog and should not - obscure the entire window that it is popping up over. - - touch - The Authorization Server SHOULD display the - authentication and consent UI consistent with a device that - leverages a touch interface. - - wap - The Authorization Server SHOULD display the - authentication and consent UI consistent with a "feature - phone" type display. - - The Authorization Server MAY also attempt to detect the - capabilities of the User Agent and present an appropriate - display. - - prompt - OPTIONAL. Space delimited, case sensitive list of ASCII string - values that specifies whether the Authorization Server prompts - the End-User for reauthentication and consent. The defined - values are: - - none - The Authorization Server MUST NOT display any - authentication or consent user interface pages. An error is - returned if an End-User is not already authenticated or the - Client does not have pre-configured consent for the - requested Claims or does not fulfill other conditions for - processing the request. The error code will typically be - login_required, interaction_required, or another code - defined in Section 3.1.2.6. This can be used as a method to - check for existing authentication and/or consent. - - login - The Authorization Server SHOULD prompt the End-User - for reauthentication. If it cannot reauthenticate the - End-User, it MUST return an error, typically - login_required. - - consent - The Authorization Server SHOULD prompt the - End-User for consent before returning information to the - Client. If it cannot obtain consent, it MUST return an - error, typically consent_required. - - select_account - The Authorization Server SHOULD prompt the - End-User to select a user account. This enables an End-User - who has multiple accounts at the Authorization Server to - select amongst the multiple accounts that they might have - current sessions for. If it cannot obtain an account - selection choice made by the End-User, it MUST return an - error, typically account_selection_required. - - The prompt parameter can be used by the Client to make sure - that the End-User is still present for the current session or - to bring attention to the request. If this parameter contains - none with any other value, an error is returned. - - max_age - OPTIONAL. Maximum Authentication Age. Specifies the allowable - elapsed time in seconds since the last time the End-User was - actively authenticated by the OP. If the elapsed time is - greater than this value, the OP MUST attempt to actively - re-authenticate the End-User. (The max_age request parameter - corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age - request parameter.) When max_age is used, the ID Token returned - MUST include an auth_time Claim Value. - - ui_locales - OPTIONAL. End-User's preferred languages and scripts for the - user interface, represented as a space-separated list of BCP47 - [RFC5646] language tag values, ordered by preference. For - instance, the value "fr-CA fr en" represents a preference for - French as spoken in Canada, then French (without a region - designation), followed by English (without a region - designation). An error SHOULD NOT result if some or all of the - requested locales are not supported by the OpenID Provider. - - id_token_hint - OPTIONAL. ID Token previously issued by the Authorization - Server being passed as a hint about the End-User's current or - past authenticated session with the Client. If the End-User - identified by the ID Token is logged in or is logged in by the - request, then the Authorization Server returns a positive - response; otherwise, it SHOULD return an error, such as - login_required. When possible, an id_token_hint SHOULD be - present when prompt=none is used and an invalid_request error - MAY be returned if it is not; however, the server SHOULD - respond successfully when possible, even if it is not present. - The Authorization Server need not be listed as an audience of - the ID Token when it is used as an id_token_hint value. If the - ID Token received by the RP from the OP is encrypted, to use it - as an id_token_hint, the Client MUST decrypt the signed ID - Token contained within the encrypted ID Token. The Client MAY - re-encrypt the signed ID token to the Authentication Server - using a key that enables the server to decrypt the ID Token, - and use the re-encrypted ID token as the id_token_hint value. - - login_hint - OPTIONAL. Hint to the Authorization Server about the login - identifier the End-User might use to log in (if necessary). - This hint can be used by an RP if it first asks the End-User - for their e-mail address (or other identifier) and then wants - to pass that value as a hint to the discovered authorization - service. It is RECOMMENDED that the hint value match the value - used for discovery. This value MAY also be a phone number in - the format specified for the phone_number Claim. The use of - this parameter is left to the OP's discretion. - - acr_values - OPTIONAL. Requested Authentication Context Class Reference - values. Space-separated string that specifies the acr values - that the Authorization Server is being requested to use for - processing this Authentication Request, with the values - appearing in order of preference. The Authentication Context - Class satisfied by the authentication performed is returned as - the acr Claim Value, as specified in Section 2. The acr Claim - is requested as a Voluntary Claim by this parameter. - """ - - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return {} - - prompt = request.prompt if request.prompt else [] - if hasattr(prompt, 'split'): - prompt = prompt.strip().split() - prompt = set(prompt) - - if 'none' in prompt: - - if len(prompt) > 1: - msg = "Prompt none is mutually exclusive with other values." - raise InvalidRequestError(request=request, description=msg) - - # prompt other than 'none' should be handled by the server code that - # uses oauthlib - if not request.id_token_hint: - msg = "Prompt is set to none yet id_token_hint is missing." - raise InvalidRequestError(request=request, description=msg) - - if not self.request_validator.validate_silent_login(request): - raise LoginRequired(request=request) - - if not self.request_validator.validate_silent_authorization(request): - raise ConsentRequired(request=request) - - self._inflate_claims(request) - - if not self.request_validator.validate_user_match( - request.id_token_hint, request.scopes, request.claims, request): - msg = "Session user does not match client supplied user." - raise LoginRequired(request=request, description=msg) - - request_info = { - 'display': request.display, - 'nonce': request.nonce, - 'prompt': prompt, - 'ui_locales': request.ui_locales.split() if request.ui_locales else [], - 'id_token_hint': request.id_token_hint, - 'login_hint': request.login_hint, - 'claims': request.claims - } - - return request_info - - def openid_implicit_authorization_validator(self, request): - """Additional validation when following the implicit flow. - """ - # Undefined in OpenID Connect, fall back to OAuth2 definition. - if request.response_type == 'token': - return {} - - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return {} - - # REQUIRED. String value used to associate a Client session with an ID - # Token, and to mitigate replay attacks. The value is passed through - # unmodified from the Authentication Request to the ID Token. - # Sufficient entropy MUST be present in the nonce values used to - # prevent attackers from guessing values. For implementation notes, see - # Section 15.5.2. - if not request.nonce: - desc = 'Request is missing mandatory nonce parameter.' - raise InvalidRequestError(request=request, description=desc) - - return {} - - -class OpenIDConnectAuthCode(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.proxy_target = AuthorizationCodeGrant( - request_validator=request_validator, **kwargs) - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - self.register_token_modifier(self.add_id_token) - - -class OpenIDConnectImplicit(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.proxy_target = ImplicitGrant( - request_validator=request_validator, **kwargs) - self.register_response_type('id_token') - self.register_response_type('id_token token') - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - self.custom_validators.post_auth.append( - self.openid_implicit_authorization_validator) - self.register_token_modifier(self.add_id_token) - - -class OpenIDConnectHybrid(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.request_validator = request_validator or RequestValidator() - - self.proxy_target = AuthorizationCodeGrant( - request_validator=request_validator, **kwargs) - # All hybrid response types should be fragment-encoded. - self.proxy_target.default_response_mode = "fragment" - self.register_response_type('code id_token') - self.register_response_type('code token') - self.register_response_type('code id_token token') - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - # Hybrid flows can return the id_token from the authorization - # endpoint as part of the 'code' response - self.register_code_modifier(self.add_token) - self.register_code_modifier(self.add_id_token) - self.register_token_modifier(self.add_id_token) +warnings.warn( + "Should not use this module! Use: " + "oauthlib.openid.connect.core.grant_types.authorization_code.AuthorizationCodeGrant, " + "oauthlib.openid.connect.core.grant_types.base.GrantTypeBase, " + "oauthlib.openid.connect.core.grant_types.implicit.ImplicitGrant, " + "oauthlib.openid.connect.core.grant_types.hybrid.HybridGrant, " + "oauthlib.openid.connect.core.grant_types.dispatchers.ImplicitTokenGrantDispatcher, " + "oauthlib.openid.connect.core.grant_types.dispatchers.AuthorizationTokenGrantDispatcher, " + "oauthlib.openid.connect.core.grant_types.dispatchers.AuthorizationCodeGrantDispatcher, " + "oauthlib.openid.connect.core.grant_types.exceptions.OIDCNoPrompt " + "istead.", + DeprecationWarning) + + +OIDCNoPrompt = NewOIDCNoPrompt +AuthCodeGrantDispatcher = AuthorizationCodeGrantDispatcher +ImplicitTokenGrantDispatcher = NewImplicitTokenGrantDispatcher +AuthTokenGrantDispatcher = AuthorizationTokenGrantDispatcher +OpenIDConnectBase = GrantTypeBase +OpenIDConnectAuthCode = AuthorizationCodeGrant +OpenIDConnectImplicit = ImplicitGrant +OpenIDConnectHybrid = HybridGrant From 7284ce8e9b5359ff16a355657ce38c1b6335a0fc Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:36:04 -0300 Subject: [PATCH 04/14] Fix doc string for rfc6749.request_validator --- oauthlib/oauth2/rfc6749/request_validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 56ecc3dd..92edba62 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749.grant_types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.oauth2.rfc6749.request_validator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import absolute_import, unicode_literals From 9734d842309fd828f565ba8dfa314e072dc77df1 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:36:39 -0300 Subject: [PATCH 05/14] Remove unused import --- tests/oauth2/rfc6749/test_server.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index da303ceb..25240178 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -3,14 +3,12 @@ import json -import jwt import mock from oauthlib import common from oauthlib.oauth2.rfc6749 import errors, tokens from oauthlib.oauth2.rfc6749.endpoints import Server -from oauthlib.oauth2.rfc6749.endpoints.authorization import \ - AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint from oauthlib.oauth2.rfc6749.grant_types import (AuthorizationCodeGrant, @@ -41,7 +39,6 @@ def setUp(self): response_types = { 'code': auth_code, 'token': implicit, - 'id_token': openid_connect_implicit, 'id_token token': openid_connect_implicit, 'code token': openid_connect_auth, From a69cb7ddec3fc8af91ade0cd5be0782bdba85b75 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:37:44 -0300 Subject: [PATCH 06/14] Change import to be explicity --- tests/oauth2/rfc6749/test_tokens.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index ecac03e2..52b3d56f 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -3,7 +3,14 @@ import mock from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.tokens import * +from oauthlib.oauth2.rfc6749.tokens import ( + prepare_mac_header, + prepare_bearer_headers, + prepare_bearer_body, + prepare_bearer_uri, + BearerToken, + JWTToken, +) from ...unittest import TestCase From 07aef4ed317d473c3e86d58ace9d9813a37de1dc Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:38:36 -0300 Subject: [PATCH 07/14] Move JWTTokenTestCase to openid.connect.core.test_token --- tests/oauth2/rfc6749/test_tokens.py | 198 ---------------------------- 1 file changed, 198 deletions(-) diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index 52b3d56f..061754f6 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -1,15 +1,10 @@ from __future__ import absolute_import, unicode_literals -import mock - -from oauthlib.common import Request from oauthlib.oauth2.rfc6749.tokens import ( prepare_mac_header, prepare_bearer_headers, prepare_bearer_body, prepare_bearer_uri, - BearerToken, - JWTToken, ) from ...unittest import TestCase @@ -103,196 +98,3 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_headers(self.token), self.bearer_headers) 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): - """ - Test retrieval of the expires in value by calling the callable expires_in property - """ - - expires_in_mock = mock.MagicMock() - request_mock = mock.MagicMock() - - token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) - token.create_token(request=request_mock) - - expires_in_mock.assert_called_once_with(request_mock) - - def test_create_token_non_callable_expires_in(self): - """ - When a non callable expires in is set this should just be set to the request - """ - - expires_in_mock = mock.NonCallableMagicMock() - request_mock = mock.MagicMock() - - token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) - token.create_token(request=request_mock) - - self.assertFalse(expires_in_mock.called) - self.assertEqual(request_mock.expires_in, expires_in_mock) - - def test_create_token_calls_get_id_token(self): - """ - When create_token is called the call should be forwarded to the get_id_token on the token validator - """ - request_mock = mock.MagicMock() - - with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', - autospec=True) as RequestValidatorMock: - - request_validator = RequestValidatorMock() - - token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator) - token.create_token(request=request_mock) - - request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock) - - def test_validate_request_token_from_headers(self): - """ - Bearer token get retrieved from headers. - """ - - with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ - mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', - autospec=True) as RequestValidatorMock: - request_validator_mock = RequestValidatorMock() - - token = JWTToken(request_validator=request_validator_mock) - - request = RequestMock('/uri') - # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch - # with autospec=True - request.scopes = mock.MagicMock() - request.headers = { - 'Authorization': 'Bearer some-token-from-header' - } - - token.validate_request(request=request) - - request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header', - request.scopes, - request) - - def test_validate_token_from_request(self): - """ - Token get retrieved from request object. - """ - - with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ - mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', - autospec=True) as RequestValidatorMock: - request_validator_mock = RequestValidatorMock() - - token = JWTToken(request_validator=request_validator_mock) - - request = RequestMock('/uri') - # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch - # with autospec=True - request.scopes = mock.MagicMock() - request.access_token = 'some-token-from-request-object' - request.headers = {} - - token.validate_request(request=request) - - request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object', - 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 - """ - - def test_token(token, expected_result): - with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock: - jwt_token = JWTToken() - - request = RequestMock('/uri') - # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch - # with autospec=True - request.headers = { - 'Authorization': 'Bearer {}'.format(token) - } - - result = jwt_token.estimate_type(request=request) - - self.assertEqual(result, expected_result) - - test_items = ( - ('eyfoo.foo.foo', 10), - ('eyfoo.foo.foo.foo.foo', 10), - ('eyfoobar', 0) - ) - - for token, expected_result in test_items: - test_token(token, expected_result) From 87b93b1624faa060ba5c12767d8550d4de0eb0f4 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 16:40:51 -0300 Subject: [PATCH 08/14] Move JWTToken to oauthlib.openid.connect.core.tokens --- oauthlib/oauth2/rfc6749/tokens.py | 41 +++---------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index a7491f47..1f9e6bc3 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -16,6 +16,8 @@ from oauthlib import common from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type +from oauthlib.openid.connect.core.tokens import JWTToken as NewJWTToken + from . import utils try: @@ -317,41 +319,4 @@ def estimate_type(self, request): return 0 -class JWTToken(TokenBase): - __slots__ = ( - 'request_validator', 'token_generator', - 'refresh_token_generator', 'expires_in' - ) - - def __init__(self, request_validator=None, token_generator=None, - expires_in=None, refresh_token_generator=None): - self.request_validator = request_validator - self.token_generator = token_generator or random_token_generator - self.refresh_token_generator = ( - refresh_token_generator or self.token_generator - ) - self.expires_in = expires_in or 3600 - - def create_token(self, request, refresh_token=False, save_token=False): - """Create a JWT Token, using requestvalidator method.""" - - if callable(self.expires_in): - expires_in = self.expires_in(request) - else: - expires_in = self.expires_in - - request.expires_in = expires_in - - return self.request_validator.get_jwt_bearer_token(None, None, request) - - def validate_request(self, request): - token = get_token_from_header(request) - return self.request_validator.validate_jwt_bearer_token( - token, request.scopes, request) - - def estimate_type(self, request): - 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 - return 0 +JWTToken = NewJWTToken From 7cedf7ae22fce0bb9c2b148d057f172f204b143e Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 17:37:49 -0300 Subject: [PATCH 09/14] Move to openid connect test --- .../rfc6749/endpoints/test_claims_handling.py | 105 ----- .../test_openid_connect_params_handling.py | 85 ---- .../grant_types/test_openid_connect.py | 403 ------------------ 3 files changed, 593 deletions(-) delete mode 100644 tests/oauth2/rfc6749/endpoints/test_claims_handling.py delete mode 100644 tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py delete mode 100644 tests/oauth2/rfc6749/grant_types/test_openid_connect.py diff --git a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py deleted file mode 100644 index ff72673f..00000000 --- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Ensure OpenID Connect Authorization Request 'claims' are preserved across authorization. - -The claims parameter is an optional query param for the Authorization Request endpoint - but if it is provided and is valid it needs to be deserialized (from urlencoded JSON) - and persisted with the authorization code itself, then in the subsequent Access Token - request the claims should be transferred (via the oauthlib request) to be persisted - with the Access Token when it is created. -""" -from __future__ import absolute_import, unicode_literals - -import mock - -from oauthlib.oauth2 import InvalidRequestError, RequestValidator, Server - -from ....unittest import TestCase -from .test_utils import get_fragment_credentials, get_query_credentials - - -class TestClaimsHandling(TestCase): - - DEFAULT_REDIRECT_URI = 'http://i.b./path' - - def set_scopes(self, scopes): - def set_request_scopes(client_id, code, client, request): - request.scopes = scopes - return True - return set_request_scopes - - def set_user(self, request): - request.user = 'foo' - request.client_id = 'bar' - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - def save_claims_with_code(self, client_id, code, request, *args, **kwargs): - # a real validator would save the claims with the code during save_authorization_code() - self.claims_from_auth_code_request = request.claims - self.scopes = request.scopes.split() - - def retrieve_claims_saved_with_code(self, client_id, code, client, request, *args, **kwargs): - request.claims = self.claims_from_auth_code_request - request.scopes = self.scopes - - return True - - def save_claims_with_bearer_token(self, token, request, *args, **kwargs): - # a real validator would save the claims with the access token during save_bearer_token() - self.claims_saved_with_bearer_token = request.claims - - def setUp(self): - self.validator = mock.MagicMock(spec=RequestValidator) - self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI - self.validator.authenticate_client.side_effect = self.set_client - - self.validator.save_authorization_code.side_effect = self.save_claims_with_code - self.validator.validate_code.side_effect = self.retrieve_claims_saved_with_code - self.validator.save_token.side_effect = self.save_claims_with_bearer_token - - self.server = Server(self.validator) - - def test_claims_stored_on_code_creation(self): - - claims = { - "id_token": { - "claim_1": None, - "claim_2": { - "essential": True - } - }, - "userinfo": { - "claim_3": { - "essential": True - }, - "claim_4": None - } - } - - claims_urlquoted='%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D' - uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s' - - h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope') - - self.assertDictEqual(self.claims_from_auth_code_request, claims) - - code = get_query_credentials(h['Location'])['code'][0] - token_uri = 'http://example.com/path' - _, body, _ = self.server.create_token_response(token_uri, - body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code) - - self.assertDictEqual(self.claims_saved_with_bearer_token, claims) - - def test_invalid_claims(self): - uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=this-is-not-json' - - h, b, s = self.server.create_authorization_response(uri, scopes='openid test_scope') - error = get_query_credentials(h['Location'])['error'][0] - error_desc = get_query_credentials(h['Location'])['error_description'][0] - self.assertEqual(error, 'invalid_request') - self.assertEqual(error_desc, "Malformed claims parameter") diff --git a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py b/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py deleted file mode 100644 index 89431b64..00000000 --- a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import mock - -from oauthlib.oauth2 import InvalidRequestError -from oauthlib.oauth2.rfc6749.endpoints.authorization import \ - AuthorizationEndpoint -from oauthlib.oauth2.rfc6749.grant_types import OpenIDConnectAuthCode -from oauthlib.oauth2.rfc6749.tokens import BearerToken - -from ....unittest import TestCase - -try: - from urllib.parse import urlencode -except ImportError: - from urllib import urlencode - - - - -class OpenIDConnectEndpointTest(TestCase): - - def setUp(self): - self.mock_validator = mock.MagicMock() - self.mock_validator.authenticate_client.side_effect = self.set_client - grant = OpenIDConnectAuthCode(request_validator=self.mock_validator) - bearer = BearerToken(self.mock_validator) - self.endpoint = AuthorizationEndpoint(grant, bearer, - response_types={'code': grant}) - params = { - 'prompt': 'consent', - 'display': 'touch', - 'nonce': 'abcd', - 'state': 'abc', - 'redirect_uri': 'https://a.b/cb', - 'response_type': 'code', - 'client_id': 'abcdef', - 'scope': 'hello openid' - } - self.url = 'http://a.b/path?' + urlencode(params) - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - @mock.patch('oauthlib.common.generate_token') - def test_authorization_endpoint_handles_prompt(self, generate_token): - generate_token.return_value = "MOCK_CODE" - # In the GET view: - scopes, creds = self.endpoint.validate_authorization_request(self.url) - # In the POST view: - creds['scopes'] = scopes - h, b, s = self.endpoint.create_authorization_response(self.url, - credentials=creds) - expected = 'https://a.b/cb?state=abc&code=MOCK_CODE' - self.assertURLEqual(h['Location'], expected) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - def test_prompt_none_exclusiveness(self): - """ - Test that prompt=none can't be used with another prompt value. - """ - params = { - 'prompt': 'none consent', - 'state': 'abc', - 'redirect_uri': 'https://a.b/cb', - 'response_type': 'code', - 'client_id': 'abcdef', - 'scope': 'hello openid' - } - url = 'http://a.b/path?' + urlencode(params) - with self.assertRaises(InvalidRequestError): - self.endpoint.validate_authorization_request(url) - - def test_oidc_params_preservation(self): - """ - Test that the nonce parameter is passed through. - """ - scopes, creds = self.endpoint.validate_authorization_request(self.url) - - self.assertEqual(creds['prompt'], {'consent'}) - self.assertEqual(creds['nonce'], 'abcd') - self.assertEqual(creds['display'], 'touch') diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py deleted file mode 100644 index 573d491d..00000000 --- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py +++ /dev/null @@ -1,403 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import json - -import mock - -from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher, - AuthorizationCodeGrant, - ImplicitGrant, - ImplicitTokenGrantDispatcher, - OIDCNoPrompt, - OpenIDConnectAuthCode, - OpenIDConnectHybrid, - OpenIDConnectImplicit) -from oauthlib.oauth2.rfc6749.tokens import BearerToken - -from ....unittest import TestCase -from .test_authorization_code import AuthorizationCodeGrantTest -from .test_implicit import ImplicitGrantTest - - -class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDAuthCodeInterferenceTest, self).setUp() - self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) - - -class OpenIDImplicitInterferenceTest(ImplicitGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDImplicitInterferenceTest, self).setUp() - self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator) - - -class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDHybridInterferenceTest, self).setUp() - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - - -def get_id_token_mock(token, token_handler, request): - return "MOCKED_TOKEN" - - -class OpenIDAuthCodeTest(TestCase): - - def setUp(self): - self.request = Request('http://a.b/path') - self.request.scopes = ('hello', 'openid') - self.request.expires_in = 1800 - self.request.client_id = 'abcdef' - self.request.code = '1234' - self.request.response_type = 'code' - self.request.grant_type = 'authorization_code' - self.request.redirect_uri = 'https://a.b/cb' - self.request.state = 'abc' - - self.mock_validator = mock.MagicMock() - self.mock_validator.authenticate_client.side_effect = self.set_client - self.mock_validator.get_id_token.side_effect = get_id_token_mock - self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) - - self.url_query = 'https://a.b/cb?code=abc&state=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc' - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - @mock.patch('oauthlib.common.generate_token') - def test_authorization(self, generate_token): - - scope, info = self.auth.validate_authorization_request(self.request) - - generate_token.return_value = 'abc' - bearer = BearerToken(self.mock_validator) - self.request.response_mode = 'query' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_mode = 'fragment' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - @mock.patch('oauthlib.common.generate_token') - def test_no_prompt_authorization(self, generate_token): - generate_token.return_value = 'abc' - scope, info = self.auth.validate_authorization_request(self.request) - self.request.prompt = 'none' - self.assertRaises(OIDCNoPrompt, - self.auth.validate_authorization_request, - self.request) - - # prompt == none requires id token hint - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_mode = 'query' - self.request.id_token_hint = 'me@email.com' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - # Test alernative response modes - self.request.response_mode = 'fragment' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - - # Ensure silent authentication and authorization is done - self.mock_validator.validate_silent_login.return_value = False - self.mock_validator.validate_silent_authorization.return_value = True - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - self.mock_validator.validate_silent_login.return_value = True - self.mock_validator.validate_silent_authorization.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=consent_required', h['Location']) - - # ID token hint must match logged in user - self.mock_validator.validate_silent_authorization.return_value = True - self.mock_validator.validate_user_match.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - def set_scopes(self, client_id, code, client, request): - request.scopes = self.request.scopes - request.state = self.request.state - request.user = 'bob' - return True - - def test_create_token_response(self): - self.request.response_type = None - self.mock_validator.validate_code.side_effect = self.set_scopes - - bearer = BearerToken(self.mock_validator) - - h, token, s = self.auth.create_token_response(self.request, bearer) - token = json.loads(token) - self.assertEqual(self.mock_validator.save_token.call_count, 1) - self.assertIn('access_token', token) - self.assertIn('refresh_token', token) - self.assertIn('expires_in', token) - self.assertIn('scope', token) - self.assertIn('id_token', token) - self.assertIn('openid', token['scope']) - - self.mock_validator.reset_mock() - - self.request.scopes = ('hello', 'world') - h, token, s = self.auth.create_token_response(self.request, bearer) - token = json.loads(token) - self.assertEqual(self.mock_validator.save_token.call_count, 1) - self.assertIn('access_token', token) - self.assertIn('refresh_token', token) - self.assertIn('expires_in', token) - self.assertIn('scope', token) - self.assertNotIn('id_token', token) - self.assertNotIn('openid', token['scope']) - - -class OpenIDImplicitTest(TestCase): - - def setUp(self): - self.request = Request('http://a.b/path') - self.request.scopes = ('hello', 'openid') - self.request.expires_in = 1800 - self.request.client_id = 'abcdef' - self.request.response_type = 'id_token token' - self.request.redirect_uri = 'https://a.b/cb' - self.request.nonce = 'zxc' - self.request.state = 'abc' - - self.mock_validator = mock.MagicMock() - self.mock_validator.get_id_token.side_effect = get_id_token_mock - self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator) - - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_authorization(self, generate_token): - scope, info = self.auth.validate_authorization_request(self.request) - - generate_token.return_value = 'abc' - bearer = BearerToken(self.mock_validator) - - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_type = 'id_token' - token = 'MOCKED_TOKEN' - url = 'https://a.b/cb#state=abc&id_token=%s' % token - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], url, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.nonce = None - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - @mock.patch('oauthlib.common.generate_token') - def test_no_prompt_authorization(self, generate_token): - generate_token.return_value = 'abc' - scope, info = self.auth.validate_authorization_request(self.request) - self.request.prompt = 'none' - self.assertRaises(OIDCNoPrompt, - self.auth.validate_authorization_request, - self.request) - - # prompt == none requires id token hint - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.id_token_hint = 'me@email.com' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - # Test alernative response modes - self.request.response_mode = 'query' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - - # Ensure silent authentication and authorization is done - self.mock_validator.validate_silent_login.return_value = False - self.mock_validator.validate_silent_authorization.return_value = True - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - self.mock_validator.validate_silent_login.return_value = True - self.mock_validator.validate_silent_authorization.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=consent_required', h['Location']) - - # ID token hint must match logged in user - self.mock_validator.validate_silent_authorization.return_value = True - self.mock_validator.validate_user_match.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - -class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeTokenTest, self).setUp() - self.request.response_type = 'code token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - - -class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTest, self).setUp() - self.request.response_type = 'code id_token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token - - -class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() - self.request.response_type = 'code id_token token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - -class ImplicitTokenGrantDispatcherTest(TestCase): - def setUp(self): - self.request = Request('http://a.b/path') - request_validator = mock.MagicMock() - implicit_grant = ImplicitGrant(request_validator) - openid_connect_implicit = OpenIDConnectImplicit(request_validator) - - self.dispatcher = ImplicitTokenGrantDispatcher( - default_implicit_grant=implicit_grant, - oidc_implicit_grant=openid_connect_implicit - ) - - def test_create_authorization_response_openid(self): - self.request.scopes = ('hello', 'openid') - self.request.response_type = 'id_token' - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) - - def test_validate_authorization_request_openid(self): - self.request.scopes = ('hello', 'openid') - self.request.response_type = 'id_token' - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) - - def test_create_authorization_response_oauth(self): - self.request.scopes = ('hello', 'world') - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) - - def test_validate_authorization_request_oauth(self): - self.request.scopes = ('hello', 'world') - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) - - -class DispatcherTest(TestCase): - def setUp(self): - self.request = Request('http://a.b/path') - self.request.decoded_body = ( - ("client_id", "me"), - ("code", "code"), - ("redirect_url", "https://a.b/cb"), - ) - - self.request_validator = mock.MagicMock() - self.auth_grant = AuthorizationCodeGrant(self.request_validator) - self.openid_connect_auth = OpenIDConnectAuthCode(self.request_validator) - - -class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): - - def setUp(self): - super(AuthTokenGrantDispatcherOpenIdTest, self).setUp() - self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') - self.dispatcher = AuthTokenGrantDispatcher( - self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth - ) - - def test_create_token_response_openid(self): - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OpenIDConnectAuthCode)) - self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) - - -class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest): - - def setUp(self): - super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp() - self.request.decoded_body = ( - ("client_id", "me"), - ("code", ""), - ("redirect_url", "https://a.b/cb"), - ) - self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') - self.dispatcher = AuthTokenGrantDispatcher( - self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth - ) - - def test_create_token_response_openid_without_code(self): - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) - self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) - - -class AuthTokenGrantDispatcherOAuthTest(DispatcherTest): - - def setUp(self): - super(AuthTokenGrantDispatcherOAuthTest, self).setUp() - self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') - self.dispatcher = AuthTokenGrantDispatcher( - self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth - ) - - def test_create_token_response_oauth(self): - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) - self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) From 7035a7091507553f5d3b4279200fd0feeff2b4fe Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 17:39:51 -0300 Subject: [PATCH 10/14] Move openid connect exceptions to its own file --- oauthlib/oauth2/__init__.py | 2 +- oauthlib/oauth2/rfc6749/errors.py | 123 ++++++------------------------ 2 files changed, 24 insertions(+), 101 deletions(-) diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index dc7b431c..303c6a1d 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -24,7 +24,7 @@ from .rfc6749.endpoints import MobileApplicationServer from .rfc6749.endpoints import LegacyApplicationServer from .rfc6749.endpoints import BackendApplicationServer -from .rfc6749.errors import AccessDeniedError, AccountSelectionRequired, ConsentRequired, FatalClientError, FatalOpenIDClientError, InsecureTransportError, InteractionRequired, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, LoginRequired, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, OAuth2Error, OpenIDClientError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError +from .rfc6749.errors import AccessDeniedError, OAuth2Error, FatalClientError, InsecureTransportError, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError from .rfc6749.grant_types import AuthorizationCodeGrant from .rfc6749.grant_types import ImplicitGrant from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 1d5e98d5..5a0cca2b 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -274,106 +274,6 @@ class UnsupportedTokenTypeError(OAuth2Error): error = 'unsupported_token_type' -class FatalOpenIDClientError(FatalClientError): - pass - - -class OpenIDClientError(OAuth2Error): - pass - - -class InteractionRequired(OpenIDClientError): - """ - The Authorization Server requires End-User interaction to proceed. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User interaction. - """ - error = 'interaction_required' - status_code = 401 - - -class LoginRequired(OpenIDClientError): - """ - The Authorization Server requires End-User authentication. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User authentication. - """ - error = 'login_required' - status_code = 401 - - -class AccountSelectionRequired(OpenIDClientError): - """ - The End-User is REQUIRED to select a session at the Authorization Server. - - The End-User MAY be authenticated at the Authorization Server with - different associated accounts, but the End-User did not select a session. - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface to prompt for a session to - use. - """ - error = 'account_selection_required' - - -class ConsentRequired(OpenIDClientError): - """ - The Authorization Server requires End-User consent. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User consent. - """ - error = 'consent_required' - status_code = 401 - - -class InvalidRequestURI(OpenIDClientError): - """ - The request_uri in the Authorization Request returns an error or - contains invalid data. - """ - error = 'invalid_request_uri' - description = 'The request_uri in the Authorization Request returns an ' \ - 'error or contains invalid data.' - - -class InvalidRequestObject(OpenIDClientError): - """ - The request parameter contains an invalid Request Object. - """ - error = 'invalid_request_object' - description = 'The request parameter contains an invalid Request Object.' - - -class RequestNotSupported(OpenIDClientError): - """ - The OP does not support use of the request parameter. - """ - error = 'request_not_supported' - description = 'The request parameter is not supported.' - - -class RequestURINotSupported(OpenIDClientError): - """ - The OP does not support use of the request_uri parameter. - """ - error = 'request_uri_not_supported' - description = 'The request_uri parameter is not supported.' - - -class RegistrationNotSupported(OpenIDClientError): - """ - The OP does not support use of the registration parameter. - """ - error = 'registration_not_supported' - description = 'The registration parameter is not supported.' - - class InvalidTokenError(OAuth2Error): """ The access token provided is expired, revoked, malformed, or @@ -402,6 +302,29 @@ class InsufficientScopeError(OAuth2Error): "the access token.") +class ConsentRequired(OAuth2Error): + """ + The Authorization Server requires End-User consent. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User consent. + """ + error = 'consent_required' + status_code = 401 + +class LoginRequired(OAuth2Error): + """ + The Authorization Server requires End-User authentication. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User authentication. + """ + error = 'login_required' + status_code = 401 + + def raise_from_error(error, params=None): import inspect import sys From e82e6d1e55ea4bcab2b48af0b5aa0f3fa882e2ce Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 17:41:16 -0300 Subject: [PATCH 11/14] Remove openid connect from oauth2 server --- .../rfc6749/endpoints/pre_configured.py | 35 ++----- tests/oauth2/rfc6749/test_server.py | 94 +++++++++---------- 2 files changed, 54 insertions(+), 75 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 7758f581..e2cc9db7 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -8,15 +8,12 @@ """ from __future__ import absolute_import, unicode_literals -from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, - AuthTokenGrantDispatcher, +from ..grant_types import (AuthorizationCodeGrant, ClientCredentialsGrant, - ImplicitTokenGrantDispatcher, ImplicitGrant, - OpenIDConnectAuthCode, OpenIDConnectImplicit, - OpenIDConnectHybrid, + ImplicitGrant, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) -from ..tokens import BearerToken, JWTToken +from ..tokens import BearerToken from .authorization import AuthorizationEndpoint from .introspect import IntrospectEndpoint from .resource import ResourceEndpoint @@ -51,46 +48,28 @@ def __init__(self, request_validator, token_expires_in=None, request_validator) credentials_grant = ClientCredentialsGrant(request_validator) refresh_grant = RefreshTokenGrant(request_validator) - openid_connect_auth = OpenIDConnectAuthCode(request_validator) - openid_connect_implicit = OpenIDConnectImplicit(request_validator) - openid_connect_hybrid = OpenIDConnectHybrid(request_validator) bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) - jwt = JWTToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - - auth_grant_choice = AuthCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) - implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) - - # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations - # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination AuthorizationEndpoint.__init__(self, default_response_type='code', response_types={ - 'code': auth_grant_choice, - 'token': implicit_grant_choice, - 'id_token': openid_connect_implicit, - 'id_token token': openid_connect_implicit, - 'code token': openid_connect_hybrid, - 'code id_token': openid_connect_hybrid, - 'code id_token token': openid_connect_hybrid, + 'code': auth_grant, + 'token': implicit_grant, 'none': auth_grant }, default_token_type=bearer) - token_grant_choice = AuthTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', grant_types={ - 'authorization_code': token_grant_choice, + '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, 'JWT': jwt}) + token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index 25240178..bc7a2b7c 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -14,8 +14,6 @@ from oauthlib.oauth2.rfc6749.grant_types import (AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - OpenIDConnectAuthCode, - OpenIDConnectImplicit, ResourceOwnerPasswordCredentialsGrant) from ...unittest import TestCase @@ -27,39 +25,34 @@ def setUp(self): self.mock_validator = mock.MagicMock() self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) auth_code.save_authorization_code = mock.MagicMock() implicit = ImplicitGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) implicit.save_token = mock.MagicMock() - openid_connect_auth = OpenIDConnectAuthCode(self.mock_validator) - openid_connect_implicit = OpenIDConnectImplicit(self.mock_validator) - response_types = { - 'code': auth_code, - 'token': implicit, - 'id_token': openid_connect_implicit, - 'id_token token': openid_connect_implicit, - 'code token': openid_connect_auth, - 'code id_token': openid_connect_auth, - 'code token id_token': openid_connect_auth, - 'none': auth_code + 'code': auth_code, + 'token': implicit, + 'none': auth_code } self.expires_in = 1800 - token = tokens.BearerToken(self.mock_validator, - expires_in=self.expires_in) + token = tokens.BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) self.endpoint = AuthorizationEndpoint( - default_response_type='code', - default_token_type=token, - response_types=response_types) + default_response_type='code', + default_token_type=token, + response_types=response_types + ) @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz') @@ -68,7 +61,7 @@ def test_implicit_grant(self): uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) @@ -76,7 +69,7 @@ def test_none_grant(self): uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True) self.assertEqual(body, None) @@ -96,9 +89,9 @@ def test_missing_type(self): uri = 'http://i.b/l?client_id=me&scope=all+of+them' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( - side_effect=errors.InvalidRequestError()) + side_effect=errors.InvalidRequestError()) headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') @@ -106,9 +99,9 @@ def test_invalid_type(self): uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( - side_effect=errors.UnsupportedResponseTypeError()) + side_effect=errors.UnsupportedResponseTypeError()) headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type') @@ -126,27 +119,32 @@ def set_user(request): self.mock_validator.authenticate_client.side_effect = set_user self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) password = ResourceOwnerPasswordCredentialsGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) client = ClientCredentialsGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) supported_types = { - 'authorization_code': auth_code, - 'password': password, - 'client_credentials': client, + 'authorization_code': auth_code, + 'password': password, + 'client_credentials': client, } self.expires_in = 1800 - token = tokens.BearerToken(self.mock_validator, - expires_in=self.expires_in) - self.endpoint = TokenEndpoint('authorization_code', - default_token_type=token, grant_types=supported_types) + token = tokens.BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = TokenEndpoint( + 'authorization_code', + default_token_type=token, + grant_types=supported_types + ) @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -173,7 +171,7 @@ def test_authorization_grant(self): def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -187,7 +185,7 @@ def test_password_grant(self): def test_client_grant(self): body = 'grant_type=client_credentials&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -278,7 +276,7 @@ def set_user(request): def test_authorization_grant(self): body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -292,7 +290,7 @@ def test_authorization_grant(self): body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -307,7 +305,7 @@ def test_authorization_grant(self): def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -322,7 +320,7 @@ def test_password_grant(self): def test_scopes_and_user_id_stored_in_access_token(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) access_token = json.loads(body)['access_token'] @@ -335,7 +333,7 @@ def test_scopes_and_user_id_stored_in_access_token(self): def test_client_grant(self): body = 'grant_type=client_credentials&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -363,8 +361,10 @@ def setUp(self): self.mock_validator = mock.MagicMock() self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) token = tokens.BearerToken(request_validator=self.mock_validator) - self.endpoint = ResourceEndpoint(default_token='Bearer', - token_types={'Bearer': token}) + self.endpoint = ResourceEndpoint( + default_token='Bearer', + token_types={'Bearer': token} + ) def test_defaults(self): uri = 'http://a.b/path?some=query' From cdc0d0c925f685909239d117877c6157a2c12dd2 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 17:42:01 -0300 Subject: [PATCH 12/14] Remove JWTToken from oauth tokens --- oauthlib/oauth2/rfc6749/tokens.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 1f9e6bc3..1d2b5eb4 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -16,8 +16,6 @@ from oauthlib import common from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type -from oauthlib.openid.connect.core.tokens import JWTToken as NewJWTToken - from . import utils try: @@ -317,6 +315,3 @@ def estimate_type(self, request): return 5 else: return 0 - - -JWTToken = NewJWTToken From 24a1b2ac3d4b71c711cb4ff224f861a6be6d7dbe Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 17:44:20 -0300 Subject: [PATCH 13/14] Remove grant_types.openid_connect file --- .../oauth2/rfc6749/grant_types/__init__.py | 8 ---- .../rfc6749/grant_types/openid_connect.py | 45 ------------------- 2 files changed, 53 deletions(-) delete mode 100644 oauthlib/oauth2/rfc6749/grant_types/openid_connect.py diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py index 2e4bfe4a..2ec8e4fa 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -10,11 +10,3 @@ from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant from .client_credentials import ClientCredentialsGrant from .refresh_token import RefreshTokenGrant -from .openid_connect import OpenIDConnectBase -from .openid_connect import OpenIDConnectAuthCode -from .openid_connect import OpenIDConnectImplicit -from .openid_connect import OpenIDConnectHybrid -from .openid_connect import OIDCNoPrompt -from .openid_connect import AuthCodeGrantDispatcher -from .openid_connect import AuthTokenGrantDispatcher -from .openid_connect import ImplicitTokenGrantDispatcher diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py deleted file mode 100644 index 63c56f3b..00000000 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -oauthlib.oauth2.rfc6749.grant_types.openid_connect -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -""" -from __future__ import absolute_import, unicode_literals - -import logging -import warnings - -from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant -from oauthlib.openid.connect.core.grant_types.base import GrantTypeBase -from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant -from oauthlib.openid.connect.core.grant_types.dispatchers import ( - ImplicitTokenGrantDispatcher as NewImplicitTokenGrantDispatcher, - AuthorizationTokenGrantDispatcher, - AuthorizationCodeGrantDispatcher -) -from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt as NewOIDCNoPrompt - -log = logging.getLogger(__name__) - -warnings.warn( - "Should not use this module! Use: " - "oauthlib.openid.connect.core.grant_types.authorization_code.AuthorizationCodeGrant, " - "oauthlib.openid.connect.core.grant_types.base.GrantTypeBase, " - "oauthlib.openid.connect.core.grant_types.implicit.ImplicitGrant, " - "oauthlib.openid.connect.core.grant_types.hybrid.HybridGrant, " - "oauthlib.openid.connect.core.grant_types.dispatchers.ImplicitTokenGrantDispatcher, " - "oauthlib.openid.connect.core.grant_types.dispatchers.AuthorizationTokenGrantDispatcher, " - "oauthlib.openid.connect.core.grant_types.dispatchers.AuthorizationCodeGrantDispatcher, " - "oauthlib.openid.connect.core.grant_types.exceptions.OIDCNoPrompt " - "istead.", - DeprecationWarning) - - -OIDCNoPrompt = NewOIDCNoPrompt -AuthCodeGrantDispatcher = AuthorizationCodeGrantDispatcher -ImplicitTokenGrantDispatcher = NewImplicitTokenGrantDispatcher -AuthTokenGrantDispatcher = AuthorizationTokenGrantDispatcher -OpenIDConnectBase = GrantTypeBase -OpenIDConnectAuthCode = AuthorizationCodeGrant -OpenIDConnectImplicit = ImplicitGrant -OpenIDConnectHybrid = HybridGrant From 5f857f8fb96ce5b2029a5b5f74586e549f2d56fe Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sat, 17 Mar 2018 17:45:52 -0300 Subject: [PATCH 14/14] Add oauthlib/openid estructure and tests --- oauthlib/openid/__init__.py | 0 oauthlib/openid/connect/__init__.py | 0 oauthlib/openid/connect/core/__init__.py | 0 .../connect/core/endpoints/pre_configured.py | 103 +++++++ oauthlib/openid/connect/core/exceptions.py | 152 ++++++++++ .../connect/core/grant_types/__init__.py | 17 ++ .../core/grant_types/authorization_code.py | 24 ++ .../openid/connect/core/grant_types/base.py | 283 ++++++++++++++++++ .../connect/core/grant_types/dispatchers.py | 86 ++++++ .../connect/core/grant_types/exceptions.py | 32 ++ .../openid/connect/core/grant_types/hybrid.py | 36 +++ .../connect/core/grant_types/implicit.py | 28 ++ .../openid/connect/core/request_validator.py | 188 ++++++++++++ oauthlib/openid/connect/core/tokens.py | 54 ++++ tests/openid/__init__.py | 0 tests/openid/connect/__init__.py | 0 tests/openid/connect/core/__init__.py | 0 .../core/endpoints/test_claims_handling.py | 109 +++++++ .../test_openid_connect_params_handling.py | 85 ++++++ .../grant_types/test_authorization_code.py | 153 ++++++++++ .../core/grant_types/test_dispatchers.py | 125 ++++++++ .../connect/core/grant_types/test_hybrid.py | 13 + .../connect/core/grant_types/test_implicit.py | 148 +++++++++ .../connect/core/test_request_validator.py | 52 ++++ tests/openid/connect/core/test_server.py | 178 +++++++++++ tests/openid/connect/core/test_tokens.py | 133 ++++++++ 26 files changed, 1999 insertions(+) create mode 100644 oauthlib/openid/__init__.py create mode 100644 oauthlib/openid/connect/__init__.py create mode 100644 oauthlib/openid/connect/core/__init__.py create mode 100644 oauthlib/openid/connect/core/endpoints/pre_configured.py create mode 100644 oauthlib/openid/connect/core/exceptions.py create mode 100644 oauthlib/openid/connect/core/grant_types/__init__.py create mode 100644 oauthlib/openid/connect/core/grant_types/authorization_code.py create mode 100644 oauthlib/openid/connect/core/grant_types/base.py create mode 100644 oauthlib/openid/connect/core/grant_types/dispatchers.py create mode 100644 oauthlib/openid/connect/core/grant_types/exceptions.py create mode 100644 oauthlib/openid/connect/core/grant_types/hybrid.py create mode 100644 oauthlib/openid/connect/core/grant_types/implicit.py create mode 100644 oauthlib/openid/connect/core/request_validator.py create mode 100644 oauthlib/openid/connect/core/tokens.py create mode 100644 tests/openid/__init__.py create mode 100644 tests/openid/connect/__init__.py create mode 100644 tests/openid/connect/core/__init__.py create mode 100644 tests/openid/connect/core/endpoints/test_claims_handling.py create mode 100644 tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py create mode 100644 tests/openid/connect/core/grant_types/test_authorization_code.py create mode 100644 tests/openid/connect/core/grant_types/test_dispatchers.py create mode 100644 tests/openid/connect/core/grant_types/test_hybrid.py create mode 100644 tests/openid/connect/core/grant_types/test_implicit.py create mode 100644 tests/openid/connect/core/test_request_validator.py create mode 100644 tests/openid/connect/core/test_server.py create mode 100644 tests/openid/connect/core/test_tokens.py diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/__init__.py b/oauthlib/openid/connect/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/core/__init__.py b/oauthlib/openid/connect/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py new file mode 100644 index 00000000..3bcd24de --- /dev/null +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.endpoints.pre_configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various endpoints needed +for providing OpenID Connect servers. +""" +from __future__ import absolute_import, unicode_literals + +from ..grant_types import ( + AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant as OAuth2ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant +) + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + AuthorizationCodeGrantDispatcher, + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.tokens import JWTToken + +from ..tokens import BearerToken +from .authorization import AuthorizationEndpoint +from .resource import ResourceEndpoint +from .revocation import RevocationEndpoint +from .token import TokenEndpoint + + +class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, + RevocationEndpoint): + + """An all-in-one endpoint featuring all four major grant types.""" + + def __init__(self, request_validator, token_expires_in=None, + token_generator=None, refresh_token_generator=None, + *args, **kwargs): + """Construct a new all-grants-in-one server. + + :param request_validator: An implementation of + oauthlib.oauth2.RequestValidator. + :param token_expires_in: An int or a function to generate a token + expiration offset (in seconds) given a + oauthlib.common.Request object. + :param token_generator: A function to generate a token from a request. + :param refresh_token_generator: A function to generate a token from a + request for the refresh token. + :param kwargs: Extra parameters to pass to authorization-, + token-, resource-, and revocation-endpoint constructors. + """ + auth_grant = OAuth2AuthorizationCodeGrant(request_validator) + implicit_grant = OAuth2ImplicitGrant(request_validator) + password_grant = ResourceOwnerPasswordCredentialsGrant( + request_validator) + credentials_grant = ClientCredentialsGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + openid_connect_auth = AuthorizationCodeGrant(request_validator) + openid_connect_implicit = ImplicitGrant(request_validator) + openid_connect_hybrid = HybridGrant(request_validator) + + bearer = BearerToken(request_validator, token_generator, + token_expires_in, refresh_token_generator) + + jwt = JWTToken(request_validator, token_generator, + token_expires_in, refresh_token_generator) + + auth_grant_choice = AuthorizationCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) + implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) + + # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations + # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={ + 'code': auth_grant_choice, + 'token': implicit_grant_choice, + 'id_token': openid_connect_implicit, + 'id_token token': openid_connect_implicit, + 'code token': openid_connect_hybrid, + 'code id_token': openid_connect_hybrid, + 'code id_token token': openid_connect_hybrid, + 'none': auth_grant + }, + default_token_type=bearer) + + token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) + + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': token_grant_choice, + '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, 'JWT': jwt}) + RevocationEndpoint.__init__(self, request_validator) diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py new file mode 100644 index 00000000..8b08d21f --- /dev/null +++ b/oauthlib/openid/connect/core/exceptions.py @@ -0,0 +1,152 @@ +# coding=utf-8 +""" +oauthlib.oauth2.rfc6749.errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 + +from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error + + +class FatalOpenIDClientError(FatalClientError): + pass + + +class OpenIDClientError(OAuth2Error): + pass + + +class InteractionRequired(OpenIDClientError): + """ + The Authorization Server requires End-User interaction to proceed. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User interaction. + """ + error = 'interaction_required' + status_code = 401 + + +class LoginRequired(OpenIDClientError): + """ + The Authorization Server requires End-User authentication. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User authentication. + """ + error = 'login_required' + status_code = 401 + + +class AccountSelectionRequired(OpenIDClientError): + """ + The End-User is REQUIRED to select a session at the Authorization Server. + + The End-User MAY be authenticated at the Authorization Server with + different associated accounts, but the End-User did not select a session. + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface to prompt for a session to + use. + """ + error = 'account_selection_required' + + +class ConsentRequired(OpenIDClientError): + """ + The Authorization Server requires End-User consent. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User consent. + """ + error = 'consent_required' + status_code = 401 + + +class InvalidRequestURI(OpenIDClientError): + """ + The request_uri in the Authorization Request returns an error or + contains invalid data. + """ + error = 'invalid_request_uri' + description = 'The request_uri in the Authorization Request returns an ' \ + 'error or contains invalid data.' + + +class InvalidRequestObject(OpenIDClientError): + """ + The request parameter contains an invalid Request Object. + """ + error = 'invalid_request_object' + description = 'The request parameter contains an invalid Request Object.' + + +class RequestNotSupported(OpenIDClientError): + """ + The OP does not support use of the request parameter. + """ + error = 'request_not_supported' + description = 'The request parameter is not supported.' + + +class RequestURINotSupported(OpenIDClientError): + """ + The OP does not support use of the request_uri parameter. + """ + error = 'request_uri_not_supported' + description = 'The request_uri parameter is not supported.' + + +class RegistrationNotSupported(OpenIDClientError): + """ + The OP does not support use of the registration parameter. + """ + error = 'registration_not_supported' + description = 'The registration parameter is not supported.' + + +class InvalidTokenError(OAuth2Error): + """ + The access token provided is expired, revoked, malformed, or + invalid for other reasons. The resource SHOULD respond with + the HTTP 401 (Unauthorized) status code. The client MAY + request a new access token and retry the protected resource + request. + """ + error = 'invalid_token' + status_code = 401 + description = ("The access token provided is expired, revoked, malformed, " + "or invalid for other reasons.") + + +class InsufficientScopeError(OAuth2Error): + """ + The request requires higher privileges than provided by the + access token. The resource server SHOULD respond with the HTTP + 403 (Forbidden) status code and MAY include the "scope" + attribute with the scope necessary to access the protected + resource. + """ + error = 'insufficient_scope' + status_code = 403 + description = ("The request requires higher privileges than provided by " + "the access token.") + + +def raise_from_error(error, params=None): + import inspect + import sys + kwargs = { + 'description': params.get('error_description'), + 'uri': params.get('error_uri'), + 'state': params.get('state') + } + for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): + if cls.error == error: + raise cls(**kwargs) diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py new file mode 100644 index 00000000..7fc183d6 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -0,0 +1,17 @@ +# -*- 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 .base import GrantTypeBase +from .hybrid import HybridGrant +from .exceptions import OIDCNoPrompt +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + AuthorizationCodeGrantDispatcher, + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) diff --git a/oauthlib/openid/connect/core/grant_types/authorization_code.py b/oauthlib/openid/connect/core/grant_types/authorization_code.py new file mode 100644 index 00000000..b0b10155 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/authorization_code.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant + +from .base import GrantTypeBase + +log = logging.getLogger(__name__) + + +class AuthorizationCodeGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2AuthorizationCodeGrant( + request_validator=request_validator, **kwargs) + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py new file mode 100644 index 00000000..2bb48b1e --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -0,0 +1,283 @@ +from .exceptions import OIDCNoPrompt + +import datetime +import logging +from json import loads + +from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired + +log = logging.getLogger(__name__) + + +class GrantTypeBase(object): + + # Just proxy the majority of method calls through to the + # proxy_target grant type handler, which will usually be either + # the standard OAuth2 AuthCode or Implicit grant types. + def __getattr__(self, attr): + return getattr(self.proxy_target, attr) + + def __setattr__(self, attr, value): + proxied_attrs = set(('refresh_token', 'response_types')) + if attr in proxied_attrs: + setattr(self.proxy_target, attr, value) + else: + super(OpenIDConnectBase, self).__setattr__(attr, value) + + def validate_authorization_request(self, request): + """Validates the OpenID Connect authorization request parameters. + + :returns: (list of scopes, dict of request info) + """ + # If request.prompt is 'none' then no login/authorization form should + # be presented to the user. Instead, a silent login/authorization + # should be performed. + if request.prompt == 'none': + raise OIDCNoPrompt() + else: + return self.proxy_target.validate_authorization_request(request) + + def _inflate_claims(self, request): + # this may be called multiple times in a single request so make sure we only de-serialize the claims once + if request.claims and not isinstance(request.claims, dict): + # specific claims are requested during the Authorization Request and may be requested for inclusion + # in either the id_token or the UserInfo endpoint response + # see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter + try: + request.claims = loads(request.claims) + except Exception as ex: + raise InvalidRequestError(description="Malformed claims parameter", + uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter") + + def add_id_token(self, token, token_handler, request): + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return token + + # Only add an id token on auth/token step if asked for. + if request.response_type and 'id_token' not in request.response_type: + return token + + if 'state' not in token: + token['state'] = request.state + + if request.max_age: + d = datetime.datetime.utcnow() + token['auth_time'] = d.isoformat("T") + "Z" + + # TODO: acr claims (probably better handled by server code using oauthlib in get_id_token) + + token['id_token'] = self.request_validator.get_id_token(token, token_handler, request) + + return token + + def openid_authorization_validator(self, request): + """Perform OpenID Connect specific authorization request validation. + + nonce + OPTIONAL. String value used to associate a Client session with + an ID Token, and to mitigate replay attacks. The value is + passed through unmodified from the Authentication Request to + the ID Token. Sufficient entropy MUST be present in the nonce + values used to prevent attackers from guessing values + + display + OPTIONAL. ASCII string value that specifies how the + Authorization Server displays the authentication and consent + user interface pages to the End-User. The defined values are: + + page - The Authorization Server SHOULD display the + authentication and consent UI consistent with a full User + Agent page view. If the display parameter is not specified, + this is the default display mode. + + popup - The Authorization Server SHOULD display the + authentication and consent UI consistent with a popup User + Agent window. The popup User Agent window should be of an + appropriate size for a login-focused dialog and should not + obscure the entire window that it is popping up over. + + touch - The Authorization Server SHOULD display the + authentication and consent UI consistent with a device that + leverages a touch interface. + + wap - The Authorization Server SHOULD display the + authentication and consent UI consistent with a "feature + phone" type display. + + The Authorization Server MAY also attempt to detect the + capabilities of the User Agent and present an appropriate + display. + + prompt + OPTIONAL. Space delimited, case sensitive list of ASCII string + values that specifies whether the Authorization Server prompts + the End-User for reauthentication and consent. The defined + values are: + + none - The Authorization Server MUST NOT display any + authentication or consent user interface pages. An error is + returned if an End-User is not already authenticated or the + Client does not have pre-configured consent for the + requested Claims or does not fulfill other conditions for + processing the request. The error code will typically be + login_required, interaction_required, or another code + defined in Section 3.1.2.6. This can be used as a method to + check for existing authentication and/or consent. + + login - The Authorization Server SHOULD prompt the End-User + for reauthentication. If it cannot reauthenticate the + End-User, it MUST return an error, typically + login_required. + + consent - The Authorization Server SHOULD prompt the + End-User for consent before returning information to the + Client. If it cannot obtain consent, it MUST return an + error, typically consent_required. + + select_account - The Authorization Server SHOULD prompt the + End-User to select a user account. This enables an End-User + who has multiple accounts at the Authorization Server to + select amongst the multiple accounts that they might have + current sessions for. If it cannot obtain an account + selection choice made by the End-User, it MUST return an + error, typically account_selection_required. + + The prompt parameter can be used by the Client to make sure + that the End-User is still present for the current session or + to bring attention to the request. If this parameter contains + none with any other value, an error is returned. + + max_age + OPTIONAL. Maximum Authentication Age. Specifies the allowable + elapsed time in seconds since the last time the End-User was + actively authenticated by the OP. If the elapsed time is + greater than this value, the OP MUST attempt to actively + re-authenticate the End-User. (The max_age request parameter + corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age + request parameter.) When max_age is used, the ID Token returned + MUST include an auth_time Claim Value. + + ui_locales + OPTIONAL. End-User's preferred languages and scripts for the + user interface, represented as a space-separated list of BCP47 + [RFC5646] language tag values, ordered by preference. For + instance, the value "fr-CA fr en" represents a preference for + French as spoken in Canada, then French (without a region + designation), followed by English (without a region + designation). An error SHOULD NOT result if some or all of the + requested locales are not supported by the OpenID Provider. + + id_token_hint + OPTIONAL. ID Token previously issued by the Authorization + Server being passed as a hint about the End-User's current or + past authenticated session with the Client. If the End-User + identified by the ID Token is logged in or is logged in by the + request, then the Authorization Server returns a positive + response; otherwise, it SHOULD return an error, such as + login_required. When possible, an id_token_hint SHOULD be + present when prompt=none is used and an invalid_request error + MAY be returned if it is not; however, the server SHOULD + respond successfully when possible, even if it is not present. + The Authorization Server need not be listed as an audience of + the ID Token when it is used as an id_token_hint value. If the + ID Token received by the RP from the OP is encrypted, to use it + as an id_token_hint, the Client MUST decrypt the signed ID + Token contained within the encrypted ID Token. The Client MAY + re-encrypt the signed ID token to the Authentication Server + using a key that enables the server to decrypt the ID Token, + and use the re-encrypted ID token as the id_token_hint value. + + login_hint + OPTIONAL. Hint to the Authorization Server about the login + identifier the End-User might use to log in (if necessary). + This hint can be used by an RP if it first asks the End-User + for their e-mail address (or other identifier) and then wants + to pass that value as a hint to the discovered authorization + service. It is RECOMMENDED that the hint value match the value + used for discovery. This value MAY also be a phone number in + the format specified for the phone_number Claim. The use of + this parameter is left to the OP's discretion. + + acr_values + OPTIONAL. Requested Authentication Context Class Reference + values. Space-separated string that specifies the acr values + that the Authorization Server is being requested to use for + processing this Authentication Request, with the values + appearing in order of preference. The Authentication Context + Class satisfied by the authentication performed is returned as + the acr Claim Value, as specified in Section 2. The acr Claim + is requested as a Voluntary Claim by this parameter. + """ + + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return {} + + prompt = request.prompt if request.prompt else [] + if hasattr(prompt, 'split'): + prompt = prompt.strip().split() + prompt = set(prompt) + + if 'none' in prompt: + + if len(prompt) > 1: + msg = "Prompt none is mutually exclusive with other values." + raise InvalidRequestError(request=request, description=msg) + + # prompt other than 'none' should be handled by the server code that + # uses oauthlib + if not request.id_token_hint: + msg = "Prompt is set to none yet id_token_hint is missing." + raise InvalidRequestError(request=request, description=msg) + + if not self.request_validator.validate_silent_login(request): + raise LoginRequired(request=request) + + if not self.request_validator.validate_silent_authorization(request): + raise ConsentRequired(request=request) + + self._inflate_claims(request) + + if not self.request_validator.validate_user_match( + request.id_token_hint, request.scopes, request.claims, request): + msg = "Session user does not match client supplied user." + raise LoginRequired(request=request, description=msg) + + request_info = { + 'display': request.display, + 'nonce': request.nonce, + 'prompt': prompt, + 'ui_locales': request.ui_locales.split() if request.ui_locales else [], + 'id_token_hint': request.id_token_hint, + 'login_hint': request.login_hint, + 'claims': request.claims + } + + return request_info + + def openid_implicit_authorization_validator(self, request): + """Additional validation when following the implicit flow. + """ + # Undefined in OpenID Connect, fall back to OAuth2 definition. + if request.response_type == 'token': + return {} + + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return {} + + # REQUIRED. String value used to associate a Client session with an ID + # Token, and to mitigate replay attacks. The value is passed through + # unmodified from the Authentication Request to the ID Token. + # Sufficient entropy MUST be present in the nonce values used to + # prevent attackers from guessing values. For implementation notes, see + # Section 15.5.2. + if not request.nonce: + desc = 'Request is missing mandatory nonce parameter.' + raise InvalidRequestError(request=request, description=desc) + + return {} + + +OpenIDConnectBase = GrantTypeBase diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py new file mode 100644 index 00000000..2c334063 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py @@ -0,0 +1,86 @@ +import logging +log = logging.getLogger(__name__) + + +class AuthorizationCodeGrantDispatcher(object): + """ + This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope + including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + """ + def __init__(self, default_auth_grant=None, oidc_auth_grant=None): + self.default_auth_grant = default_auth_grant + self.oidc_auth_grant = oidc_auth_grant + + def _handler_for_request(self, request): + handler = self.default_auth_grant + + if request.scopes and "openid" in request.scopes: + handler = self.oidc_auth_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_authorization_response(self, request, token_handler): + return self._handler_for_request(request).create_authorization_response(request, token_handler) + + def validate_authorization_request(self, request): + return self._handler_for_request(request).validate_authorization_request(request) + + +class ImplicitTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope + including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + """ + def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): + self.default_implicit_grant = default_implicit_grant + self.oidc_implicit_grant = oidc_implicit_grant + + def _handler_for_request(self, request): + handler = self.default_implicit_grant + + if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: + handler = self.oidc_implicit_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_authorization_response(self, request, token_handler): + return self._handler_for_request(request).create_authorization_response(request, token_handler) + + def validate_authorization_request(self, request): + return self._handler_for_request(request).validate_authorization_request(request) + + +class AuthorizationTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Token requests, those that authorization_code have a scope + including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. + """ + def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): + self.default_token_grant = default_token_grant + self.oidc_token_grant = oidc_token_grant + self.request_validator = request_validator + + def _handler_for_request(self, request): + handler = self.default_token_grant + scopes = () + parameters = dict(request.decoded_body) + client_id = parameters.get('client_id', None) + code = parameters.get('code', None) + redirect_uri = parameters.get('redirect_uri', None) + + # If code is not pressent fallback to `default_token_grant` wich will + # raise an error for the missing `code` in `create_token_response` step. + if code: + scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) + + if 'openid' in scopes: + handler = self.oidc_token_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_token_response(self, request, token_handler): + handler = self._handler_for_request(request) + return handler.create_token_response(request, token_handler) diff --git a/oauthlib/openid/connect/core/grant_types/exceptions.py b/oauthlib/openid/connect/core/grant_types/exceptions.py new file mode 100644 index 00000000..809f1b3b --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/exceptions.py @@ -0,0 +1,32 @@ +class OIDCNoPrompt(Exception): + """Exception used to inform users that no explicit authorization is needed. + + Normally users authorize requests after validation of the request is done. + Then post-authorization validation is again made and a response containing + an auth code or token is created. However, when OIDC clients request + no prompting of user authorization the final response is created directly. + + Example (without the shortcut for no prompt) + + scopes, req_info = endpoint.validate_authorization_request(url, ...) + authorization_view = create_fancy_auth_form(scopes, req_info) + return authorization_view + + Example (with the no prompt shortcut) + try: + scopes, req_info = endpoint.validate_authorization_request(url, ...) + authorization_view = create_fancy_auth_form(scopes, req_info) + return authorization_view + except OIDCNoPrompt: + # Note: Location will be set for you + headers, body, status = endpoint.create_authorization_response(url, ...) + redirect_view = create_redirect(headers, body, status) + return redirect_view + """ + + def __init__(self): + msg = ("OIDC request for no user interaction received. Do not ask user " + "for authorization, it should been done using silent " + "authentication through create_authorization_response. " + "See OIDCNoPrompt.__doc__ for more details.") + super(OIDCNoPrompt, self).__init__(msg) diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py new file mode 100644 index 00000000..54669ae4 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant + +from .base import GrantTypeBase +from ..request_validator import RequestValidator + +log = logging.getLogger(__name__) + + +class HybridGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.request_validator = request_validator or RequestValidator() + + self.proxy_target = OAuth2AuthorizationCodeGrant( + request_validator=request_validator, **kwargs) + # All hybrid response types should be fragment-encoded. + self.proxy_target.default_response_mode = "fragment" + self.register_response_type('code id_token') + self.register_response_type('code token') + self.register_response_type('code id_token token') + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + # Hybrid flows can return the id_token from the authorization + # endpoint as part of the 'code' response + self.register_code_modifier(self.add_token) + self.register_code_modifier(self.add_id_token) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py new file mode 100644 index 00000000..0eaa5b38 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from .base import GrantTypeBase + +from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant + +log = logging.getLogger(__name__) + + +class ImplicitGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2ImplicitGrant( + request_validator=request_validator, **kwargs) + self.register_response_type('id_token') + self.register_response_type('id_token token') + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + self.custom_validators.post_auth.append( + self.openid_implicit_authorization_validator) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py new file mode 100644 index 00000000..f3bcbdb5 --- /dev/null +++ b/oauthlib/openid/connect/core/request_validator.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.request_validator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.request_validator import RequestValidator as OAuth2RequestValidator + +log = logging.getLogger(__name__) + + +class RequestValidator(OAuth2RequestValidator): + + def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): + """ Extracts scopes from saved authorization code. + + The scopes returned by this method is used to route token requests + based on scopes passed to Authorization Code requests. + + With that the token endpoint knows when to include OpenIDConnect + id_token in token response only based on authorization code scopes. + + Only code param should be sufficient to retrieve grant code from + any storage you are using, `client_id` and `redirect_uri` can gave a + blank value `""` don't forget to check it before using those values + in a select query if a database is used. + + :param client_id: Unicode client identifier + :param code: Unicode authorization code grant + :param redirect_uri: Unicode absolute URI + :return: A list of scope + + Method is used by: + - Authorization Token Grant Dispatcher + """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_jwt_bearer_token(self, token, token_handler, request): + """Get JWT Bearer token or OpenID Connect ID token + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) + + Method is used by JWT Bearer and OpenID Connect tokens: + - JWTToken.create_token + """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_id_token(self, token, token_handler, request): + """Get OpenID Connect ID token + + In the OpenID Connect workflows when an ID Token is requested this method is called. + Subclasses should implement the construction, signing and optional encryption of the + ID Token as described in the OpenID Connect spec. + + In addition to the standard OAuth2 request properties, the request may also contain + these OIDC specific properties which are useful to this method: + + - nonce, if workflow is implicit or hybrid and it was provided + - claims, if provided to the original Authorization Code request + + The token parameter is a dict which may contain an ``access_token`` entry, in which + case the resulting ID Token *should* include a calculated ``at_hash`` claim. + + Similarly, when the request parameter has a ``code`` property defined, the ID Token + *should* include a calculated ``c_hash`` claim. + + http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_) + + .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken + .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken + .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The ID Token (a JWS signed JWT) + """ + # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token + raise NotImplementedError('Subclasses must implement this method.') + + def validate_jwt_bearer_token(self, token, scopes, request): + """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes. + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_id_token(self, token, scopes, request): + """Ensure the id token is valid and authorized access to scopes. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_silent_authorization(self, request): + """Ensure the logged in user has authorized silent OpenID authorization. + + Silent OpenID authorization allows access tokens and id tokens to be + granted to clients without any user prompt or interaction. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_silent_login(self, request): + """Ensure session user has authorized silent OpenID login. + + If no user is logged in or has not authorized silent login, this + method should return False. + + If the user is logged in but associated with multiple accounts and + not selected which one to link to the token then this method should + raise an oauthlib.oauth2.AccountSelectionRequired error. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_user_match(self, id_token_hint, scopes, claims, request): + """Ensure client supplied user id hint matches session user. + + If the sub claim or id_token_hint is supplied then the session + user must match the given ID. + + :param id_token_hint: User identifier string. + :param scopes: List of OAuth 2 scopes and OpenID claims (strings). + :param claims: OpenID Connect claims dict. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py new file mode 100644 index 00000000..6b68891a --- /dev/null +++ b/oauthlib/openid/connect/core/tokens.py @@ -0,0 +1,54 @@ +""" +authlib.openid.connect.core.tokens +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains methods for adding JWT tokens to requests. +""" +from __future__ import absolute_import, unicode_literals + + +from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator + + +class JWTToken(TokenBase): + __slots__ = ( + 'request_validator', 'token_generator', + 'refresh_token_generator', 'expires_in' + ) + + def __init__(self, request_validator=None, token_generator=None, + expires_in=None, refresh_token_generator=None): + self.request_validator = request_validator + self.token_generator = token_generator or random_token_generator + self.refresh_token_generator = ( + refresh_token_generator or self.token_generator + ) + self.expires_in = expires_in or 3600 + + def create_token(self, request, refresh_token=False, save_token=False): + """Create a JWT Token, using requestvalidator method.""" + + if callable(self.expires_in): + expires_in = self.expires_in(request) + else: + expires_in = self.expires_in + + request.expires_in = expires_in + + 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 + 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): + return 10 + else: + return 0 diff --git a/tests/openid/__init__.py b/tests/openid/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/__init__.py b/tests/openid/connect/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/core/__init__.py b/tests/openid/connect/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/core/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py new file mode 100644 index 00000000..37a7cdda --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_claims_handling.py @@ -0,0 +1,109 @@ +"""Ensure OpenID Connect Authorization Request 'claims' are preserved across authorization. + +The claims parameter is an optional query param for the Authorization Request endpoint + but if it is provided and is valid it needs to be deserialized (from urlencoded JSON) + and persisted with the authorization code itself, then in the subsequent Access Token + request the claims should be transferred (via the oauthlib request) to be persisted + with the Access Token when it is created. +""" +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.oauth2 import RequestValidator + +from oauthlib.oauth2.rfc6749.endpoints.pre_configured import Server + +from ....unittest import TestCase +from .test_utils import get_query_credentials + + +class TestClaimsHandling(TestCase): + + DEFAULT_REDIRECT_URI = 'http://i.b./path' + + def set_scopes(self, scopes): + def set_request_scopes(client_id, code, client, request): + request.scopes = scopes + return True + return set_request_scopes + + def set_user(self, request): + request.user = 'foo' + request.client_id = 'bar' + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + def save_claims_with_code(self, client_id, code, request, *args, **kwargs): + # a real validator would save the claims with the code during save_authorization_code() + self.claims_from_auth_code_request = request.claims + self.scopes = request.scopes.split() + + def retrieve_claims_saved_with_code(self, client_id, code, client, request, *args, **kwargs): + request.claims = self.claims_from_auth_code_request + request.scopes = self.scopes + + return True + + def save_claims_with_bearer_token(self, token, request, *args, **kwargs): + # a real validator would save the claims with the access token during save_bearer_token() + self.claims_saved_with_bearer_token = request.claims + + def setUp(self): + self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI + self.validator.authenticate_client.side_effect = self.set_client + + self.validator.save_authorization_code.side_effect = self.save_claims_with_code + self.validator.validate_code.side_effect = self.retrieve_claims_saved_with_code + self.validator.save_token.side_effect = self.save_claims_with_bearer_token + + self.server = Server(self.validator) + + def test_claims_stored_on_code_creation(self): + + claims = { + "id_token": { + "claim_1": None, + "claim_2": { + "essential": True + } + }, + "userinfo": { + "claim_3": { + "essential": True + }, + "claim_4": None + } + } + + claims_urlquoted = '%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D' + uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s' + + h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope') + + self.assertDictEqual(self.claims_from_auth_code_request, claims) + + code = get_query_credentials(h['Location'])['code'][0] + token_uri = 'http://example.com/path' + _, body, _ = self.server.create_token_response( + token_uri, + body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code + ) + + self.assertDictEqual(self.claims_saved_with_bearer_token, claims) + + def test_invalid_claims(self): + uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=this-is-not-json' + + h, b, s = self.server.create_authorization_response(uri, scopes='openid test_scope') + error = get_query_credentials(h['Location'])['error'][0] + error_desc = get_query_credentials(h['Location'])['error_description'][0] + self.assertEqual(error, 'invalid_request') + self.assertEqual(error_desc, "Malformed claims parameter") diff --git a/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py new file mode 100644 index 00000000..89431b64 --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py @@ -0,0 +1,85 @@ +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.oauth2 import InvalidRequestError +from oauthlib.oauth2.rfc6749.endpoints.authorization import \ + AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.grant_types import OpenIDConnectAuthCode +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from ....unittest import TestCase + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + + + + +class OpenIDConnectEndpointTest(TestCase): + + def setUp(self): + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = self.set_client + grant = OpenIDConnectAuthCode(request_validator=self.mock_validator) + bearer = BearerToken(self.mock_validator) + self.endpoint = AuthorizationEndpoint(grant, bearer, + response_types={'code': grant}) + params = { + 'prompt': 'consent', + 'display': 'touch', + 'nonce': 'abcd', + 'state': 'abc', + 'redirect_uri': 'https://a.b/cb', + 'response_type': 'code', + 'client_id': 'abcdef', + 'scope': 'hello openid' + } + self.url = 'http://a.b/path?' + urlencode(params) + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + @mock.patch('oauthlib.common.generate_token') + def test_authorization_endpoint_handles_prompt(self, generate_token): + generate_token.return_value = "MOCK_CODE" + # In the GET view: + scopes, creds = self.endpoint.validate_authorization_request(self.url) + # In the POST view: + creds['scopes'] = scopes + h, b, s = self.endpoint.create_authorization_response(self.url, + credentials=creds) + expected = 'https://a.b/cb?state=abc&code=MOCK_CODE' + self.assertURLEqual(h['Location'], expected) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + def test_prompt_none_exclusiveness(self): + """ + Test that prompt=none can't be used with another prompt value. + """ + params = { + 'prompt': 'none consent', + 'state': 'abc', + 'redirect_uri': 'https://a.b/cb', + 'response_type': 'code', + 'client_id': 'abcdef', + 'scope': 'hello openid' + } + url = 'http://a.b/path?' + urlencode(params) + with self.assertRaises(InvalidRequestError): + self.endpoint.validate_authorization_request(url) + + def test_oidc_params_preservation(self): + """ + Test that the nonce parameter is passed through. + """ + scopes, creds = self.endpoint.validate_authorization_request(self.url) + + self.assertEqual(creds['prompt'], {'consent'}) + self.assertEqual(creds['nonce'], 'abcd') + self.assertEqual(creds['display'], 'touch') diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py new file mode 100644 index 00000000..1bad120f --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import json + +import mock + +from oauthlib.common import Request +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt + +from ....unittest import TestCase +from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest + + +def get_id_token_mock(token, token_handler, request): + return "MOCKED_TOKEN" + + +class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDAuthCodeInterferenceTest, self).setUp() + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) + + +class OpenIDAuthCodeTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.code = '1234' + self.request.response_type = 'code' + self.request.grant_type = 'authorization_code' + self.request.redirect_uri = 'https://a.b/cb' + self.request.state = 'abc' + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = self.set_client + self.mock_validator.get_id_token.side_effect = get_id_token_mock + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) + + self.url_query = 'https://a.b/cb?code=abc&state=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc' + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + @mock.patch('oauthlib.common.generate_token') + def test_authorization(self, generate_token): + + scope, info = self.auth.validate_authorization_request(self.request) + + generate_token.return_value = 'abc' + bearer = BearerToken(self.mock_validator) + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_mode = 'fragment' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + @mock.patch('oauthlib.common.generate_token') + def test_no_prompt_authorization(self, generate_token): + generate_token.return_value = 'abc' + scope, info = self.auth.validate_authorization_request(self.request) + self.request.prompt = 'none' + self.assertRaises(OIDCNoPrompt, + self.auth.validate_authorization_request, + self.request) + + # prompt == none requires id token hint + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_mode = 'query' + self.request.id_token_hint = 'me@email.com' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + # Test alernative response modes + self.request.response_mode = 'fragment' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + + # Ensure silent authentication and authorization is done + self.mock_validator.validate_silent_login.return_value = False + self.mock_validator.validate_silent_authorization.return_value = True + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + self.mock_validator.validate_silent_login.return_value = True + self.mock_validator.validate_silent_authorization.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=consent_required', h['Location']) + + # ID token hint must match logged in user + self.mock_validator.validate_silent_authorization.return_value = True + self.mock_validator.validate_user_match.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + def set_scopes(self, client_id, code, client, request): + request.scopes = self.request.scopes + request.state = self.request.state + request.user = 'bob' + return True + + def test_create_token_response(self): + self.request.response_type = None + self.mock_validator.validate_code.side_effect = self.set_scopes + + bearer = BearerToken(self.mock_validator) + + h, token, s = self.auth.create_token_response(self.request, bearer) + token = json.loads(token) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('expires_in', token) + self.assertIn('scope', token) + self.assertIn('id_token', token) + self.assertIn('openid', token['scope']) + + self.mock_validator.reset_mock() + + self.request.scopes = ('hello', 'world') + h, token, s = self.auth.create_token_response(self.request, bearer) + token = json.loads(token) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('expires_in', token) + self.assertIn('scope', token) + self.assertNotIn('id_token', token) + self.assertNotIn('openid', token['scope']) diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py new file mode 100644 index 00000000..f90ec468 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_dispatchers.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +import mock + +from oauthlib.common import Request + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) + +from oauthlib.oauth2.rfc6749.grant_types import ( + AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, + ImplicitGrant as OAuth2ImplicitGrant, +) + + +from ....unittest import TestCase + + +class ImplicitTokenGrantDispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + request_validator = mock.MagicMock() + implicit_grant = OAuth2ImplicitGrant(request_validator) + openid_connect_implicit = ImplicitGrant(request_validator) + + self.dispatcher = ImplicitTokenGrantDispatcher( + default_implicit_grant=implicit_grant, + oidc_implicit_grant=openid_connect_implicit + ) + + def test_create_authorization_response_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_validate_authorization_request_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_create_authorization_response_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_validate_authorization_request_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + +class DispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + self.request.decoded_body = ( + ("client_id", "me"), + ("code", "code"), + ("redirect_url", "https://a.b/cb"), + ) + + self.request_validator = mock.MagicMock() + self.auth_grant = OAuth2AuthorizationCodeGrant(self.request_validator) + self.openid_connect_auth = OAuth2AuthorizationCodeGrant(self.request_validator) + + +class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthorizationTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp() + self.request.decoded_body = ( + ("client_id", "me"), + ("code", ""), + ("redirect_url", "https://a.b/cb"), + ) + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthorizationTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid_without_code(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OAuth2AuthorizationCodeGrant)) + self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOAuthTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOAuthTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') + self.dispatcher = AuthorizationTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_oauth(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OAuth2AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py new file mode 100644 index 00000000..531ae7f2 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant + +from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest + + +class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDHybridInterferenceTest, self).setUp() + self.auth = HybridGrant(request_validator=self.mock_validator) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py new file mode 100644 index 00000000..56247d99 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.common import Request + +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt + +from ....unittest import TestCase +from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest + +from ....oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest + + +class OpenIDImplicitInterferenceTest(ImplicitGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDImplicitInterferenceTest, self).setUp() + self.auth = ImplicitGrant(request_validator=self.mock_validator) + + +class OpenIDImplicitTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.response_type = 'id_token token' + self.request.redirect_uri = 'https://a.b/cb' + self.request.nonce = 'zxc' + self.request.state = 'abc' + + self.mock_validator = mock.MagicMock() + self.mock_validator.get_id_token.side_effect = get_id_token_mock + self.auth = ImplicitGrant(request_validator=self.mock_validator) + + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_authorization(self, generate_token): + scope, info = self.auth.validate_authorization_request(self.request) + + generate_token.return_value = 'abc' + bearer = BearerToken(self.mock_validator) + + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_type = 'id_token' + token = 'MOCKED_TOKEN' + url = 'https://a.b/cb#state=abc&id_token=%s' % token + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], url, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.nonce = None + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + @mock.patch('oauthlib.common.generate_token') + def test_no_prompt_authorization(self, generate_token): + generate_token.return_value = 'abc' + scope, info = self.auth.validate_authorization_request(self.request) + self.request.prompt = 'none' + self.assertRaises(OIDCNoPrompt, + self.auth.validate_authorization_request, + self.request) + + # prompt == none requires id token hint + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.id_token_hint = 'me@email.com' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + # Test alernative response modes + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + + # Ensure silent authentication and authorization is done + self.mock_validator.validate_silent_login.return_value = False + self.mock_validator.validate_silent_authorization.return_value = True + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + self.mock_validator.validate_silent_login.return_value = True + self.mock_validator.validate_silent_authorization.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=consent_required', h['Location']) + + # ID token hint must match logged in user + self.mock_validator.validate_silent_authorization.return_value = True + self.mock_validator.validate_user_match.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + +class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeTokenTest, self).setUp() + self.request.response_type = 'code token' + self.auth = HybridGrant(request_validator=self.mock_validator) + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + + +class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.request.response_type = 'code id_token' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + + +class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.request.response_type = 'code id_token token' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py new file mode 100644 index 00000000..14a7c232 --- /dev/null +++ b/tests/openid/connect/core/test_request_validator.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from oauthlib.openid.connect.core.request_validator import RequestValidator + +from ....unittest import TestCase + + +class RequestValidatorTest(TestCase): + + def test_method_contracts(self): + v = RequestValidator() + self.assertRaises( + NotImplementedError, + v.get_authorization_code_scopes, + 'client_id', 'code', 'redirect_uri', 'request' + ) + self.assertRaises( + NotImplementedError, + v.get_jwt_bearer_token, + 'token', 'token_handler', 'request' + ) + self.assertRaises( + NotImplementedError, + v.get_id_token, + 'token', 'token_handler', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_jwt_bearer_token, + 'token', 'scopes', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_id_token, + 'token', 'scopes', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_silent_authorization, + 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_silent_login, + 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_user_match, + 'id_token_hint', 'scopes', 'claims', 'request' + ) diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py new file mode 100644 index 00000000..83290db8 --- /dev/null +++ b/tests/openid/connect/core/test_server.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import json + +import mock + +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant + +from ....unittest import TestCase + + +class AuthorizationEndpointTest(TestCase): + + def setUp(self): + self.mock_validator = mock.MagicMock() + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = AuthorizationCodeGrant(request_validator=self.mock_validator) + auth_code.save_authorization_code = mock.MagicMock() + implicit = ImplicitGrant( + request_validator=self.mock_validator) + implicit.save_token = mock.MagicMock() + hybrid = HybridGrant(self.mock_validator) + + response_types = { + 'code': auth_code, + 'token': implicit, + 'id_token': implicit, + 'id_token token': implicit, + 'code token': hybrid, + 'code id_token': hybrid, + 'code token id_token': hybrid, + 'none': auth_code + } + self.expires_in = 1800 + token = BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = AuthorizationEndpoint( + default_response_type='code', + default_token_type=token, + response_types=response_types + ) + + # TODO: Add hybrid grant test + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_authorization_grant(self): + uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz') + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_implicit_grant(self): + uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) + + def test_none_grant(self): + uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True) + self.assertEqual(body, None) + self.assertEqual(status_code, 302) + + # and without the state parameter + uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me', parse_fragment=True) + self.assertEqual(body, None) + self.assertEqual(status_code, 302) + + def test_missing_type(self): + uri = 'http://i.b/l?client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.InvalidRequestError()) + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') + + def test_invalid_type(self): + uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.UnsupportedResponseTypeError()) + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type') + + +class TokenEndpointTest(TestCase): + + def setUp(self): + def set_user(request): + request.user = mock.MagicMock() + request.client = mock.MagicMock() + request.client.client_id = 'mocked_client_id' + return True + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = set_user + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = AuthorizationCodeGrant( + request_validator=self.mock_validator) + supported_types = { + 'authorization_code': auth_code, + } + self.expires_in = 1800 + token = BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = TokenEndpoint( + 'authorization_code', + default_token_type=token, + grant_types=supported_types + ) + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_authorization_grant(self): + body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'scope': 'all of them', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + + body = 'grant_type=authorization_code&code=abc&state=xyz' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + + def test_missing_type(self): + _, body, _ = self.endpoint.create_token_response('', body='') + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) + + def test_invalid_type(self): + body = 'grant_type=invalid' + _, body, _ = self.endpoint.create_token_response('', body=body) + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) diff --git a/tests/openid/connect/core/test_tokens.py b/tests/openid/connect/core/test_tokens.py new file mode 100644 index 00000000..12c75f10 --- /dev/null +++ b/tests/openid/connect/core/test_tokens.py @@ -0,0 +1,133 @@ +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.openid.connect.core.tokens import JWTToken + +from ....unittest import TestCase + + +class JWTTokenTestCase(TestCase): + + def test_create_token_callable_expires_in(self): + """ + Test retrieval of the expires in value by calling the callable expires_in property + """ + + expires_in_mock = mock.MagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + expires_in_mock.assert_called_once_with(request_mock) + + def test_create_token_non_callable_expires_in(self): + """ + When a non callable expires in is set this should just be set to the request + """ + + expires_in_mock = mock.NonCallableMagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + self.assertFalse(expires_in_mock.called) + self.assertEqual(request_mock.expires_in, expires_in_mock) + + def test_create_token_calls_get_id_token(self): + """ + When create_token is called the call should be forwarded to the get_id_token on the token validator + """ + request_mock = mock.MagicMock() + + with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + + request_validator = RequestValidatorMock() + + token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator) + token.create_token(request=request_mock) + + request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock) + + def test_validate_request_token_from_headers(self): + """ + Bearer token get retrieved from headers. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.headers = { + 'Authorization': 'Bearer some-token-from-header' + } + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header', + request.scopes, + request) + + def test_validate_token_from_request(self): + """ + Token get retrieved from request object. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.access_token = 'some-token-from-request-object' + request.headers = {} + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object', + request.scopes, + request) + + def test_estimate_type(self): + """ + Estimate type results for a jwt token + """ + + def test_token(token, expected_result): + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock: + jwt_token = JWTToken() + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.headers = { + 'Authorization': 'Bearer {}'.format(token) + } + + result = jwt_token.estimate_type(request=request) + + self.assertEqual(result, expected_result) + + test_items = ( + ('eyfoo.foo.foo', 10), + ('eyfoo.foo.foo.foo.foo', 10), + ('eyfoobar', 0) + ) + + for token, expected_result in test_items: + test_token(token, expected_result)