venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 deletions

View File

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

View File

@@ -0,0 +1,62 @@
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.
if self.header['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')

View File

@@ -0,0 +1,126 @@
from ..rfc7662 import IntrospectionEndpoint
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
class JWTIntrospectionEndpoint(IntrospectionEndpoint):
'''
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(JWTRevocationEndpoint):
def get_jwks(self):
...
def get_username(self, user_id):
...
authorization_server.register_endpoint(
MyJWTAccessTokenIntrospectionEndpoint(
issuer="https://authorization-server.example.org",
)
)
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:
raise ContinueIteration()
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()
raise InvalidTokenError()
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

View File

@@ -0,0 +1,70 @@
from ..rfc6749 import UnsupportedTokenTypeError
from ..rfc7009 import RevocationEndpoint
from authlib.common.errors import ContinueIteration
from authlib.oauth2.rfc6750.errors import InvalidTokenError
from authlib.oauth2.rfc9068.token_validator import JWTBearerTokenValidator
class JWTRevocationEndpoint(RevocationEndpoint):
'''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):
...
authorization_server.register_endpoint(
MyJWTAccessTokenRevocationEndpoint(
issuer="https://authorization-server.example.org",
)
)
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:
raise ContinueIteration()
# 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()

View File

@@ -0,0 +1,218 @@
import time
from typing import List
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):
'''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. Developpers 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 criterias 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()

View File

@@ -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:
raise InvalidTokenError(
realm=self.realm, extra_attributes=self.extra_attributes
)
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()