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,37 @@
"""
authlib.oauth2.rfc7523
~~~~~~~~~~~~~~~~~~~~~~
This module represents a direct implementation of
JSON Web Token (JWT) Profile for OAuth 2.0 Client
Authentication and Authorization Grants.
https://tools.ietf.org/html/rfc7523
"""
from .jwt_bearer import JWTBearerGrant
from .client import (
JWTBearerClientAssertion,
)
from .assertion import (
client_secret_jwt_sign,
private_key_jwt_sign,
)
from .auth import (
ClientSecretJWT, PrivateKeyJWT,
)
from .token import JWTBearerTokenGenerator
from .validator import JWTBearerToken, JWTBearerTokenValidator
__all__ = [
'JWTBearerGrant',
'JWTBearerClientAssertion',
'client_secret_jwt_sign',
'private_key_jwt_sign',
'ClientSecretJWT',
'PrivateKeyJWT',
'JWTBearerToken',
'JWTBearerTokenGenerator',
'JWTBearerTokenValidator',
]

View File

@@ -0,0 +1,66 @@
import time
from authlib.jose import jwt
from authlib.common.security import generate_token
def sign_jwt_bearer_assertion(
key, issuer, audience, subject=None, issued_at=None,
expires_at=None, claims=None, header=None, **kwargs):
if header is None:
header = {}
alg = kwargs.pop('alg', None)
if alg:
header['alg'] = alg
if 'alg' not in header:
raise ValueError('Missing "alg" in header')
payload = {'iss': issuer, 'aud': audience}
# subject is not required in Google service
if subject:
payload['sub'] = subject
if not issued_at:
issued_at = int(time.time())
expires_in = kwargs.pop('expires_in', 3600)
if not expires_at:
expires_at = issued_at + expires_in
payload['iat'] = issued_at
payload['exp'] = expires_at
if claims:
payload.update(claims)
return jwt.encode(header, payload, key)
def client_secret_jwt_sign(client_secret, client_id, token_endpoint, alg='HS256',
claims=None, **kwargs):
return _sign(client_secret, client_id, token_endpoint, alg, claims, **kwargs)
def private_key_jwt_sign(private_key, client_id, token_endpoint, alg='RS256',
claims=None, **kwargs):
return _sign(private_key, client_id, token_endpoint, alg, claims, **kwargs)
def _sign(key, client_id, token_endpoint, alg, claims=None, **kwargs):
# REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client.
issuer = client_id
# REQUIRED. Subject. This MUST contain the client_id of the OAuth Client.
subject = client_id
# The Audience SHOULD be the URL of the Authorization Server's Token Endpoint.
audience = token_endpoint
# jti is required
if claims is None:
claims = {}
if 'jti' not in claims:
claims['jti'] = generate_token(36)
return sign_jwt_bearer_assertion(
key=key, issuer=issuer, audience=audience, subject=subject,
claims=claims, alg=alg, **kwargs)

View File

@@ -0,0 +1,94 @@
from authlib.common.urls import add_params_to_qs
from .assertion import client_secret_jwt_sign, private_key_jwt_sign
from .client import ASSERTION_TYPE
class ClientSecretJWT:
"""Authentication method for OAuth 2.0 Client. This authentication
method is called ``client_secret_jwt``, which is using ``client_id``
and ``client_secret`` constructed with JWT to identify a client.
Here is an example of use ``client_secret_jwt`` with Requests Session::
from authlib.integrations.requests_client import OAuth2Session
token_endpoint = 'https://example.com/oauth/token'
session = OAuth2Session(
'your-client-id', 'your-client-secret',
token_endpoint_auth_method='client_secret_jwt'
)
session.register_client_auth_method(ClientSecretJWT(token_endpoint))
session.fetch_token(token_endpoint)
:param token_endpoint: A string URL of the token endpoint
:param claims: Extra JWT claims
:param headers: Extra JWT headers
:param alg: ``alg`` value, default is HS256
"""
name = 'client_secret_jwt'
alg = 'HS256'
def __init__(self, token_endpoint=None, claims=None, headers=None, alg=None):
self.token_endpoint = token_endpoint
self.claims = claims
self.headers = headers
if alg is not None:
self.alg = alg
def sign(self, auth, token_endpoint):
return client_secret_jwt_sign(
auth.client_secret,
client_id=auth.client_id,
token_endpoint=token_endpoint,
claims=self.claims,
header=self.headers,
alg=self.alg,
)
def __call__(self, auth, method, uri, headers, body):
token_endpoint = self.token_endpoint
if not token_endpoint:
token_endpoint = uri
client_assertion = self.sign(auth, token_endpoint)
body = add_params_to_qs(body or '', [
('client_assertion_type', ASSERTION_TYPE),
('client_assertion', client_assertion)
])
return uri, headers, body
class PrivateKeyJWT(ClientSecretJWT):
"""Authentication method for OAuth 2.0 Client. This authentication
method is called ``private_key_jwt``, which is using ``client_id``
and ``private_key`` constructed with JWT to identify a client.
Here is an example of use ``private_key_jwt`` with Requests Session::
from authlib.integrations.requests_client import OAuth2Session
token_endpoint = 'https://example.com/oauth/token'
session = OAuth2Session(
'your-client-id', 'your-client-private-key',
token_endpoint_auth_method='private_key_jwt'
)
session.register_client_auth_method(PrivateKeyJWT(token_endpoint))
session.fetch_token(token_endpoint)
:param token_endpoint: A string URL of the token endpoint
:param claims: Extra JWT claims
:param headers: Extra JWT headers
:param alg: ``alg`` value, default is RS256
"""
name = 'private_key_jwt'
alg = 'RS256'
def sign(self, auth, token_endpoint):
return private_key_jwt_sign(
auth.client_secret,
client_id=auth.client_id,
token_endpoint=token_endpoint,
claims=self.claims,
header=self.headers,
alg=self.alg,
)

