This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -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",
]

View File

@@ -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))

View File

@@ -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"

View File

@@ -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",
]

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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"]

View File

@@ -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')

View File

@@ -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"

View File

@@ -0,0 +1,3 @@
from .claims import ClientMetadataClaims
__all__ = ["ClientMetadataClaims"]

View File

@@ -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")