diff options
Diffstat (limited to 'oauthlib')
-rw-r--r-- | oauthlib/oauth2/rfc6749/__init__.py | 50 | ||||
-rw-r--r-- | oauthlib/openid/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/endpoints/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/endpoints/pre_configured.py | 4 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/endpoints/userinfo.py | 102 | ||||
-rw-r--r-- | oauthlib/openid/connect/core/request_validator.py | 42 |
6 files changed, 151 insertions, 49 deletions
diff --git a/oauthlib/oauth2/rfc6749/__init__.py b/oauthlib/oauth2/rfc6749/__init__.py index aff0ed8..1a4128c 100644 --- a/oauthlib/oauth2/rfc6749/__init__.py +++ b/oauthlib/oauth2/rfc6749/__init__.py @@ -11,56 +11,10 @@ from __future__ import absolute_import, unicode_literals import functools import logging +from .endpoints.base import BaseEndpoint +from .endpoints.base import catch_errors_and_unavailability from .errors import TemporarilyUnavailableError, ServerError from .errors import FatalClientError, OAuth2Error log = logging.getLogger(__name__) - - -class BaseEndpoint(object): - - def __init__(self): - self._available = True - self._catch_errors = False - - @property - def available(self): - return self._available - - @available.setter - def available(self, available): - self._available = available - - @property - def catch_errors(self): - return self._catch_errors - - @catch_errors.setter - def catch_errors(self, catch_errors): - self._catch_errors = catch_errors - - -def catch_errors_and_unavailability(f): - @functools.wraps(f) - def wrapper(endpoint, uri, *args, **kwargs): - if not endpoint.available: - e = TemporarilyUnavailableError() - log.info('Endpoint unavailable, ignoring request %s.' % uri) - return {}, e.json, 503 - - if endpoint.catch_errors: - try: - return f(endpoint, uri, *args, **kwargs) - except OAuth2Error: - raise - except FatalClientError: - raise - except Exception as e: - error = ServerError() - log.warning( - 'Exception caught while processing request, %s.' % e) - return {}, error.json, 500 - else: - return f(endpoint, uri, *args, **kwargs) - return wrapper diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py index 7f1a876..8157c29 100644 --- a/oauthlib/openid/__init__.py +++ b/oauthlib/openid/__init__.py @@ -7,4 +7,5 @@ oauthlib.openid from __future__ import absolute_import, unicode_literals from .connect.core.endpoints import Server +from .connect.core.endpoints import UserInfoEndpoint from .connect.core.request_validator import RequestValidator diff --git a/oauthlib/openid/connect/core/endpoints/__init__.py b/oauthlib/openid/connect/core/endpoints/__init__.py index 719f883..528841f 100644 --- a/oauthlib/openid/connect/core/endpoints/__init__.py +++ b/oauthlib/openid/connect/core/endpoints/__init__.py @@ -9,3 +9,4 @@ for consuming and providing OpenID Connect from __future__ import absolute_import, unicode_literals from .pre_configured import Server +from .userinfo import UserInfoEndpoint diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 6367847..fde2739 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -34,10 +34,11 @@ from ..grant_types.dispatchers import ( AuthorizationTokenGrantDispatcher ) from ..tokens import JWTToken +from .userinfo import UserInfoEndpoint class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint): + ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -105,3 +106,4 @@ class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, token_types={'Bearer': bearer, 'JWT': jwt}) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) + UserInfoEndpoint.__init__(self, request_validator) diff --git a/oauthlib/openid/connect/core/endpoints/userinfo.py b/oauthlib/openid/connect/core/endpoints/userinfo.py new file mode 100644 index 0000000..7a39f76 --- /dev/null +++ b/oauthlib/openid/connect/core/endpoints/userinfo.py @@ -0,0 +1,102 @@ +""" +oauthlib.openid.connect.core.endpoints.userinfo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of userinfo endpoint. +""" +from __future__ import absolute_import, unicode_literals + +import json +import logging + +from oauthlib.common import Request +from oauthlib.common import unicode_type +from oauthlib.oauth2.rfc6749.endpoints.base import BaseEndpoint +from oauthlib.oauth2.rfc6749.endpoints.base import catch_errors_and_unavailability +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.oauth2.rfc6749 import errors + + +log = logging.getLogger(__name__) + + +class UserInfoEndpoint(BaseEndpoint): + """Authorizes access to userinfo resource. + """ + def __init__(self, request_validator): + self.bearer = BearerToken(request_validator, None, None, None) + self.request_validator = request_validator + BaseEndpoint.__init__(self) + + @catch_errors_and_unavailability + def create_userinfo_response(self, uri, http_method='GET', body=None, headers=None): + """Validate BearerToken and return userinfo from RequestValidator + + The UserInfo Endpoint MUST return a + content-type header to indicate which format is being returned. The + content-type of the HTTP response MUST be application/json if the + response body is a text JSON object; the response body SHOULD be encoded + using UTF-8. + """ + request = Request(uri, http_method, body, headers) + request.scopes = ["openid"] + self.validate_userinfo_request(request) + + claims = self.request_validator.get_userinfo_claims(request) + if claims is None: + log.error('Userinfo MUST have claims for %r.', request) + raise errors.ServerError(status_code=500) + + if isinstance(claims, dict): + resp_headers = { + 'Content-Type': 'application/json' + } + if "sub" not in claims: + log.error('Userinfo MUST have "sub" for %r.', request) + raise errors.ServerError(status_code=500) + body = json.dumps(claims) + elif isinstance(claims, unicode_type): + resp_headers = { + 'Content-Type': 'application/jwt' + } + body = claims + else: + log.error('Userinfo return unknown response for %r.', request) + raise errors.ServerError(status_code=500) + log.debug('Userinfo access valid for %r.', request) + return resp_headers, body, 200 + + def validate_userinfo_request(self, request): + """Ensure the request is valid. + + 5.3.1. UserInfo Request + The Client sends the UserInfo Request using either HTTP GET or HTTP + POST. The Access Token obtained from an OpenID Connect Authentication + Request MUST be sent as a Bearer Token, per Section 2 of OAuth 2.0 + Bearer Token Usage [RFC6750]. + + It is RECOMMENDED that the request use the HTTP GET method and the + Access Token be sent using the Authorization header field. + + The following is a non-normative example of a UserInfo Request: + + GET /userinfo HTTP/1.1 + Host: server.example.com + Authorization: Bearer SlAV32hkKG + + 5.3.3. UserInfo Error Response + When an error condition occurs, the UserInfo Endpoint returns an Error + Response as defined in Section 3 of OAuth 2.0 Bearer Token Usage + [RFC6750]. (HTTP errors unrelated to RFC 6750 are returned to the User + Agent using the appropriate HTTP status code.) + + The following is a non-normative example of a UserInfo Error Response: + + HTTP/1.1 401 Unauthorized + WWW-Authenticate: Bearer error="invalid_token", + error_description="The Access Token expired" + """ + if not self.bearer.validate_request(request): + raise errors.InvalidTokenError() + if "openid" not in request.scopes: + raise errors.InsufficientScopeError() diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index d96c9ef..e853d39 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -265,3 +265,45 @@ class RequestValidator(OAuth2RequestValidator): - OpenIDConnectHybrid """ raise NotImplementedError('Subclasses must implement this method.') + + def get_userinfo_claims(self, request): + """Return the UserInfo claims in JSON or Signed or Encrypted. + + The UserInfo Claims MUST be returned as the members of a JSON object + unless a signed or encrypted response was requested during Client + Registration. The Claims defined in Section 5.1 can be returned, as can + additional Claims not specified there. + + For privacy reasons, OpenID Providers MAY elect to not return values for + some requested Claims. + + If a Claim is not returned, that Claim Name SHOULD be omitted from the + JSON object representing the Claims; it SHOULD NOT be present with a + null or empty string value. + + The sub (subject) Claim MUST always be returned in the UserInfo + Response. + + Upon receipt of the UserInfo Request, the UserInfo Endpoint MUST return + the JSON Serialization of the UserInfo Response as in Section 13.3 in + the HTTP response body unless a different format was specified during + Registration [OpenID.Registration]. + + If the UserInfo Response is signed and/or encrypted, then the Claims are + returned in a JWT and the content-type MUST be application/jwt. The + response MAY be encrypted without also being signed. If both signing and + encryption are requested, the response MUST be signed then encrypted, + with the result being a Nested JWT, as defined in [JWT]. + + If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) + and aud (audience) as members. The iss value SHOULD be the OP's Issuer + Identifier URL. The aud value SHOULD be or include the RP's Client ID + value. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: Claims as a dict OR JWT/JWS/JWE as a string + + Method is used by: + UserInfoEndpoint + """ |