updates
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
"""authlib.oidc.core.
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenID Connect Core 1.0 Implementation.
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html
|
||||
"""
|
||||
|
||||
from .claims import CodeIDToken
|
||||
from .claims import HybridIDToken
|
||||
from .claims import IDToken
|
||||
from .claims import ImplicitIDToken
|
||||
from .claims import UserInfo
|
||||
from .claims import get_claim_cls_by_response_type
|
||||
from .grants import OpenIDCode
|
||||
from .grants import OpenIDHybridGrant
|
||||
from .grants import OpenIDImplicitGrant
|
||||
from .grants import OpenIDToken
|
||||
from .models import AuthorizationCodeMixin
|
||||
from .userinfo import UserInfoEndpoint
|
||||
|
||||
__all__ = [
|
||||
"AuthorizationCodeMixin",
|
||||
"IDToken",
|
||||
"CodeIDToken",
|
||||
"ImplicitIDToken",
|
||||
"HybridIDToken",
|
||||
"UserInfo",
|
||||
"UserInfoEndpoint",
|
||||
"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.
Binary file not shown.
@@ -0,0 +1,308 @@
|
||||
import hmac
|
||||
import time
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.jose import JWTClaims
|
||||
from authlib.jose.errors import InvalidClaimError
|
||||
from authlib.jose.errors import MissingClaimError
|
||||
from authlib.oauth2.rfc6749.util import scope_to_list
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
SCOPES_CLAIMS_MAPPING = {
|
||||
"openid": ["sub"],
|
||||
"profile": [
|
||||
"name",
|
||||
"family_name",
|
||||
"given_name",
|
||||
"middle_name",
|
||||
"nickname",
|
||||
"preferred_username",
|
||||
"profile",
|
||||
"picture",
|
||||
"website",
|
||||
"gender",
|
||||
"birthdate",
|
||||
"zoneinfo",
|
||||
"locale",
|
||||
"updated_at",
|
||||
],
|
||||
"email": ["email", "email_verified"],
|
||||
"address": ["address"],
|
||||
"phone": ["phone_number", "phone_number_verified"],
|
||||
}
|
||||
|
||||
def filter(self, scope: str):
|
||||
"""Return a new UserInfo object containing only the claims matching the scope passed in parameter."""
|
||||
scope = scope_to_list(scope)
|
||||
filtered_claims = [
|
||||
claim
|
||||
for scope_part in scope
|
||||
for claim in self.SCOPES_CLAIMS_MAPPING.get(scope_part, [])
|
||||
]
|
||||
filtered_items = {
|
||||
key: val for key, val in self.items() if key in filtered_claims
|
||||
}
|
||||
return UserInfo(filtered_items)
|
||||
|
||||
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,87 @@
|
||||
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,11 @@
|
||||
from .code import OpenIDCode
|
||||
from .code import OpenIDToken
|
||||
from .hybrid import OpenIDHybridGrant
|
||||
from .implicit import OpenIDImplicitGrant
|
||||
|
||||
__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,162 @@
|
||||
"""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 generate_id_token
|
||||
from .util import is_openid_scope
|
||||
from .util import validate_nonce
|
||||
from .util import validate_request_prompt
|
||||
|
||||
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``.
|
||||
If ``alg`` is undefined, the ``id_token_signed_response_alg`` client
|
||||
metadata will be used. By default ``RS256`` will be used.
|
||||
If ``key`` is undefined, the ``jwks_uri`` or ``jwks`` client metadata
|
||||
will be used.
|
||||
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, response):
|
||||
_, token, _ = response
|
||||
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)
|
||||
|
||||
# Per OpenID Connect Registration 1.0 Section 2:
|
||||
# Use client's id_token_signed_response_alg if specified
|
||||
if not config.get("alg") and (
|
||||
client_alg := request.client.id_token_signed_response_alg
|
||||
):
|
||||
config["alg"] = client_alg
|
||||
|
||||
if authorization_code:
|
||||
config["nonce"] = authorization_code.get_nonce()
|
||||
config["auth_time"] = authorization_code.get_auth_time()
|
||||
|
||||
if acr := authorization_code.get_acr():
|
||||
config["acr"] = acr
|
||||
|
||||
if amr := authorization_code.get_amr():
|
||||
config["amr"] = amr
|
||||
|
||||
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("after_create_token_response", 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.payload.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.payload.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, redirect_uri):
|
||||
validate_nonce(grant.request, self.exists_nonce, self.require_nonce)
|
||||
|
||||
def __call__(self, grant):
|
||||
grant.register_hook("after_create_token_response", self.process_token)
|
||||
if is_openid_scope(grant.request.payload.scope):
|
||||
grant.register_hook(
|
||||
"after_validate_authorization_request_payload",
|
||||
self.validate_openid_authorization_request,
|
||||
)
|
||||
grant.register_hook(
|
||||
"after_validate_consent_request", validate_request_prompt
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
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
|
||||
from .util import 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.payload.redirect_uri,
|
||||
scope=request.payload.scope,
|
||||
nonce=request.payload.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.payload.scope):
|
||||
raise InvalidScopeError(
|
||||
"Missing 'openid' scope",
|
||||
redirect_uri=self.request.payload.redirect_uri,
|
||||
redirect_fragment=True,
|
||||
)
|
||||
self.register_hook(
|
||||
"after_validate_authorization_request_payload",
|
||||
lambda grant, redirect_uri: 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.payload.scope,
|
||||
include_refresh_token=False,
|
||||
)
|
||||
|
||||
response_types = self.request.payload.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,175 @@
|
||||
import logging
|
||||
|
||||
from authlib.oauth2.rfc6749 import AccessDeniedError
|
||||
from authlib.oauth2.rfc6749 import ImplicitGrant
|
||||
from authlib.oauth2.rfc6749 import InvalidScopeError
|
||||
from authlib.oauth2.rfc6749 import OAuth2Error
|
||||
from authlib.oauth2.rfc6749.errors import InvalidRequestError
|
||||
from authlib.oauth2.rfc6749.hooks import hooked
|
||||
|
||||
from .util import create_response_mode_response
|
||||
from .util import generate_id_token
|
||||
from .util import is_openid_scope
|
||||
from .util import validate_nonce
|
||||
from .util import validate_request_prompt
|
||||
|
||||
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.payload.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.payload.scope):
|
||||
raise InvalidScopeError(
|
||||
"Missing 'openid' scope",
|
||||
redirect_uri=self.request.payload.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
|
||||
|
||||
@hooked
|
||||
def validate_consent_request(self):
|
||||
redirect_uri = self.validate_authorization_request()
|
||||
validate_request_prompt(self, redirect_uri, redirect_fragment=True)
|
||||
return redirect_uri
|
||||
|
||||
def create_authorization_response(self, redirect_uri, grant_user):
|
||||
state = self.request.payload.state
|
||||
if grant_user:
|
||||
params = self.create_granted_params(grant_user)
|
||||
if state:
|
||||
params.append(("state", state))
|
||||
else:
|
||||
error = AccessDeniedError()
|
||||
params = error.get_body()
|
||||
|
||||
# http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
|
||||
response_mode = self.request.payload.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.payload.scope,
|
||||
include_refresh_token=False,
|
||||
)
|
||||
if self.request.payload.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.payload.data.get("nonce")
|
||||
if code is not None:
|
||||
config["code"] = code
|
||||
|
||||
# Per OpenID Connect Registration 1.0 Section 2:
|
||||
# Use client's id_token_signed_response_alg if specified
|
||||
if not config.get("alg") and (
|
||||
client_alg := self.request.client.id_token_signed_response_alg
|
||||
):
|
||||
if client_alg == "none":
|
||||
# According to oidc-registration §2 the 'none' alg is not valid in
|
||||
# implicit flows:
|
||||
# The value none MUST NOT be used as the ID Token alg value unless
|
||||
# the Client uses only Response Types that return no ID Token from
|
||||
# the Authorization Endpoint (such as when only using the
|
||||
# Authorization Code Flow).
|
||||
raise InvalidRequestError(
|
||||
"id_token must be signed in implicit flows",
|
||||
redirect_uri=self.request.payload.redirect_uri,
|
||||
redirect_fragment=True,
|
||||
)
|
||||
|
||||
config["alg"] = client_alg
|
||||
|
||||
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,160 @@
|
||||
import time
|
||||
|
||||
from authlib.common.encoding import to_native
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
from authlib.common.urls import quote_url
|
||||
from authlib.jose import jwt
|
||||
from authlib.oauth2.rfc6749 import InvalidRequestError
|
||||
from authlib.oauth2.rfc6749 import scope_to_list
|
||||
|
||||
from ..errors import AccountSelectionRequiredError
|
||||
from ..errors import ConsentRequiredError
|
||||
from ..errors import LoginRequiredError
|
||||
from ..util import create_half_hash
|
||||
|
||||
|
||||
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.payload.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.payload.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,
|
||||
acr=None,
|
||||
amr=None,
|
||||
code=None,
|
||||
kid=None,
|
||||
):
|
||||
now = int(time.time())
|
||||
if auth_time is None:
|
||||
auth_time = now
|
||||
|
||||
header = {"alg": alg}
|
||||
if kid:
|
||||
header["kid"] = kid
|
||||
|
||||
payload = {
|
||||
"iss": iss,
|
||||
"aud": aud,
|
||||
"iat": now,
|
||||
"exp": now + exp,
|
||||
"auth_time": auth_time,
|
||||
}
|
||||
if nonce:
|
||||
payload["nonce"] = nonce
|
||||
|
||||
if acr:
|
||||
payload["acr"] = acr
|
||||
|
||||
if amr:
|
||||
payload["amr"] = amr
|
||||
|
||||
if code:
|
||||
c_hash = create_half_hash(code, alg)
|
||||
if c_hash is not None:
|
||||
payload["c_hash"] = to_native(c_hash)
|
||||
|
||||
access_token = token.get("access_token")
|
||||
if access_token:
|
||||
at_hash = create_half_hash(access_token, alg)
|
||||
if at_hash is not None:
|
||||
payload["at_hash"] = to_native(at_hash)
|
||||
|
||||
payload.update(user_info)
|
||||
return to_native(jwt.encode(header, 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(
|
||||
[
|
||||
f'<input type="hidden" name="{quote_url(k)}" value="{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 or "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,29 @@
|
||||
from authlib.oauth2.rfc6749 import AuthorizationCodeMixin as _AuthorizationCodeMixin
|
||||
|
||||
|
||||
class AuthorizationCodeMixin(_AuthorizationCodeMixin):
|
||||
def get_nonce(self):
|
||||
"""Get "nonce" value of the authorization code object."""
|
||||
# OPs MUST support the prompt parameter, as defined in Section 3.1.2, including the specified user interface behaviors such as none and login.
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_auth_time(self):
|
||||
"""Get "auth_time" value of the authorization code object."""
|
||||
# OPs MUST support returning the time at which the End-User authenticated via the auth_time Claim, when requested, as defined in Section 2.
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_acr(self) -> str:
|
||||
"""Get the "acr" (Authentication Method Class) value of the authorization code object."""
|
||||
# OPs MUST support requests for specific Authentication Context Class Reference values via the acr_values parameter, as defined in Section 3.1.2. (Note that the minimum level of support required for this parameter is simply to have its use not result in an error.)
|
||||
return None
|
||||
|
||||
def get_amr(self) -> list[str]:
|
||||
"""Get the "amr" (Authentication Method Reference) value of the authorization code object.
|
||||
|
||||
Have a look at :rfc:`RFC8176 <8176>` to see the full list of registered amr.
|
||||
|
||||
def get_amr(self) -> list[str]:
|
||||
return ["pwd", "otp"]
|
||||
|
||||
"""
|
||||
return None
|
||||
@@ -0,0 +1,120 @@
|
||||
from typing import Optional
|
||||
|
||||
from authlib.consts import default_json_headers
|
||||
from authlib.jose import jwt
|
||||
from authlib.oauth2.rfc6749.authorization_server import AuthorizationServer
|
||||
from authlib.oauth2.rfc6749.authorization_server import OAuth2Request
|
||||
from authlib.oauth2.rfc6749.resource_protector import ResourceProtector
|
||||
|
||||
from .claims import UserInfo
|
||||
|
||||
|
||||
class UserInfoEndpoint:
|
||||
"""OpenID Connect Core UserInfo Endpoint.
|
||||
|
||||
This endpoint returns information about a given user, as a JSON payload or as a JWT.
|
||||
It must be subclassed and a few methods needs to be manually implemented::
|
||||
|
||||
class UserInfoEndpoint(oidc.core.UserInfoEndpoint):
|
||||
def get_issuer(self):
|
||||
return "https://auth.example"
|
||||
|
||||
def generate_user_info(self, user, scope):
|
||||
return UserInfo(
|
||||
sub=user.id,
|
||||
name=user.name,
|
||||
...
|
||||
).filter(scope)
|
||||
|
||||
def resolve_private_key(self):
|
||||
return server_private_jwk_set()
|
||||
|
||||
It is also needed to pass a :class:`~authlib.oauth2.rfc6749.ResourceProtector` instance
|
||||
with a registered :class:`~authlib.oauth2.rfc6749.TokenValidator` at initialization,
|
||||
so the access to the endpoint can be restricter to valid token bearers::
|
||||
|
||||
resource_protector = ResourceProtector()
|
||||
resource_protector.register_token_validator(BearerTokenValidator())
|
||||
server.register_endpoint(
|
||||
UserInfoEndpoint(resource_protector=resource_protector)
|
||||
)
|
||||
|
||||
And then you can plug the endpoint to your application::
|
||||
|
||||
@app.route("/oauth/userinfo", methods=["GET", "POST"])
|
||||
def userinfo():
|
||||
return server.create_endpoint_response("userinfo")
|
||||
|
||||
"""
|
||||
|
||||
ENDPOINT_NAME = "userinfo"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server: Optional[AuthorizationServer] = None,
|
||||
resource_protector: Optional[ResourceProtector] = None,
|
||||
):
|
||||
self.server = server
|
||||
self.resource_protector = resource_protector
|
||||
|
||||
def create_endpoint_request(self, request: OAuth2Request):
|
||||
return self.server.create_oauth2_request(request)
|
||||
|
||||
def __call__(self, request: OAuth2Request):
|
||||
token = self.resource_protector.acquire_token("openid")
|
||||
client = token.get_client()
|
||||
user = token.get_user()
|
||||
user_info = self.generate_user_info(user, token.scope)
|
||||
|
||||
if alg := client.client_metadata.get("userinfo_signed_response_alg"):
|
||||
# If signed, the UserInfo Response MUST contain the Claims iss
|
||||
# (issuer) and aud (audience) as members. The iss value MUST be
|
||||
# the OP's Issuer Identifier URL. The aud value MUST be or
|
||||
# include the RP's Client ID value.
|
||||
user_info["iss"] = self.get_issuer()
|
||||
user_info["aud"] = client.client_id
|
||||
|
||||
data = jwt.encode({"alg": alg}, user_info, self.resolve_private_key())
|
||||
return 200, data, [("Content-Type", "application/jwt")]
|
||||
|
||||
return 200, user_info, default_json_headers
|
||||
|
||||
def generate_user_info(self, user, scope: str) -> UserInfo:
|
||||
"""
|
||||
Generate a :class:`~authlib.oidc.core.UserInfo` object for an user::
|
||||
|
||||
def generate_user_info(self, user, scope: str) -> UserInfo:
|
||||
return UserInfo(
|
||||
given_name=user.given_name,
|
||||
family_name=user.last_name,
|
||||
email=user.email,
|
||||
...
|
||||
).filter(scope)
|
||||
|
||||
This method must be implemented by developers.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
"""The OP's Issuer Identifier URL.
|
||||
|
||||
The value is used to fill the ``iss`` claim that is mandatory in signed userinfo::
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
return "https://auth.example"
|
||||
|
||||
This method must be implemented by developers to support JWT userinfo.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def resolve_private_key(self):
|
||||
"""Return the server JSON Web Key Set.
|
||||
|
||||
This is used to sign userinfo payloads::
|
||||
|
||||
def resolve_private_key(self):
|
||||
return server_private_jwk_set()
|
||||
|
||||
This method must be implemented by developers to support JWT userinfo signing.
|
||||
"""
|
||||
return None # pragma: no cover
|
||||
@@ -0,0 +1,18 @@
|
||||
import hashlib
|
||||
|
||||
from authlib.common.encoding import to_bytes
|
||||
from authlib.common.encoding import urlsafe_b64encode
|
||||
|
||||
|
||||
def create_half_hash(s, alg):
|
||||
if alg == "EdDSA":
|
||||
hash_alg = hashlib.sha512
|
||||
else:
|
||||
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,12 @@
|
||||
"""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,287 @@
|
||||
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",
|
||||
"service_documentation",
|
||||
"ui_locales_supported",
|
||||
"op_policy_uri",
|
||||
"op_tos_uri",
|
||||
# added by OpenID
|
||||
"token_endpoint_auth_signing_alg_values_supported",
|
||||
"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'
|
||||
)
|
||||
|
||||
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"
|
||||
@@ -0,0 +1,3 @@
|
||||
from .claims import ClientMetadataClaims
|
||||
|
||||
__all__ = ["ClientMetadataClaims"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,355 @@
|
||||
from authlib.common.urls import is_valid_url
|
||||
from authlib.jose import BaseClaims
|
||||
from authlib.jose.errors import InvalidClaimError
|
||||
|
||||
|
||||
class ClientMetadataClaims(BaseClaims):
|
||||
REGISTERED_CLAIMS = [
|
||||
"token_endpoint_auth_signing_alg",
|
||||
"application_type",
|
||||
"sector_identifier_uri",
|
||||
"subject_type",
|
||||
"id_token_signed_response_alg",
|
||||
"id_token_encrypted_response_alg",
|
||||
"id_token_encrypted_response_enc",
|
||||
"userinfo_signed_response_alg",
|
||||
"userinfo_encrypted_response_alg",
|
||||
"userinfo_encrypted_response_enc",
|
||||
"default_max_age",
|
||||
"require_auth_time",
|
||||
"default_acr_values",
|
||||
"initiate_login_uri",
|
||||
"request_object_signing_alg",
|
||||
"request_object_encryption_alg",
|
||||
"request_object_encryption_enc",
|
||||
"request_uris",
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
self._validate_essential_claims()
|
||||
self.validate_token_endpoint_auth_signing_alg()
|
||||
self.validate_application_type()
|
||||
self.validate_sector_identifier_uri()
|
||||
self.validate_subject_type()
|
||||
self.validate_id_token_signed_response_alg()
|
||||
self.validate_id_token_encrypted_response_alg()
|
||||
self.validate_id_token_encrypted_response_enc()
|
||||
self.validate_userinfo_signed_response_alg()
|
||||
self.validate_userinfo_encrypted_response_alg()
|
||||
self.validate_userinfo_encrypted_response_enc()
|
||||
self.validate_default_max_age()
|
||||
self.validate_require_auth_time()
|
||||
self.validate_default_acr_values()
|
||||
self.validate_initiate_login_uri()
|
||||
self.validate_request_object_signing_alg()
|
||||
self.validate_request_object_encryption_alg()
|
||||
self.validate_request_object_encryption_enc()
|
||||
self.validate_request_uris()
|
||||
|
||||
def _validate_uri(self, key):
|
||||
uri = self.get(key)
|
||||
uris = uri if isinstance(uri, list) else [uri]
|
||||
for uri in uris:
|
||||
if uri and not is_valid_url(uri):
|
||||
raise InvalidClaimError(key)
|
||||
|
||||
@classmethod
|
||||
def get_claims_options(self, metadata):
|
||||
"""Generate claims options validation from Authorization Server metadata."""
|
||||
options = {}
|
||||
|
||||
if acr_values_supported := metadata.get("acr_values_supported"):
|
||||
|
||||
def _validate_default_acr_values(claims, value):
|
||||
return not value or set(value).issubset(set(acr_values_supported))
|
||||
|
||||
options["default_acr_values"] = {"validate": _validate_default_acr_values}
|
||||
|
||||
values_mapping = {
|
||||
"token_endpoint_auth_signing_alg_values_supported": "token_endpoint_auth_signing_alg",
|
||||
"subject_types_supported": "subject_type",
|
||||
"id_token_signing_alg_values_supported": "id_token_signed_response_alg",
|
||||
"id_token_encryption_alg_values_supported": "id_token_encrypted_response_alg",
|
||||
"id_token_encryption_enc_values_supported": "id_token_encrypted_response_enc",
|
||||
"userinfo_signing_alg_values_supported": "userinfo_signed_response_alg",
|
||||
"userinfo_encryption_alg_values_supported": "userinfo_encrypted_response_alg",
|
||||
"userinfo_encryption_enc_values_supported": "userinfo_encrypted_response_enc",
|
||||
"request_object_signing_alg_values_supported": "request_object_signing_alg",
|
||||
"request_object_encryption_alg_values_supported": "request_object_encryption_alg",
|
||||
"request_object_encryption_enc_values_supported": "request_object_encryption_enc",
|
||||
}
|
||||
|
||||
def make_validator(metadata_claim_values):
|
||||
def _validate(claims, value):
|
||||
return not value or value in metadata_claim_values
|
||||
|
||||
return _validate
|
||||
|
||||
for metadata_claim_name, request_claim_name in values_mapping.items():
|
||||
if metadata_claim_values := metadata.get(metadata_claim_name):
|
||||
options[request_claim_name] = {
|
||||
"validate": make_validator(metadata_claim_values)
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
def validate_token_endpoint_auth_signing_alg(self):
|
||||
"""JWS [JWS] alg algorithm [JWA] that MUST be used for signing the JWT [JWT]
|
||||
used to authenticate the Client at the Token Endpoint for the private_key_jwt
|
||||
and client_secret_jwt authentication methods.
|
||||
|
||||
All Token Requests using these authentication methods from this Client MUST be
|
||||
rejected, if the JWT is not signed with this algorithm. Servers SHOULD support
|
||||
RS256. The value none MUST NOT be used. The default, if omitted, is that any
|
||||
algorithm supported by the OP and the RP MAY be used.
|
||||
"""
|
||||
if self.get("token_endpoint_auth_signing_alg") == "none":
|
||||
raise InvalidClaimError("token_endpoint_auth_signing_alg")
|
||||
|
||||
self._validate_claim_value("token_endpoint_auth_signing_alg")
|
||||
|
||||
def validate_application_type(self):
|
||||
"""Kind of the application.
|
||||
|
||||
The default, if omitted, is web. The defined values are native or web. Web
|
||||
Clients using the OAuth Implicit Grant Type MUST only register URLs using the
|
||||
https scheme as redirect_uris; they MUST NOT use localhost as the hostname.
|
||||
Native Clients MUST only register redirect_uris using custom URI schemes or
|
||||
loopback URLs using the http scheme; loopback URLs use localhost or the IP
|
||||
loopback literals 127.0.0.1 or [::1] as the hostname. Authorization Servers MAY
|
||||
place additional constraints on Native Clients. Authorization Servers MAY
|
||||
reject Redirection URI values using the http scheme, other than the loopback
|
||||
case for Native Clients. The Authorization Server MUST verify that all the
|
||||
registered redirect_uris conform to these constraints. This prevents sharing a
|
||||
Client ID across different types of Clients.
|
||||
"""
|
||||
self.setdefault("application_type", "web")
|
||||
if self.get("application_type") not in ("web", "native"):
|
||||
raise InvalidClaimError("application_type")
|
||||
|
||||
self._validate_claim_value("application_type")
|
||||
|
||||
def validate_sector_identifier_uri(self):
|
||||
"""URL using the https scheme to be used in calculating Pseudonymous Identifiers
|
||||
by the OP.
|
||||
|
||||
The URL references a file with a single JSON array of redirect_uri values.
|
||||
Please see Section 5. Providers that use pairwise sub (subject) values SHOULD
|
||||
utilize the sector_identifier_uri value provided in the Subject Identifier
|
||||
calculation for pairwise identifiers.
|
||||
"""
|
||||
self._validate_uri("sector_identifier_uri")
|
||||
|
||||
def validate_subject_type(self):
|
||||
"""subject_type requested for responses to this Client.
|
||||
|
||||
The subject_types_supported discovery parameter contains a list of the supported
|
||||
subject_type values for the OP. Valid types include pairwise and public.
|
||||
"""
|
||||
self._validate_claim_value("subject_type")
|
||||
|
||||
def validate_id_token_signed_response_alg(self):
|
||||
"""JWS alg algorithm [JWA] REQUIRED for signing the ID Token issued to this
|
||||
Client.
|
||||
|
||||
The value none MUST NOT be used as the ID Token alg value unless the Client uses
|
||||
only Response Types that return no ID Token from the Authorization Endpoint
|
||||
(such as when only using the Authorization Code Flow). The default, if omitted,
|
||||
is RS256. The public key for validating the signature is provided by retrieving
|
||||
the JWK Set referenced by the jwks_uri element from OpenID Connect Discovery 1.0
|
||||
[OpenID.Discovery].
|
||||
"""
|
||||
if self.get(
|
||||
"id_token_signed_response_alg"
|
||||
) == "none" and "id_token" in self.get("response_type", ""):
|
||||
raise InvalidClaimError("id_token_signed_response_alg")
|
||||
|
||||
self.setdefault("id_token_signed_response_alg", "RS256")
|
||||
self._validate_claim_value("id_token_signed_response_alg")
|
||||
|
||||
def validate_id_token_encrypted_response_alg(self):
|
||||
"""JWE alg algorithm [JWA] REQUIRED for encrypting the ID Token issued to this
|
||||
Client.
|
||||
|
||||
If this is requested, the response will be signed then encrypted, with the
|
||||
result being a Nested JWT, as defined in [JWT]. The default, if omitted, is that
|
||||
no encryption is performed.
|
||||
"""
|
||||
self._validate_claim_value("id_token_encrypted_response_alg")
|
||||
|
||||
def validate_id_token_encrypted_response_enc(self):
|
||||
"""JWE enc algorithm [JWA] REQUIRED for encrypting the ID Token issued to this
|
||||
Client.
|
||||
|
||||
If id_token_encrypted_response_alg is specified, the default
|
||||
id_token_encrypted_response_enc value is A128CBC-HS256. When
|
||||
id_token_encrypted_response_enc is included, id_token_encrypted_response_alg
|
||||
MUST also be provided.
|
||||
"""
|
||||
if self.get("id_token_encrypted_response_enc") and not self.get(
|
||||
"id_token_encrypted_response_alg"
|
||||
):
|
||||
raise InvalidClaimError("id_token_encrypted_response_enc")
|
||||
|
||||
if self.get("id_token_encrypted_response_alg"):
|
||||
self.setdefault("id_token_encrypted_response_enc", "A128CBC-HS256")
|
||||
|
||||
self._validate_claim_value("id_token_encrypted_response_enc")
|
||||
|
||||
def validate_userinfo_signed_response_alg(self):
|
||||
"""JWS alg algorithm [JWA] REQUIRED for signing UserInfo Responses.
|
||||
|
||||
If this is specified, the response will be JWT [JWT] serialized, and signed
|
||||
using JWS. The default, if omitted, is for the UserInfo Response to return the
|
||||
Claims as a UTF-8 [RFC3629] encoded JSON object using the application/json
|
||||
content-type.
|
||||
"""
|
||||
self._validate_claim_value("userinfo_signed_response_alg")
|
||||
|
||||
def validate_userinfo_encrypted_response_alg(self):
|
||||
"""JWE [JWE] alg algorithm [JWA] REQUIRED for encrypting UserInfo Responses.
|
||||
|
||||
If both signing and encryption are requested, the response will be signed then
|
||||
encrypted, with the result being a Nested JWT, as defined in [JWT]. The default,
|
||||
if omitted, is that no encryption is performed.
|
||||
"""
|
||||
self._validate_claim_value("userinfo_encrypted_response_alg")
|
||||
|
||||
def validate_userinfo_encrypted_response_enc(self):
|
||||
"""JWE enc algorithm [JWA] REQUIRED for encrypting UserInfo Responses.
|
||||
|
||||
If userinfo_encrypted_response_alg is specified, the default
|
||||
userinfo_encrypted_response_enc value is A128CBC-HS256. When
|
||||
userinfo_encrypted_response_enc is included, userinfo_encrypted_response_alg
|
||||
MUST also be provided.
|
||||
"""
|
||||
if self.get("userinfo_encrypted_response_enc") and not self.get(
|
||||
"userinfo_encrypted_response_alg"
|
||||
):
|
||||
raise InvalidClaimError("userinfo_encrypted_response_enc")
|
||||
|
||||
if self.get("userinfo_encrypted_response_alg"):
|
||||
self.setdefault("userinfo_encrypted_response_enc", "A128CBC-HS256")
|
||||
|
||||
self._validate_claim_value("userinfo_encrypted_response_enc")
|
||||
|
||||
def validate_default_max_age(self):
|
||||
"""Default Maximum Authentication Age.
|
||||
|
||||
Specifies that the End-User MUST be actively authenticated if the End-User was
|
||||
authenticated longer ago than the specified number of seconds. The max_age
|
||||
request parameter overrides this default value. If omitted, no default Maximum
|
||||
Authentication Age is specified.
|
||||
"""
|
||||
if self.get("default_max_age") is not None and not isinstance(
|
||||
self["default_max_age"], (int, float)
|
||||
):
|
||||
raise InvalidClaimError("default_max_age")
|
||||
|
||||
self._validate_claim_value("default_max_age")
|
||||
|
||||
def validate_require_auth_time(self):
|
||||
"""Boolean value specifying whether the auth_time Claim in the ID Token is
|
||||
REQUIRED.
|
||||
|
||||
It is REQUIRED when the value is true. (If this is false, the auth_time Claim
|
||||
can still be dynamically requested as an individual Claim for the ID Token using
|
||||
the claims request parameter described in Section 5.5.1 of OpenID Connect Core
|
||||
1.0 [OpenID.Core].) If omitted, the default value is false.
|
||||
"""
|
||||
self.setdefault("require_auth_time", False)
|
||||
if self.get("require_auth_time") is not None and not isinstance(
|
||||
self["require_auth_time"], bool
|
||||
):
|
||||
raise InvalidClaimError("require_auth_time")
|
||||
|
||||
self._validate_claim_value("require_auth_time")
|
||||
|
||||
def validate_default_acr_values(self):
|
||||
"""Default requested Authentication Context Class Reference values.
|
||||
|
||||
Array of strings that specifies the default acr values that the OP is being
|
||||
requested to use for processing requests from this Client, with the values
|
||||
appearing in order of preference. The Authentication Context Class satisfied by
|
||||
the authentication performed is returned as the acr Claim Value in the issued ID
|
||||
Token. The acr Claim is requested as a Voluntary Claim by this parameter. The
|
||||
acr_values_supported discovery element contains a list of the supported acr
|
||||
values supported by the OP. Values specified in the acr_values request parameter
|
||||
or an individual acr Claim request override these default values.
|
||||
"""
|
||||
self._validate_claim_value("default_acr_values")
|
||||
|
||||
def validate_initiate_login_uri(self):
|
||||
"""RI using the https scheme that a third party can use to initiate a login by
|
||||
the RP, as specified in Section 4 of OpenID Connect Core 1.0 [OpenID.Core].
|
||||
|
||||
The URI MUST accept requests via both GET and POST. The Client MUST understand
|
||||
the login_hint and iss parameters and SHOULD support the target_link_uri
|
||||
parameter.
|
||||
"""
|
||||
self._validate_uri("initiate_login_uri")
|
||||
|
||||
def validate_request_object_signing_alg(self):
|
||||
"""JWS [JWS] alg algorithm [JWA] that MUST be used for signing Request Objects
|
||||
sent to the OP.
|
||||
|
||||
All Request Objects from this Client MUST be rejected, if not signed with this
|
||||
algorithm. Request Objects are described in Section 6.1 of OpenID Connect Core
|
||||
1.0 [OpenID.Core]. This algorithm MUST be 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 RS256. The value none
|
||||
MAY be used. The default, if omitted, is that any algorithm supported by the OP
|
||||
and the RP MAY be used.
|
||||
"""
|
||||
self._validate_claim_value("request_object_signing_alg")
|
||||
|
||||
def validate_request_object_encryption_alg(self):
|
||||
"""JWE [JWE] alg algorithm [JWA] the RP is declaring that it may use for
|
||||
encrypting Request Objects sent to the OP.
|
||||
|
||||
This parameter SHOULD be included when symmetric encryption will be used, since
|
||||
this signals to the OP that a client_secret value needs to be returned from
|
||||
which the symmetric key will be derived, that might not otherwise be returned.
|
||||
The RP MAY still use other supported encryption algorithms or send unencrypted
|
||||
Request Objects, even when this parameter is present. If both signing and
|
||||
encryption are requested, the Request Object will be signed then encrypted, with
|
||||
the result being a Nested JWT, as defined in [JWT]. The default, if omitted, is
|
||||
that the RP is not declaring whether it might encrypt any Request Objects.
|
||||
"""
|
||||
self._validate_claim_value("request_object_encryption_alg")
|
||||
|
||||
def validate_request_object_encryption_enc(self):
|
||||
"""JWE enc algorithm [JWA] the RP is declaring that it may use for encrypting
|
||||
Request Objects sent to the OP.
|
||||
|
||||
If request_object_encryption_alg is specified, the default
|
||||
request_object_encryption_enc value is A128CBC-HS256. When
|
||||
request_object_encryption_enc is included, request_object_encryption_alg MUST
|
||||
also be provided.
|
||||
"""
|
||||
if self.get("request_object_encryption_enc") and not self.get(
|
||||
"request_object_encryption_alg"
|
||||
):
|
||||
raise InvalidClaimError("request_object_encryption_enc")
|
||||
|
||||
if self.get("request_object_encryption_alg"):
|
||||
self.setdefault("request_object_encryption_enc", "A128CBC-HS256")
|
||||
|
||||
self._validate_claim_value("request_object_encryption_enc")
|
||||
|
||||
def validate_request_uris(self):
|
||||
"""Array of request_uri values that are pre-registered by the RP for use at the
|
||||
OP.
|
||||
|
||||
These URLs MUST use the https scheme unless the target Request Object is signed
|
||||
in a way that is verifiable by the OP. Servers MAY cache the contents of the
|
||||
files referenced by these URIs and not retrieve them at the time they are used
|
||||
in a request. OPs can require that request_uri values used be pre-registered
|
||||
with the require_request_uri_registration discovery parameter. If the contents
|
||||
of the request file could ever change, these URI values SHOULD include the
|
||||
base64url-encoded SHA-256 hash value of the file contents referenced by the URI
|
||||
as the value of the URI fragment. If the fragment value used for a URI changes,
|
||||
that signals the server that its cached value for that URI with the old fragment
|
||||
value is no longer valid.
|
||||
"""
|
||||
self._validate_uri("request_uris")
|
||||
Reference in New Issue
Block a user