summaryrefslogtreecommitdiff
path: root/oauthlib/openid
diff options
context:
space:
mode:
Diffstat (limited to 'oauthlib/openid')
-rw-r--r--oauthlib/openid/__init__.py0
-rw-r--r--oauthlib/openid/connect/__init__.py0
-rw-r--r--oauthlib/openid/connect/core/__init__.py0
-rw-r--r--oauthlib/openid/connect/core/endpoints/pre_configured.py103
-rw-r--r--oauthlib/openid/connect/core/exceptions.py152
-rw-r--r--oauthlib/openid/connect/core/grant_types/__init__.py17
-rw-r--r--oauthlib/openid/connect/core/grant_types/authorization_code.py24
-rw-r--r--oauthlib/openid/connect/core/grant_types/base.py277
-rw-r--r--oauthlib/openid/connect/core/grant_types/dispatchers.py86
-rw-r--r--oauthlib/openid/connect/core/grant_types/exceptions.py32
-rw-r--r--oauthlib/openid/connect/core/grant_types/hybrid.py36
-rw-r--r--oauthlib/openid/connect/core/grant_types/implicit.py28
-rw-r--r--oauthlib/openid/connect/core/request_validator.py188
-rw-r--r--oauthlib/openid/connect/core/tokens.py54
14 files changed, 997 insertions, 0 deletions
diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/oauthlib/openid/__init__.py
diff --git a/oauthlib/openid/connect/__init__.py b/oauthlib/openid/connect/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/oauthlib/openid/connect/__init__.py
diff --git a/oauthlib/openid/connect/core/__init__.py b/oauthlib/openid/connect/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/oauthlib/openid/connect/core/__init__.py
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 0000000..3bcd24d
--- /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 0000000..8b08d21
--- /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 0000000..7fc183d
--- /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 0000000..b0b1015
--- /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 0000000..fa578a5
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/base.py
@@ -0,0 +1,277 @@
+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)
+
+ 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 0000000..2c33406
--- /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 0000000..809f1b3
--- /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 0000000..54669ae
--- /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 0000000..0eaa5b3
--- /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 0000000..f3bcbdb
--- /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 0000000..6b68891
--- /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