You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
18 KiB
391 lines
18 KiB
6 years ago
|
# -*- 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 .base import GrantTypeBase
|
||
|
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 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)
|