View File

@@ -0,0 +1,113 @@
import logging
from authlib.jose import jwt
from authlib.jose.errors import JoseError
from ..rfc6749 import InvalidClientError
ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
log = logging.getLogger(__name__)
class JWTBearerClientAssertion:
"""Implementation of Using JWTs for Client Authentication, which is
defined by RFC7523.
"""
#: Value of ``client_assertion_type`` of JWTs
CLIENT_ASSERTION_TYPE = ASSERTION_TYPE
#: Name of the client authentication method
CLIENT_AUTH_METHOD = 'client_assertion_jwt'
def __init__(self, token_url, validate_jti=True):
self.token_url = token_url
self._validate_jti = validate_jti
def __call__(self, query_client, request):
data = request.form
assertion_type = data.get('client_assertion_type')
assertion = data.get('client_assertion')
if assertion_type == ASSERTION_TYPE and assertion:
resolve_key = self.create_resolve_key_func(query_client, request)
self.process_assertion_claims(assertion, resolve_key)
return self.authenticate_client(request.client)
log.debug('Authenticate via %r failed', self.CLIENT_AUTH_METHOD)
def create_claims_options(self):
"""Create a claims_options for verify JWT payload claims. Developers
MAY overwrite this method to create a more strict options."""
# https://tools.ietf.org/html/rfc7523#section-3
# The Audience SHOULD be the URL of the Authorization Server's Token Endpoint
options = {
'iss': {'essential': True, 'validate': _validate_iss},
'sub': {'essential': True},
'aud': {'essential': True, 'value': self.token_url},
'exp': {'essential': True},
}
if self._validate_jti:
options['jti'] = {'essential': True, 'validate': self.validate_jti}
return options
def process_assertion_claims(self, assertion, resolve_key):
"""Extract JWT payload claims from request "assertion", per
`Section 3.1`_.
:param assertion: assertion string value in the request
:param resolve_key: function to resolve the sign key
:return: JWTClaims
:raise: InvalidClientError
.. _`Section 3.1`: https://tools.ietf.org/html/rfc7523#section-3.1
"""
try:
claims = jwt.decode(
assertion, resolve_key,
claims_options=self.create_claims_options()
)
claims.validate()
except JoseError as e:
log.debug('Assertion Error: %r', e)
raise InvalidClientError()
return claims
def authenticate_client(self, client):
if client.check_endpoint_auth_method(self.CLIENT_AUTH_METHOD, 'token'):
return client
raise InvalidClientError()
def create_resolve_key_func(self, query_client, request):
def resolve_key(headers, payload):
# https://tools.ietf.org/html/rfc7523#section-3
# For client authentication, the subject MUST be the
# "client_id" of the OAuth client
client_id = payload['sub']
client = query_client(client_id)
if not client:
raise InvalidClientError()
request.client = client
return self.resolve_client_public_key(client, headers)
return resolve_key
def validate_jti(self, claims, jti):
"""Validate if the given ``jti`` value is used before. Developers
MUST implement this method::
def validate_jti(self, claims, jti):
key = 'jti:{}-{}'.format(claims['sub'], jti)
if redis.get(key):
return False
redis.set(key, 1, ex=3600)
return True
"""
raise NotImplementedError()
def resolve_client_public_key(self, client, headers):
"""Resolve the client public key for verifying the JWT signature.
A client may have many public keys, in this case, we can retrieve it
via ``kid`` value in headers. Developers MUST implement this method::
def resolve_client_public_key(self, client, headers):
return client.public_key
"""
raise NotImplementedError()
def _validate_iss(claims, iss):
return claims['sub'] == iss

