diff options
author | Jonathan Huot <JonathanHuot@users.noreply.github.com> | 2018-06-26 09:33:28 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-26 09:33:28 +0200 |
commit | 048befd55de7924fd3414fe6a24a28eaaaba2a66 (patch) | |
tree | 91b0ffe20afa4362dd806f8e57b577d3d3ed5529 /oauthlib/oauth2/rfc6749 | |
parent | dd120a5c4ccdcc5e3de85746266990192202203d (diff) | |
parent | d5a4d5ea0eab04ddddefac7d1e7a4902fc469286 (diff) | |
download | oauthlib-048befd55de7924fd3414fe6a24a28eaaaba2a66.tar.gz |
Merge branch 'master' into master
Diffstat (limited to 'oauthlib/oauth2/rfc6749')
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/base.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/introspect.py | 135 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 71 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/errors.py | 125 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/__init__.py | 8 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/grant_types/openid_connect.py | 451 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/request_validator.py | 44 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/tokens.py | 70 |
9 files changed, 252 insertions, 654 deletions
diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 07ef894..406832d 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -143,6 +143,7 @@ class Client(object): def parse_request_uri_response(self, *args, **kwargs): """Abstract method used to parse redirection responses.""" + raise NotImplementedError("Must be implemented by inheriting classes.") def add_token(self, uri, http_method='GET', body=None, headers=None, token_placement=None, **kwargs): diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py index 848bec6..9557f92 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py @@ -9,6 +9,7 @@ for consuming and providing OAuth 2.0 RFC6749. from __future__ import absolute_import, unicode_literals from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .token import TokenEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py new file mode 100644 index 0000000..7613acc --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.endpoint.introspect +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An implementation of the OAuth 2.0 `Token Introspection`. + +.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662 +""" +from __future__ import absolute_import, unicode_literals + +import json +import logging + +from oauthlib.common import Request + +from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error, + UnsupportedTokenTypeError) +from .base import BaseEndpoint, catch_errors_and_unavailability + +log = logging.getLogger(__name__) + + +class IntrospectEndpoint(BaseEndpoint): + + """Introspect token endpoint. + + This endpoint defines a method to query an OAuth 2.0 authorization + server to determine the active state of an OAuth 2.0 token and to + determine meta-information about this token. OAuth 2.0 deployments + can use this method to convey information about the authorization + context of the token from the authorization server to the protected + resource. + + To prevent the values of access tokens from leaking into + server-side logs via query parameters, an authorization server + offering token introspection MAY disallow the use of HTTP GET on + the introspection endpoint and instead require the HTTP POST method + to be used at the introspection endpoint. + """ + + valid_token_types = ('access_token', 'refresh_token') + + def __init__(self, request_validator, supported_token_types=None): + BaseEndpoint.__init__(self) + self.request_validator = request_validator + self.supported_token_types = ( + supported_token_types or self.valid_token_types) + + @catch_errors_and_unavailability + def create_introspect_response(self, uri, http_method='POST', body=None, + headers=None): + """Create introspect valid or invalid response + + If the authorization server is unable to determine the state + of the token without additional information, it SHOULD return + an introspection response indicating the token is not active + as described in Section 2.2. + """ + request = Request(uri, http_method, body, headers) + try: + self.validate_introspect_request(request) + log.debug('Token introspect valid for %r.', request) + except OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + return {}, e.json, e.status_code + + claims = self.request_validator.introspect_token( + request.token, + request.token_type_hint, + request + ) + headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + if claims is None: + return headers, json.dumps(dict(active=False)), 200 + if "active" in claims: + claims.pop("active") + return headers, json.dumps(dict(active=True, **claims)), 200 + + def validate_introspect_request(self, request): + """Ensure the request is valid. + + The protected resource calls the introspection endpoint using + an HTTP POST request with parameters sent as + "application/x-www-form-urlencoded". + + token REQUIRED. The string value of the token. + + token_type_hint OPTIONAL. + A hint about the type of the token submitted for + introspection. The protected resource MAY pass this parameter to + help the authorization server optimize the token lookup. If the + server is unable to locate the token using the given hint, it MUST + extend its search across all of its supported token types. An + authorization server MAY ignore this parameter, particularly if it + is able to detect the token type automatically. + * access_token: An Access Token as defined in [`RFC6749`], + `section 1.4`_ + + * refresh_token: A Refresh Token as defined in [`RFC6749`], + `section 1.5`_ + + The introspection endpoint MAY accept other OPTIONAL + parameters to provide further context to the query. For + instance, an authorization server may desire to know the IP + address of the client accessing the protected resource to + determine if the correct client is likely to be presenting the + token. The definition of this or any other parameters are + outside the scope of this specification, to be defined by + service documentation or extensions to this specification. + + .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 + .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 + .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 + """ + if not request.token: + raise InvalidRequestError(request=request, + description='Missing token parameter.') + + if self.request_validator.client_authentication_required(request): + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + elif not self.request_validator.authenticate_client_id(request.client_id, request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + + if (request.token_type_hint and + request.token_type_hint in self.valid_token_types and + request.token_type_hint not in self.supported_token_types): + raise UnsupportedTokenTypeError(request=request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 0c26986..e2cc9db 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -1,30 +1,28 @@ # -*- 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 ..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 from .revocation import RevocationEndpoint from .token import TokenEndpoint -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -50,51 +48,34 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, 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) -class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" @@ -129,10 +110,11 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint, - RevocationEndpoint): +class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" @@ -162,10 +144,12 @@ class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) -class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" @@ -198,10 +182,11 @@ class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class BackendApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" @@ -231,3 +216,5 @@ class BackendApplicationServer(TokenEndpoint, ResourceEndpoint, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 180f636..5a0cca2 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -267,113 +267,13 @@ class UnsupportedGrantTypeError(OAuth2Error): class UnsupportedTokenTypeError(OAuth2Error): """ - The authorization server does not support the revocation of the + The authorization server does not support the hint of the presented token type. I.e. the client tried to revoke an access token on a server not supporting this feature. """ 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 diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py index 2e4bfe4..2ec8e4f 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -10,11 +10,3 @@ from .implicit import ImplicitGrant 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 4371b28..0000000 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ /dev/null @@ -1,451 +0,0 @@ -# -*- coding: utf-8 -*- -""" -oauthlib.oauth2.rfc6749.grant_types.openid_connect -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -""" -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 - -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) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index c0b69a1..92edba6 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 @@ -166,6 +166,46 @@ class RequestValidator(object): """ return False + def introspect_token(self, token, token_type_hint, request, *args, **kwargs): + """Introspect an access or refresh token. + + Called once the introspect request is validated. This method should + verify the *token* and either return a dictionary with the list of + claims associated, or `None` in case the token is unknown. + + Below the list of registered claims you should be interested in: + - scope : space-separated list of scopes + - client_id : client identifier + - username : human-readable identifier for the resource owner + - token_type : type of the token + - exp : integer timestamp indicating when this token will expire + - iat : integer timestamp indicating when this token was issued + - nbf : integer timestamp indicating when it can be "not-before" used + - sub : subject of the token - identifier of the resource owner + - aud : list of string identifiers representing the intended audience + - iss : string representing issuer of this token + - jti : string identifier for the token + + Note that most of them are coming directly from JWT RFC. More details + can be found in `Introspect Claims`_ or `_JWT Claims`_. + + The implementation can use *token_type_hint* to improve lookup + efficency, but must fallback to other types to be compliant with RFC. + + The dict of claims is added to request.token after this method. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + + Method is used by: + - Introspect Endpoint (all grants are compatible) + + .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2 + .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4 + """ + raise NotImplementedError('Subclasses must implement this method.') + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """Invalidate an authorization code after use. diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 4ae20e0..1d2b5eb 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -220,6 +220,24 @@ def signed_token_generator(private_pem, **kwargs): return signed_token_generator +def get_token_from_header(request): + """ + Helper function to extract a token from the request header. + :param request: The request object + :return: Return the token or None if the Authorization header is malformed. + """ + token = None + + if 'Authorization' in request.headers: + split_header = request.headers.get('Authorization').split() + if len(split_header) == 2 and split_header[0] == 'Bearer': + token = split_header[1] + else: + token = request.access_token + + return token + + class TokenBase(object): def __call__(self, request, refresh_token=False): @@ -286,62 +304,14 @@ class BearerToken(TokenBase): return token def validate_request(self, request): - token = None - if 'Authorization' in request.headers: - token = request.headers.get('Authorization')[7:] - else: - token = request.access_token + token = get_token_from_header(request) return self.request_validator.validate_bearer_token( token, request.scopes, request) def estimate_type(self, request): - if request.headers.get('Authorization', '').startswith('Bearer'): + if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer': return 9 elif request.access_token is not None: return 5 else: 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 = 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 |