updates
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""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 .assertion import client_secret_jwt_sign
|
||||
from .assertion import private_key_jwt_sign
|
||||
from .auth import ClientSecretJWT
|
||||
from .auth import PrivateKeyJWT
|
||||
from .client import JWTBearerClientAssertion
|
||||
from .jwt_bearer import JWTBearerGrant
|
||||
from .token import JWTBearerTokenGenerator
|
||||
from .validator import JWTBearerToken
|
||||
from .validator import JWTBearerTokenValidator
|
||||
|
||||
__all__ = [
|
||||
"JWTBearerGrant",
|
||||
"JWTBearerClientAssertion",
|
||||
"client_secret_jwt_sign",
|
||||
"private_key_jwt_sign",
|
||||
"ClientSecretJWT",
|
||||
"PrivateKeyJWT",
|
||||
"JWTBearerToken",
|
||||
"JWTBearerTokenGenerator",
|
||||
"JWTBearerTokenValidator",
|
||||
]
|
||||
Binary file not shown.
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,82 @@
|
||||
import time
|
||||
|
||||
from authlib.common.security import generate_token
|
||||
from authlib.jose import jwt
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
from authlib.common.urls import add_params_to_qs
|
||||
|
||||
from .assertion import client_secret_jwt_sign
|
||||
from .assertion import 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,
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
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, leeway=60):
|
||||
self.token_url = token_url
|
||||
self._validate_jti = validate_jti
|
||||
# A small allowance of time, typically no more than a few minutes,
|
||||
# to account for clock skew. The default is 60 seconds.
|
||||
self.leeway = leeway
|
||||
|
||||
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(leeway=self.leeway)
|
||||
except JoseError as e:
|
||||
log.debug("Assertion Error: %r", e)
|
||||
raise InvalidClientError(description=e.description) from e
|
||||
return claims
|
||||
|
||||
def authenticate_client(self, client):
|
||||
if client.check_endpoint_auth_method(self.CLIENT_AUTH_METHOD, "token"):
|
||||
return client
|
||||
raise InvalidClientError(
|
||||
description=f"The client cannot authenticate with method: {self.CLIENT_AUTH_METHOD}"
|
||||
)
|
||||
|
||||
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(
|
||||
description="The client does not exist on this server."
|
||||
)
|
||||
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
|
||||
@@ -0,0 +1,199 @@
|
||||
import logging
|
||||
|
||||
from authlib.jose import JoseError
|
||||
from authlib.jose import jwt
|
||||
|
||||
from ..rfc6749 import BaseGrant
|
||||
from ..rfc6749 import InvalidClientError
|
||||
from ..rfc6749 import InvalidGrantError
|
||||
from ..rfc6749 import InvalidRequestError
|
||||
from ..rfc6749 import TokenEndpointMixin
|
||||
from ..rfc6749 import UnauthorizedClientError
|
||||
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},
|
||||
}
|
||||
|
||||
# A small allowance of time, typically no more than a few minutes,
|
||||
# to account for clock skew. The default is 60 seconds.
|
||||
LEEWAY = 60
|
||||
|
||||
@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(leeway=self.LEEWAY)
|
||||
except JoseError as e:
|
||||
log.debug("Assertion Error: %r", e)
|
||||
raise InvalidGrantError(description=e.description) from e
|
||||
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(
|
||||
f"The client is not authorized to use 'grant_type={self.GRANT_TYPE}'"
|
||||
)
|
||||
|
||||
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.payload.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()
|
||||
@@ -0,0 +1,104 @@
|
||||
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)
|
||||
@@ -0,0 +1,59 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from authlib.jose import JoseError
|
||||
from authlib.jose import JWTClaims
|
||||
from authlib.jose import jwt
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user