View File

@@ -0,0 +1,182 @@
import logging
from authlib.jose import jwt, JoseError
from ..rfc6749 import BaseGrant, TokenEndpointMixin
from ..rfc6749 import (
UnauthorizedClientError,
InvalidRequestError,
InvalidGrantError,
InvalidClientError,
)
from .assertion import sign_jwt_bearer_assertion
log = logging.getLogger(__name__)
JWT_BEARER_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
class JWTBearerGrant(BaseGrant, TokenEndpointMixin):
GRANT_TYPE = JWT_BEARER_GRANT_TYPE
#: Options for verifying JWT payload claims. Developers MAY
#: overwrite this constant to create a more strict options.
CLAIMS_OPTIONS = {
'iss': {'essential': True},
'aud': {'essential': True},
'exp': {'essential': True},
}
@staticmethod
def sign(key, issuer, audience, subject=None,
issued_at=None, expires_at=None, claims=None, **kwargs):
return sign_jwt_bearer_assertion(
key, issuer, audience, subject, issued_at,
expires_at, claims, **kwargs)
def process_assertion_claims(self, assertion):
"""Extract JWT payload claims from request "assertion", per
`Section 3.1`_.
:param assertion: assertion string value in the request
:return: JWTClaims
:raise: InvalidGrantError
.. _`Section 3.1`: https://tools.ietf.org/html/rfc7523#section-3.1
"""
try:
claims = jwt.decode(
assertion, self.resolve_public_key,
claims_options=self.CLAIMS_OPTIONS)
claims.validate()
except JoseError as e:
log.debug('Assertion Error: %r', e)
raise InvalidGrantError(description=e.description)
return claims
def resolve_public_key(self, headers, payload):
client = self.resolve_issuer_client(payload['iss'])
return self.resolve_client_key(client, headers, payload)
def validate_token_request(self):
"""The client makes a request to the token endpoint by sending the
following parameters using the "application/x-www-form-urlencoded"
format per `Section 2.1`_:
grant_type
REQUIRED. Value MUST be set to
"urn:ietf:params:oauth:grant-type:jwt-bearer".
assertion
REQUIRED. Value MUST contain a single JWT.
scope
OPTIONAL.
The following example demonstrates an access token request with a JWT
as an authorization grant:
.. code-block:: http
POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]
.. _`Section 2.1`: https://tools.ietf.org/html/rfc7523#section-2.1
"""
assertion = self.request.form.get('assertion')
if not assertion:
raise InvalidRequestError('Missing "assertion" in request')
claims = self.process_assertion_claims(assertion)
client = self.resolve_issuer_client(claims['iss'])
log.debug('Validate token request of %s', client)
if not client.check_grant_type(self.GRANT_TYPE):
raise UnauthorizedClientError()
self.request.client = client
self.validate_requested_scope()
subject = claims.get('sub')
if subject:
user = self.authenticate_user(subject)
if not user:
raise InvalidGrantError(description='Invalid "sub" value in assertion')
log.debug('Check client(%s) permission to User(%s)', client, user)
if not self.has_granted_permission(client, user):
raise InvalidClientError(
description='Client has no permission to access user data')
self.request.user = user
def create_token_response(self):
"""If valid and authorized, the authorization server issues an access
token.
"""
token = self.generate_token(
scope=self.request.scope,
user=self.request.user,
include_refresh_token=False,
)
log.debug('Issue token %r to %r', token, self.request.client)
self.save_token(token)
return 200, token, self.TOKEN_RESPONSE_HEADER
def resolve_issuer_client(self, issuer):
"""Fetch client via "iss" in assertion claims. Developers MUST
implement this method in subclass, e.g.::
def resolve_issuer_client(self, issuer):
return Client.query_by_iss(issuer)
:param issuer: "iss" value in assertion
:return: Client instance
"""
raise NotImplementedError()
def resolve_client_key(self, client, headers, payload):
"""Resolve client key to decode assertion data. Developers MUST
implement this method in subclass. For instance, there is a
"jwks" column on client table, e.g.::
def resolve_client_key(self, client, headers, payload):
# from authlib.jose import JsonWebKey
key_set = JsonWebKey.import_key_set(client.jwks)
return key_set.find_by_kid(headers['kid'])
:param client: instance of OAuth client model
:param headers: headers part of the JWT
:param payload: payload part of the JWT
:return: ``authlib.jose.Key`` instance
"""
raise NotImplementedError()
def authenticate_user(self, subject):
"""Authenticate user with the given assertion claims. Developers MUST
implement it in subclass, e.g.::
def authenticate_user(self, subject):
return User.get_by_sub(subject)
:param subject: "sub" value in claims
:return: User instance
"""
raise NotImplementedError()
def has_granted_permission(self, client, user):
"""Check if the client has permission to access the given user's resource.
Developers MUST implement it in subclass, e.g.::
def has_granted_permission(self, client, user):
permission = ClientUserGrant.query(client=client, user=user)
return permission.granted
:param client: instance of OAuth client model
:param user: instance of User model
:return: bool
"""
raise NotImplementedError()

