updates
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from .introspection import JWTIntrospectionEndpoint
|
||||
from .revocation import JWTRevocationEndpoint
|
||||
from .token import JWTBearerTokenGenerator
|
||||
from .token_validator import JWTBearerTokenValidator
|
||||
|
||||
__all__ = [
|
||||
"JWTBearerTokenGenerator",
|
||||
"JWTBearerTokenValidator",
|
||||
"JWTIntrospectionEndpoint",
|
||||
"JWTRevocationEndpoint",
|
||||
]
|
||||
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,64 @@
|
||||
from authlib.jose.errors import InvalidClaimError
|
||||
from authlib.jose.rfc7519 import JWTClaims
|
||||
|
||||
|
||||
class JWTAccessTokenClaims(JWTClaims):
|
||||
REGISTERED_CLAIMS = JWTClaims.REGISTERED_CLAIMS + [
|
||||
"client_id",
|
||||
"auth_time",
|
||||
"acr",
|
||||
"amr",
|
||||
"scope",
|
||||
"groups",
|
||||
"roles",
|
||||
"entitlements",
|
||||
]
|
||||
|
||||
def validate(self, **kwargs):
|
||||
self.validate_typ()
|
||||
|
||||
super().validate(**kwargs)
|
||||
self.validate_client_id()
|
||||
self.validate_auth_time()
|
||||
self.validate_acr()
|
||||
self.validate_amr()
|
||||
self.validate_scope()
|
||||
self.validate_groups()
|
||||
self.validate_roles()
|
||||
self.validate_entitlements()
|
||||
|
||||
def validate_typ(self):
|
||||
# The resource server MUST verify that the 'typ' header value is 'at+jwt'
|
||||
# or 'application/at+jwt' and reject tokens carrying any other value.
|
||||
# 'typ' is not a required claim, so we don't raise an error if it's missing.
|
||||
typ = self.header.get("typ")
|
||||
if typ and typ.lower() not in ("at+jwt", "application/at+jwt"):
|
||||
raise InvalidClaimError("typ")
|
||||
|
||||
def validate_client_id(self):
|
||||
return self._validate_claim_value("client_id")
|
||||
|
||||
def validate_auth_time(self):
|
||||
auth_time = self.get("auth_time")
|
||||
if auth_time and not isinstance(auth_time, (int, float)):
|
||||
raise InvalidClaimError("auth_time")
|
||||
|
||||
def validate_acr(self):
|
||||
return self._validate_claim_value("acr")
|
||||
|
||||
def validate_amr(self):
|
||||
amr = self.get("amr")
|
||||
if amr and not isinstance(self["amr"], list):
|
||||
raise InvalidClaimError("amr")
|
||||
|
||||
def validate_scope(self):
|
||||
return self._validate_claim_value("scope")
|
||||
|
||||
def validate_groups(self):
|
||||
return self._validate_claim_value("groups")
|
||||
|
||||
def validate_roles(self):
|
||||
return self._validate_claim_value("roles")
|
||||
|
||||
def validate_entitlements(self):
|
||||
return self._validate_claim_value("entitlements")
|
||||
@@ -0,0 +1,127 @@
|
||||
from authlib.common.errors import ContinueIteration
|
||||
from authlib.consts import default_json_headers
|
||||
from authlib.jose.errors import ExpiredTokenError
|
||||
from authlib.jose.errors import InvalidClaimError
|
||||
from authlib.oauth2.rfc6750.errors import InvalidTokenError
|
||||
from authlib.oauth2.rfc9068.token_validator import JWTBearerTokenValidator
|
||||
|
||||
from ..rfc7662 import IntrospectionEndpoint
|
||||
|
||||
|
||||
class JWTIntrospectionEndpoint(IntrospectionEndpoint):
|
||||
r"""JWTIntrospectionEndpoint inherits from :ref:`specs/rfc7662`
|
||||
:class:`~authlib.oauth2.rfc7662.IntrospectionEndpoint` and implements the machinery
|
||||
to automatically process the JWT access tokens.
|
||||
|
||||
:param issuer: The issuer identifier for which tokens will be introspected.
|
||||
|
||||
:param \\*\\*kwargs: Other parameters are inherited from
|
||||
:class:`~authlib.oauth2.rfc7662.introspection.IntrospectionEndpoint`.
|
||||
|
||||
::
|
||||
|
||||
class MyJWTAccessTokenIntrospectionEndpoint(JWTIntrospectionEndpoint):
|
||||
def get_jwks(self): ...
|
||||
|
||||
def get_username(self, user_id): ...
|
||||
|
||||
|
||||
# endpoint dedicated to JWT access token introspection
|
||||
authorization_server.register_endpoint(
|
||||
MyJWTAccessTokenIntrospectionEndpoint(
|
||||
issuer="https://authorization-server.example.org",
|
||||
)
|
||||
)
|
||||
|
||||
# another endpoint dedicated to refresh token introspection
|
||||
authorization_server.register_endpoint(MyRefreshTokenIntrospectionEndpoint)
|
||||
|
||||
"""
|
||||
|
||||
#: Endpoint name to be registered
|
||||
ENDPOINT_NAME = "introspection"
|
||||
|
||||
def __init__(self, issuer, server=None, *args, **kwargs):
|
||||
super().__init__(*args, server=server, **kwargs)
|
||||
self.issuer = issuer
|
||||
|
||||
def create_endpoint_response(self, request):
|
||||
""""""
|
||||
# The authorization server first validates the client credentials
|
||||
client = self.authenticate_endpoint_client(request)
|
||||
|
||||
# then verifies whether the token was issued to the client making
|
||||
# the revocation request
|
||||
token = self.authenticate_token(request, client)
|
||||
|
||||
# the authorization server invalidates the token
|
||||
body = self.create_introspection_payload(token)
|
||||
return 200, body, default_json_headers
|
||||
|
||||
def authenticate_token(self, request, client):
|
||||
""""""
|
||||
self.check_params(request, client)
|
||||
|
||||
# do not attempt to decode refresh_tokens
|
||||
if request.form.get("token_type_hint") not in ("access_token", None):
|
||||
raise ContinueIteration()
|
||||
|
||||
validator = JWTBearerTokenValidator(issuer=self.issuer, resource_server=None)
|
||||
validator.get_jwks = self.get_jwks
|
||||
try:
|
||||
token = validator.authenticate_token(request.form["token"])
|
||||
|
||||
# if the token is not a JWT, fall back to the regular flow
|
||||
except InvalidTokenError as exc:
|
||||
raise ContinueIteration() from exc
|
||||
|
||||
if token and self.check_permission(token, client, request):
|
||||
return token
|
||||
|
||||
def create_introspection_payload(self, token):
|
||||
if not token:
|
||||
return {"active": False}
|
||||
|
||||
try:
|
||||
token.validate()
|
||||
except ExpiredTokenError:
|
||||
return {"active": False}
|
||||
except InvalidClaimError as exc:
|
||||
if exc.claim_name == "iss":
|
||||
raise ContinueIteration() from exc
|
||||
raise InvalidTokenError() from exc
|
||||
|
||||
payload = {
|
||||
"active": True,
|
||||
"token_type": "Bearer",
|
||||
"client_id": token["client_id"],
|
||||
"scope": token["scope"],
|
||||
"sub": token["sub"],
|
||||
"aud": token["aud"],
|
||||
"iss": token["iss"],
|
||||
"exp": token["exp"],
|
||||
"iat": token["iat"],
|
||||
}
|
||||
|
||||
if username := self.get_username(token["sub"]):
|
||||
payload["username"] = username
|
||||
|
||||
return payload
|
||||
|
||||
def get_jwks(self):
|
||||
"""Return the JWKs that will be used to check the JWT access token signature.
|
||||
Developers MUST re-implement this method::
|
||||
|
||||
def get_jwks(self):
|
||||
return load_jwks("jwks.json")
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_username(self, user_id: str) -> str:
|
||||
"""Returns an username from a user ID.
|
||||
Developers MAY re-implement this method::
|
||||
|
||||
def get_username(self, user_id):
|
||||
return User.get(id=user_id).username
|
||||
"""
|
||||
return None
|
||||
@@ -0,0 +1,74 @@
|
||||
from authlib.common.errors import ContinueIteration
|
||||
from authlib.oauth2.rfc6750.errors import InvalidTokenError
|
||||
from authlib.oauth2.rfc9068.token_validator import JWTBearerTokenValidator
|
||||
|
||||
from ..rfc6749 import UnsupportedTokenTypeError
|
||||
from ..rfc7009 import RevocationEndpoint
|
||||
|
||||
|
||||
class JWTRevocationEndpoint(RevocationEndpoint):
|
||||
r"""JWTRevocationEndpoint inherits from `RFC7009`_
|
||||
:class:`~authlib.oauth2.rfc7009.RevocationEndpoint`.
|
||||
|
||||
The JWT access tokens cannot be revoked.
|
||||
If the submitted token is a JWT access token, then revocation returns
|
||||
a `invalid_token_error`.
|
||||
|
||||
:param issuer: The issuer identifier.
|
||||
|
||||
:param \\*\\*kwargs: Other parameters are inherited from
|
||||
:class:`~authlib.oauth2.rfc7009.RevocationEndpoint`.
|
||||
|
||||
Plain text access tokens and other kind of tokens such as refresh_tokens
|
||||
will be ignored by this endpoint and passed to the next revocation endpoint::
|
||||
|
||||
class MyJWTAccessTokenRevocationEndpoint(JWTRevocationEndpoint):
|
||||
def get_jwks(self): ...
|
||||
|
||||
|
||||
# endpoint dedicated to JWT access token revokation
|
||||
authorization_server.register_endpoint(
|
||||
MyJWTAccessTokenRevocationEndpoint(
|
||||
issuer="https://authorization-server.example.org",
|
||||
)
|
||||
)
|
||||
|
||||
# another endpoint dedicated to refresh token revokation
|
||||
authorization_server.register_endpoint(MyRefreshTokenRevocationEndpoint)
|
||||
|
||||
.. _RFC7009: https://tools.ietf.org/html/rfc7009
|
||||
"""
|
||||
|
||||
def __init__(self, issuer, server=None, *args, **kwargs):
|
||||
super().__init__(*args, server=server, **kwargs)
|
||||
self.issuer = issuer
|
||||
|
||||
def authenticate_token(self, request, client):
|
||||
""""""
|
||||
self.check_params(request, client)
|
||||
|
||||
# do not attempt to revoke refresh_tokens
|
||||
if request.form.get("token_type_hint") not in ("access_token", None):
|
||||
raise ContinueIteration()
|
||||
|
||||
validator = JWTBearerTokenValidator(issuer=self.issuer, resource_server=None)
|
||||
validator.get_jwks = self.get_jwks
|
||||
|
||||
try:
|
||||
validator.authenticate_token(request.form["token"])
|
||||
|
||||
# if the token is not a JWT, fall back to the regular flow
|
||||
except InvalidTokenError as exc:
|
||||
raise ContinueIteration() from exc
|
||||
|
||||
# JWT access token cannot be revoked
|
||||
raise UnsupportedTokenTypeError()
|
||||
|
||||
def get_jwks(self):
|
||||
"""Return the JWKs that will be used to check the JWT access token signature.
|
||||
Developers MUST re-implement this method::
|
||||
|
||||
def get_jwks(self):
|
||||
return load_jwks("jwks.json")
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,218 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from authlib.common.security import generate_token
|
||||
from authlib.jose import jwt
|
||||
from authlib.oauth2.rfc6750.token import BearerTokenGenerator
|
||||
|
||||
|
||||
class JWTBearerTokenGenerator(BearerTokenGenerator):
|
||||
r"""A JWT formatted access token generator.
|
||||
|
||||
:param issuer: The issuer identifier. Will appear in the JWT ``iss`` claim.
|
||||
|
||||
:param \\*\\*kwargs: Other parameters are inherited from
|
||||
:class:`~authlib.oauth2.rfc6750.token.BearerTokenGenerator`.
|
||||
|
||||
This token generator can be registered into the authorization server::
|
||||
|
||||
class MyJWTBearerTokenGenerator(JWTBearerTokenGenerator):
|
||||
def get_jwks(self): ...
|
||||
|
||||
def get_extra_claims(self, client, grant_type, user, scope): ...
|
||||
|
||||
|
||||
authorization_server.register_token_generator(
|
||||
"default",
|
||||
MyJWTBearerTokenGenerator(
|
||||
issuer="https://authorization-server.example.org"
|
||||
),
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
issuer,
|
||||
alg="RS256",
|
||||
refresh_token_generator=None,
|
||||
expires_generator=None,
|
||||
):
|
||||
super().__init__(
|
||||
self.access_token_generator, refresh_token_generator, expires_generator
|
||||
)
|
||||
self.issuer = issuer
|
||||
self.alg = alg
|
||||
|
||||
def get_jwks(self):
|
||||
"""Return the JWKs that will be used to sign the JWT access token.
|
||||
Developers MUST re-implement this method::
|
||||
|
||||
def get_jwks(self):
|
||||
return load_jwks("jwks.json")
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_extra_claims(self, client, grant_type, user, scope):
|
||||
"""Return extra claims to add in the JWT access token. Developers MAY
|
||||
re-implement this method to add identity claims like the ones in
|
||||
:ref:`specs/oidc` ID Token, or any other arbitrary claims::
|
||||
|
||||
def get_extra_claims(self, client, grant_type, user, scope):
|
||||
return generate_user_info(user, scope)
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_audiences(self, client, user, scope) -> Union[str, list[str]]:
|
||||
"""Return the audience for the token. By default this simply returns
|
||||
the client ID. Developers MAY re-implement this method to add extra
|
||||
audiences::
|
||||
|
||||
def get_audiences(self, client, user, scope):
|
||||
return [
|
||||
client.get_client_id(),
|
||||
resource_server.get_id(),
|
||||
]
|
||||
"""
|
||||
return client.get_client_id()
|
||||
|
||||
def get_acr(self, user) -> Optional[str]:
|
||||
"""Authentication Context Class Reference.
|
||||
Returns a user-defined case sensitive string indicating the class of
|
||||
authentication the used performed. Token audience may refuse to give access to
|
||||
some resources if some ACR criteria are not met.
|
||||
:ref:`specs/oidc` defines one special value: ``0`` means that the user
|
||||
authentication did not respect `ISO29115`_ level 1, and will be refused monetary
|
||||
operations. Developers MAY re-implement this method::
|
||||
|
||||
def get_acr(self, user):
|
||||
if user.insecure_session():
|
||||
return "0"
|
||||
return "urn:mace:incommon:iap:silver"
|
||||
|
||||
.. _ISO29115: https://www.iso.org/standard/45138.html
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_auth_time(self, user) -> Optional[int]:
|
||||
"""User authentication time.
|
||||
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. Developers MAY re-implement this method::
|
||||
|
||||
def get_auth_time(self, user):
|
||||
return datetime.timestamp(user.get_auth_time())
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_amr(self, user) -> Optional[list[str]]:
|
||||
"""Authentication Methods References.
|
||||
Defined by :ref:`specs/oidc` as an option list of user-defined case-sensitive
|
||||
strings indication which authentication methods have been used to authenticate
|
||||
the user. Developers MAY re-implement this method::
|
||||
|
||||
def get_amr(self, user):
|
||||
return ["2FA"] if user.has_2fa_enabled() else []
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_jti(self, client, grant_type, user, scope) -> str:
|
||||
"""JWT ID.
|
||||
Create an unique identifier for the token. Developers MAY re-implement
|
||||
this method::
|
||||
|
||||
def get_jti(self, client, grant_type, user scope):
|
||||
return generate_random_string(16)
|
||||
"""
|
||||
return generate_token(16)
|
||||
|
||||
def access_token_generator(self, client, grant_type, user, scope):
|
||||
now = int(time.time())
|
||||
expires_in = now + self._get_expires_in(client, grant_type)
|
||||
|
||||
token_data = {
|
||||
"iss": self.issuer,
|
||||
"exp": expires_in,
|
||||
"client_id": client.get_client_id(),
|
||||
"iat": now,
|
||||
"jti": self.get_jti(client, grant_type, user, scope),
|
||||
"scope": scope,
|
||||
}
|
||||
|
||||
# In cases of access tokens obtained through grants where a resource owner is
|
||||
# involved, such as the authorization code grant, the value of 'sub' SHOULD
|
||||
# correspond to the subject identifier of the resource owner.
|
||||
|
||||
if user:
|
||||
token_data["sub"] = user.get_user_id()
|
||||
|
||||
# In cases of access tokens obtained through grants where no resource owner is
|
||||
# involved, such as the client credentials grant, the value of 'sub' SHOULD
|
||||
# correspond to an identifier the authorization server uses to indicate the
|
||||
# client application.
|
||||
|
||||
else:
|
||||
token_data["sub"] = client.get_client_id()
|
||||
|
||||
# If the request includes a 'resource' parameter (as defined in [RFC8707]), the
|
||||
# resulting JWT access token 'aud' claim SHOULD have the same value as the
|
||||
# 'resource' parameter in the request.
|
||||
|
||||
# TODO: Implement this with RFC8707
|
||||
if False: # pragma: no cover
|
||||
...
|
||||
|
||||
# If the request does not include a 'resource' parameter, the authorization
|
||||
# server MUST use a default resource indicator in the 'aud' claim. If a 'scope'
|
||||
# parameter is present in the request, the authorization server SHOULD use it to
|
||||
# infer the value of the default resource indicator to be used in the 'aud'
|
||||
# claim. The mechanism through which scopes are associated with default resource
|
||||
# indicator values is outside the scope of this specification.
|
||||
|
||||
else:
|
||||
token_data["aud"] = self.get_audiences(client, user, scope)
|
||||
|
||||
# If the values in the 'scope' parameter refer to different default resource
|
||||
# indicator values, the authorization server SHOULD reject the request with
|
||||
# 'invalid_scope' as described in Section 4.1.2.1 of [RFC6749].
|
||||
# TODO: Implement this with RFC8707
|
||||
|
||||
if auth_time := self.get_auth_time(user):
|
||||
token_data["auth_time"] = auth_time
|
||||
|
||||
# The meaning and processing of acr Claim Values is out of scope for this
|
||||
# specification.
|
||||
|
||||
if acr := self.get_acr(user):
|
||||
token_data["acr"] = acr
|
||||
|
||||
# The definition of particular values to be used in the amr Claim is beyond the
|
||||
# scope of this specification.
|
||||
|
||||
if amr := self.get_amr(user):
|
||||
token_data["amr"] = amr
|
||||
|
||||
# Authorization servers MAY return arbitrary attributes not defined in any
|
||||
# existing specification, as long as the corresponding claim names are collision
|
||||
# resistant or the access tokens are meant to be used only within a private
|
||||
# subsystem. Please refer to Sections 4.2 and 4.3 of [RFC7519] for details.
|
||||
|
||||
token_data.update(self.get_extra_claims(client, grant_type, user, scope))
|
||||
|
||||
# This specification registers the 'application/at+jwt' media type, which can
|
||||
# be used to indicate that the content is a JWT access token. JWT access tokens
|
||||
# MUST include this media type in the 'typ' header parameter to explicitly
|
||||
# declare that the JWT represents an access token complying with this profile.
|
||||
# Per the definition of 'typ' in Section 4.1.9 of [RFC7515], it is RECOMMENDED
|
||||
# that the 'application/' prefix be omitted. Therefore, the 'typ' value used
|
||||
# SHOULD be 'at+jwt'.
|
||||
|
||||
header = {"alg": self.alg, "typ": "at+jwt"}
|
||||
|
||||
access_token = jwt.encode(
|
||||
header,
|
||||
token_data,
|
||||
key=self.get_jwks(),
|
||||
check=False,
|
||||
)
|
||||
return access_token.decode()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""authlib.oauth2.rfc9068.token_validator.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implementation of Validating JWT Access Tokens per `Section 4`_.
|
||||
|
||||
.. _`Section 7`: https://www.rfc-editor.org/rfc/rfc9068.html#name-validating-jwt-access-token
|
||||
"""
|
||||
|
||||
from authlib.jose import jwt
|
||||
from authlib.jose.errors import DecodeError
|
||||
from authlib.jose.errors import JoseError
|
||||
from authlib.oauth2.rfc6750.errors import InsufficientScopeError
|
||||
from authlib.oauth2.rfc6750.errors import InvalidTokenError
|
||||
from authlib.oauth2.rfc6750.validator import BearerTokenValidator
|
||||
|
||||
from .claims import JWTAccessTokenClaims
|
||||
|
||||
|
||||
class JWTBearerTokenValidator(BearerTokenValidator):
|
||||
"""JWTBearerTokenValidator can protect your resource server endpoints.
|
||||
|
||||
:param issuer: The issuer from which tokens will be accepted.
|
||||
:param resource_server: An identifier for the current resource server,
|
||||
which must appear in the JWT ``aud`` claim.
|
||||
|
||||
Developers needs to implement the missing methods::
|
||||
|
||||
class MyJWTBearerTokenValidator(JWTBearerTokenValidator):
|
||||
def get_jwks(self): ...
|
||||
|
||||
|
||||
require_oauth = ResourceProtector()
|
||||
require_oauth.register_token_validator(
|
||||
MyJWTBearerTokenValidator(
|
||||
issuer="https://authorization-server.example.org",
|
||||
resource_server="https://resource-server.example.org",
|
||||
)
|
||||
)
|
||||
|
||||
You can then protect resources depending on the JWT `scope`, `groups`,
|
||||
`roles` or `entitlements` claims::
|
||||
|
||||
@require_oauth(
|
||||
scope="profile",
|
||||
groups="admins",
|
||||
roles="student",
|
||||
entitlements="captain",
|
||||
)
|
||||
def resource_endpoint(): ...
|
||||
"""
|
||||
|
||||
def __init__(self, issuer, resource_server, *args, **kwargs):
|
||||
self.issuer = issuer
|
||||
self.resource_server = resource_server
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_jwks(self):
|
||||
"""Return the JWKs that will be used to check the JWT access token signature.
|
||||
Developers MUST re-implement this method. Typically the JWKs are statically
|
||||
stored in the resource server configuration, or dynamically downloaded and
|
||||
cached using :ref:`specs/rfc8414`::
|
||||
|
||||
def get_jwks(self):
|
||||
if "jwks" in cache:
|
||||
return cache.get("jwks")
|
||||
|
||||
server_metadata = get_server_metadata(self.issuer)
|
||||
jwks_uri = server_metadata.get("jwks_uri")
|
||||
cache["jwks"] = requests.get(jwks_uri).json()
|
||||
return cache["jwks"]
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_iss(self, claims, iss: "str") -> bool:
|
||||
# The issuer identifier for the authorization server (which is typically
|
||||
# obtained during discovery) MUST exactly match the value of the 'iss'
|
||||
# claim.
|
||||
return iss == self.issuer
|
||||
|
||||
def authenticate_token(self, token_string):
|
||||
""""""
|
||||
# empty docstring avoids to display the irrelevant parent docstring
|
||||
|
||||
claims_options = {
|
||||
"iss": {"essential": True, "validate": self.validate_iss},
|
||||
"exp": {"essential": True},
|
||||
"aud": {"essential": True, "value": self.resource_server},
|
||||
"sub": {"essential": True},
|
||||
"client_id": {"essential": True},
|
||||
"iat": {"essential": True},
|
||||
"jti": {"essential": True},
|
||||
"auth_time": {"essential": False},
|
||||
"acr": {"essential": False},
|
||||
"amr": {"essential": False},
|
||||
"scope": {"essential": False},
|
||||
"groups": {"essential": False},
|
||||
"roles": {"essential": False},
|
||||
"entitlements": {"essential": False},
|
||||
}
|
||||
jwks = self.get_jwks()
|
||||
|
||||
# If the JWT access token is encrypted, decrypt it using the keys and algorithms
|
||||
# that the resource server specified during registration. If encryption was
|
||||
# negotiated with the authorization server at registration time and the incoming
|
||||
# JWT access token is not encrypted, the resource server SHOULD reject it.
|
||||
|
||||
# The resource server MUST validate the signature of all incoming JWT access
|
||||
# tokens according to [RFC7515] using the algorithm specified in the JWT 'alg'
|
||||
# Header Parameter. The resource server MUST reject any JWT in which the value
|
||||
# of 'alg' is 'none'. The resource server MUST use the keys provided by the
|
||||
# authorization server.
|
||||
try:
|
||||
return jwt.decode(
|
||||
token_string,
|
||||
key=jwks,
|
||||
claims_cls=JWTAccessTokenClaims,
|
||||
claims_options=claims_options,
|
||||
)
|
||||
except DecodeError as exc:
|
||||
raise InvalidTokenError(
|
||||
realm=self.realm, extra_attributes=self.extra_attributes
|
||||
) from exc
|
||||
|
||||
def validate_token(
|
||||
self, token, scopes, request, groups=None, roles=None, entitlements=None
|
||||
):
|
||||
""""""
|
||||
# empty docstring avoids to display the irrelevant parent docstring
|
||||
try:
|
||||
token.validate()
|
||||
except JoseError as exc:
|
||||
raise InvalidTokenError(
|
||||
realm=self.realm, extra_attributes=self.extra_attributes
|
||||
) from exc
|
||||
|
||||
# If an authorization request includes a scope parameter, the corresponding
|
||||
# issued JWT access token SHOULD include a 'scope' claim as defined in Section
|
||||
# 4.2 of [RFC8693]. All the individual scope strings in the 'scope' claim MUST
|
||||
# have meaning for the resources indicated in the 'aud' claim. See Section 5 for
|
||||
# more considerations about the relationship between scope strings and resources
|
||||
# indicated by the 'aud' claim.
|
||||
|
||||
if self.scope_insufficient(token.get("scope", []), scopes):
|
||||
raise InsufficientScopeError()
|
||||
|
||||
# Many authorization servers embed authorization attributes that go beyond the
|
||||
# delegated scenarios described by [RFC7519] in the access tokens they issue.
|
||||
# Typical examples include resource owner memberships in roles and groups that
|
||||
# are relevant to the resource being accessed, entitlements assigned to the
|
||||
# resource owner for the targeted resource that the authorization server knows
|
||||
# about, and so on. An authorization server wanting to include such attributes
|
||||
# in a JWT access token SHOULD use the 'groups', 'roles', and 'entitlements'
|
||||
# attributes of the 'User' resource schema defined by Section 4.1.2 of
|
||||
# [RFC7643]) as claim types.
|
||||
|
||||
if self.scope_insufficient(token.get("groups"), groups):
|
||||
raise InvalidTokenError()
|
||||
|
||||
if self.scope_insufficient(token.get("roles"), roles):
|
||||
raise InvalidTokenError()
|
||||
|
||||
if self.scope_insufficient(token.get("entitlements"), entitlements):
|
||||
raise InvalidTokenError()
|
||||
Reference in New Issue
Block a user