diff options
author | Jonathan Huot <JonathanHuot@users.noreply.github.com> | 2019-08-05 17:55:44 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-05 17:55:44 +0200 |
commit | 55bcc38b1d537fecbf7cd0ee4c80a2ba076cc92c (patch) | |
tree | ed7b41de40e29f4f5eabb6a89869f835fabc1514 | |
parent | a97cada2c456358242a4b099fcfed300d8173cf1 (diff) | |
parent | a99c71490095bed06aa848a2324f782d38edf14f (diff) | |
download | oauthlib-55bcc38b1d537fecbf7cd0ee4c80a2ba076cc92c.tar.gz |
Merge branch 'master' into release-3.1.0
-rw-r--r-- | docs/oauth2/oauth2provider-server.dot | 87 | ||||
-rw-r--r-- | docs/oauth2/oidc/validator.rst | 1 | ||||
-rw-r--r-- | oauthlib/oauth1/rfc5849/endpoints/base.py | 7 | ||||
-rw-r--r-- | oauthlib/oauth1/rfc5849/signature.py | 30 | ||||
-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 | ||||
-rw-r--r-- | tests/openid/connect/core/endpoints/test_userinfo_endpoint.py | 70 |
11 files changed, 326 insertions, 69 deletions
diff --git a/docs/oauth2/oauth2provider-server.dot b/docs/oauth2/oauth2provider-server.dot index ec24078..934bd20 100644 --- a/docs/oauth2/oauth2provider-server.dot +++ b/docs/oauth2/oauth2provider-server.dot @@ -5,6 +5,7 @@ digraph oauthlib { webapi_ : oauthlib entry/exit points in shape=hexagon if_ : internal conditions r_ : used when returning from two functions into one for improving clarity + h_ : callbacks/hooks available but not required */ center="1" edge [ style=bold ]; @@ -62,6 +63,7 @@ digraph oauthlib { f_is_within_original_scope [ label="{{<top>is_within_original_scope|{refresh_scopes|refresh_token|request}}|{<true>True|<false>False}}"; ]; f_validate_user [ label="{{<top>validate_user|{username|password|client|request}}|{<true>True|<false>False}}"; ]; f_introspect_token [ label="{{<top>introspect_token|{token|token_type_hint|request}}|{<claims>\{claims\}|<none>None}}"; ]; + f_rotate_refresh_token [ label="{{<top>rotate_refresh_token|{request}}|{<true>True|<false>False}}"; ]; } /* OAuthlib Conditions */ @@ -115,11 +117,41 @@ digraph oauthlib { f_is_within_original_scope; } + { + node [ shape=record,color=grey ]; + edge [ color=grey ]; + + h_pre_auth [ label="{{<top>pre_auth|<arg>request}|<resp>\{credentials\}}}"; ]; + h_post_auth [ label="{{<top>post_auth|<arg>request}|<resp>\{credentials\}}}"; ]; + h_pre_token [ label="{{<top>pre_token|<arg>request}|<resp>}}"; ]; + h_pre_token_password [ label="{{<top>pre_token|<arg>request}|<resp>}}"; ]; + h_pre_token_implicit [ label="{{<top>pre_token|<arg>request}|<resp>}}"; ]; + h_post_token [ label="{{<top>post_token|<arg>request}|<resp>}}"; ]; + h_token_modifiers [ label="{{<top>token_modifiers|{token|token_handler|<arg>request}}|<resp>\{token\}}}"; ]; + h_code_modifiers [ label="{{<top>code_modifiers|{grant|token_handler|<arg>request}}|<resp>\{grant\}}}"; ]; + h_generate_access_token [ label="{{<top>generate_access_token|<arg>request}|<resp>\{access token\}}}"; ]; + h_generate_refresh_token [ label="{{<top>generate_refresh_token|<arg>request}|<resp>\{refresh token\}}}"; ]; + + h_pre_auth:resp:se -> h_pre_auth:arg:ne; + h_post_auth:resp:se -> h_post_auth:arg:ne; + h_pre_token:resp:se -> h_pre_token:arg:ne; + h_pre_token_password:resp:se -> h_pre_token_password:arg:ne; + h_pre_token_implicit:resp:se -> h_pre_token_implicit:arg:ne; + h_post_token:resp:se -> h_post_token:arg:ne; + h_token_modifiers:resp:se -> h_token_modifiers:arg:ne; + h_code_modifiers:resp:se -> h_code_modifiers:arg:ne; + } + { + rank = same; + h_token_modifiers; + h_code_modifiers; + } + /* Authorization Code - Access Token Request */ { edge [ color=darkgreen ]; - endpoint_token:authorization_code:s -> f_client_authentication_required; + endpoint_token:authorization_code:s -> h_pre_token -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; f_client_authentication_required:false:s -> f_authenticate_client_id; f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; @@ -134,8 +166,12 @@ digraph oauthlib { if_redirect_uri_missing -> f_get_default_redirect_uri; f_get_default_redirect_uri:redirect_uri:s -> f_confirm_redirect_uri; - f_confirm_redirect_uri:true:s -> f_save_bearer_token; - f_save_bearer_token -> f_invalidate_authorization_code; + f_confirm_redirect_uri:true:s -> h_post_token; + + h_post_token -> h_generate_access_token -> f_rotate_refresh_token; + f_rotate_refresh_token:true:s -> h_generate_refresh_token -> h_token_modifiers; + f_rotate_refresh_token:false:s -> h_token_modifiers; + h_token_modifiers -> f_save_bearer_token -> f_invalidate_authorization_code -> webapi_response; } /* Authorization Code - Authorization Request */ @@ -149,8 +185,9 @@ digraph oauthlib { if_redirect_uri_present -> f_validate_redirect_uri; if_redirect_uri_missing -> f_get_default_redirect_uri; - f_validate_redirect_uri:true:s -> f_validate_response_type; - f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type; + f_validate_redirect_uri:true:s -> h_pre_auth; + f_get_default_redirect_uri:redirect_uri:s -> h_pre_auth; + h_pre_auth -> f_validate_response_type; f_validate_response_type:true:s -> f_is_pkce_required; f_is_pkce_required:true:s -> if_code_challenge; f_is_pkce_required:false:s -> f_validate_scopes; @@ -158,7 +195,8 @@ digraph oauthlib { if_code_challenge -> f_validate_scopes [ label="present" ]; if_code_challenge -> e_normal [ label="missing",style=dashed ]; - f_validate_scopes:true:s -> f_save_authorization_code; + f_validate_scopes:true:s -> h_post_auth; + h_post_auth -> h_code_modifiers -> f_save_authorization_code; f_save_authorization_code -> webapi_response; } @@ -173,10 +211,13 @@ digraph oauthlib { if_redirect_uri_present -> f_validate_redirect_uri; if_redirect_uri_missing -> f_get_default_redirect_uri; - f_validate_redirect_uri:true:s -> f_validate_response_type; - f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type; + f_validate_redirect_uri:true:s -> h_pre_auth; + f_get_default_redirect_uri:redirect_uri:s -> h_pre_auth; + h_pre_auth -> h_pre_token_implicit -> f_validate_response_type; + f_validate_response_type:true:s -> f_validate_scopes; - f_validate_scopes:true:s -> f_save_bearer_token; + f_validate_scopes:true:s -> h_post_auth -> h_post_token -> + h_generate_access_token -> h_token_modifiers -> f_save_bearer_token -> webapi_response; } @@ -189,15 +230,19 @@ digraph oauthlib { f_client_authentication_required:false:s -> f_authenticate_client_id; f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; - r_client_authenticated -> f_validate_user; + r_client_authenticated -> h_pre_token_password -> f_validate_user; f_validate_user:true:s -> f_validate_grant_type; f_validate_grant_type:true:s -> if_scopes; if_scopes -> f_validate_scopes [ label="present" ]; if_scopes -> f_get_default_scopes [ label="missing" ]; - f_validate_scopes:true:s -> f_save_bearer_token; - f_get_default_scopes -> f_save_bearer_token; + f_validate_scopes:true:s -> h_post_token; + f_get_default_scopes -> h_post_token; + + h_post_token -> h_generate_access_token -> f_rotate_refresh_token; + f_rotate_refresh_token:true:s -> h_generate_refresh_token -> h_token_modifiers; + f_rotate_refresh_token:false:s -> h_token_modifiers -> f_save_bearer_token -> webapi_response; } @@ -205,10 +250,13 @@ digraph oauthlib { { edge [ color=blue ]; - endpoint_token:client_credentials:s -> f_authenticate_client; + endpoint_token:client_credentials:s -> h_pre_token -> f_authenticate_client; + f_authenticate_client:true:s -> f_validate_grant_type; f_validate_grant_type:true:s -> f_validate_scopes; - f_validate_scopes:true:s -> f_save_bearer_token; + f_validate_scopes:true:s -> h_post_token; + + h_post_token -> h_generate_access_token -> h_token_modifiers -> f_save_bearer_token -> webapi_response; } @@ -216,7 +264,7 @@ digraph oauthlib { { edge [ color=brown ]; - endpoint_token:refresh_token:s -> f_client_authentication_required; + endpoint_token:refresh_token:s -> h_pre_token -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; f_client_authentication_required:false:s -> f_authenticate_client_id; f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; @@ -227,9 +275,12 @@ digraph oauthlib { f_validate_refresh_token:true:s -> f_get_original_scopes; f_get_original_scopes -> if_all; if_all -> f_is_within_original_scope [ label="True" ]; - if_all -> f_save_bearer_token [ label="False" ]; - f_is_within_original_scope:true:s -> f_save_bearer_token; - f_save_bearer_token -> webapi_response; + if_all -> h_post_token [ label="False" ]; + f_is_within_original_scope:true:s -> h_post_token; + h_post_token -> h_generate_access_token -> f_rotate_refresh_token; + f_rotate_refresh_token:true:s -> h_generate_refresh_token -> h_token_modifiers; + f_rotate_refresh_token:false:s -> h_token_modifiers; + h_token_modifiers -> f_save_bearer_token -> webapi_response; } /* Introspect Endpoint */ diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst index 7a6f574..17f5825 100644 --- a/docs/oauth2/oidc/validator.rst +++ b/docs/oauth2/oidc/validator.rst @@ -20,6 +20,7 @@ Into from oauthlib.openid import RequestValidator Then, you have to implement the new RequestValidator methods as shown below. +Note that a new UserInfo endpoint is defined and need a new controller into your webserver. RequestValidator Extension ---------------------------------------------------- diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index ecf8a50..f005256 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -12,7 +12,7 @@ import time from oauthlib.common import CaseInsensitiveDict, Request, generate_token -from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC, SIGNATURE_RSA, +from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, errors, signature, utils) @@ -204,9 +204,12 @@ class BaseEndpoint(object): resource_owner_secret = self.request_validator.get_access_token_secret( request.client_key, request.resource_owner_key, request) - if request.signature_method == SIGNATURE_HMAC: + if request.signature_method == SIGNATURE_HMAC_SHA1: valid_signature = signature.verify_hmac_sha1(request, client_secret, resource_owner_secret) + elif request.signature_method == SIGNATURE_HMAC_SHA256: + valid_signature = signature.verify_hmac_sha256(request, + client_secret, resource_owner_secret) else: valid_signature = signature.verify_plaintext(request, client_secret, resource_owner_secret) diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index f899aca..a60bee2 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -661,6 +661,36 @@ def verify_hmac_sha1(request, client_secret=None, return match +def verify_hmac_sha256(request, client_secret=None, + resource_owner_secret=None): + """Verify a HMAC-SHA256 signature. + + Per `section 3.4`_ of the spec. + + .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 + + To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri + attribute MUST be an absolute URI whose netloc part identifies the + origin server or gateway on which the resource resides. Any Host + item of the request argument's headers dict attribute will be + ignored. + + .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 + + """ + norm_params = normalize_parameters(request.params) + bs_uri = base_string_uri(request.uri) + sig_base_str = signature_base_string(request.http_method, bs_uri, + norm_params) + signature = sign_hmac_sha256(sig_base_str, client_secret, + resource_owner_secret) + match = safe_string_equals(signature, request.signature) + if not match: + log.debug('Verify HMAC-SHA256 failed: signature base string: %s', + sig_base_str) + return match + + def _prepare_key_plus(alg, keystr): if isinstance(keystr, bytes): keystr = keystr.decode('utf-8') 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 + """ diff --git a/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py b/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py new file mode 100644 index 0000000..4593d79 --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import mock +import json + +from oauthlib.openid import RequestValidator +from oauthlib.openid import UserInfoEndpoint +from oauthlib.oauth2.rfc6749 import errors + +from tests.unittest import TestCase + + +def set_scopes_valid(token, scopes, request): + request.scopes = ["openid", "bar"] + return True + + +class UserInfoEndpointTest(TestCase): + def setUp(self): + self.claims = { + "sub": "john", + "fruit": "banana" + } + # Can't use MagicMock/wraps below. + # Triggers error when endpoint copies to self.bearer.request_validator + self.validator = RequestValidator() + self.validator.validate_bearer_token = mock.Mock() + self.validator.validate_bearer_token.side_effect = set_scopes_valid + self.validator.get_userinfo_claims = mock.Mock() + self.validator.get_userinfo_claims.return_value = self.claims + self.endpoint = UserInfoEndpoint(self.validator) + + self.uri = 'should_not_matter' + self.headers = { + 'Authorization': 'Bearer eyJxx' + } + + def test_userinfo_no_auth(self): + self.endpoint.create_userinfo_response(self.uri) + + def test_userinfo_wrong_auth(self): + self.headers['Authorization'] = 'Basic foifoifoi' + self.endpoint.create_userinfo_response(self.uri, headers=self.headers) + + def test_userinfo_token_expired(self): + self.validator.validate_bearer_token.return_value = False + self.endpoint.create_userinfo_response(self.uri, headers=self.headers) + + def test_userinfo_token_no_openid_scope(self): + def set_scopes_invalid(token, scopes, request): + request.scopes = ["foo", "bar"] + return True + self.validator.validate_bearer_token.side_effect = set_scopes_invalid + with self.assertRaises(errors.InsufficientScopeError) as context: + self.endpoint.create_userinfo_response(self.uri) + + def test_userinfo_json_response(self): + h, b, s = self.endpoint.create_userinfo_response(self.uri) + self.assertEqual(s, 200) + body_json = json.loads(b) + self.assertEqual(self.claims, body_json) + self.assertEqual("application/json", h['Content-Type']) + + def test_userinfo_jwt_response(self): + self.validator.get_userinfo_claims.return_value = "eyJzzzzz" + h, b, s = self.endpoint.create_userinfo_response(self.uri) + self.assertEqual(s, 200) + self.assertEqual(b, "eyJzzzzz") + self.assertEqual("application/jwt", h['Content-Type']) |