View File

@@ -0,0 +1,93 @@
import time
from authlib.common.encoding import to_native
from authlib.jose import jwt
class JWTBearerTokenGenerator:
"""A JSON Web Token formatted bearer token generator for jwt-bearer grant type.
This token generator can be registered into authorization server::
authorization_server.register_token_generator(
'urn:ietf:params:oauth:grant-type:jwt-bearer',
JWTBearerTokenGenerator(private_rsa_key),
)
In this way, we can generate the token into JWT format. And we don't have to
save this token into database, since it will be short time valid. Consider to
rewrite ``JWTBearerGrant.save_token``::
class MyJWTBearerGrant(JWTBearerGrant):
def save_token(self, token):
pass
:param secret_key: private RSA key in bytes, JWK or JWK Set.
:param issuer: a string or URI of the issuer
:param alg: ``alg`` to use in JWT
"""
DEFAULT_EXPIRES_IN = 3600
def __init__(self, secret_key, issuer=None, alg='RS256'):
self.secret_key = secret_key
self.issuer = issuer
self.alg = alg
@staticmethod
def get_allowed_scope(client, scope):
if scope:
scope = client.get_allowed_scope(scope)
return scope
@staticmethod
def get_sub_value(user):
"""Return user's ID as ``sub`` value in token payload. For instance::
@staticmethod
def get_sub_value(user):
return str(user.id)
"""
return user.get_user_id()
def get_token_data(self, grant_type, client, expires_in, user=None, scope=None):
scope = self.get_allowed_scope(client, scope)
issued_at = int(time.time())
data = {
'scope': scope,
'grant_type': grant_type,
'iat': issued_at,
'exp': issued_at + expires_in,
'client_id': client.get_client_id(),
}
if self.issuer:
data['iss'] = self.issuer
if user:
data['sub'] = self.get_sub_value(user)
return data
def generate(self, grant_type, client, user=None, scope=None, expires_in=None):
"""Generate a bearer token for OAuth 2.0 authorization token endpoint.
:param client: the client that making the request.
:param grant_type: current requested grant_type.
:param user: current authorized user.
:param expires_in: if provided, use this value as expires_in.
:param scope: current requested scope.
:return: Token dict
"""
if expires_in is None:
expires_in = self.DEFAULT_EXPIRES_IN
token_data = self.get_token_data(grant_type, client, expires_in, user, scope)
access_token = jwt.encode({'alg': self.alg}, token_data, key=self.secret_key, check=False)
token = {
'token_type': 'Bearer',
'access_token': to_native(access_token),
'expires_in': expires_in
}
if scope:
token['scope'] = scope
return token
def __call__(self, grant_type, client, user=None, scope=None,
expires_in=None, include_refresh_token=True):
# there is absolutely no refresh token in JWT format
return self.generate(grant_type, client, user, scope, expires_in)

View File

@@ -0,0 +1,54 @@
import time
import logging
from authlib.jose import jwt, JoseError, JWTClaims
from ..rfc6749 import TokenMixin
from ..rfc6750 import BearerTokenValidator
logger = logging.getLogger(__name__)
class JWTBearerToken(TokenMixin, JWTClaims):
def check_client(self, client):
return self['client_id'] == client.get_client_id()
def get_scope(self):
return self.get('scope')
def get_expires_in(self):
return self['exp'] - self['iat']
def is_expired(self):
return self['exp'] < time.time()
def is_revoked(self):
return False
class JWTBearerTokenValidator(BearerTokenValidator):
TOKEN_TYPE = 'bearer'
token_cls = JWTBearerToken
def __init__(self, public_key, issuer=None, realm=None, **extra_attributes):
super().__init__(realm, **extra_attributes)
self.public_key = public_key
claims_options = {
'exp': {'essential': True},
'client_id': {'essential': True},
'grant_type': {'essential': True},
}
if issuer:
claims_options['iss'] = {'essential': True, 'value': issuer}
self.claims_options = claims_options
def authenticate_token(self, token_string):
try:
claims = jwt.decode(
token_string, self.public_key,
claims_options=self.claims_options,
claims_cls=self.token_cls,
)
claims.validate()
return claims
except JoseError as error:
logger.debug('Authenticate token failed. %r', error)
return None