summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/pre_configured.py23
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/__init__.py2
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/implicit.py32
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/openid_connect.py65
-rw-r--r--oauthlib/oauth2/rfc6749/request_validator.py24
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_claims_handling.py2
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_scope_handling.py2
-rw-r--r--tests/oauth2/rfc6749/grant_types/test_openid_connect.py114
-rw-r--r--tests/oauth2/rfc6749/test_server.py8
9 files changed, 241 insertions, 31 deletions
diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
index 6428b8d..07c3715 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
@@ -9,8 +9,11 @@ for consuming and providing OAuth 2.0 RFC6749.
from __future__ import absolute_import, unicode_literals
from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant,
- ClientCredentialsGrant, ImplicitGrant,
+ AuthTokenGrantDispatcher,
+ ClientCredentialsGrant,
+ ImplicitTokenGrantDispatcher, ImplicitGrant,
OpenIDConnectAuthCode, OpenIDConnectImplicit,
+ OpenIDConnectHybrid,
RefreshTokenGrant,
ResourceOwnerPasswordCredentialsGrant)
from ..tokens import BearerToken
@@ -49,33 +52,37 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
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)
- auth_grant_choice = AuthCodeGrantDispatcher( default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth)
+ 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,
+ 'token': implicit_grant_choice,
'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,
+ '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 = 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': auth_grant,
+ 'authorization_code': token_grant_choice,
'password': password_grant,
'client_credentials': credentials_grant,
'refresh_token': refresh_grant,
- 'openid': openid_connect_auth
},
default_token_type=bearer)
ResourceEndpoint.__init__(self, default_token='Bearer',
diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py
index 1da1281..2e4bfe4 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py
@@ -16,3 +16,5 @@ 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/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py
index 858ef77..2b9c49d 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py
@@ -11,7 +11,6 @@ from oauthlib import common
from oauthlib.uri_validate import is_absolute_uri
from .. import errors
-from ..request_validator import RequestValidator
from .base import GrantTypeBase
log = logging.getLogger(__name__)
@@ -229,7 +228,7 @@ class ImplicitGrant(GrantTypeBase):
return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
fragment=True)}, None, 302
- # In OIDC implicit flow it is possible to have a request_type that does not include the access token!
+ # In OIDC implicit flow it is possible to have a request_type that does not include the access_token!
# "id_token token" - return the access token and the id token
# "id_token" - don't return the access token
if "token" in request.response_type.split():
@@ -239,7 +238,12 @@ class ImplicitGrant(GrantTypeBase):
for modifier in self._token_modifiers:
token = modifier(token, token_handler, request)
- self.request_validator.save_token(token, request)
+
+ # In OIDC implicit flow it is possible to have a request_type that does
+ # not include the access_token! In this case there is no need to save a token.
+ if "token" in request.response_type.split():
+ self.request_validator.save_token(token, request)
+
return self.prepare_authorization_response(
request, token, {}, None, 302)
@@ -317,8 +321,7 @@ class ImplicitGrant(GrantTypeBase):
# Then check for normal errors.
request_info = self._run_custom_validators(request,
- self.custom_validators.all_pre)
-
+ self.custom_validators.all_pre)
# If the resource owner denies the access request or if the request
# fails for reasons other than a missing or invalid redirection URI,
@@ -352,20 +355,21 @@ class ImplicitGrant(GrantTypeBase):
self.validate_scopes(request)
request_info.update({
- 'client_id': request.client_id,
- 'redirect_uri': request.redirect_uri,
- 'response_type': request.response_type,
- 'state': request.state,
- 'request': request,
+ 'client_id': request.client_id,
+ 'redirect_uri': request.redirect_uri,
+ 'response_type': request.response_type,
+ 'state': request.state,
+ 'request': request,
})
- request_info = self._run_custom_validators(request,
- self.custom_validators.all_post,
- request_info)
+ request_info = self._run_custom_validators(
+ request,
+ self.custom_validators.all_post,
+ request_info
+ )
return request.scopes, request_info
-
def _run_custom_validators(self,
request,
validations,
diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py
index 4c98864..4371b28 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py
@@ -12,11 +12,11 @@ from json import loads
from ..errors import ConsentRequired, InvalidRequestError, LoginRequired
from ..request_validator import RequestValidator
from .authorization_code import AuthorizationCodeGrant
-from .base import GrantTypeBase
from .implicit import ImplicitGrant
log = logging.getLogger(__name__)
+
class OIDCNoPrompt(Exception):
"""Exception used to inform users that no explicit authorization is needed.
@@ -76,6 +76,65 @@ class AuthCodeGrantDispatcher(object):
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
@@ -307,7 +366,7 @@ class OpenIDConnectBase(object):
self._inflate_claims(request)
if not self.request_validator.validate_user_match(
- request.id_token_hint, request.scopes, request.claims, request):
+ request.id_token_hint, request.scopes, request.claims, request):
msg = "Session user does not match client supplied user."
raise LoginRequired(request=request, description=msg)
@@ -356,6 +415,7 @@ class OpenIDConnectAuthCode(OpenIDConnectBase):
self.openid_authorization_validator)
self.register_token_modifier(self.add_id_token)
+
class OpenIDConnectImplicit(OpenIDConnectBase):
def __init__(self, request_validator=None, **kwargs):
@@ -369,6 +429,7 @@ class OpenIDConnectImplicit(OpenIDConnectBase):
self.openid_implicit_authorization_validator)
self.register_token_modifier(self.add_id_token)
+
class OpenIDConnectHybrid(OpenIDConnectBase):
def __init__(self, request_validator=None, **kwargs):
diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py
index 0adfa1b..ba129d5 100644
--- a/oauthlib/oauth2/rfc6749/request_validator.py
+++ b/oauthlib/oauth2/rfc6749/request_validator.py
@@ -238,6 +238,30 @@ class RequestValidator(object):
"""
raise NotImplementedError('Subclasses must implement this method.')
+ 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 save_token(self, token, request, *args, **kwargs):
"""Persist the token with a token type specific method.
diff --git a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py
index 9795c80..ff72673 100644
--- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py
+++ b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py
@@ -91,7 +91,7 @@ class TestClaimsHandling(TestCase):
code = get_query_credentials(h['Location'])['code'][0]
token_uri = 'http://example.com/path'
_, body, _ = self.server.create_token_response(token_uri,
- body='grant_type=authorization_code&code=%s' % code)
+ 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)
diff --git a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
index 87781b3..8490c03 100644
--- a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
+++ b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
@@ -87,7 +87,7 @@ class TestScopeHandling(TestCase):
self.assertIn('Location', h)
code = get_query_credentials(h['Location'])['code'][0]
_, body, _ = getattr(self, backend_server_type).create_token_response(token_uri,
- body='grant_type=authorization_code&code=%s' % code)
+ body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code)
self.assertEqual(json.loads(body)['scope'], decoded_scope)
# implicit grant
diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py
index f10d36c..573d491 100644
--- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py
+++ b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py
@@ -6,7 +6,11 @@ import json
import mock
from oauthlib.common import Request
-from oauthlib.oauth2.rfc6749.grant_types import (OIDCNoPrompt,
+from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher,
+ AuthorizationCodeGrant,
+ ImplicitGrant,
+ ImplicitTokenGrantDispatcher,
+ OIDCNoPrompt,
OpenIDConnectAuthCode,
OpenIDConnectHybrid,
OpenIDConnectImplicit)
@@ -24,6 +28,7 @@ class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest):
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."""
@@ -270,6 +275,7 @@ class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest):
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):
@@ -280,6 +286,7 @@ class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest):
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):
@@ -289,3 +296,108 @@ class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest):
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)
diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py
index 305b795..da303ce 100644
--- a/tests/oauth2/rfc6749/test_server.py
+++ b/tests/oauth2/rfc6749/test_server.py
@@ -279,7 +279,7 @@ twIDAQAB
@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'
+ 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 = json.loads(body)
@@ -293,7 +293,7 @@ twIDAQAB
}
self.assertEqual(body, token)
- body = 'grant_type=authorization_code&code=abc&state=xyz'
+ 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 = json.loads(body)
@@ -349,12 +349,12 @@ twIDAQAB
self.assertEqual(body, token)
def test_missing_type(self):
- _, body, _ = self.endpoint.create_token_response('', body='')
+ _, body, _ = self.endpoint.create_token_response('', body='client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&code=abc')
token = {'error': 'unsupported_grant_type'}
self.assertEqual(json.loads(body), token)
def test_invalid_type(self):
- body = 'grant_type=invalid'
+ body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=invalid&code=abc'
_, body, _ = self.endpoint.create_token_response('', body=body)
token = {'error': 'unsupported_grant_type'}
self.assertEqual(json.loads(body), token)