first commit
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""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 joserfc import jwt
|
||||
from joserfc.errors import JoseError
|
||||
|
||||
from authlib._joserfc_helpers import import_any_key
|
||||
from authlib.oauth2.claims import ClaimsOption
|
||||
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: dict[str, ClaimsOption] = {
|
||||
"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},
|
||||
}
|
||||
key = import_any_key(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:
|
||||
token = jwt.decode(token_string, key=key)
|
||||
return JWTAccessTokenClaims(token.claims, token.header, claims_options)
|
||||
except JoseError as exc:
|
||||
raise InvalidTokenError(
|
||||
realm=self.realm, extra_attributes=self.extra_attributes
|
||||
) from exc
|
||||
|
||||
def validate_token(
|
||||
self,
|
||||
token: JWTAccessTokenClaims,
|
||||
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