-
-
Notifications
You must be signed in to change notification settings - Fork 480
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Oidc userinfo
- Loading branch information
Showing
8 changed files
with
222 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
tests/openid/connect/core/endpoints/test_userinfo_endpoint.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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']) |