venv added, updated
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
authlib.oidc.core
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenID Connect Core 1.0 Implementation.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html
|
||||
"""
|
||||
|
||||
from .models import AuthorizationCodeMixin
|
||||
from .claims import (
|
||||
IDToken, CodeIDToken, ImplicitIDToken, HybridIDToken,
|
||||
UserInfo, get_claim_cls_by_response_type,
|
||||
)
|
||||
from .grants import OpenIDToken, OpenIDCode, OpenIDHybridGrant, OpenIDImplicitGrant
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AuthorizationCodeMixin',
|
||||
'IDToken', 'CodeIDToken', 'ImplicitIDToken', 'HybridIDToken',
|
||||
'UserInfo', 'get_claim_cls_by_response_type',
|
||||
'OpenIDToken', 'OpenIDCode', 'OpenIDHybridGrant', 'OpenIDImplicitGrant',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
242
myenv/lib/python3.12/site-packages/authlib/oidc/core/claims.py
Normal file
242
myenv/lib/python3.12/site-packages/authlib/oidc/core/claims.py
Normal file
@@ -0,0 +1,242 @@
|
||||
import time
|
||||
import hmac
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.jose import JWTClaims
|
||||
from authlib.jose.errors import (
|
||||
MissingClaimError,
|
||||
InvalidClaimError,
|
||||
)
|
||||
from .util import create_half_hash
|
||||
|
||||
__all__ = [
|
||||
'IDToken', 'CodeIDToken', 'ImplicitIDToken', 'HybridIDToken',
|
||||
'UserInfo', 'get_claim_cls_by_response_type'
|
||||
]
|
||||
|
||||
_REGISTERED_CLAIMS = [
|
||||
'iss', 'sub', 'aud', 'exp', 'nbf', 'iat',
|
||||
'auth_time', 'nonce', 'acr', 'amr', 'azp',
|
||||
'at_hash',
|
||||
]
|
||||
|
||||
|
||||
class IDToken(JWTClaims):
|
||||
ESSENTIAL_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat']
|
||||
|
||||
def validate(self, now=None, leeway=0):
|
||||
for k in self.ESSENTIAL_CLAIMS:
|
||||
if k not in self:
|
||||
raise MissingClaimError(k)
|
||||
|
||||
self._validate_essential_claims()
|
||||
if now is None:
|
||||
now = int(time.time())
|
||||
|
||||
self.validate_iss()
|
||||
self.validate_sub()
|
||||
self.validate_aud()
|
||||
self.validate_exp(now, leeway)
|
||||
self.validate_nbf(now, leeway)
|
||||
self.validate_iat(now, leeway)
|
||||
self.validate_auth_time()
|
||||
self.validate_nonce()
|
||||
self.validate_acr()
|
||||
self.validate_amr()
|
||||
self.validate_azp()
|
||||
self.validate_at_hash()
|
||||
|
||||
def validate_auth_time(self):
|
||||
"""Time when the End-User authentication occurred. Its value is a JSON
|
||||
number representing the number of seconds from 1970-01-01T0:0:0Z as
|
||||
measured in UTC until the date/time. When a max_age request is made or
|
||||
when auth_time is requested as an Essential Claim, then this Claim is
|
||||
REQUIRED; otherwise, its inclusion is OPTIONAL.
|
||||
"""
|
||||
auth_time = self.get('auth_time')
|
||||
if self.params.get('max_age') and not auth_time:
|
||||
raise MissingClaimError('auth_time')
|
||||
|
||||
if auth_time and not isinstance(auth_time, (int, float)):
|
||||
raise InvalidClaimError('auth_time')
|
||||
|
||||
def validate_nonce(self):
|
||||
"""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. If present in the ID
|
||||
Token, Clients MUST verify that the nonce Claim Value is equal to the
|
||||
value of the nonce parameter sent in the Authentication Request. If
|
||||
present in the Authentication Request, Authorization Servers MUST
|
||||
include a nonce Claim in the ID Token with the Claim Value being the
|
||||
nonce value sent in the Authentication Request. Authorization Servers
|
||||
SHOULD perform no other processing on nonce values used. The nonce
|
||||
value is a case sensitive string.
|
||||
"""
|
||||
nonce_value = self.params.get('nonce')
|
||||
if nonce_value:
|
||||
if 'nonce' not in self:
|
||||
raise MissingClaimError('nonce')
|
||||
if nonce_value != self['nonce']:
|
||||
raise InvalidClaimError('nonce')
|
||||
|
||||
def validate_acr(self):
|
||||
"""OPTIONAL. Authentication Context Class Reference. String specifying
|
||||
an Authentication Context Class Reference value that identifies the
|
||||
Authentication Context Class that the authentication performed
|
||||
satisfied. The value "0" indicates the End-User authentication did not
|
||||
meet the requirements of `ISO/IEC 29115`_ level 1. Authentication
|
||||
using a long-lived browser cookie, for instance, is one example where
|
||||
the use of "level 0" is appropriate. Authentications with level 0
|
||||
SHOULD NOT be used to authorize access to any resource of any monetary
|
||||
value. An absolute URI or an `RFC 6711`_ registered name SHOULD be
|
||||
used as the acr value; registered names MUST NOT be used with a
|
||||
different meaning than that which is registered. Parties using this
|
||||
claim will need to agree upon the meanings of the values used, which
|
||||
may be context-specific. The acr value is a case sensitive string.
|
||||
|
||||
.. _`ISO/IEC 29115`: https://www.iso.org/standard/45138.html
|
||||
.. _`RFC 6711`: https://tools.ietf.org/html/rfc6711
|
||||
"""
|
||||
return self._validate_claim_value('acr')
|
||||
|
||||
def validate_amr(self):
|
||||
"""OPTIONAL. Authentication Methods References. JSON array of strings
|
||||
that are identifiers for authentication methods used in the
|
||||
authentication. For instance, values might indicate that both password
|
||||
and OTP authentication methods were used. The definition of particular
|
||||
values to be used in the amr Claim is beyond the scope of this
|
||||
specification. Parties using this claim will need to agree upon the
|
||||
meanings of the values used, which may be context-specific. The amr
|
||||
value is an array of case sensitive strings.
|
||||
"""
|
||||
amr = self.get('amr')
|
||||
if amr and not isinstance(self['amr'], list):
|
||||
raise InvalidClaimError('amr')
|
||||
|
||||
def validate_azp(self):
|
||||
"""OPTIONAL. Authorized party - the party to which the ID Token was
|
||||
issued. If present, it MUST contain the OAuth 2.0 Client ID of this
|
||||
party. This Claim is only needed when the ID Token has a single
|
||||
audience value and that audience is different than the authorized
|
||||
party. It MAY be included even when the authorized party is the same
|
||||
as the sole audience. The azp value is a case sensitive string
|
||||
containing a StringOrURI value.
|
||||
"""
|
||||
aud = self.get('aud')
|
||||
client_id = self.params.get('client_id')
|
||||
required = False
|
||||
if aud and client_id:
|
||||
if isinstance(aud, list) and len(aud) == 1:
|
||||
aud = aud[0]
|
||||
if aud != client_id:
|
||||
required = True
|
||||
|
||||
azp = self.get('azp')
|
||||
if required and not azp:
|
||||
raise MissingClaimError('azp')
|
||||
|
||||
if azp and client_id and azp != client_id:
|
||||
raise InvalidClaimError('azp')
|
||||
|
||||
def validate_at_hash(self):
|
||||
"""OPTIONAL. Access Token hash value. Its value is the base64url
|
||||
encoding of the left-most half of the hash of the octets of the ASCII
|
||||
representation of the access_token value, where the hash algorithm
|
||||
used is the hash algorithm used in the alg Header Parameter of the
|
||||
ID Token's JOSE Header. For instance, if the alg is RS256, hash the
|
||||
access_token value with SHA-256, then take the left-most 128 bits and
|
||||
base64url encode them. The at_hash value is a case sensitive string.
|
||||
"""
|
||||
access_token = self.params.get('access_token')
|
||||
at_hash = self.get('at_hash')
|
||||
if at_hash and access_token:
|
||||
if not _verify_hash(at_hash, access_token, self.header['alg']):
|
||||
raise InvalidClaimError('at_hash')
|
||||
|
||||
|
||||
class CodeIDToken(IDToken):
|
||||
RESPONSE_TYPES = ('code',)
|
||||
REGISTERED_CLAIMS = _REGISTERED_CLAIMS
|
||||
|
||||
|
||||
class ImplicitIDToken(IDToken):
|
||||
RESPONSE_TYPES = ('id_token', 'id_token token')
|
||||
ESSENTIAL_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat', 'nonce']
|
||||
REGISTERED_CLAIMS = _REGISTERED_CLAIMS
|
||||
|
||||
def validate_at_hash(self):
|
||||
"""If the ID Token is issued from the Authorization Endpoint with an
|
||||
access_token value, which is the case for the response_type value
|
||||
id_token token, this is REQUIRED; it MAY NOT be used when no Access
|
||||
Token is issued, which is the case for the response_type value
|
||||
id_token.
|
||||
"""
|
||||
access_token = self.params.get('access_token')
|
||||
if access_token and 'at_hash' not in self:
|
||||
raise MissingClaimError('at_hash')
|
||||
super().validate_at_hash()
|
||||
|
||||
|
||||
class HybridIDToken(ImplicitIDToken):
|
||||
RESPONSE_TYPES = ('code id_token', 'code token', 'code id_token token')
|
||||
REGISTERED_CLAIMS = _REGISTERED_CLAIMS + ['c_hash']
|
||||
|
||||
def validate(self, now=None, leeway=0):
|
||||
super().validate(now=now, leeway=leeway)
|
||||
self.validate_c_hash()
|
||||
|
||||
def validate_c_hash(self):
|
||||
"""Code hash value. Its value is the base64url encoding of the
|
||||
left-most half of the hash of the octets of the ASCII representation
|
||||
of the code value, where the hash algorithm used is the hash algorithm
|
||||
used in the alg Header Parameter of the ID Token's JOSE Header. For
|
||||
instance, if the alg is HS512, hash the code value with SHA-512, then
|
||||
take the left-most 256 bits and base64url encode them. The c_hash
|
||||
value is a case sensitive string.
|
||||
If the ID Token is issued from the Authorization Endpoint with a code,
|
||||
which is the case for the response_type values code id_token and code
|
||||
id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL.
|
||||
"""
|
||||
code = self.params.get('code')
|
||||
c_hash = self.get('c_hash')
|
||||
if code:
|
||||
if not c_hash:
|
||||
raise MissingClaimError('c_hash')
|
||||
if not _verify_hash(c_hash, code, self.header['alg']):
|
||||
raise InvalidClaimError('c_hash')
|
||||
|
||||
|
||||
class UserInfo(dict):
|
||||
"""The standard claims of a UserInfo object. Defined per `Section 5.1`_.
|
||||
|
||||
.. _`Section 5.1`: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
"""
|
||||
|
||||
#: registered claims that UserInfo supports
|
||||
REGISTERED_CLAIMS = [
|
||||
'sub', 'name', 'given_name', 'family_name', 'middle_name', 'nickname',
|
||||
'preferred_username', 'profile', 'picture', 'website', 'email',
|
||||
'email_verified', 'gender', 'birthdate', 'zoneinfo', 'locale',
|
||||
'phone_number', 'phone_number_verified', 'address', 'updated_at',
|
||||
]
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return object.__getattribute__(self, key)
|
||||
except AttributeError as error:
|
||||
if key in self.REGISTERED_CLAIMS:
|
||||
return self.get(key)
|
||||
raise error
|
||||
|
||||
|
||||
def get_claim_cls_by_response_type(response_type):
|
||||
claims_classes = (CodeIDToken, ImplicitIDToken, HybridIDToken)
|
||||
for claims_cls in claims_classes:
|
||||
if response_type in claims_cls.RESPONSE_TYPES:
|
||||
return claims_cls
|
||||
|
||||
|
||||
def _verify_hash(signature, s, alg):
|
||||
hash_value = create_half_hash(s, alg)
|
||||
if not hash_value:
|
||||
return True
|
||||
return hmac.compare_digest(hash_value, to_bytes(signature))
|
||||
@@ -0,0 +1,78 @@
|
||||
from authlib.oauth2 import OAuth2Error
|
||||
|
||||
|
||||
class InteractionRequiredError(OAuth2Error):
|
||||
"""The Authorization Server requires End-User interaction of some form
|
||||
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.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
"""
|
||||
error = 'interaction_required'
|
||||
|
||||
|
||||
class LoginRequiredError(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.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
"""
|
||||
error = 'login_required'
|
||||
|
||||
|
||||
class AccountSelectionRequiredError(OAuth2Error):
|
||||
"""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.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
"""
|
||||
error = 'account_selection_required'
|
||||
|
||||
|
||||
class ConsentRequiredError(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.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
"""
|
||||
error = 'consent_required'
|
||||
|
||||
|
||||
class InvalidRequestURIError(OAuth2Error):
|
||||
"""The request_uri in the Authorization Request returns an error or
|
||||
contains invalid data.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
"""
|
||||
error = 'invalid_request_uri'
|
||||
|
||||
|
||||
class InvalidRequestObjectError(OAuth2Error):
|
||||
"""The request parameter contains an invalid Request Object."""
|
||||
error = 'invalid_request_object'
|
||||
|
||||
|
||||
class RequestNotSupportedError(OAuth2Error):
|
||||
"""The OP does not support use of the request parameter."""
|
||||
error = 'request_not_supported'
|
||||
|
||||
|
||||
class RequestURINotSupportedError(OAuth2Error):
|
||||
"""The OP does not support use of the request_uri parameter."""
|
||||
error = 'request_uri_not_supported'
|
||||
|
||||
|
||||
class RegistrationNotSupportedError(OAuth2Error):
|
||||
"""The OP does not support use of the registration parameter."""
|
||||
error = 'registration_not_supported'
|
||||
@@ -0,0 +1,10 @@
|
||||
from .code import OpenIDToken, OpenIDCode
|
||||
from .implicit import OpenIDImplicitGrant
|
||||
from .hybrid import OpenIDHybridGrant
|
||||
|
||||
__all__ = [
|
||||
'OpenIDToken',
|
||||
'OpenIDCode',
|
||||
'OpenIDImplicitGrant',
|
||||
'OpenIDHybridGrant',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
authlib.oidc.core.grants.code
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implementation of Authentication using the Authorization Code Flow
|
||||
per `Section 3.1`_.
|
||||
|
||||
.. _`Section 3.1`: http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
|
||||
"""
|
||||
|
||||
import logging
|
||||
from authlib.oauth2.rfc6749 import OAuth2Request
|
||||
from .util import (
|
||||
is_openid_scope,
|
||||
validate_nonce,
|
||||
validate_request_prompt,
|
||||
generate_id_token,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenIDToken:
|
||||
def get_jwt_config(self, grant): # pragma: no cover
|
||||
"""Get the JWT configuration for OpenIDCode extension. The JWT
|
||||
configuration will be used to generate ``id_token``. Developers
|
||||
MUST implement this method in subclass, e.g.::
|
||||
|
||||
def get_jwt_config(self, grant):
|
||||
return {
|
||||
'key': read_private_key_file(key_path),
|
||||
'alg': 'RS256',
|
||||
'iss': 'issuer-identity',
|
||||
'exp': 3600
|
||||
}
|
||||
|
||||
:param grant: AuthorizationCodeGrant instance
|
||||
:return: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_user_info(self, user, scope):
|
||||
"""Provide user information for the given scope. Developers
|
||||
MUST implement this method in subclass, e.g.::
|
||||
|
||||
from authlib.oidc.core import UserInfo
|
||||
|
||||
def generate_user_info(self, user, scope):
|
||||
user_info = UserInfo(sub=user.id, name=user.name)
|
||||
if 'email' in scope:
|
||||
user_info['email'] = user.email
|
||||
return user_info
|
||||
|
||||
:param user: user instance
|
||||
:param scope: scope of the token
|
||||
:return: ``authlib.oidc.core.UserInfo`` instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_audiences(self, request):
|
||||
"""Parse `aud` value for id_token, default value is client id. Developers
|
||||
MAY rewrite this method to provide a customized audience value.
|
||||
"""
|
||||
client = request.client
|
||||
return [client.get_client_id()]
|
||||
|
||||
def process_token(self, grant, token):
|
||||
scope = token.get('scope')
|
||||
if not scope or not is_openid_scope(scope):
|
||||
# standard authorization code flow
|
||||
return token
|
||||
|
||||
request: OAuth2Request = grant.request
|
||||
authorization_code = request.authorization_code
|
||||
|
||||
config = self.get_jwt_config(grant)
|
||||
config['aud'] = self.get_audiences(request)
|
||||
|
||||
if authorization_code:
|
||||
config['nonce'] = authorization_code.get_nonce()
|
||||
config['auth_time'] = authorization_code.get_auth_time()
|
||||
|
||||
user_info = self.generate_user_info(request.user, token['scope'])
|
||||
id_token = generate_id_token(token, user_info, **config)
|
||||
token['id_token'] = id_token
|
||||
return token
|
||||
|
||||
def __call__(self, grant):
|
||||
grant.register_hook('process_token', self.process_token)
|
||||
|
||||
|
||||
class OpenIDCode(OpenIDToken):
|
||||
"""An extension from OpenID Connect for "grant_type=code" request. Developers
|
||||
MUST implement the missing methods::
|
||||
|
||||
class MyOpenIDCode(OpenIDCode):
|
||||
def get_jwt_config(self, grant):
|
||||
return {...}
|
||||
|
||||
def exists_nonce(self, nonce, request):
|
||||
return check_if_nonce_in_cache(request.client_id, nonce)
|
||||
|
||||
def generate_user_info(self, user, scope):
|
||||
return {...}
|
||||
|
||||
The register this extension with AuthorizationCodeGrant::
|
||||
|
||||
authorization_server.register_grant(AuthorizationCodeGrant, extensions=[MyOpenIDCode()])
|
||||
"""
|
||||
def __init__(self, require_nonce=False):
|
||||
self.require_nonce = require_nonce
|
||||
|
||||
def exists_nonce(self, nonce, request):
|
||||
"""Check if the given nonce is existing in your database. Developers
|
||||
MUST implement this method in subclass, e.g.::
|
||||
|
||||
def exists_nonce(self, nonce, request):
|
||||
exists = AuthorizationCode.query.filter_by(
|
||||
client_id=request.client_id, nonce=nonce
|
||||
).first()
|
||||
return bool(exists)
|
||||
|
||||
:param nonce: A string of "nonce" parameter in request
|
||||
:param request: OAuth2Request instance
|
||||
:return: Boolean
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_openid_authorization_request(self, grant):
|
||||
validate_nonce(grant.request, self.exists_nonce, self.require_nonce)
|
||||
|
||||
def __call__(self, grant):
|
||||
grant.register_hook('process_token', self.process_token)
|
||||
if is_openid_scope(grant.request.scope):
|
||||
grant.register_hook(
|
||||
'after_validate_authorization_request',
|
||||
self.validate_openid_authorization_request
|
||||
)
|
||||
grant.register_hook(
|
||||
'after_validate_consent_request',
|
||||
validate_request_prompt
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
import logging
|
||||
from authlib.common.security import generate_token
|
||||
from authlib.oauth2.rfc6749 import InvalidScopeError
|
||||
from authlib.oauth2.rfc6749.grants.authorization_code import (
|
||||
validate_code_authorization_request
|
||||
)
|
||||
from .implicit import OpenIDImplicitGrant
|
||||
from .util import is_openid_scope, validate_nonce
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenIDHybridGrant(OpenIDImplicitGrant):
|
||||
#: Generated "code" length
|
||||
AUTHORIZATION_CODE_LENGTH = 48
|
||||
|
||||
RESPONSE_TYPES = {'code id_token', 'code token', 'code id_token token'}
|
||||
GRANT_TYPE = 'code'
|
||||
DEFAULT_RESPONSE_MODE = 'fragment'
|
||||
|
||||
def generate_authorization_code(self):
|
||||
""""The method to generate "code" value for authorization code data.
|
||||
Developers may rewrite this method, or customize the code length with::
|
||||
|
||||
class MyAuthorizationCodeGrant(AuthorizationCodeGrant):
|
||||
AUTHORIZATION_CODE_LENGTH = 32 # default is 48
|
||||
"""
|
||||
return generate_token(self.AUTHORIZATION_CODE_LENGTH)
|
||||
|
||||
def save_authorization_code(self, code, request):
|
||||
"""Save authorization_code for later use. Developers MUST implement
|
||||
it in subclass. Here is an example::
|
||||
|
||||
def save_authorization_code(self, code, request):
|
||||
client = request.client
|
||||
auth_code = AuthorizationCode(
|
||||
code=code,
|
||||
client_id=client.client_id,
|
||||
redirect_uri=request.redirect_uri,
|
||||
scope=request.scope,
|
||||
nonce=request.data.get('nonce'),
|
||||
user_id=request.user.id,
|
||||
)
|
||||
auth_code.save()
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_authorization_request(self):
|
||||
if not is_openid_scope(self.request.scope):
|
||||
raise InvalidScopeError(
|
||||
'Missing "openid" scope',
|
||||
redirect_uri=self.request.redirect_uri,
|
||||
redirect_fragment=True,
|
||||
)
|
||||
self.register_hook(
|
||||
'after_validate_authorization_request',
|
||||
lambda grant: validate_nonce(
|
||||
grant.request, grant.exists_nonce, required=True)
|
||||
)
|
||||
return validate_code_authorization_request(self)
|
||||
|
||||
def create_granted_params(self, grant_user):
|
||||
self.request.user = grant_user
|
||||
client = self.request.client
|
||||
code = self.generate_authorization_code()
|
||||
self.save_authorization_code(code, self.request)
|
||||
params = [('code', code)]
|
||||
token = self.generate_token(
|
||||
grant_type='implicit',
|
||||
user=grant_user,
|
||||
scope=self.request.scope,
|
||||
include_refresh_token=False
|
||||
)
|
||||
|
||||
response_types = self.request.response_type.split()
|
||||
if 'token' in response_types:
|
||||
log.debug('Grant token %r to %r', token, client)
|
||||
self.server.save_token(token, self.request)
|
||||
if 'id_token' in response_types:
|
||||
token = self.process_implicit_token(token, code)
|
||||
else:
|
||||
# response_type is "code id_token"
|
||||
token = {
|
||||
'expires_in': token['expires_in'],
|
||||
'scope': token['scope']
|
||||
}
|
||||
token = self.process_implicit_token(token, code)
|
||||
|
||||
params.extend([(k, token[k]) for k in token])
|
||||
return params
|
||||
@@ -0,0 +1,150 @@
|
||||
import logging
|
||||
from authlib.oauth2.rfc6749 import (
|
||||
OAuth2Error,
|
||||
InvalidScopeError,
|
||||
AccessDeniedError,
|
||||
ImplicitGrant,
|
||||
)
|
||||
from .util import (
|
||||
is_openid_scope,
|
||||
validate_nonce,
|
||||
validate_request_prompt,
|
||||
create_response_mode_response,
|
||||
generate_id_token,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenIDImplicitGrant(ImplicitGrant):
|
||||
RESPONSE_TYPES = {'id_token token', 'id_token'}
|
||||
DEFAULT_RESPONSE_MODE = 'fragment'
|
||||
|
||||
def exists_nonce(self, nonce, request):
|
||||
"""Check if the given nonce is existing in your database. Developers
|
||||
should implement this method in subclass, e.g.::
|
||||
|
||||
def exists_nonce(self, nonce, request):
|
||||
exists = AuthorizationCode.query.filter_by(
|
||||
client_id=request.client_id, nonce=nonce
|
||||
).first()
|
||||
return bool(exists)
|
||||
|
||||
:param nonce: A string of "nonce" parameter in request
|
||||
:param request: OAuth2Request instance
|
||||
:return: Boolean
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_jwt_config(self):
|
||||
"""Get the JWT configuration for OpenIDImplicitGrant. The JWT
|
||||
configuration will be used to generate ``id_token``. Developers
|
||||
MUST implement this method in subclass, e.g.::
|
||||
|
||||
def get_jwt_config(self):
|
||||
return {
|
||||
'key': read_private_key_file(key_path),
|
||||
'alg': 'RS256',
|
||||
'iss': 'issuer-identity',
|
||||
'exp': 3600
|
||||
}
|
||||
|
||||
:return: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_user_info(self, user, scope):
|
||||
"""Provide user information for the given scope. Developers
|
||||
MUST implement this method in subclass, e.g.::
|
||||
|
||||
from authlib.oidc.core import UserInfo
|
||||
|
||||
def generate_user_info(self, user, scope):
|
||||
user_info = UserInfo(sub=user.id, name=user.name)
|
||||
if 'email' in scope:
|
||||
user_info['email'] = user.email
|
||||
return user_info
|
||||
|
||||
:param user: user instance
|
||||
:param scope: scope of the token
|
||||
:return: ``authlib.oidc.core.UserInfo`` instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_audiences(self, request):
|
||||
"""Parse `aud` value for id_token, default value is client id. Developers
|
||||
MAY rewrite this method to provide a customized audience value.
|
||||
"""
|
||||
client = request.client
|
||||
return [client.get_client_id()]
|
||||
|
||||
def validate_authorization_request(self):
|
||||
if not is_openid_scope(self.request.scope):
|
||||
raise InvalidScopeError(
|
||||
'Missing "openid" scope',
|
||||
redirect_uri=self.request.redirect_uri,
|
||||
redirect_fragment=True,
|
||||
)
|
||||
redirect_uri = super().validate_authorization_request()
|
||||
try:
|
||||
validate_nonce(self.request, self.exists_nonce, required=True)
|
||||
except OAuth2Error as error:
|
||||
error.redirect_uri = redirect_uri
|
||||
error.redirect_fragment = True
|
||||
raise error
|
||||
return redirect_uri
|
||||
|
||||
def validate_consent_request(self):
|
||||
redirect_uri = self.validate_authorization_request()
|
||||
validate_request_prompt(self, redirect_uri, redirect_fragment=True)
|
||||
|
||||
def create_authorization_response(self, redirect_uri, grant_user):
|
||||
state = self.request.state
|
||||
if grant_user:
|
||||
params = self.create_granted_params(grant_user)
|
||||
if state:
|
||||
params.append(('state', state))
|
||||
else:
|
||||
error = AccessDeniedError(state=state)
|
||||
params = error.get_body()
|
||||
|
||||
# http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
|
||||
response_mode = self.request.data.get('response_mode', self.DEFAULT_RESPONSE_MODE)
|
||||
return create_response_mode_response(
|
||||
redirect_uri=redirect_uri,
|
||||
params=params,
|
||||
response_mode=response_mode,
|
||||
)
|
||||
|
||||
def create_granted_params(self, grant_user):
|
||||
self.request.user = grant_user
|
||||
client = self.request.client
|
||||
token = self.generate_token(
|
||||
user=grant_user,
|
||||
scope=self.request.scope,
|
||||
include_refresh_token=False
|
||||
)
|
||||
if self.request.response_type == 'id_token':
|
||||
token = {
|
||||
'expires_in': token['expires_in'],
|
||||
'scope': token['scope'],
|
||||
}
|
||||
token = self.process_implicit_token(token)
|
||||
else:
|
||||
log.debug('Grant token %r to %r', token, client)
|
||||
self.server.save_token(token, self.request)
|
||||
token = self.process_implicit_token(token)
|
||||
params = [(k, token[k]) for k in token]
|
||||
return params
|
||||
|
||||
def process_implicit_token(self, token, code=None):
|
||||
config = self.get_jwt_config()
|
||||
config['aud'] = self.get_audiences(self.request)
|
||||
config['nonce'] = self.request.data.get('nonce')
|
||||
if code is not None:
|
||||
config['code'] = code
|
||||
|
||||
user_info = self.generate_user_info(self.request.user, token['scope'])
|
||||
id_token = generate_id_token(token, user_info, **config)
|
||||
token['id_token'] = id_token
|
||||
return token
|
||||
@@ -0,0 +1,131 @@
|
||||
import time
|
||||
from authlib.oauth2.rfc6749 import InvalidRequestError
|
||||
from authlib.oauth2.rfc6749 import scope_to_list
|
||||
from authlib.jose import jwt
|
||||
from authlib.common.encoding import to_native
|
||||
from authlib.common.urls import add_params_to_uri, quote_url
|
||||
from ..util import create_half_hash
|
||||
from ..errors import (
|
||||
LoginRequiredError,
|
||||
AccountSelectionRequiredError,
|
||||
ConsentRequiredError,
|
||||
)
|
||||
|
||||
|
||||
def is_openid_scope(scope):
|
||||
scopes = scope_to_list(scope)
|
||||
return scopes and 'openid' in scopes
|
||||
|
||||
|
||||
def validate_request_prompt(grant, redirect_uri, redirect_fragment=False):
|
||||
prompt = grant.request.data.get('prompt')
|
||||
end_user = grant.request.user
|
||||
if not prompt:
|
||||
if not end_user:
|
||||
grant.prompt = 'login'
|
||||
return grant
|
||||
|
||||
if prompt == 'none' and not end_user:
|
||||
raise LoginRequiredError(
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=redirect_fragment)
|
||||
|
||||
prompts = prompt.split()
|
||||
if 'none' in prompts and len(prompts) > 1:
|
||||
# If this parameter contains none with any other value,
|
||||
# an error is returned
|
||||
raise InvalidRequestError(
|
||||
'Invalid "prompt" parameter.',
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=redirect_fragment)
|
||||
|
||||
prompt = _guess_prompt_value(
|
||||
end_user, prompts, redirect_uri, redirect_fragment=redirect_fragment)
|
||||
if prompt:
|
||||
grant.prompt = prompt
|
||||
return grant
|
||||
|
||||
|
||||
def validate_nonce(request, exists_nonce, required=False):
|
||||
nonce = request.data.get('nonce')
|
||||
if not nonce:
|
||||
if required:
|
||||
raise InvalidRequestError('Missing "nonce" in request.')
|
||||
return True
|
||||
|
||||
if exists_nonce(nonce, request):
|
||||
raise InvalidRequestError('Replay attack')
|
||||
|
||||
|
||||
def generate_id_token(
|
||||
token, user_info, key, iss, aud, alg='RS256', exp=3600,
|
||||
nonce=None, auth_time=None, code=None):
|
||||
|
||||
now = int(time.time())
|
||||
if auth_time is None:
|
||||
auth_time = now
|
||||
|
||||
payload = {
|
||||
'iss': iss,
|
||||
'aud': aud,
|
||||
'iat': now,
|
||||
'exp': now + exp,
|
||||
'auth_time': auth_time,
|
||||
}
|
||||
if nonce:
|
||||
payload['nonce'] = nonce
|
||||
|
||||
if code:
|
||||
payload['c_hash'] = to_native(create_half_hash(code, alg))
|
||||
|
||||
access_token = token.get('access_token')
|
||||
if access_token:
|
||||
payload['at_hash'] = to_native(create_half_hash(access_token, alg))
|
||||
|
||||
payload.update(user_info)
|
||||
return to_native(jwt.encode({'alg': alg}, payload, key))
|
||||
|
||||
|
||||
def create_response_mode_response(redirect_uri, params, response_mode):
|
||||
if response_mode == 'form_post':
|
||||
tpl = (
|
||||
'<html><head><title>Redirecting</title></head>'
|
||||
'<body onload="javascript:document.forms[0].submit()">'
|
||||
'<form method="post" action="{}">{}</form></body></html>'
|
||||
)
|
||||
inputs = ''.join([
|
||||
'<input type="hidden" name="{}" value="{}"/>'.format(
|
||||
quote_url(k), quote_url(v))
|
||||
for k, v in params
|
||||
])
|
||||
body = tpl.format(quote_url(redirect_uri), inputs)
|
||||
return 200, body, [('Content-Type', 'text/html; charset=utf-8')]
|
||||
|
||||
if response_mode == 'query':
|
||||
uri = add_params_to_uri(redirect_uri, params, fragment=False)
|
||||
elif response_mode == 'fragment':
|
||||
uri = add_params_to_uri(redirect_uri, params, fragment=True)
|
||||
else:
|
||||
raise InvalidRequestError('Invalid "response_mode" value')
|
||||
|
||||
return 302, '', [('Location', uri)]
|
||||
|
||||
|
||||
def _guess_prompt_value(end_user, prompts, redirect_uri, redirect_fragment):
|
||||
# http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
|
||||
if not end_user and 'login' in prompts:
|
||||
return 'login'
|
||||
|
||||
if 'consent' in prompts:
|
||||
if not end_user:
|
||||
raise ConsentRequiredError(
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=redirect_fragment)
|
||||
return 'consent'
|
||||
elif 'select_account' in prompts:
|
||||
if not end_user:
|
||||
raise AccountSelectionRequiredError(
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=redirect_fragment)
|
||||
return 'select_account'
|
||||
@@ -0,0 +1,13 @@
|
||||
from authlib.oauth2.rfc6749 import (
|
||||
AuthorizationCodeMixin as _AuthorizationCodeMixin
|
||||
)
|
||||
|
||||
|
||||
class AuthorizationCodeMixin(_AuthorizationCodeMixin):
|
||||
def get_nonce(self):
|
||||
"""Get "nonce" value of the authorization code object."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_auth_time(self):
|
||||
"""Get "auth_time" value of the authorization code object."""
|
||||
raise NotImplementedError()
|
||||
12
myenv/lib/python3.12/site-packages/authlib/oidc/core/util.py
Normal file
12
myenv/lib/python3.12/site-packages/authlib/oidc/core/util.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import hashlib
|
||||
from authlib.common.encoding import to_bytes, urlsafe_b64encode
|
||||
|
||||
|
||||
def create_half_hash(s, alg):
|
||||
hash_type = f'sha{alg[2:]}'
|
||||
hash_alg = getattr(hashlib, hash_type, None)
|
||||
if not hash_alg:
|
||||
return None
|
||||
data_digest = hash_alg(to_bytes(s)).digest()
|
||||
slice_index = int(len(data_digest) / 2)
|
||||
return urlsafe_b64encode(data_digest[:slice_index])
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
authlib.oidc.discover
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenID Connect Discovery 1.0 Implementation.
|
||||
|
||||
https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
"""
|
||||
|
||||
from .models import OpenIDProviderMetadata
|
||||
from .well_known import get_well_known_url
|
||||
|
||||
__all__ = ['OpenIDProviderMetadata', 'get_well_known_url']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,283 @@
|
||||
from authlib.oauth2.rfc8414 import AuthorizationServerMetadata
|
||||
from authlib.oauth2.rfc8414.models import validate_array_value
|
||||
|
||||
|
||||
class OpenIDProviderMetadata(AuthorizationServerMetadata):
|
||||
REGISTRY_KEYS = [
|
||||
'issuer', 'authorization_endpoint', 'token_endpoint',
|
||||
'jwks_uri', 'registration_endpoint', 'scopes_supported',
|
||||
'response_types_supported', 'response_modes_supported',
|
||||
'grant_types_supported',
|
||||
'token_endpoint_auth_methods_supported',
|
||||
'token_endpoint_auth_signing_alg_values_supported',
|
||||
'service_documentation', 'ui_locales_supported',
|
||||
'op_policy_uri', 'op_tos_uri',
|
||||
|
||||
# added by OpenID
|
||||
'acr_values_supported', 'subject_types_supported',
|
||||
'id_token_signing_alg_values_supported',
|
||||
'id_token_encryption_alg_values_supported',
|
||||
'id_token_encryption_enc_values_supported',
|
||||
'userinfo_signing_alg_values_supported',
|
||||
'userinfo_encryption_alg_values_supported',
|
||||
'userinfo_encryption_enc_values_supported',
|
||||
'request_object_signing_alg_values_supported',
|
||||
'request_object_encryption_alg_values_supported',
|
||||
'request_object_encryption_enc_values_supported',
|
||||
'display_values_supported',
|
||||
'claim_types_supported',
|
||||
'claims_supported',
|
||||
'claims_locales_supported',
|
||||
'claims_parameter_supported',
|
||||
'request_parameter_supported',
|
||||
'request_uri_parameter_supported',
|
||||
'require_request_uri_registration',
|
||||
|
||||
# not defined by OpenID
|
||||
# 'revocation_endpoint',
|
||||
# 'revocation_endpoint_auth_methods_supported',
|
||||
# 'revocation_endpoint_auth_signing_alg_values_supported',
|
||||
# 'introspection_endpoint',
|
||||
# 'introspection_endpoint_auth_methods_supported',
|
||||
# 'introspection_endpoint_auth_signing_alg_values_supported',
|
||||
# 'code_challenge_methods_supported',
|
||||
]
|
||||
|
||||
def validate_jwks_uri(self):
|
||||
# REQUIRED in OpenID Connect
|
||||
jwks_uri = self.get('jwks_uri')
|
||||
if jwks_uri is None:
|
||||
raise ValueError('"jwks_uri" is required')
|
||||
return super().validate_jwks_uri()
|
||||
|
||||
def validate_acr_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the Authentication
|
||||
Context Class References that this OP supports.
|
||||
"""
|
||||
validate_array_value(self, 'acr_values_supported')
|
||||
|
||||
def validate_subject_types_supported(self):
|
||||
"""REQUIRED. JSON array containing a list of the Subject Identifier
|
||||
types that this OP supports. Valid types include pairwise and public.
|
||||
"""
|
||||
# 1. REQUIRED
|
||||
values = self.get('subject_types_supported')
|
||||
if values is None:
|
||||
raise ValueError('"subject_types_supported" is required')
|
||||
|
||||
# 2. JSON array
|
||||
if not isinstance(values, list):
|
||||
raise ValueError('"subject_types_supported" MUST be JSON array')
|
||||
|
||||
# 3. Valid types include pairwise and public
|
||||
valid_types = {'pairwise', 'public'}
|
||||
if not valid_types.issuperset(set(values)):
|
||||
raise ValueError(
|
||||
'"subject_types_supported" contains invalid values')
|
||||
|
||||
def validate_id_token_signing_alg_values_supported(self):
|
||||
"""REQUIRED. JSON array containing a list of the JWS signing
|
||||
algorithms (alg values) supported by the OP for the ID Token to
|
||||
encode the Claims in a JWT [JWT]. The algorithm RS256 MUST be
|
||||
included. The value none MAY be supported, but MUST NOT be used
|
||||
unless the Response Type used returns no ID Token from the
|
||||
Authorization Endpoint (such as when using the Authorization
|
||||
Code Flow).
|
||||
"""
|
||||
# 1. REQUIRED
|
||||
values = self.get('id_token_signing_alg_values_supported')
|
||||
if values is None:
|
||||
raise ValueError('"id_token_signing_alg_values_supported" is required')
|
||||
|
||||
# 2. JSON array
|
||||
if not isinstance(values, list):
|
||||
raise ValueError('"id_token_signing_alg_values_supported" MUST be JSON array')
|
||||
|
||||
# 3. The algorithm RS256 MUST be included
|
||||
if 'RS256' not in values:
|
||||
raise ValueError(
|
||||
'"RS256" MUST be included in "id_token_signing_alg_values_supported"')
|
||||
|
||||
def validate_id_token_encryption_alg_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWE encryption
|
||||
algorithms (alg values) supported by the OP for the ID Token to
|
||||
encode the Claims in a JWT.
|
||||
"""
|
||||
validate_array_value(self, 'id_token_encryption_alg_values_supported')
|
||||
|
||||
def validate_id_token_encryption_enc_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWE encryption
|
||||
algorithms (enc values) supported by the OP for the ID Token to
|
||||
encode the Claims in a JWT.
|
||||
"""
|
||||
validate_array_value(self, 'id_token_encryption_enc_values_supported')
|
||||
|
||||
def validate_userinfo_signing_alg_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWS signing
|
||||
algorithms (alg values) [JWA] supported by the UserInfo Endpoint
|
||||
to encode the Claims in a JWT. The value none MAY be included.
|
||||
"""
|
||||
validate_array_value(self, 'userinfo_signing_alg_values_supported')
|
||||
|
||||
def validate_userinfo_encryption_alg_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWE encryption
|
||||
algorithms (alg values) [JWA] supported by the UserInfo Endpoint
|
||||
to encode the Claims in a JWT.
|
||||
"""
|
||||
validate_array_value(self, 'userinfo_encryption_alg_values_supported')
|
||||
|
||||
def validate_userinfo_encryption_enc_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWE encryption
|
||||
algorithms (enc values) [JWA] supported by the UserInfo Endpoint
|
||||
to encode the Claims in a JWT.
|
||||
"""
|
||||
validate_array_value(self, 'userinfo_encryption_enc_values_supported')
|
||||
|
||||
def validate_request_object_signing_alg_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWS signing
|
||||
algorithms (alg values) supported by the OP for Request Objects,
|
||||
which are described in Section 6.1 of OpenID Connect Core 1.0.
|
||||
These algorithms are used both when the Request Object is passed
|
||||
by value (using the request parameter) and when it is passed by
|
||||
reference (using the request_uri parameter). Servers SHOULD support
|
||||
none and RS256.
|
||||
"""
|
||||
values = self.get('request_object_signing_alg_values_supported')
|
||||
if not values:
|
||||
return
|
||||
|
||||
if not isinstance(values, list):
|
||||
raise ValueError('"request_object_signing_alg_values_supported" MUST be JSON array')
|
||||
|
||||
# Servers SHOULD support none and RS256
|
||||
if 'none' not in values or 'RS256' not in values:
|
||||
raise ValueError(
|
||||
'"request_object_signing_alg_values_supported" '
|
||||
'SHOULD support none and RS256')
|
||||
|
||||
def validate_request_object_encryption_alg_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWE encryption
|
||||
algorithms (alg values) supported by the OP for Request Objects.
|
||||
These algorithms are used both when the Request Object is passed
|
||||
by value and when it is passed by reference.
|
||||
"""
|
||||
validate_array_value(self, 'request_object_encryption_alg_values_supported')
|
||||
|
||||
def validate_request_object_encryption_enc_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the JWE encryption
|
||||
algorithms (enc values) supported by the OP for Request Objects.
|
||||
These algorithms are used both when the Request Object is passed
|
||||
by value and when it is passed by reference.
|
||||
"""
|
||||
validate_array_value(self, 'request_object_encryption_enc_values_supported')
|
||||
|
||||
def validate_display_values_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the display parameter
|
||||
values that the OpenID Provider supports. These values are described
|
||||
in Section 3.1.2.1 of OpenID Connect Core 1.0.
|
||||
"""
|
||||
values = self.get('display_values_supported')
|
||||
if not values:
|
||||
return
|
||||
|
||||
if not isinstance(values, list):
|
||||
raise ValueError('"display_values_supported" MUST be JSON array')
|
||||
|
||||
valid_values = {'page', 'popup', 'touch', 'wap'}
|
||||
if not valid_values.issuperset(set(values)):
|
||||
raise ValueError('"display_values_supported" contains invalid values')
|
||||
|
||||
def validate_claim_types_supported(self):
|
||||
"""OPTIONAL. JSON array containing a list of the Claim Types that
|
||||
the OpenID Provider supports. These Claim Types are described in
|
||||
Section 5.6 of OpenID Connect Core 1.0. Values defined by this
|
||||
specification are normal, aggregated, and distributed. If omitted,
|
||||
the implementation supports only normal Claims.
|
||||
"""
|
||||
values = self.get('claim_types_supported')
|
||||
if not values:
|
||||
return
|
||||
|
||||
if not isinstance(values, list):
|
||||
raise ValueError('"claim_types_supported" MUST be JSON array')
|
||||
|
||||
valid_values = {'normal', 'aggregated', 'distributed'}
|
||||
if not valid_values.issuperset(set(values)):
|
||||
raise ValueError('"claim_types_supported" contains invalid values')
|
||||
|
||||
def validate_claims_supported(self):
|
||||
"""RECOMMENDED. JSON array containing a list of the Claim Names
|
||||
of the Claims that the OpenID Provider MAY be able to supply values
|
||||
for. Note that for privacy or other reasons, this might not be an
|
||||
exhaustive list.
|
||||
"""
|
||||
validate_array_value(self, 'claims_supported')
|
||||
|
||||
def validate_claims_locales_supported(self):
|
||||
"""OPTIONAL. Languages and scripts supported for values in Claims
|
||||
being returned, represented as a JSON array of BCP47 [RFC5646]
|
||||
language tag values. Not all languages and scripts are necessarily
|
||||
supported for all Claim values.
|
||||
"""
|
||||
validate_array_value(self, 'claims_locales_supported')
|
||||
|
||||
def validate_claims_parameter_supported(self):
|
||||
"""OPTIONAL. Boolean value specifying whether the OP supports use of
|
||||
the claims parameter, with true indicating support. If omitted, the
|
||||
default value is false.
|
||||
"""
|
||||
_validate_boolean_value(self, 'claims_parameter_supported')
|
||||
|
||||
def validate_request_parameter_supported(self):
|
||||
"""OPTIONAL. Boolean value specifying whether the OP supports use of
|
||||
the request parameter, with true indicating support. If omitted, the
|
||||
default value is false.
|
||||
"""
|
||||
_validate_boolean_value(self, 'request_parameter_supported')
|
||||
|
||||
def validate_request_uri_parameter_supported(self):
|
||||
"""OPTIONAL. Boolean value specifying whether the OP supports use of
|
||||
the request_uri parameter, with true indicating support. If omitted,
|
||||
the default value is true.
|
||||
"""
|
||||
_validate_boolean_value(self, 'request_uri_parameter_supported')
|
||||
|
||||
def validate_require_request_uri_registration(self):
|
||||
"""OPTIONAL. Boolean value specifying whether the OP requires any
|
||||
request_uri values used to be pre-registered using the request_uris
|
||||
registration parameter. Pre-registration is REQUIRED when the value
|
||||
is true. If omitted, the default value is false.
|
||||
"""
|
||||
_validate_boolean_value(self, 'require_request_uri_registration')
|
||||
|
||||
@property
|
||||
def claim_types_supported(self):
|
||||
# If omitted, the implementation supports only normal Claims
|
||||
return self.get('claim_types_supported', ['normal'])
|
||||
|
||||
@property
|
||||
def claims_parameter_supported(self):
|
||||
# If omitted, the default value is false.
|
||||
return self.get('claims_parameter_supported', False)
|
||||
|
||||
@property
|
||||
def request_parameter_supported(self):
|
||||
# If omitted, the default value is false.
|
||||
return self.get('request_parameter_supported', False)
|
||||
|
||||
@property
|
||||
def request_uri_parameter_supported(self):
|
||||
# If omitted, the default value is true.
|
||||
return self.get('request_uri_parameter_supported', True)
|
||||
|
||||
@property
|
||||
def require_request_uri_registration(self):
|
||||
# If omitted, the default value is false.
|
||||
return self.get('require_request_uri_registration', False)
|
||||
|
||||
|
||||
def _validate_boolean_value(metadata, key):
|
||||
if key not in metadata:
|
||||
return
|
||||
if metadata[key] not in (True, False):
|
||||
raise ValueError(f'"{key}" MUST be boolean')
|
||||
@@ -0,0 +1,17 @@
|
||||
from authlib.common.urls import urlparse
|
||||
|
||||
|
||||
def get_well_known_url(issuer, external=False):
|
||||
"""Get well-known URI with issuer via Section 4.1.
|
||||
|
||||
:param issuer: URL of the issuer
|
||||
:param external: return full external url or not
|
||||
:return: URL
|
||||
"""
|
||||
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
|
||||
if external:
|
||||
return issuer.rstrip('/') + '/.well-known/openid-configuration'
|
||||
|
||||
parsed = urlparse.urlparse(issuer)
|
||||
path = parsed.path
|
||||
return path.rstrip('/') + '/.well-known/openid-configuration'
|
||||
Reference in New Issue
Block a user