first commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""authlib.oauth2.rfc6749.
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
The OAuth 2.0 Authorization Framework.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749
|
||||
"""
|
||||
|
||||
from .authenticate_client import ClientAuthentication
|
||||
from .authorization_server import AuthorizationServer
|
||||
from .endpoint import Endpoint
|
||||
from .endpoint import EndpointRequest
|
||||
from .errors import AccessDeniedError
|
||||
from .errors import InsecureTransportError
|
||||
from .errors import InvalidClientError
|
||||
from .errors import InvalidGrantError
|
||||
from .errors import InvalidRequestError
|
||||
from .errors import InvalidScopeError
|
||||
from .errors import MismatchingStateException
|
||||
from .errors import MissingAuthorizationError
|
||||
from .errors import MissingCodeException # exceptions for clients
|
||||
from .errors import MissingTokenException
|
||||
from .errors import MissingTokenTypeException
|
||||
from .errors import OAuth2Error
|
||||
from .errors import UnauthorizedClientError
|
||||
from .errors import UnsupportedGrantTypeError
|
||||
from .errors import UnsupportedResponseTypeError
|
||||
from .errors import UnsupportedTokenTypeError
|
||||
from .grants import AuthorizationCodeGrant
|
||||
from .grants import AuthorizationEndpointMixin
|
||||
from .grants import BaseGrant
|
||||
from .grants import ClientCredentialsGrant
|
||||
from .grants import ImplicitGrant
|
||||
from .grants import RefreshTokenGrant
|
||||
from .grants import ResourceOwnerPasswordCredentialsGrant
|
||||
from .grants import TokenEndpointMixin
|
||||
from .models import AuthorizationCodeMixin
|
||||
from .models import ClientMixin
|
||||
from .models import TokenMixin
|
||||
from .requests import JsonPayload
|
||||
from .requests import JsonRequest
|
||||
from .requests import OAuth2Payload
|
||||
from .requests import OAuth2Request
|
||||
from .resource_protector import ResourceProtector
|
||||
from .resource_protector import TokenValidator
|
||||
from .token_endpoint import TokenEndpoint
|
||||
from .util import list_to_scope
|
||||
from .util import scope_to_list
|
||||
from .wrappers import OAuth2Token
|
||||
|
||||
__all__ = [
|
||||
"OAuth2Payload",
|
||||
"OAuth2Token",
|
||||
"OAuth2Request",
|
||||
"JsonPayload",
|
||||
"JsonRequest",
|
||||
"OAuth2Error",
|
||||
"AccessDeniedError",
|
||||
"MissingAuthorizationError",
|
||||
"InvalidGrantError",
|
||||
"InvalidClientError",
|
||||
"InvalidRequestError",
|
||||
"InvalidScopeError",
|
||||
"InsecureTransportError",
|
||||
"UnauthorizedClientError",
|
||||
"UnsupportedResponseTypeError",
|
||||
"UnsupportedGrantTypeError",
|
||||
"UnsupportedTokenTypeError",
|
||||
"MissingCodeException",
|
||||
"MissingTokenException",
|
||||
"MissingTokenTypeException",
|
||||
"MismatchingStateException",
|
||||
"ClientMixin",
|
||||
"AuthorizationCodeMixin",
|
||||
"TokenMixin",
|
||||
"ClientAuthentication",
|
||||
"AuthorizationServer",
|
||||
"ResourceProtector",
|
||||
"TokenValidator",
|
||||
"Endpoint",
|
||||
"EndpointRequest",
|
||||
"TokenEndpoint",
|
||||
"BaseGrant",
|
||||
"AuthorizationEndpointMixin",
|
||||
"TokenEndpointMixin",
|
||||
"AuthorizationCodeGrant",
|
||||
"ImplicitGrant",
|
||||
"ResourceOwnerPasswordCredentialsGrant",
|
||||
"ClientCredentialsGrant",
|
||||
"RefreshTokenGrant",
|
||||
"scope_to_list",
|
||||
"list_to_scope",
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
"""authlib.oauth2.rfc6749.authenticate_client.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Registry of client authentication methods, with 3 built-in methods:
|
||||
|
||||
1. client_secret_basic
|
||||
2. client_secret_post
|
||||
3. none
|
||||
|
||||
The "client_secret_basic" method is used a lot in examples of `RFC6749`_,
|
||||
but the concept of naming are introduced in `RFC7591`_.
|
||||
|
||||
.. _`RFC6749`: https://tools.ietf.org/html/rfc6749
|
||||
.. _`RFC7591`: https://tools.ietf.org/html/rfc7591
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from .errors import InvalidClientError
|
||||
from .util import extract_basic_authorization
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ["ClientAuthentication"]
|
||||
|
||||
|
||||
class ClientAuthentication:
|
||||
def __init__(self, query_client):
|
||||
self.query_client = query_client
|
||||
self._methods = {
|
||||
"none": authenticate_none,
|
||||
"client_secret_basic": authenticate_client_secret_basic,
|
||||
"client_secret_post": authenticate_client_secret_post,
|
||||
}
|
||||
|
||||
def register(self, method, func):
|
||||
self._methods[method] = func
|
||||
|
||||
def authenticate(self, request, methods, endpoint):
|
||||
for method in methods:
|
||||
func = self._methods[method]
|
||||
client = func(self.query_client, request)
|
||||
if client and client.check_endpoint_auth_method(method, endpoint):
|
||||
request.auth_method = method
|
||||
return client
|
||||
|
||||
if "client_secret_basic" in methods:
|
||||
raise InvalidClientError(
|
||||
status_code=401,
|
||||
description=f"The client cannot authenticate with methods: {methods}",
|
||||
)
|
||||
raise InvalidClientError(
|
||||
description=f"The client cannot authenticate with methods: {methods}",
|
||||
)
|
||||
|
||||
def __call__(self, request, methods, endpoint="token"):
|
||||
return self.authenticate(request, methods, endpoint)
|
||||
|
||||
|
||||
def authenticate_client_secret_basic(query_client, request):
|
||||
"""Authenticate client by ``client_secret_basic`` method. The client
|
||||
uses HTTP Basic for authentication.
|
||||
"""
|
||||
client_id, client_secret = extract_basic_authorization(request.headers)
|
||||
if client_id and client_secret:
|
||||
client = _validate_client(query_client, client_id, 401)
|
||||
if client.check_client_secret(client_secret):
|
||||
log.debug(f'Authenticate {client_id} via "client_secret_basic" success')
|
||||
return client
|
||||
log.debug(f'Authenticate {client_id} via "client_secret_basic" failed')
|
||||
|
||||
|
||||
def authenticate_client_secret_post(query_client, request):
|
||||
"""Authenticate client by ``client_secret_post`` method. The client
|
||||
uses POST parameters for authentication.
|
||||
"""
|
||||
data = request.form
|
||||
client_id = data.get("client_id")
|
||||
client_secret = data.get("client_secret")
|
||||
if client_id and client_secret:
|
||||
client = _validate_client(query_client, client_id)
|
||||
if client.check_client_secret(client_secret):
|
||||
log.debug(f'Authenticate {client_id} via "client_secret_post" success')
|
||||
return client
|
||||
log.debug(f'Authenticate {client_id} via "client_secret_post" failed')
|
||||
|
||||
|
||||
def authenticate_none(query_client, request):
|
||||
"""Authenticate public client by ``none`` method. The client
|
||||
does not have a client secret.
|
||||
"""
|
||||
client_id = request.payload.client_id
|
||||
if client_id and not request.payload.data.get("client_secret"):
|
||||
client = _validate_client(query_client, client_id)
|
||||
log.debug(f'Authenticate {client_id} via "none" success')
|
||||
return client
|
||||
log.debug(f'Authenticate {client_id} via "none" failed')
|
||||
|
||||
|
||||
def _validate_client(query_client, client_id, status_code=400):
|
||||
if client_id is None:
|
||||
raise InvalidClientError(
|
||||
status_code=status_code,
|
||||
description="Missing 'client_id' parameter.",
|
||||
)
|
||||
|
||||
client = query_client(client_id)
|
||||
if not client:
|
||||
raise InvalidClientError(
|
||||
status_code=status_code,
|
||||
description="The client does not exist on this server.",
|
||||
)
|
||||
|
||||
return client
|
||||
@@ -0,0 +1,419 @@
|
||||
from authlib.common.errors import ContinueIteration
|
||||
from authlib.deprecate import deprecate
|
||||
|
||||
from .authenticate_client import ClientAuthentication
|
||||
from .endpoint import Endpoint
|
||||
from .endpoint import EndpointRequest
|
||||
from .errors import InvalidScopeError
|
||||
from .errors import OAuth2Error
|
||||
from .errors import UnsupportedGrantTypeError
|
||||
from .errors import UnsupportedResponseTypeError
|
||||
from .hooks import Hookable
|
||||
from .hooks import hooked
|
||||
from .requests import JsonRequest
|
||||
from .requests import OAuth2Request
|
||||
from .util import scope_to_list
|
||||
|
||||
|
||||
class AuthorizationServer(Hookable):
|
||||
"""Authorization server that handles Authorization Endpoint and Token
|
||||
Endpoint.
|
||||
|
||||
:param scopes_supported: A list of supported scopes by this authorization server.
|
||||
"""
|
||||
|
||||
def __init__(self, scopes_supported=None):
|
||||
super().__init__()
|
||||
self.scopes_supported = scopes_supported
|
||||
self._token_generators = {}
|
||||
self._client_auth = None
|
||||
self._authorization_grants = []
|
||||
self._token_grants = []
|
||||
self._endpoints = {}
|
||||
self._extensions = []
|
||||
|
||||
def query_client(self, client_id):
|
||||
"""Query OAuth client by client_id. The client model class MUST
|
||||
implement the methods described by
|
||||
:class:`~authlib.oauth2.rfc6749.ClientMixin`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_token(self, token, request):
|
||||
"""Define function to save the generated token into database."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_token(
|
||||
self,
|
||||
grant_type,
|
||||
client,
|
||||
user=None,
|
||||
scope=None,
|
||||
expires_in=None,
|
||||
include_refresh_token=True,
|
||||
):
|
||||
"""Generate the token dict.
|
||||
|
||||
:param grant_type: current requested grant_type.
|
||||
:param client: the client that making the request.
|
||||
:param user: current authorized user.
|
||||
:param expires_in: if provided, use this value as expires_in.
|
||||
:param scope: current requested scope.
|
||||
:param include_refresh_token: should refresh_token be included.
|
||||
:return: Token dict
|
||||
"""
|
||||
# generator for a specified grant type
|
||||
func = self._token_generators.get(grant_type)
|
||||
if not func:
|
||||
# default generator for all grant types
|
||||
func = self._token_generators.get("default")
|
||||
if not func:
|
||||
raise RuntimeError("No configured token generator")
|
||||
|
||||
return func(
|
||||
grant_type=grant_type,
|
||||
client=client,
|
||||
user=user,
|
||||
scope=scope,
|
||||
expires_in=expires_in,
|
||||
include_refresh_token=include_refresh_token,
|
||||
)
|
||||
|
||||
def register_token_generator(self, grant_type, func):
|
||||
"""Register a function as token generator for the given ``grant_type``.
|
||||
Developers MUST register a default token generator with a special
|
||||
``grant_type=default``::
|
||||
|
||||
def generate_bearer_token(
|
||||
grant_type,
|
||||
client,
|
||||
user=None,
|
||||
scope=None,
|
||||
expires_in=None,
|
||||
include_refresh_token=True,
|
||||
):
|
||||
token = {"token_type": "Bearer", "access_token": ...}
|
||||
if include_refresh_token:
|
||||
token["refresh_token"] = ...
|
||||
...
|
||||
return token
|
||||
|
||||
|
||||
authorization_server.register_token_generator(
|
||||
"default", generate_bearer_token
|
||||
)
|
||||
|
||||
If you register a generator for a certain grant type, that generator will only works
|
||||
for the given grant type::
|
||||
|
||||
authorization_server.register_token_generator(
|
||||
"client_credentials",
|
||||
generate_bearer_token,
|
||||
)
|
||||
|
||||
:param grant_type: string name of the grant type
|
||||
:param func: a function to generate token
|
||||
"""
|
||||
self._token_generators[grant_type] = func
|
||||
|
||||
def authenticate_client(self, request, methods, endpoint="token"):
|
||||
"""Authenticate client via HTTP request information with the given
|
||||
methods, such as ``client_secret_basic``, ``client_secret_post``.
|
||||
"""
|
||||
if self._client_auth is None and self.query_client:
|
||||
self._client_auth = ClientAuthentication(self.query_client)
|
||||
return self._client_auth(request, methods, endpoint)
|
||||
|
||||
def register_client_auth_method(self, method, func):
|
||||
"""Add more client auth method. The default methods are:
|
||||
|
||||
* none: The client is a public client and does not have a client secret
|
||||
* client_secret_post: The client uses the HTTP POST parameters
|
||||
* client_secret_basic: The client uses HTTP Basic
|
||||
|
||||
:param method: Name of the Auth method
|
||||
:param func: Function to authenticate the client
|
||||
|
||||
The auth method accept two parameters: ``query_client`` and ``request``,
|
||||
an example for this method::
|
||||
|
||||
def authenticate_client_via_custom(query_client, request):
|
||||
client_id = request.headers["X-Client-Id"]
|
||||
client = query_client(client_id)
|
||||
do_some_validation(client)
|
||||
return client
|
||||
|
||||
|
||||
authorization_server.register_client_auth_method(
|
||||
"custom", authenticate_client_via_custom
|
||||
)
|
||||
"""
|
||||
if self._client_auth is None and self.query_client:
|
||||
self._client_auth = ClientAuthentication(self.query_client)
|
||||
|
||||
self._client_auth.register(method, func)
|
||||
|
||||
def register_extension(self, extension):
|
||||
self._extensions.append(extension(self))
|
||||
|
||||
def get_error_uri(self, request, error):
|
||||
"""Return a URI for the given error, framework may implement this method."""
|
||||
return None
|
||||
|
||||
def send_signal(self, name, *args, **kwargs):
|
||||
"""Framework integration can re-implement this method to support
|
||||
signal system.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_oauth2_request(self, request) -> OAuth2Request:
|
||||
"""This method MUST be implemented in framework integrations. It is
|
||||
used to create an OAuth2Request instance.
|
||||
|
||||
:param request: the "request" instance in framework
|
||||
:return: OAuth2Request instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_json_request(self, request) -> JsonRequest:
|
||||
"""This method MUST be implemented in framework integrations. It is
|
||||
used to create an HttpRequest instance.
|
||||
|
||||
:param request: the "request" instance in framework
|
||||
:return: HttpRequest instance
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def handle_response(self, status, body, headers):
|
||||
"""Return HTTP response. Framework MUST implement this function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_requested_scope(self, scope):
|
||||
"""Validate if requested scope is supported by Authorization Server.
|
||||
Developers CAN re-write this method to meet your needs.
|
||||
"""
|
||||
if scope and self.scopes_supported:
|
||||
scopes = set(scope_to_list(scope))
|
||||
if not set(self.scopes_supported).issuperset(scopes):
|
||||
raise InvalidScopeError()
|
||||
|
||||
def register_grant(self, grant_cls, extensions=None):
|
||||
"""Register a grant class into the endpoint registry. Developers
|
||||
can implement the grants in ``authlib.oauth2.rfc6749.grants`` and
|
||||
register with this method::
|
||||
|
||||
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
||||
def authenticate_user(self, credential):
|
||||
# ...
|
||||
|
||||
authorization_server.register_grant(AuthorizationCodeGrant)
|
||||
|
||||
:param grant_cls: a grant class.
|
||||
:param extensions: extensions for the grant class.
|
||||
"""
|
||||
if hasattr(grant_cls, "check_authorization_endpoint"):
|
||||
self._authorization_grants.append((grant_cls, extensions))
|
||||
if hasattr(grant_cls, "check_token_endpoint"):
|
||||
self._token_grants.append((grant_cls, extensions))
|
||||
|
||||
def register_endpoint(self, endpoint: type[Endpoint] | Endpoint):
|
||||
"""Add extra endpoint to authorization server. e.g.
|
||||
RevocationEndpoint::
|
||||
|
||||
authorization_server.register_endpoint(RevocationEndpoint)
|
||||
|
||||
:param endpoint: An endpoint class or instance.
|
||||
"""
|
||||
if isinstance(endpoint, type):
|
||||
endpoint = endpoint(self)
|
||||
else:
|
||||
endpoint.server = self
|
||||
|
||||
endpoints = self._endpoints.setdefault(endpoint.ENDPOINT_NAME, [])
|
||||
endpoints.append(endpoint)
|
||||
|
||||
@hooked
|
||||
def get_authorization_grant(self, request):
|
||||
"""Find the authorization grant for current request.
|
||||
|
||||
:param request: OAuth2Request instance.
|
||||
:return: grant instance
|
||||
"""
|
||||
for grant_cls, extensions in self._authorization_grants:
|
||||
if grant_cls.check_authorization_endpoint(request):
|
||||
return _create_grant(grant_cls, extensions, request, self)
|
||||
|
||||
# Per RFC 6749 §4.1.2.1, only redirect with the error if the client
|
||||
# exists and the redirect_uri has been validated against it.
|
||||
redirect_uri = None
|
||||
if client_id := request.payload.client_id:
|
||||
if client := self.query_client(client_id):
|
||||
if requested_uri := request.payload.redirect_uri:
|
||||
if client.check_redirect_uri(requested_uri):
|
||||
redirect_uri = requested_uri
|
||||
else:
|
||||
redirect_uri = client.get_default_redirect_uri()
|
||||
|
||||
raise UnsupportedResponseTypeError(
|
||||
f"The response type '{request.payload.response_type}' is not supported by the server.",
|
||||
request.payload.response_type,
|
||||
redirect_uri=redirect_uri,
|
||||
)
|
||||
|
||||
def get_consent_grant(self, request=None, end_user=None):
|
||||
"""Validate current HTTP request for authorization page. This page
|
||||
is designed for resource owner to grant or deny the authorization.
|
||||
"""
|
||||
request = self.create_oauth2_request(request)
|
||||
|
||||
try:
|
||||
request.user = end_user
|
||||
|
||||
grant = self.get_authorization_grant(request)
|
||||
grant.validate_no_multiple_request_parameter(request)
|
||||
grant.validate_consent_request()
|
||||
|
||||
except OAuth2Error as error:
|
||||
# REQUIRED if a "state" parameter was present in the client
|
||||
# authorization request. The exact value received from the
|
||||
# client.
|
||||
error.state = request.payload.state
|
||||
raise
|
||||
return grant
|
||||
|
||||
def get_token_grant(self, request):
|
||||
"""Find the token grant for current request.
|
||||
|
||||
:param request: OAuth2Request instance.
|
||||
:return: grant instance
|
||||
"""
|
||||
for grant_cls, extensions in self._token_grants:
|
||||
if grant_cls.check_token_endpoint(request):
|
||||
return _create_grant(grant_cls, extensions, request, self)
|
||||
raise UnsupportedGrantTypeError(request.payload.grant_type)
|
||||
|
||||
def validate_endpoint_request(self, name, request=None) -> EndpointRequest:
|
||||
"""Validate endpoint request and return the validated request object.
|
||||
|
||||
Use this for interactive endpoints where you need to handle UI
|
||||
between validation and response creation.
|
||||
|
||||
:param name: Endpoint name
|
||||
:param request: HTTP request instance
|
||||
:returns: Validated EndpointRequest object
|
||||
:raises OAuth2Error: If validation fails
|
||||
:raises RuntimeError: If endpoint not found
|
||||
|
||||
Example::
|
||||
|
||||
req = server.validate_endpoint_request("end_session")
|
||||
if req.needs_confirmation:
|
||||
return render_template("confirm_logout.html", ...)
|
||||
return server.create_endpoint_response("end_session", req)
|
||||
"""
|
||||
if name not in self._endpoints:
|
||||
raise RuntimeError(f"There is no '{name}' endpoint.")
|
||||
|
||||
endpoint = self._endpoints[name][0]
|
||||
request = endpoint.create_endpoint_request(request)
|
||||
return endpoint.validate_request(request)
|
||||
|
||||
def create_endpoint_response(self, name, request=None):
|
||||
"""Validate endpoint request and create endpoint response.
|
||||
|
||||
Can be called with:
|
||||
- A raw HTTP request or None: validates and responds in one step
|
||||
- A validated EndpointRequest: skips validation, creates response directly
|
||||
|
||||
:param name: Endpoint name
|
||||
:param request: HTTP request instance or validated EndpointRequest
|
||||
:return: Response, or None if the endpoint returns None
|
||||
"""
|
||||
if name not in self._endpoints:
|
||||
raise RuntimeError(f"There is no '{name}' endpoint.")
|
||||
|
||||
endpoints = self._endpoints[name]
|
||||
|
||||
# If request is already validated, create response directly
|
||||
if isinstance(request, EndpointRequest):
|
||||
endpoint = endpoints[0]
|
||||
try:
|
||||
result = endpoint.create_response(request)
|
||||
if result is None:
|
||||
return None
|
||||
return self.handle_response(*result)
|
||||
except OAuth2Error as error:
|
||||
return self.handle_error_response(request.request, error)
|
||||
|
||||
# Otherwise, validate and respond (existing behavior)
|
||||
for endpoint in endpoints:
|
||||
request = endpoint.create_endpoint_request(request)
|
||||
try:
|
||||
result = endpoint(request)
|
||||
if result is None:
|
||||
return None
|
||||
return self.handle_response(*result)
|
||||
except ContinueIteration:
|
||||
continue
|
||||
except OAuth2Error as error:
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
@hooked
|
||||
def create_authorization_response(self, request=None, grant_user=None, grant=None):
|
||||
"""Validate authorization request and create authorization response.
|
||||
|
||||
:param request: HTTP request instance.
|
||||
:param grant_user: if granted, it is resource owner. If denied,
|
||||
it is None.
|
||||
:returns: Response
|
||||
"""
|
||||
if not isinstance(request, OAuth2Request):
|
||||
request = self.create_oauth2_request(request)
|
||||
|
||||
if not grant:
|
||||
deprecate("The 'grant' parameter will become mandatory.", version="1.8")
|
||||
try:
|
||||
grant = self.get_authorization_grant(request)
|
||||
except UnsupportedResponseTypeError as error:
|
||||
error.state = request.payload.state
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
try:
|
||||
redirect_uri = grant.validate_authorization_request()
|
||||
args = grant.create_authorization_response(redirect_uri, grant_user)
|
||||
response = self.handle_response(*args)
|
||||
except OAuth2Error as error:
|
||||
error.state = request.payload.state
|
||||
response = self.handle_error_response(request, error)
|
||||
|
||||
grant.execute_hook("after_authorization_response", response)
|
||||
return response
|
||||
|
||||
def create_token_response(self, request=None):
|
||||
"""Validate token request and create token response.
|
||||
|
||||
:param request: HTTP request instance
|
||||
"""
|
||||
request = self.create_oauth2_request(request)
|
||||
try:
|
||||
grant = self.get_token_grant(request)
|
||||
except UnsupportedGrantTypeError as error:
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
try:
|
||||
grant.validate_token_request()
|
||||
args = grant.create_token_response()
|
||||
return self.handle_response(*args)
|
||||
except OAuth2Error as error:
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
def handle_error_response(self, request, error):
|
||||
return self.handle_response(*error(self.get_error_uri(request, error)))
|
||||
|
||||
|
||||
def _create_grant(grant_cls, extensions, request, server):
|
||||
grant = grant_cls(request, server)
|
||||
if extensions:
|
||||
for ext in extensions:
|
||||
ext(grant)
|
||||
return grant
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
authlib.oauth2.rfc6749.endpoint
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Base class for OAuth2 endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .requests import OAuth2Request
|
||||
|
||||
|
||||
@dataclass
|
||||
class EndpointRequest:
|
||||
"""Base class for validated endpoint requests.
|
||||
|
||||
This object is returned by :meth:`Endpoint.validate_request` and contains
|
||||
all validated information from the endpoint request. Subclasses add
|
||||
endpoint-specific fields.
|
||||
"""
|
||||
|
||||
request: OAuth2Request
|
||||
client: Any = None
|
||||
|
||||
|
||||
class Endpoint:
|
||||
"""Base class for OAuth2 endpoints.
|
||||
|
||||
Supports two modes of operation:
|
||||
|
||||
**Automatic mode** (non-interactive endpoints):
|
||||
Call ``server.create_endpoint_response(name)`` which validates the request
|
||||
and creates the response in one step.
|
||||
|
||||
**Interactive mode** (endpoints requiring user confirmation):
|
||||
1. Call ``server.validate_endpoint_request(name)`` to get a validated request
|
||||
2. Handle user interaction (e.g., show confirmation page)
|
||||
3. Call ``server.create_endpoint_response(name, validated_request)`` to complete
|
||||
|
||||
Subclasses must implement :meth:`validate_request` and :meth:`create_response`.
|
||||
"""
|
||||
|
||||
#: Endpoint name used for registration
|
||||
ENDPOINT_NAME: str | None = None
|
||||
|
||||
def __init__(self, server=None):
|
||||
self.server = server
|
||||
|
||||
def create_endpoint_request(self, request):
|
||||
"""Convert framework request to OAuth2Request."""
|
||||
return self.server.create_oauth2_request(request)
|
||||
|
||||
def validate_request(self, request: OAuth2Request) -> EndpointRequest:
|
||||
"""Validate the request and return a validated request object.
|
||||
|
||||
:param request: The OAuth2Request to validate
|
||||
:returns: EndpointRequest with validated data
|
||||
:raises OAuth2Error: If validation fails
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_response(
|
||||
self, validated_request: EndpointRequest
|
||||
) -> tuple[int, Any, list] | None:
|
||||
"""Create the HTTP response from a validated request.
|
||||
|
||||
:param validated_request: The validated EndpointRequest
|
||||
:returns: Tuple of (status_code, body, headers), or None if the
|
||||
application should provide its own response
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_endpoint_response(
|
||||
self, request: OAuth2Request
|
||||
) -> tuple[int, Any, list] | None:
|
||||
"""Validate and respond in one step (non-interactive mode).
|
||||
|
||||
:param request: The OAuth2Request to process
|
||||
:returns: Tuple of (status_code, body, headers), or None
|
||||
"""
|
||||
validated = self.validate_request(request)
|
||||
return self.create_response(validated)
|
||||
|
||||
def __call__(self, request: OAuth2Request) -> tuple[int, Any, list] | None:
|
||||
return self.create_endpoint_response(request)
|
||||
@@ -0,0 +1,247 @@
|
||||
"""authlib.oauth2.rfc6749.errors.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implementation for OAuth 2 Error Response. A basic error has
|
||||
parameters:
|
||||
|
||||
Error:
|
||||
REQUIRED. A single ASCII [USASCII] error code.
|
||||
|
||||
error_description
|
||||
OPTIONAL. Human-readable ASCII [USASCII] text providing
|
||||
additional information, used to assist the client developer in
|
||||
understanding the error that occurred.
|
||||
|
||||
error_uri
|
||||
OPTIONAL. A URI identifying a human-readable web page with
|
||||
information about the error, used to provide the client
|
||||
developer with additional information about the error.
|
||||
Values for the "error_uri" parameter MUST conform to the
|
||||
URI-reference syntax and thus MUST NOT include characters
|
||||
outside the set %x21 / %x23-5B / %x5D-7E.
|
||||
|
||||
state
|
||||
REQUIRED if a "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
|
||||
:copyright: (c) 2017 by Hsiaoming Yang.
|
||||
|
||||
"""
|
||||
|
||||
from authlib.common.security import is_secure_transport
|
||||
from authlib.oauth2.base import OAuth2Error
|
||||
|
||||
__all__ = [
|
||||
"OAuth2Error",
|
||||
"InsecureTransportError",
|
||||
"InvalidRequestError",
|
||||
"InvalidClientError",
|
||||
"UnauthorizedClientError",
|
||||
"InvalidGrantError",
|
||||
"UnsupportedResponseTypeError",
|
||||
"UnsupportedGrantTypeError",
|
||||
"InvalidScopeError",
|
||||
"AccessDeniedError",
|
||||
"MissingAuthorizationError",
|
||||
"UnsupportedTokenTypeError",
|
||||
"MissingCodeException",
|
||||
"MissingTokenException",
|
||||
"MissingTokenTypeException",
|
||||
"MismatchingStateException",
|
||||
]
|
||||
|
||||
|
||||
class InsecureTransportError(OAuth2Error):
|
||||
error = "insecure_transport"
|
||||
description = "OAuth 2 MUST utilize https."
|
||||
|
||||
@classmethod
|
||||
def check(cls, uri):
|
||||
"""Check and raise InsecureTransportError with the given URI."""
|
||||
if not is_secure_transport(uri):
|
||||
raise cls()
|
||||
|
||||
|
||||
class InvalidRequestError(OAuth2Error):
|
||||
"""The request is missing a required parameter, includes an
|
||||
unsupported parameter value (other than grant type),
|
||||
repeats a parameter, includes multiple credentials,
|
||||
utilizes more than one mechanism for authenticating the
|
||||
client, or is otherwise malformed.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
error = "invalid_request"
|
||||
|
||||
|
||||
class InvalidClientError(OAuth2Error):
|
||||
"""Client authentication failed (e.g., unknown client, no
|
||||
client authentication included, or unsupported
|
||||
authentication method). The authorization server MAY
|
||||
return an HTTP 401 (Unauthorized) status code to indicate
|
||||
which HTTP authentication schemes are supported. If the
|
||||
client attempted to authenticate via the "Authorization"
|
||||
request header field, the authorization server MUST
|
||||
respond with an HTTP 401 (Unauthorized) status code and
|
||||
include the "WWW-Authenticate" response header field
|
||||
matching the authentication scheme used by the client.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
error = "invalid_client"
|
||||
status_code = 400
|
||||
|
||||
def get_headers(self):
|
||||
headers = super().get_headers()
|
||||
if self.status_code == 401:
|
||||
error_description = self.get_error_description()
|
||||
# safe escape
|
||||
error_description = error_description.replace('"', "|")
|
||||
extras = [
|
||||
f'error="{self.error}"',
|
||||
f'error_description="{error_description}"',
|
||||
]
|
||||
headers.append(("WWW-Authenticate", "Basic " + ", ".join(extras)))
|
||||
return headers
|
||||
|
||||
|
||||
class InvalidGrantError(OAuth2Error):
|
||||
"""The provided authorization grant (e.g., authorization
|
||||
code, resource owner credentials) or refresh token is
|
||||
invalid, expired, revoked, does not match the redirection
|
||||
URI used in the authorization request, or was issued to
|
||||
another client.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
error = "invalid_grant"
|
||||
|
||||
|
||||
class UnauthorizedClientError(OAuth2Error):
|
||||
"""The authenticated client is not authorized to use this
|
||||
authorization grant type.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
error = "unauthorized_client"
|
||||
|
||||
|
||||
class UnsupportedResponseTypeError(OAuth2Error):
|
||||
"""The authorization server does not support obtaining
|
||||
an access token using this method.
|
||||
"""
|
||||
|
||||
error = "unsupported_response_type"
|
||||
|
||||
def __init__(self, response_type, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.response_type = response_type
|
||||
|
||||
def get_error_description(self):
|
||||
return f"response_type={self.response_type} is not supported"
|
||||
|
||||
|
||||
class UnsupportedGrantTypeError(OAuth2Error):
|
||||
"""The authorization grant type is not supported by the
|
||||
authorization server.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
error = "unsupported_grant_type"
|
||||
|
||||
def __init__(self, grant_type):
|
||||
super().__init__()
|
||||
self.grant_type = grant_type
|
||||
|
||||
def get_error_description(self):
|
||||
return f"grant_type={self.grant_type} is not supported"
|
||||
|
||||
|
||||
class InvalidScopeError(OAuth2Error):
|
||||
"""The requested scope is invalid, unknown, malformed, or
|
||||
exceeds the scope granted by the resource owner.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
error = "invalid_scope"
|
||||
description = "The requested scope is invalid, unknown, or malformed."
|
||||
|
||||
|
||||
class AccessDeniedError(OAuth2Error):
|
||||
"""The resource owner or authorization server denied the request.
|
||||
|
||||
Used in authorization endpoint for "code" and "implicit". Defined in
|
||||
`Section 4.1.2.1`_.
|
||||
|
||||
.. _`Section 4.1.2.1`: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
||||
"""
|
||||
|
||||
error = "access_denied"
|
||||
description = "The resource owner or authorization server denied the request"
|
||||
|
||||
|
||||
# -- below are extended errors -- #
|
||||
|
||||
|
||||
class ForbiddenError(OAuth2Error):
|
||||
status_code = 401
|
||||
|
||||
def __init__(self, auth_type=None, realm=None):
|
||||
super().__init__()
|
||||
self.auth_type = auth_type
|
||||
self.realm = realm
|
||||
|
||||
def get_headers(self):
|
||||
headers = super().get_headers()
|
||||
if not self.auth_type:
|
||||
return headers
|
||||
|
||||
extras = []
|
||||
if self.realm:
|
||||
extras.append(f'realm="{self.realm}"')
|
||||
extras.append(f'error="{self.error}"')
|
||||
error_description = self.description
|
||||
extras.append(f'error_description="{error_description}"')
|
||||
headers.append(("WWW-Authenticate", f"{self.auth_type} " + ", ".join(extras)))
|
||||
return headers
|
||||
|
||||
|
||||
class MissingAuthorizationError(ForbiddenError):
|
||||
error = "missing_authorization"
|
||||
description = "Missing 'Authorization' in headers."
|
||||
|
||||
|
||||
class UnsupportedTokenTypeError(ForbiddenError):
|
||||
error = "unsupported_token_type"
|
||||
|
||||
|
||||
# -- exceptions for clients -- #
|
||||
|
||||
|
||||
class MissingCodeException(OAuth2Error):
|
||||
error = "missing_code"
|
||||
description = "Missing 'code' in response."
|
||||
|
||||
|
||||
class MissingTokenException(OAuth2Error):
|
||||
error = "missing_token"
|
||||
description = "Missing 'access_token' in response."
|
||||
|
||||
|
||||
class MissingTokenTypeException(OAuth2Error):
|
||||
error = "missing_token_type"
|
||||
description = "Missing 'token_type' in response."
|
||||
|
||||
|
||||
class MismatchingStateException(OAuth2Error):
|
||||
error = "mismatching_state"
|
||||
description = "CSRF Warning! State not equal in request and response."
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
authlib.oauth2.rfc6749.grants
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implementation for `Section 4`_ of "Obtaining Authorization".
|
||||
|
||||
To request an access token, the client obtains authorization from the
|
||||
resource owner. The authorization is expressed in the form of an
|
||||
authorization grant, which the client uses to request the access
|
||||
token. OAuth defines four grant types:
|
||||
|
||||
1. authorization code
|
||||
2. implicit
|
||||
3. resource owner password credentials
|
||||
4. client credentials.
|
||||
|
||||
It also provides an extension mechanism for defining additional grant
|
||||
types. Authlib defines refresh_token as a grant type too.
|
||||
|
||||
.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4
|
||||
"""
|
||||
|
||||
from .authorization_code import AuthorizationCodeGrant
|
||||
from .base import AuthorizationEndpointMixin
|
||||
from .base import BaseGrant
|
||||
from .base import TokenEndpointMixin
|
||||
from .client_credentials import ClientCredentialsGrant
|
||||
from .implicit import ImplicitGrant
|
||||
from .refresh_token import RefreshTokenGrant
|
||||
from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
|
||||
|
||||
__all__ = [
|
||||
"BaseGrant",
|
||||
"AuthorizationEndpointMixin",
|
||||
"TokenEndpointMixin",
|
||||
"AuthorizationCodeGrant",
|
||||
"ImplicitGrant",
|
||||
"ResourceOwnerPasswordCredentialsGrant",
|
||||
"ClientCredentialsGrant",
|
||||
"RefreshTokenGrant",
|
||||
]
|
||||
@@ -0,0 +1,400 @@
|
||||
import logging
|
||||
|
||||
from authlib.common.security import generate_token
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
|
||||
from ..errors import AccessDeniedError
|
||||
from ..errors import InvalidClientError
|
||||
from ..errors import InvalidGrantError
|
||||
from ..errors import InvalidRequestError
|
||||
from ..errors import InvalidScopeError
|
||||
from ..errors import OAuth2Error
|
||||
from ..errors import UnauthorizedClientError
|
||||
from ..hooks import hooked
|
||||
from .base import AuthorizationEndpointMixin
|
||||
from .base import BaseGrant
|
||||
from .base import TokenEndpointMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthorizationCodeGrant(BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin):
|
||||
"""The authorization code grant type is used to obtain both access
|
||||
tokens and refresh tokens and is optimized for confidential clients.
|
||||
Since this is a redirection-based flow, the client must be capable of
|
||||
interacting with the resource owner's user-agent (typically a web
|
||||
browser) and capable of receiving incoming requests (via redirection)
|
||||
from the authorization server::
|
||||
|
||||
+----------+
|
||||
| Resource |
|
||||
| Owner |
|
||||
| |
|
||||
+----------+
|
||||
^
|
||||
|
|
||||
(B)
|
||||
+----|-----+ Client Identifier +---------------+
|
||||
| -+----(A)-- & Redirection URI ---->| |
|
||||
| User- | | Authorization |
|
||||
| Agent -+----(B)-- User authenticates --->| Server |
|
||||
| | | |
|
||||
| -+----(C)-- Authorization Code ---<| |
|
||||
+-|----|---+ +---------------+
|
||||
| | ^ v
|
||||
(A) (C) | |
|
||||
| | | |
|
||||
^ v | |
|
||||
+---------+ | |
|
||||
| |>---(D)-- Authorization Code ---------' |
|
||||
| Client | & Redirection URI |
|
||||
| | |
|
||||
| |<---(E)----- Access Token -------------------'
|
||||
+---------+ (w/ Optional Refresh Token)
|
||||
"""
|
||||
|
||||
#: Allowed client auth methods for token endpoint
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"]
|
||||
|
||||
#: Generated "code" length
|
||||
AUTHORIZATION_CODE_LENGTH = 48
|
||||
|
||||
RESPONSE_TYPES = {"code"}
|
||||
GRANT_TYPE = "authorization_code"
|
||||
|
||||
def validate_authorization_request(self):
|
||||
"""The client constructs the request URI by adding the following
|
||||
parameters to the query component of the authorization endpoint URI
|
||||
using the "application/x-www-form-urlencoded" format.
|
||||
Per `Section 4.1.1`_.
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "code".
|
||||
|
||||
client_id
|
||||
REQUIRED. The client identifier as described in Section 2.2.
|
||||
|
||||
redirect_uri
|
||||
OPTIONAL. As described in Section 3.1.2.
|
||||
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3.
|
||||
|
||||
state
|
||||
RECOMMENDED. An opaque value used by the client to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this value when redirecting the user-agent back
|
||||
to the client. The parameter SHOULD be used for preventing
|
||||
cross-site request forgery as described in Section 10.12.
|
||||
|
||||
The client directs the resource owner to the constructed URI using an
|
||||
HTTP redirection response, or by other means available to it via the
|
||||
user-agent.
|
||||
|
||||
For example, the client directs the user-agent to make the following
|
||||
HTTP request using TLS (with extra line breaks for display purposes
|
||||
only):
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
|
||||
Host: server.example.com
|
||||
|
||||
The authorization server validates the request to ensure that all
|
||||
required parameters are present and valid. If the request is valid,
|
||||
the authorization server authenticates the resource owner and obtains
|
||||
an authorization decision (by asking the resource owner or by
|
||||
establishing approval via other means).
|
||||
|
||||
.. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
|
||||
"""
|
||||
return validate_code_authorization_request(self)
|
||||
|
||||
def create_authorization_response(self, redirect_uri: str, grant_user):
|
||||
"""If the resource owner grants the access request, the authorization
|
||||
server issues an authorization code and delivers it to the client by
|
||||
adding the following parameters to the query component of the
|
||||
redirection URI using the "application/x-www-form-urlencoded" format.
|
||||
Per `Section 4.1.2`_.
|
||||
|
||||
code
|
||||
REQUIRED. The authorization code generated by the
|
||||
authorization server. The authorization code MUST expire
|
||||
shortly after it is issued to mitigate the risk of leaks. A
|
||||
maximum authorization code lifetime of 10 minutes is
|
||||
RECOMMENDED. The client MUST NOT use the authorization code
|
||||
more than once. If an authorization code is used more than
|
||||
once, the authorization server MUST deny the request and SHOULD
|
||||
revoke (when possible) all tokens previously issued based on
|
||||
that authorization code. The authorization code is bound to
|
||||
the client identifier and redirection URI.
|
||||
state
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
For example, the authorization server redirects the user-agent by
|
||||
sending the following HTTP response.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 302 Found
|
||||
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&state=xyz
|
||||
|
||||
.. _`Section 4.1.2`: https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
|
||||
:param redirect_uri: Redirect to the given URI for the authorization
|
||||
:param grant_user: if resource owner granted the request, pass this
|
||||
resource owner, otherwise pass None.
|
||||
:returns: (status_code, body, headers)
|
||||
"""
|
||||
if not grant_user:
|
||||
raise AccessDeniedError(redirect_uri=redirect_uri)
|
||||
|
||||
self.request.user = grant_user
|
||||
|
||||
code = self.generate_authorization_code()
|
||||
self.save_authorization_code(code, self.request)
|
||||
|
||||
params = [("code", code)]
|
||||
if self.request.payload.state:
|
||||
params.append(("state", self.request.payload.state))
|
||||
uri = add_params_to_uri(redirect_uri, params)
|
||||
headers = [("Location", uri)]
|
||||
return 302, "", headers
|
||||
|
||||
@hooked
|
||||
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 4.1.3`_:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "authorization_code".
|
||||
|
||||
code
|
||||
REQUIRED. The authorization code received from the
|
||||
authorization server.
|
||||
|
||||
redirect_uri
|
||||
REQUIRED, if the "redirect_uri" parameter was included in the
|
||||
authorization request as described in Section 4.1.1, and their
|
||||
values MUST be identical.
|
||||
|
||||
client_id
|
||||
REQUIRED, if the client is not authenticating with the
|
||||
authorization server as described in Section 3.2.1.
|
||||
|
||||
If the client type is confidential or the client was issued client
|
||||
credentials (or assigned other authentication requirements), the
|
||||
client MUST authenticate with the authorization server as described
|
||||
in Section 3.2.1.
|
||||
|
||||
For example, the client makes the following HTTP request using TLS:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /token HTTP/1.1
|
||||
Host: server.example.com
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
|
||||
|
||||
.. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
|
||||
"""
|
||||
# ignore validate for grant_type, since it is validated by
|
||||
# check_token_endpoint
|
||||
|
||||
# authenticate the client if client authentication is included
|
||||
client = self.authenticate_token_endpoint_client()
|
||||
|
||||
log.debug("Validate token request of %r", 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}'"
|
||||
)
|
||||
|
||||
code = self.request.form.get("code")
|
||||
if code is None:
|
||||
raise InvalidRequestError("Missing 'code' in request.")
|
||||
|
||||
# ensure that the authorization code was issued to the authenticated
|
||||
# confidential client, or if the client is public, ensure that the
|
||||
# code was issued to "client_id" in the request
|
||||
authorization_code = self.query_authorization_code(code, client)
|
||||
if not authorization_code:
|
||||
raise InvalidGrantError("Invalid 'code' in request.")
|
||||
|
||||
# validate redirect_uri parameter
|
||||
log.debug("Validate token redirect_uri of %r", client)
|
||||
redirect_uri = self.request.payload.redirect_uri
|
||||
original_redirect_uri = authorization_code.get_redirect_uri()
|
||||
if original_redirect_uri and redirect_uri != original_redirect_uri:
|
||||
raise InvalidGrantError("Invalid 'redirect_uri' in request.")
|
||||
|
||||
# save for create_token_response
|
||||
self.request.client = client
|
||||
self.request.authorization_code = authorization_code
|
||||
|
||||
@hooked
|
||||
def create_token_response(self):
|
||||
"""If the access token request is valid and authorized, the
|
||||
authorization server issues an access token and optional refresh
|
||||
token as described in Section 5.1. If the request client
|
||||
authentication failed or is invalid, the authorization server returns
|
||||
an error response as described in Section 5.2. Per `Section 4.1.4`_.
|
||||
|
||||
An example successful response:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Cache-Control: no-store
|
||||
Pragma: no-cache
|
||||
|
||||
{
|
||||
"access_token":"2YotnFZFEjr1zCsicMWpAA",
|
||||
"token_type":"example",
|
||||
"expires_in":3600,
|
||||
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
|
||||
"example_parameter":"example_value"
|
||||
}
|
||||
|
||||
:returns: (status_code, body, headers)
|
||||
|
||||
.. _`Section 4.1.4`: https://tools.ietf.org/html/rfc6749#section-4.1.4
|
||||
"""
|
||||
client = self.request.client
|
||||
authorization_code = self.request.authorization_code
|
||||
|
||||
user = self.authenticate_user(authorization_code)
|
||||
if not user:
|
||||
raise InvalidGrantError("There is no 'user' for this code.")
|
||||
self.request.user = user
|
||||
|
||||
scope = authorization_code.get_scope()
|
||||
token = self.generate_token(
|
||||
user=user,
|
||||
scope=scope,
|
||||
include_refresh_token=client.check_grant_type("refresh_token"),
|
||||
)
|
||||
log.debug("Issue token %r to %r", token, client)
|
||||
|
||||
self.save_token(token)
|
||||
self.delete_authorization_code(authorization_code)
|
||||
return 200, token, self.TOKEN_RESPONSE_HEADER
|
||||
|
||||
def generate_authorization_code(self):
|
||||
""" "The method to generate "code" value for authorization code data.
|
||||
Developers may rewrite this method, or customize the code length with::
|
||||
|
||||
class MyAuthorizationCodeGrant(AuthorizationCodeGrant):
|
||||
AUTHORIZATION_CODE_LENGTH = 32 # default is 48
|
||||
"""
|
||||
return generate_token(self.AUTHORIZATION_CODE_LENGTH)
|
||||
|
||||
def save_authorization_code(self, code, request):
|
||||
"""Save authorization_code for later use. Developers MUST implement
|
||||
it in subclass. Here is an example::
|
||||
|
||||
def save_authorization_code(self, code, request):
|
||||
client = request.client
|
||||
item = AuthorizationCode(
|
||||
code=code,
|
||||
client_id=client.client_id,
|
||||
redirect_uri=request.payload.redirect_uri,
|
||||
scope=request.scope,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
item.save()
|
||||
|
||||
.. note:: Use ``request.scope`` instead of ``request.payload.scope`` to get
|
||||
the resolved scope. Per RFC 6749 Section 3.3, if the client omits the
|
||||
scope parameter, the server uses a default value from
|
||||
``client.get_allowed_scope()``.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def query_authorization_code(self, code, client): # pragma: no cover
|
||||
"""Get authorization_code from previously savings. Developers MUST
|
||||
implement it in subclass::
|
||||
|
||||
def query_authorization_code(self, code, client):
|
||||
return Authorization.get(code=code, client_id=client.client_id)
|
||||
|
||||
:param code: a string represent the code.
|
||||
:param client: client related to this code.
|
||||
:return: authorization_code object
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_authorization_code(self, authorization_code):
|
||||
"""Delete authorization code from database or cache. Developers MUST
|
||||
implement it in subclass, e.g.::
|
||||
|
||||
def delete_authorization_code(self, authorization_code):
|
||||
authorization_code.delete()
|
||||
|
||||
:param authorization_code: the instance of authorization_code
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def authenticate_user(self, authorization_code):
|
||||
"""Authenticate the user related to this authorization_code. Developers
|
||||
MUST implement this method in subclass, e.g.::
|
||||
|
||||
def authenticate_user(self, authorization_code):
|
||||
return User.get(authorization_code.user_id)
|
||||
|
||||
:param authorization_code: AuthorizationCode object
|
||||
:return: user
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def validate_code_authorization_request(grant):
|
||||
request = grant.request
|
||||
client_id = request.payload.client_id
|
||||
log.debug("Validate authorization request of %r", client_id)
|
||||
|
||||
if client_id is None:
|
||||
raise InvalidClientError(
|
||||
description="Missing 'client_id' parameter.",
|
||||
)
|
||||
|
||||
client = grant.server.query_client(client_id)
|
||||
if not client:
|
||||
raise InvalidClientError(
|
||||
description="The client does not exist on this server.",
|
||||
)
|
||||
|
||||
redirect_uri = grant.validate_authorization_redirect_uri(request, client)
|
||||
response_type = request.payload.response_type
|
||||
if not client.check_response_type(response_type):
|
||||
raise UnauthorizedClientError(
|
||||
f"The client is not authorized to use 'response_type={response_type}'",
|
||||
redirect_uri=redirect_uri,
|
||||
)
|
||||
|
||||
grant.request.client = client
|
||||
|
||||
@hooked
|
||||
def validate_authorization_request_payload(grant, redirect_uri):
|
||||
grant.validate_requested_scope()
|
||||
scope = client.get_allowed_scope(request.payload.scope)
|
||||
if scope is None:
|
||||
raise InvalidScopeError()
|
||||
request.scope = scope
|
||||
|
||||
try:
|
||||
validate_authorization_request_payload(grant, redirect_uri)
|
||||
except OAuth2Error as error:
|
||||
error.redirect_uri = redirect_uri
|
||||
raise error
|
||||
return redirect_uri
|
||||
@@ -0,0 +1,158 @@
|
||||
from authlib.consts import default_json_headers
|
||||
|
||||
from ..errors import InvalidRequestError
|
||||
from ..hooks import Hookable
|
||||
from ..hooks import hooked
|
||||
from ..requests import OAuth2Request
|
||||
|
||||
|
||||
class BaseGrant(Hookable):
|
||||
#: Allowed client auth methods for token endpoint
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic"]
|
||||
|
||||
#: Designed for which "grant_type"
|
||||
GRANT_TYPE = None
|
||||
|
||||
# NOTE: there is no charset for application/json, since
|
||||
# application/json should always in UTF-8.
|
||||
# The example on RFC is incorrect.
|
||||
# https://tools.ietf.org/html/rfc4627
|
||||
TOKEN_RESPONSE_HEADER = default_json_headers
|
||||
|
||||
def __init__(self, request: OAuth2Request, server):
|
||||
super().__init__()
|
||||
self.prompt = None
|
||||
self.redirect_uri = None
|
||||
self.request = request
|
||||
self.server = server
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return self.request.client
|
||||
|
||||
def generate_token(
|
||||
self,
|
||||
user=None,
|
||||
scope=None,
|
||||
grant_type=None,
|
||||
expires_in=None,
|
||||
include_refresh_token=True,
|
||||
):
|
||||
if grant_type is None:
|
||||
grant_type = self.GRANT_TYPE
|
||||
return self.server.generate_token(
|
||||
client=self.request.client,
|
||||
grant_type=grant_type,
|
||||
user=user,
|
||||
scope=scope,
|
||||
expires_in=expires_in,
|
||||
include_refresh_token=include_refresh_token,
|
||||
)
|
||||
|
||||
def authenticate_token_endpoint_client(self):
|
||||
"""Authenticate client with the given methods for token endpoint.
|
||||
|
||||
For example, the client makes the following HTTP request using TLS:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /token HTTP/1.1
|
||||
Host: server.example.com
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
|
||||
|
||||
Default available methods are: "none", "client_secret_basic" and
|
||||
"client_secret_post".
|
||||
|
||||
:return: client
|
||||
"""
|
||||
client = self.server.authenticate_client(
|
||||
self.request, self.TOKEN_ENDPOINT_AUTH_METHODS
|
||||
)
|
||||
self.server.send_signal("after_authenticate_client", client=client, grant=self)
|
||||
return client
|
||||
|
||||
def save_token(self, token):
|
||||
"""A method to save token into database."""
|
||||
return self.server.save_token(token, self.request)
|
||||
|
||||
def validate_requested_scope(self):
|
||||
"""Validate if requested scope is supported by Authorization Server."""
|
||||
scope = self.request.payload.scope
|
||||
return self.server.validate_requested_scope(scope)
|
||||
|
||||
|
||||
class TokenEndpointMixin:
|
||||
#: Allowed HTTP methods of this token endpoint
|
||||
TOKEN_ENDPOINT_HTTP_METHODS = ["POST"]
|
||||
|
||||
#: Designed for which "grant_type"
|
||||
GRANT_TYPE = None
|
||||
|
||||
@classmethod
|
||||
def check_token_endpoint(cls, request: OAuth2Request):
|
||||
return (
|
||||
request.payload.grant_type == cls.GRANT_TYPE
|
||||
and request.method in cls.TOKEN_ENDPOINT_HTTP_METHODS
|
||||
)
|
||||
|
||||
def validate_token_request(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_token_response(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AuthorizationEndpointMixin:
|
||||
RESPONSE_TYPES = set()
|
||||
ERROR_RESPONSE_FRAGMENT = False
|
||||
|
||||
@classmethod
|
||||
def check_authorization_endpoint(cls, request: OAuth2Request):
|
||||
return request.payload.response_type in cls.RESPONSE_TYPES
|
||||
|
||||
@staticmethod
|
||||
def validate_authorization_redirect_uri(request: OAuth2Request, client):
|
||||
if request.payload.redirect_uri:
|
||||
if not client.check_redirect_uri(request.payload.redirect_uri):
|
||||
raise InvalidRequestError(
|
||||
f"Redirect URI {request.payload.redirect_uri} is not supported by client.",
|
||||
)
|
||||
return request.payload.redirect_uri
|
||||
else:
|
||||
redirect_uri = client.get_default_redirect_uri()
|
||||
if not redirect_uri:
|
||||
raise InvalidRequestError(
|
||||
"Missing 'redirect_uri' in request.", state=request.payload.state
|
||||
)
|
||||
return redirect_uri
|
||||
|
||||
@staticmethod
|
||||
def validate_no_multiple_request_parameter(request: OAuth2Request):
|
||||
"""For the Authorization Endpoint, request and response parameters MUST NOT be included
|
||||
more than once. Per `Section 3.1`_.
|
||||
|
||||
.. _`Section 3.1`: https://tools.ietf.org/html/rfc6749#section-3.1
|
||||
"""
|
||||
datalist = request.payload.datalist
|
||||
parameters = ["response_type", "client_id", "redirect_uri", "scope", "state"]
|
||||
for param in parameters:
|
||||
if len(datalist.get(param, [])) > 1:
|
||||
raise InvalidRequestError(
|
||||
f"Multiple '{param}' in request.", state=request.payload.state
|
||||
)
|
||||
|
||||
@hooked
|
||||
def validate_consent_request(self):
|
||||
redirect_uri = self.validate_authorization_request()
|
||||
self.redirect_uri = redirect_uri
|
||||
return redirect_uri
|
||||
|
||||
def validate_authorization_request(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_authorization_response(self, redirect_uri: str, grant_user):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,109 @@
|
||||
import logging
|
||||
|
||||
from ..errors import UnauthorizedClientError
|
||||
from ..hooks import hooked
|
||||
from .base import BaseGrant
|
||||
from .base import TokenEndpointMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientCredentialsGrant(BaseGrant, TokenEndpointMixin):
|
||||
"""The client can request an access token using only its client
|
||||
credentials (or other supported means of authentication) when the
|
||||
client is requesting access to the protected resources under its
|
||||
control, or those of another resource owner that have been previously
|
||||
arranged with the authorization server.
|
||||
|
||||
The client credentials grant type MUST only be used by confidential
|
||||
clients::
|
||||
|
||||
+---------+ +---------------+
|
||||
| | | |
|
||||
| |>--(A)- Client Authentication --->| Authorization |
|
||||
| Client | | Server |
|
||||
| |<--(B)---- Access Token ---------<| |
|
||||
| | | |
|
||||
+---------+ +---------------+
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-4.4
|
||||
"""
|
||||
|
||||
GRANT_TYPE = "client_credentials"
|
||||
|
||||
def validate_token_request(self):
|
||||
"""The client makes a request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format per Appendix B with a character encoding of UTF-8 in the HTTP
|
||||
request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "client_credentials".
|
||||
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3.
|
||||
|
||||
The client MUST authenticate with the authorization server as
|
||||
described in Section 3.2.1.
|
||||
|
||||
For example, the client makes the following HTTP request using
|
||||
transport-layer security (with extra line breaks for display purposes
|
||||
only):
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /token HTTP/1.1
|
||||
Host: server.example.com
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=client_credentials
|
||||
|
||||
The authorization server MUST authenticate the client.
|
||||
"""
|
||||
# ignore validate for grant_type, since it is validated by
|
||||
# check_token_endpoint
|
||||
client = self.authenticate_token_endpoint_client()
|
||||
log.debug("Validate token request of %r", 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()
|
||||
|
||||
@hooked
|
||||
def create_token_response(self):
|
||||
"""If the access token request is valid and authorized, the
|
||||
authorization server issues an access token as described in
|
||||
Section 5.1. A refresh token SHOULD NOT be included. If the request
|
||||
failed client authentication or is invalid, the authorization server
|
||||
returns an error response as described in Section 5.2.
|
||||
|
||||
An example successful response:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Cache-Control: no-store
|
||||
Pragma: no-cache
|
||||
|
||||
{
|
||||
"access_token":"2YotnFZFEjr1zCsicMWpAA",
|
||||
"token_type":"example",
|
||||
"expires_in":3600,
|
||||
"example_parameter":"example_value"
|
||||
}
|
||||
|
||||
:returns: (status_code, body, headers)
|
||||
"""
|
||||
token = self.generate_token(
|
||||
scope=self.request.payload.scope, include_refresh_token=False
|
||||
)
|
||||
log.debug("Issue token %r to %r", token, self.client)
|
||||
self.save_token(token)
|
||||
return 200, token, self.TOKEN_RESPONSE_HEADER
|
||||
@@ -0,0 +1,230 @@
|
||||
import logging
|
||||
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
|
||||
from ..errors import AccessDeniedError
|
||||
from ..errors import InvalidScopeError
|
||||
from ..errors import OAuth2Error
|
||||
from ..errors import UnauthorizedClientError
|
||||
from ..hooks import hooked
|
||||
from .base import AuthorizationEndpointMixin
|
||||
from .base import BaseGrant
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImplicitGrant(BaseGrant, AuthorizationEndpointMixin):
|
||||
"""The implicit grant type is used to obtain access tokens (it does not
|
||||
support the issuance of refresh tokens) and is optimized for public
|
||||
clients known to operate a particular redirection URI. These clients
|
||||
are typically implemented in a browser using a scripting language
|
||||
such as JavaScript.
|
||||
|
||||
Since this is a redirection-based flow, the client must be capable of
|
||||
interacting with the resource owner's user-agent (typically a web
|
||||
browser) and capable of receiving incoming requests (via redirection)
|
||||
from the authorization server.
|
||||
|
||||
Unlike the authorization code grant type, in which the client makes
|
||||
separate requests for authorization and for an access token, the
|
||||
client receives the access token as the result of the authorization
|
||||
request.
|
||||
|
||||
The implicit grant type does not include client authentication, and
|
||||
relies on the presence of the resource owner and the registration of
|
||||
the redirection URI. Because the access token is encoded into the
|
||||
redirection URI, it may be exposed to the resource owner and other
|
||||
applications residing on the same device::
|
||||
|
||||
+----------+
|
||||
| Resource |
|
||||
| Owner |
|
||||
| |
|
||||
+----------+
|
||||
^
|
||||
|
|
||||
(B)
|
||||
+----|-----+ Client Identifier +---------------+
|
||||
| -+----(A)-- & Redirection URI --->| |
|
||||
| User- | | Authorization |
|
||||
| Agent -|----(B)-- User authenticates -->| Server |
|
||||
| | | |
|
||||
| |<---(C)--- Redirection URI ----<| |
|
||||
| | with Access Token +---------------+
|
||||
| | in Fragment
|
||||
| | +---------------+
|
||||
| |----(D)--- Redirection URI ---->| Web-Hosted |
|
||||
| | without Fragment | Client |
|
||||
| | | Resource |
|
||||
| (F) |<---(E)------- Script ---------<| |
|
||||
| | +---------------+
|
||||
+-|--------+
|
||||
| |
|
||||
(A) (G) Access Token
|
||||
| |
|
||||
^ v
|
||||
+---------+
|
||||
| |
|
||||
| Client |
|
||||
| |
|
||||
+---------+
|
||||
"""
|
||||
|
||||
#: authorization_code grant type has authorization endpoint
|
||||
AUTHORIZATION_ENDPOINT = True
|
||||
#: Allowed client auth methods for token endpoint
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = ["none"]
|
||||
|
||||
RESPONSE_TYPES = {"token"}
|
||||
GRANT_TYPE = "implicit"
|
||||
ERROR_RESPONSE_FRAGMENT = True
|
||||
|
||||
@hooked
|
||||
def validate_authorization_request(self):
|
||||
"""The client constructs the request URI by adding the following
|
||||
parameters to the query component of the authorization endpoint URI
|
||||
using the "application/x-www-form-urlencoded" format.
|
||||
Per `Section 4.2.1`_.
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "token".
|
||||
|
||||
client_id
|
||||
REQUIRED. The client identifier as described in Section 2.2.
|
||||
|
||||
redirect_uri
|
||||
OPTIONAL. As described in Section 3.1.2.
|
||||
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3.
|
||||
|
||||
state
|
||||
RECOMMENDED. An opaque value used by the client to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this value when redirecting the user-agent back
|
||||
to the client. The parameter SHOULD be used for preventing
|
||||
cross-site request forgery as described in Section 10.12.
|
||||
|
||||
The client directs the resource owner to the constructed URI using an
|
||||
HTTP redirection response, or by other means available to it via the
|
||||
user-agent.
|
||||
|
||||
For example, the client directs the user-agent to make the following
|
||||
HTTP request using TLS:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
|
||||
Host: server.example.com
|
||||
|
||||
.. _`Section 4.2.1`: https://tools.ietf.org/html/rfc6749#section-4.2.1
|
||||
"""
|
||||
# ignore validate for response_type, since it is validated by
|
||||
# check_authorization_endpoint
|
||||
|
||||
# The implicit grant type is optimized for public clients
|
||||
client = self.authenticate_token_endpoint_client()
|
||||
log.debug("Validate authorization request of %r", client)
|
||||
|
||||
redirect_uri = self.validate_authorization_redirect_uri(self.request, client)
|
||||
|
||||
response_type = self.request.payload.response_type
|
||||
if not client.check_response_type(response_type):
|
||||
raise UnauthorizedClientError(
|
||||
f"The client is not authorized to use 'response_type={response_type}'",
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=True,
|
||||
)
|
||||
|
||||
try:
|
||||
self.request.client = client
|
||||
self.validate_requested_scope()
|
||||
scope = client.get_allowed_scope(self.request.payload.scope)
|
||||
if scope is None:
|
||||
raise InvalidScopeError()
|
||||
self.request.scope = scope
|
||||
except OAuth2Error as error:
|
||||
error.redirect_uri = redirect_uri
|
||||
error.redirect_fragment = True
|
||||
raise error
|
||||
return redirect_uri
|
||||
|
||||
@hooked
|
||||
def create_authorization_response(self, redirect_uri, grant_user):
|
||||
"""If the resource owner grants the access request, the authorization
|
||||
server issues an access token and delivers it to the client by adding
|
||||
the following parameters to the fragment component of the redirection
|
||||
URI using the "application/x-www-form-urlencoded" format.
|
||||
Per `Section 4.2.2`_.
|
||||
|
||||
access_token
|
||||
REQUIRED. The access token issued by the authorization server.
|
||||
|
||||
token_type
|
||||
REQUIRED. The type of the token issued as described in
|
||||
Section 7.1. Value is case insensitive.
|
||||
|
||||
expires_in
|
||||
RECOMMENDED. The lifetime in seconds of the access token. For
|
||||
example, the value "3600" denotes that the access token will
|
||||
expire in one hour from the time the response was generated.
|
||||
If omitted, the authorization server SHOULD provide the
|
||||
expiration time via other means or document the default value.
|
||||
|
||||
scope
|
||||
OPTIONAL, if identical to the scope requested by the client;
|
||||
otherwise, REQUIRED. The scope of the access token as
|
||||
described by Section 3.3.
|
||||
|
||||
state
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
The authorization server MUST NOT issue a refresh token.
|
||||
|
||||
For example, the authorization server redirects the user-agent by
|
||||
sending the following HTTP response:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 302 Found
|
||||
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
|
||||
&state=xyz&token_type=example&expires_in=3600
|
||||
|
||||
Developers should note that some user-agents do not support the
|
||||
inclusion of a fragment component in the HTTP "Location" response
|
||||
header field. Such clients will require using other methods for
|
||||
redirecting the client than a 3xx redirection response -- for
|
||||
example, returning an HTML page that includes a 'continue' button
|
||||
with an action linked to the redirection URI.
|
||||
|
||||
.. _`Section 4.2.2`: https://tools.ietf.org/html/rfc6749#section-4.2.2
|
||||
|
||||
:param redirect_uri: Redirect to the given URI for the authorization
|
||||
:param grant_user: if resource owner granted the request, pass this
|
||||
resource owner, otherwise pass None.
|
||||
:returns: (status_code, body, headers)
|
||||
"""
|
||||
state = self.request.payload.state
|
||||
if grant_user:
|
||||
self.request.user = grant_user
|
||||
token = self.generate_token(
|
||||
user=grant_user,
|
||||
scope=self.request.scope,
|
||||
include_refresh_token=False,
|
||||
)
|
||||
log.debug("Grant token %r to %r", token, self.request.client)
|
||||
|
||||
self.save_token(token)
|
||||
params = [(k, token[k]) for k in token]
|
||||
if state:
|
||||
params.append(("state", state))
|
||||
|
||||
uri = add_params_to_uri(redirect_uri, params, fragment=True)
|
||||
headers = [("Location", uri)]
|
||||
return 302, "", headers
|
||||
else:
|
||||
raise AccessDeniedError(redirect_uri=redirect_uri, redirect_fragment=True)
|
||||
@@ -0,0 +1,183 @@
|
||||
"""authlib.oauth2.rfc6749.grants.refresh_token.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A special grant endpoint for refresh_token grant_type. Refreshing an
|
||||
Access Token per `Section 6`_.
|
||||
|
||||
.. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ..errors import InvalidGrantError
|
||||
from ..errors import InvalidRequestError
|
||||
from ..errors import InvalidScopeError
|
||||
from ..errors import UnauthorizedClientError
|
||||
from ..hooks import hooked
|
||||
from ..util import scope_to_list
|
||||
from .base import BaseGrant
|
||||
from .base import TokenEndpointMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RefreshTokenGrant(BaseGrant, TokenEndpointMixin):
|
||||
"""A special grant endpoint for refresh_token grant_type. Refreshing an
|
||||
Access Token per `Section 6`_.
|
||||
|
||||
.. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
|
||||
GRANT_TYPE = "refresh_token"
|
||||
|
||||
#: The authorization server MAY issue a new refresh token
|
||||
INCLUDE_NEW_REFRESH_TOKEN = False
|
||||
|
||||
def _validate_request_client(self):
|
||||
# require client authentication for confidential clients or for any
|
||||
# client that was issued client credentials (or with other
|
||||
# authentication requirements)
|
||||
client = self.authenticate_token_endpoint_client()
|
||||
log.debug("Validate token request of %r", 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}'"
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
def _validate_request_token(self, client):
|
||||
refresh_token = self.request.form.get("refresh_token")
|
||||
if refresh_token is None:
|
||||
raise InvalidRequestError("Missing 'refresh_token' in request.")
|
||||
|
||||
token = self.authenticate_refresh_token(refresh_token)
|
||||
if not token or not token.check_client(client):
|
||||
raise InvalidGrantError()
|
||||
return token
|
||||
|
||||
def _validate_token_scope(self, token):
|
||||
scope = self.request.payload.scope
|
||||
if not scope:
|
||||
return
|
||||
|
||||
original_scope = token.get_scope()
|
||||
if not original_scope:
|
||||
raise InvalidScopeError()
|
||||
|
||||
original_scope = set(scope_to_list(original_scope))
|
||||
if not original_scope.issuperset(set(scope_to_list(scope))):
|
||||
raise InvalidScopeError()
|
||||
|
||||
def validate_token_request(self):
|
||||
"""If the authorization server issued a refresh token to the client, the
|
||||
client makes a refresh request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format per Appendix B with a character encoding of UTF-8 in the HTTP
|
||||
request entity-body, per Section 6:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "refresh_token".
|
||||
|
||||
refresh_token
|
||||
REQUIRED. The refresh token issued to the client.
|
||||
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3. The requested scope MUST NOT include any scope
|
||||
not originally granted by the resource owner, and if omitted is
|
||||
treated as equal to the scope originally granted by the
|
||||
resource owner.
|
||||
|
||||
|
||||
For example, the client makes the following HTTP request using
|
||||
transport-layer security (with extra line breaks for display purposes
|
||||
only):
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /token HTTP/1.1
|
||||
Host: server.example.com
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
|
||||
"""
|
||||
client = self._validate_request_client()
|
||||
self.request.client = client
|
||||
refresh_token = self._validate_request_token(client)
|
||||
self._validate_token_scope(refresh_token)
|
||||
self.request.refresh_token = refresh_token
|
||||
|
||||
@hooked
|
||||
def create_token_response(self):
|
||||
"""If valid and authorized, the authorization server issues an access
|
||||
token as described in Section 5.1. If the request failed
|
||||
verification or is invalid, the authorization server returns an error
|
||||
response as described in Section 5.2.
|
||||
"""
|
||||
refresh_token = self.request.refresh_token
|
||||
user = self.authenticate_user(refresh_token)
|
||||
if not user:
|
||||
raise InvalidRequestError("There is no 'user' for this token.")
|
||||
|
||||
client = self.request.client
|
||||
token = self.issue_token(user, refresh_token)
|
||||
log.debug("Issue token %r to %r", token, client)
|
||||
|
||||
self.request.user = user
|
||||
self.save_token(token)
|
||||
self.revoke_old_credential(refresh_token)
|
||||
return 200, token, self.TOKEN_RESPONSE_HEADER
|
||||
|
||||
def issue_token(self, user, refresh_token):
|
||||
scope = self.request.payload.scope
|
||||
if not scope:
|
||||
scope = refresh_token.get_scope()
|
||||
|
||||
token = self.generate_token(
|
||||
user=user,
|
||||
scope=scope,
|
||||
include_refresh_token=self.INCLUDE_NEW_REFRESH_TOKEN,
|
||||
)
|
||||
return token
|
||||
|
||||
def authenticate_refresh_token(self, refresh_token):
|
||||
"""Get token information with refresh_token string. Developers MUST
|
||||
implement this method in subclass::
|
||||
|
||||
def authenticate_refresh_token(self, refresh_token):
|
||||
token = Token.get(refresh_token=refresh_token)
|
||||
if token and not token.refresh_token_revoked:
|
||||
return token
|
||||
|
||||
:param refresh_token: The refresh token issued to the client
|
||||
:return: token
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def authenticate_user(self, refresh_token):
|
||||
"""Authenticate the user related to this credential. Developers MUST
|
||||
implement this method in subclass::
|
||||
|
||||
def authenticate_user(self, credential):
|
||||
return User.get(credential.user_id)
|
||||
|
||||
:param refresh_token: Token object
|
||||
:return: user
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def revoke_old_credential(self, refresh_token):
|
||||
"""The authorization server MAY revoke the old refresh token after
|
||||
issuing a new refresh token to the client. Developers MUST implement
|
||||
this method in subclass::
|
||||
|
||||
def revoke_old_credential(self, refresh_token):
|
||||
credential.revoked = True
|
||||
credential.save()
|
||||
|
||||
:param refresh_token: Token object
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import logging
|
||||
|
||||
from ..errors import InvalidRequestError
|
||||
from ..errors import UnauthorizedClientError
|
||||
from ..hooks import hooked
|
||||
from .base import BaseGrant
|
||||
from .base import TokenEndpointMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResourceOwnerPasswordCredentialsGrant(BaseGrant, TokenEndpointMixin):
|
||||
"""The resource owner password credentials grant type is suitable in
|
||||
cases where the resource owner has a trust relationship with the
|
||||
client, such as the device operating system or a highly privileged.
|
||||
|
||||
application. The authorization server should take special care when
|
||||
enabling this grant type and only allow it when other flows are not
|
||||
viable.
|
||||
|
||||
This grant type is suitable for clients capable of obtaining the
|
||||
resource owner's credentials (username and password, typically using
|
||||
an interactive form). It is also used to migrate existing clients
|
||||
using direct authentication schemes such as HTTP Basic or Digest
|
||||
authentication to OAuth by converting the stored credentials to an
|
||||
access token::
|
||||
|
||||
+----------+
|
||||
| Resource |
|
||||
| Owner |
|
||||
| |
|
||||
+----------+
|
||||
v
|
||||
| Resource Owner
|
||||
(A) Password Credentials
|
||||
|
|
||||
v
|
||||
+---------+ +---------------+
|
||||
| |>--(B)---- Resource Owner ------->| |
|
||||
| | Password Credentials | Authorization |
|
||||
| Client | | Server |
|
||||
| |<--(C)---- Access Token ---------<| |
|
||||
| | (w/ Optional Refresh Token) | |
|
||||
+---------+ +---------------+
|
||||
"""
|
||||
|
||||
GRANT_TYPE = "password"
|
||||
|
||||
def validate_token_request(self):
|
||||
"""The client makes a request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format per Appendix B with a character encoding of UTF-8 in the HTTP
|
||||
request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "password".
|
||||
|
||||
username
|
||||
REQUIRED. The resource owner username.
|
||||
|
||||
password
|
||||
REQUIRED. The resource owner password.
|
||||
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3.
|
||||
|
||||
If the client type is confidential or the client was issued client
|
||||
credentials (or assigned other authentication requirements), the
|
||||
client MUST authenticate with the authorization server as described
|
||||
in Section 3.2.1.
|
||||
|
||||
For example, the client makes the following HTTP request using
|
||||
transport-layer security (with extra line breaks for display purposes
|
||||
only):
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /token HTTP/1.1
|
||||
Host: server.example.com
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=password&username=johndoe&password=A3ddj3w
|
||||
"""
|
||||
# ignore validate for grant_type, since it is validated by
|
||||
# check_token_endpoint
|
||||
client = self.authenticate_token_endpoint_client()
|
||||
log.debug("Validate token request of %r", 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}'"
|
||||
)
|
||||
|
||||
params = self.request.form
|
||||
if "username" not in params:
|
||||
raise InvalidRequestError("Missing 'username' in request.")
|
||||
if "password" not in params:
|
||||
raise InvalidRequestError("Missing 'password' in request.")
|
||||
|
||||
log.debug("Authenticate user of %r", params["username"])
|
||||
user = self.authenticate_user(params["username"], params["password"])
|
||||
if not user:
|
||||
raise InvalidRequestError(
|
||||
"Invalid 'username' or 'password' in request.",
|
||||
)
|
||||
self.request.client = client
|
||||
self.request.user = user
|
||||
self.validate_requested_scope()
|
||||
|
||||
@hooked
|
||||
def create_token_response(self):
|
||||
"""If the access token request is valid and authorized, the
|
||||
authorization server issues an access token and optional refresh
|
||||
token as described in Section 5.1. If the request failed client
|
||||
authentication or is invalid, the authorization server returns an
|
||||
error response as described in Section 5.2.
|
||||
|
||||
An example successful response:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Cache-Control: no-store
|
||||
Pragma: no-cache
|
||||
|
||||
{
|
||||
"access_token":"2YotnFZFEjr1zCsicMWpAA",
|
||||
"token_type":"example",
|
||||
"expires_in":3600,
|
||||
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
|
||||
"example_parameter":"example_value"
|
||||
}
|
||||
|
||||
:returns: (status_code, body, headers)
|
||||
"""
|
||||
user = self.request.user
|
||||
scope = self.request.payload.scope
|
||||
token = self.generate_token(user=user, scope=scope)
|
||||
log.debug("Issue token %r to %r", token, self.client)
|
||||
self.save_token(token)
|
||||
return 200, token, self.TOKEN_RESPONSE_HEADER
|
||||
|
||||
def authenticate_user(self, username, password):
|
||||
"""Validate the resource owner password credentials using its
|
||||
existing password validation algorithm::
|
||||
|
||||
def authenticate_user(self, username, password):
|
||||
user = get_user_by_username(username)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,37 @@
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class Hookable:
|
||||
_hooks = None
|
||||
|
||||
def __init__(self):
|
||||
self._hooks = defaultdict(set)
|
||||
|
||||
def register_hook(self, hook_type, hook):
|
||||
self._hooks[hook_type].add(hook)
|
||||
|
||||
def execute_hook(self, hook_type, *args, **kwargs):
|
||||
for hook in self._hooks[hook_type]:
|
||||
hook(self, *args, **kwargs)
|
||||
|
||||
|
||||
def hooked(func=None, before=None, after=None):
|
||||
"""Execute hooks before and after the decorated method."""
|
||||
|
||||
def decorator(func):
|
||||
before_name = before or f"before_{func.__name__}"
|
||||
after_name = after or f"after_{func.__name__}"
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
self.execute_hook(before_name, *args, **kwargs)
|
||||
result = func(self, *args, **kwargs)
|
||||
self.execute_hook(after_name, result)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
# The decorator has been called without parenthesis
|
||||
if callable(func):
|
||||
return decorator(func)
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,243 @@
|
||||
"""authlib.oauth2.rfc6749.models.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module defines how to construct Client, AuthorizationCode and Token.
|
||||
"""
|
||||
|
||||
|
||||
class ClientMixin:
|
||||
"""Implementation of OAuth 2 Client described in `Section 2`_ with
|
||||
some methods to help validation. A client has at least these information:
|
||||
|
||||
* client_id: A string represents client identifier.
|
||||
* client_secret: A string represents client password.
|
||||
* token_endpoint_auth_method: A way to authenticate client at token
|
||||
endpoint.
|
||||
|
||||
.. _`Section 2`: https://tools.ietf.org/html/rfc6749#section-2
|
||||
"""
|
||||
|
||||
def get_client_id(self):
|
||||
"""A method to return client_id of the client. For instance, the value
|
||||
in database is saved in a column called ``client_id``::
|
||||
|
||||
def get_client_id(self):
|
||||
return self.client_id
|
||||
|
||||
:return: string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_default_redirect_uri(self):
|
||||
"""A method to get client default redirect_uri. For instance, the
|
||||
database table for client has a column called ``default_redirect_uri``::
|
||||
|
||||
def get_default_redirect_uri(self):
|
||||
return self.default_redirect_uri
|
||||
|
||||
:return: A URL string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_allowed_scope(self, scope):
|
||||
"""A method to return a list of requested scopes which are supported by
|
||||
this client. For instance, there is a ``scope`` column::
|
||||
|
||||
def get_allowed_scope(self, scope):
|
||||
if not scope:
|
||||
return ""
|
||||
allowed = set(scope_to_list(self.scope))
|
||||
return list_to_scope([s for s in scope.split() if s in allowed])
|
||||
|
||||
:param scope: the requested scope.
|
||||
:return: string of scope
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_redirect_uri(self, redirect_uri):
|
||||
"""Validate redirect_uri parameter in Authorization Endpoints. For
|
||||
instance, in the client table, there is an ``allowed_redirect_uris``
|
||||
column::
|
||||
|
||||
def check_redirect_uri(self, redirect_uri):
|
||||
return redirect_uri in self.allowed_redirect_uris
|
||||
|
||||
:param redirect_uri: A URL string for redirecting.
|
||||
:return: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_client_secret(self, client_secret):
|
||||
"""Check client_secret matching with the client. For instance, in
|
||||
the client table, the column is called ``client_secret``::
|
||||
|
||||
import secrets
|
||||
|
||||
|
||||
def check_client_secret(self, client_secret):
|
||||
return secrets.compare_digest(self.client_secret, client_secret)
|
||||
|
||||
:param client_secret: A string of client secret
|
||||
:return: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_endpoint_auth_method(self, method, endpoint):
|
||||
"""Check if client support the given method for the given endpoint.
|
||||
There is a ``token_endpoint_auth_method`` defined via `RFC7591`_.
|
||||
Developers MAY re-implement this method with::
|
||||
|
||||
def check_endpoint_auth_method(self, method, endpoint):
|
||||
if endpoint == "token":
|
||||
# if client table has ``token_endpoint_auth_method``
|
||||
return self.token_endpoint_auth_method == method
|
||||
return True
|
||||
|
||||
Method values defined by this specification are:
|
||||
|
||||
* "none": The client is a public client as defined in OAuth 2.0,
|
||||
and does not have a client secret.
|
||||
|
||||
* "client_secret_post": The client uses the HTTP POST parameters
|
||||
as defined in OAuth 2.0
|
||||
|
||||
* "client_secret_basic": The client uses HTTP Basic as defined in
|
||||
OAuth 2.0
|
||||
|
||||
.. _`RFC7591`: https://tools.ietf.org/html/rfc7591
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_response_type(self, response_type):
|
||||
"""Validate if the client can handle the given response_type. There
|
||||
are two response types defined by RFC6749: code and token. For
|
||||
instance, there is a ``allowed_response_types`` column in your client::
|
||||
|
||||
def check_response_type(self, response_type):
|
||||
return response_type in self.response_types
|
||||
|
||||
:param response_type: the requested response_type string.
|
||||
:return: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_grant_type(self, grant_type):
|
||||
"""Validate if the client can handle the given grant_type. There are
|
||||
four grant types defined by RFC6749:
|
||||
|
||||
* authorization_code
|
||||
* implicit
|
||||
* client_credentials
|
||||
* password
|
||||
|
||||
For instance, there is a ``allowed_grant_types`` column in your client::
|
||||
|
||||
def check_grant_type(self, grant_type):
|
||||
return grant_type in self.grant_types
|
||||
|
||||
:param grant_type: the requested grant_type string.
|
||||
:return: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AuthorizationCodeMixin:
|
||||
def get_redirect_uri(self):
|
||||
"""A method to get authorization code's ``redirect_uri``.
|
||||
For instance, the database table for authorization code has a
|
||||
column called ``redirect_uri``::
|
||||
|
||||
def get_redirect_uri(self):
|
||||
return self.redirect_uri
|
||||
|
||||
:return: A URL string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_scope(self):
|
||||
"""A method to get scope of the authorization code. For instance,
|
||||
the column is called ``scope``::
|
||||
|
||||
def get_scope(self):
|
||||
return self.scope
|
||||
|
||||
:return: scope string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class TokenMixin:
|
||||
def check_client(self, client):
|
||||
"""A method to check if this token is issued to the given client.
|
||||
For instance, ``client_id`` is saved on token table::
|
||||
|
||||
def check_client(self, client):
|
||||
return self.client_id == client.client_id
|
||||
|
||||
:return: bool
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_scope(self):
|
||||
"""A method to get scope of the authorization code. For instance,
|
||||
the column is called ``scope``::
|
||||
|
||||
def get_scope(self):
|
||||
return self.scope
|
||||
|
||||
:return: scope string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_expires_in(self):
|
||||
"""A method to get the ``expires_in`` value of the token. e.g.
|
||||
the column is called ``expires_in``::
|
||||
|
||||
def get_expires_in(self):
|
||||
return self.expires_in
|
||||
|
||||
:return: timestamp int
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_expired(self):
|
||||
"""A method to define if this token is expired. For instance,
|
||||
there is a column ``expired_at`` in the table::
|
||||
|
||||
def is_expired(self):
|
||||
return self.expired_at < now
|
||||
|
||||
:return: boolean
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_revoked(self):
|
||||
"""A method to define if this token is revoked. For instance,
|
||||
there is a boolean column ``revoked`` in the table::
|
||||
|
||||
def is_revoked(self):
|
||||
return self.revoked
|
||||
|
||||
:return: boolean
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_user(self):
|
||||
"""A method to get the user object associated with this token:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def get_user(self):
|
||||
return User.get(self.user_id)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_client(self) -> ClientMixin:
|
||||
"""A method to get the client object associated with this token:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def get_client(self):
|
||||
return Client.get(self.client_id)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,214 @@
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.urls import add_params_to_qs
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
from authlib.common.urls import urlparse
|
||||
|
||||
from .errors import MismatchingStateException
|
||||
from .errors import MissingCodeException
|
||||
from .errors import MissingTokenException
|
||||
from .errors import MissingTokenTypeException
|
||||
from .util import list_to_scope
|
||||
|
||||
|
||||
def prepare_grant_uri(
|
||||
uri, client_id, response_type, redirect_uri=None, scope=None, state=None, **kwargs
|
||||
):
|
||||
"""Prepare the authorization grant request URI.
|
||||
|
||||
The client constructs the request URI by adding the following
|
||||
parameters to the query component of the authorization endpoint URI
|
||||
using the ``application/x-www-form-urlencoded`` format:
|
||||
|
||||
:param uri: The authorize endpoint to fetch "code" or "token".
|
||||
:param client_id: The client identifier as described in `Section 2.2`_.
|
||||
:param response_type: To indicate which OAuth 2 grant/flow is required,
|
||||
"code" and "token".
|
||||
:param redirect_uri: The client provided URI to redirect back to after
|
||||
authorization as described in `Section 3.1.2`_.
|
||||
:param scope: The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
:param state: An opaque value used by the client to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this value when redirecting the user-agent
|
||||
back to the client. The parameter SHOULD be used for
|
||||
preventing cross-site request forgery as described in
|
||||
`Section 10.12`_.
|
||||
:param kwargs: Extra arguments to embed in the grant/authorization URL.
|
||||
|
||||
An example of an authorization code grant authorization URL::
|
||||
|
||||
/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
|
||||
|
||||
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
|
||||
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
.. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
|
||||
"""
|
||||
params = [("response_type", response_type), ("client_id", client_id)]
|
||||
|
||||
if redirect_uri:
|
||||
params.append(("redirect_uri", redirect_uri))
|
||||
if scope:
|
||||
params.append(("scope", list_to_scope(scope)))
|
||||
if state:
|
||||
params.append(("state", state))
|
||||
|
||||
for k, value in kwargs.items():
|
||||
if value is not None:
|
||||
if isinstance(value, (list, tuple)):
|
||||
for v in value:
|
||||
if v is not None:
|
||||
params.append((to_unicode(k), v))
|
||||
else:
|
||||
params.append((to_unicode(k), value))
|
||||
|
||||
return add_params_to_uri(uri, params)
|
||||
|
||||
|
||||
def prepare_token_request(grant_type, body="", redirect_uri=None, **kwargs):
|
||||
"""Prepare the access token request. Per `Section 4.1.3`_.
|
||||
|
||||
The client makes a request to the token endpoint by adding the
|
||||
following parameters using the ``application/x-www-form-urlencoded``
|
||||
format in the HTTP request entity-body:
|
||||
|
||||
:param grant_type: To indicate grant type being used, i.e. "password",
|
||||
"authorization_code" or "client_credentials".
|
||||
:param body: Existing request body to embed parameters in.
|
||||
:param redirect_uri: If the "redirect_uri" parameter was included in the
|
||||
authorization request as described in
|
||||
`Section 4.1.1`_, and their values MUST be identical.
|
||||
:param kwargs: Extra arguments to embed in the request body.
|
||||
|
||||
An example of an authorization code token request body::
|
||||
|
||||
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
|
||||
|
||||
.. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
|
||||
.. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
|
||||
"""
|
||||
params = [("grant_type", grant_type)]
|
||||
|
||||
if redirect_uri:
|
||||
params.append(("redirect_uri", redirect_uri))
|
||||
|
||||
if "scope" in kwargs:
|
||||
kwargs["scope"] = list_to_scope(kwargs["scope"])
|
||||
|
||||
if grant_type == "authorization_code" and kwargs.get("code") is None:
|
||||
raise MissingCodeException()
|
||||
|
||||
for k in kwargs:
|
||||
if kwargs[k]:
|
||||
params.append((to_unicode(k), kwargs[k]))
|
||||
|
||||
return add_params_to_qs(body, params)
|
||||
|
||||
|
||||
def parse_authorization_code_response(uri, state=None):
|
||||
"""Parse authorization grant response URI into a dict.
|
||||
|
||||
If the resource owner grants the access request, the authorization
|
||||
server issues an authorization code and delivers it to the client by
|
||||
adding the following parameters to the query component of the
|
||||
redirection URI using the ``application/x-www-form-urlencoded`` format:
|
||||
|
||||
**code**
|
||||
REQUIRED. The authorization code generated by the
|
||||
authorization server. The authorization code MUST expire
|
||||
shortly after it is issued to mitigate the risk of leaks. A
|
||||
maximum authorization code lifetime of 10 minutes is
|
||||
RECOMMENDED. The client MUST NOT use the authorization code
|
||||
more than once. If an authorization code is used more than
|
||||
once, the authorization server MUST deny the request and SHOULD
|
||||
revoke (when possible) all tokens previously issued based on
|
||||
that authorization code. The authorization code is bound to
|
||||
the client identifier and redirection URI.
|
||||
|
||||
**state**
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
:param uri: The full redirect URL back to the client.
|
||||
:param state: The state parameter from the authorization request.
|
||||
|
||||
For example, the authorization server redirects the user-agent by
|
||||
sending the following HTTP response:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 302 Found
|
||||
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&state=xyz
|
||||
|
||||
"""
|
||||
query = urlparse.urlparse(uri).query
|
||||
params = dict(urlparse.parse_qsl(query))
|
||||
|
||||
if "code" not in params:
|
||||
raise MissingCodeException()
|
||||
|
||||
params_state = params.get("state")
|
||||
if state and params_state != state:
|
||||
raise MismatchingStateException()
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_implicit_response(uri, state=None):
|
||||
"""Parse the implicit token response URI into a dict.
|
||||
|
||||
If the resource owner grants the access request, the authorization
|
||||
server issues an access token and delivers it to the client by adding
|
||||
the following parameters to the fragment component of the redirection
|
||||
URI using the ``application/x-www-form-urlencoded`` format:
|
||||
|
||||
**access_token**
|
||||
REQUIRED. The access token issued by the authorization server.
|
||||
|
||||
**token_type**
|
||||
REQUIRED. The type of the token issued as described in
|
||||
Section 7.1. Value is case insensitive.
|
||||
|
||||
**expires_in**
|
||||
RECOMMENDED. The lifetime in seconds of the access token. For
|
||||
example, the value "3600" denotes that the access token will
|
||||
expire in one hour from the time the response was generated.
|
||||
If omitted, the authorization server SHOULD provide the
|
||||
expiration time via other means or document the default value.
|
||||
|
||||
**scope**
|
||||
OPTIONAL, if identical to the scope requested by the client,
|
||||
otherwise REQUIRED. The scope of the access token as described
|
||||
by Section 3.3.
|
||||
|
||||
**state**
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
Similar to the authorization code response, but with a full token provided
|
||||
in the URL fragment:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 302 Found
|
||||
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
|
||||
&state=xyz&token_type=example&expires_in=3600
|
||||
"""
|
||||
fragment = urlparse.urlparse(uri).fragment
|
||||
params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
|
||||
|
||||
if "access_token" not in params:
|
||||
raise MissingTokenException()
|
||||
|
||||
if "token_type" not in params:
|
||||
raise MissingTokenTypeException()
|
||||
|
||||
if state and params.get("state", None) != state:
|
||||
raise MismatchingStateException()
|
||||
|
||||
return params
|
||||
@@ -0,0 +1,199 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from authlib.deprecate import deprecate
|
||||
|
||||
from .errors import InsecureTransportError
|
||||
|
||||
|
||||
class OAuth2Payload:
|
||||
@property
|
||||
def data(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def datalist(self) -> defaultdict[str, list]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def client_id(self) -> str:
|
||||
"""The authorization server issues the registered client a client
|
||||
identifier -- a unique string representing the registration
|
||||
information provided by the client. The value is extracted from
|
||||
request.
|
||||
|
||||
:return: string
|
||||
"""
|
||||
return self.data.get("client_id")
|
||||
|
||||
@property
|
||||
def response_type(self) -> str:
|
||||
rt = self.data.get("response_type")
|
||||
if rt and " " in rt:
|
||||
# sort multiple response types
|
||||
return " ".join(sorted(rt.split()))
|
||||
return rt
|
||||
|
||||
@property
|
||||
def grant_type(self) -> str:
|
||||
return self.data.get("grant_type")
|
||||
|
||||
@property
|
||||
def redirect_uri(self):
|
||||
return self.data.get("redirect_uri")
|
||||
|
||||
@property
|
||||
def scope(self) -> str:
|
||||
return self.data.get("scope")
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.data.get("state")
|
||||
|
||||
|
||||
class BasicOAuth2Payload(OAuth2Payload):
|
||||
def __init__(self, payload):
|
||||
self._data = payload
|
||||
self._datalist = {key: [value] for key, value in payload.items()}
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def datalist(self) -> defaultdict[str, list]:
|
||||
return self._datalist
|
||||
|
||||
|
||||
class OAuth2Request(OAuth2Payload):
|
||||
def __init__(self, method: str, uri: str, body=None, headers=None):
|
||||
InsecureTransportError.check(uri)
|
||||
#: HTTP method
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
#: HTTP headers
|
||||
self.headers = headers or {}
|
||||
|
||||
# Store body for backward compatibility but issue deprecation warning if used
|
||||
if body is not None:
|
||||
deprecate(
|
||||
"'body' parameter in OAuth2Request is deprecated. "
|
||||
"Use the payload system instead.",
|
||||
version="1.8",
|
||||
)
|
||||
self._body = body
|
||||
|
||||
self.payload = None
|
||||
|
||||
self.client = None
|
||||
self.auth_method = None
|
||||
self.user = None
|
||||
self.authorization_code = None
|
||||
self.refresh_token = None
|
||||
self.credential = None
|
||||
self._scope = None
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
if self._body:
|
||||
return self._body
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
deprecate(
|
||||
"'request.data' is deprecated in favor of 'request.payload.data'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.data
|
||||
|
||||
@property
|
||||
def datalist(self) -> defaultdict[str, list]:
|
||||
deprecate(
|
||||
"'request.datalist' is deprecated in favor of 'request.payload.datalist'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.datalist
|
||||
|
||||
@property
|
||||
def client_id(self) -> str:
|
||||
deprecate(
|
||||
"'request.client_id' is deprecated in favor of 'request.payload.client_id'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.client_id
|
||||
|
||||
@property
|
||||
def response_type(self) -> str:
|
||||
deprecate(
|
||||
"'request.response_type' is deprecated in favor of 'request.payload.response_type'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.response_type
|
||||
|
||||
@property
|
||||
def grant_type(self) -> str:
|
||||
deprecate(
|
||||
"'request.grant_type' is deprecated in favor of 'request.payload.grant_type'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.grant_type
|
||||
|
||||
@property
|
||||
def redirect_uri(self):
|
||||
deprecate(
|
||||
"'request.redirect_uri' is deprecated in favor of 'request.payload.redirect_uri'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.redirect_uri
|
||||
|
||||
@property
|
||||
def scope(self) -> str:
|
||||
if self._scope is not None:
|
||||
return self._scope
|
||||
return self.payload.scope
|
||||
|
||||
@scope.setter
|
||||
def scope(self, value: str):
|
||||
self._scope = value
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
deprecate(
|
||||
"'request.state' is deprecated in favor of 'request.payload.state'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.state
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
deprecate(
|
||||
"'request.body' is deprecated. Use the payload system instead.",
|
||||
version="1.8",
|
||||
)
|
||||
return self._body
|
||||
|
||||
|
||||
class JsonPayload:
|
||||
@property
|
||||
def data(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class JsonRequest:
|
||||
def __init__(self, method, uri, headers=None):
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.headers = headers or {}
|
||||
self.payload = None
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
deprecate(
|
||||
"'request.data' is deprecated in favor of 'request.payload.data'",
|
||||
version="1.8",
|
||||
)
|
||||
return self.payload.data
|
||||
@@ -0,0 +1,148 @@
|
||||
"""authlib.oauth2.rfc6749.resource_protector.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implementation of Accessing Protected Resources per `Section 7`_.
|
||||
|
||||
.. _`Section 7`: https://tools.ietf.org/html/rfc6749#section-7
|
||||
"""
|
||||
|
||||
from .errors import MissingAuthorizationError
|
||||
from .errors import UnsupportedTokenTypeError
|
||||
from .util import scope_to_list
|
||||
|
||||
|
||||
class TokenValidator:
|
||||
"""Base token validator class. Subclass this validator to register
|
||||
into ResourceProtector instance.
|
||||
"""
|
||||
|
||||
TOKEN_TYPE = "bearer"
|
||||
|
||||
def __init__(self, realm=None, **extra_attributes):
|
||||
self.realm = realm
|
||||
self.extra_attributes = extra_attributes
|
||||
|
||||
@staticmethod
|
||||
def scope_insufficient(token_scopes, required_scopes):
|
||||
if not required_scopes:
|
||||
return False
|
||||
|
||||
token_scopes = scope_to_list(token_scopes)
|
||||
if not token_scopes:
|
||||
return True
|
||||
|
||||
token_scopes = set(token_scopes)
|
||||
for scope in required_scopes:
|
||||
resource_scopes = set(scope_to_list(scope))
|
||||
if token_scopes.issuperset(resource_scopes):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def authenticate_token(self, token_string):
|
||||
"""A method to query token from database with the given token string.
|
||||
Developers MUST re-implement this method. For instance::
|
||||
|
||||
def authenticate_token(self, token_string):
|
||||
return get_token_from_database(token_string)
|
||||
|
||||
:param token_string: A string to represent the access_token.
|
||||
:return: token
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_request(self, request):
|
||||
"""A method to validate if the HTTP request is valid or not. Developers MUST
|
||||
re-implement this method. For instance, your server requires a
|
||||
"X-Device-Version" in the header::
|
||||
|
||||
def validate_request(self, request):
|
||||
if "X-Device-Version" not in request.headers:
|
||||
raise InvalidRequestError()
|
||||
|
||||
Usually, you don't have to detect if the request is valid or not. If you have
|
||||
to, you MUST re-implement this method.
|
||||
|
||||
:param request: instance of HttpRequest
|
||||
:raise: InvalidRequestError
|
||||
"""
|
||||
|
||||
def validate_token(self, token, scopes, request):
|
||||
"""A method to validate if the authorized token is valid, if it has the
|
||||
permission on the given scopes. Developers MUST re-implement this method.
|
||||
e.g, check if token is expired, revoked::
|
||||
|
||||
def validate_token(self, token, scopes, request):
|
||||
if not token:
|
||||
raise InvalidTokenError()
|
||||
if token.is_expired() or token.is_revoked():
|
||||
raise InvalidTokenError()
|
||||
if not match_token_scopes(token, scopes):
|
||||
raise InsufficientScopeError()
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ResourceProtector:
|
||||
def __init__(self):
|
||||
self._token_validators = {}
|
||||
self._default_realm = None
|
||||
self._default_auth_type = None
|
||||
|
||||
def register_token_validator(self, validator: TokenValidator):
|
||||
"""Register a token validator for a given Authorization type.
|
||||
Authlib has a built-in BearerTokenValidator per rfc6750.
|
||||
"""
|
||||
if not self._default_auth_type:
|
||||
self._default_realm = validator.realm
|
||||
self._default_auth_type = validator.TOKEN_TYPE
|
||||
|
||||
if validator.TOKEN_TYPE not in self._token_validators:
|
||||
self._token_validators[validator.TOKEN_TYPE] = validator
|
||||
|
||||
def get_token_validator(self, token_type):
|
||||
"""Get token validator from registry for the given token type."""
|
||||
validator = self._token_validators.get(token_type.lower())
|
||||
if not validator:
|
||||
raise UnsupportedTokenTypeError(
|
||||
self._default_auth_type, self._default_realm
|
||||
)
|
||||
return validator
|
||||
|
||||
def parse_request_authorization(self, request):
|
||||
"""Parse the token and token validator from request Authorization header.
|
||||
Here is an example of Authorization header::
|
||||
|
||||
Authorization: Bearer a-token-string
|
||||
|
||||
This method will parse this header, if it can find the validator for
|
||||
``Bearer``, it will return the validator and ``a-token-string``.
|
||||
|
||||
:return: validator, token_string
|
||||
:raise: MissingAuthorizationError
|
||||
:raise: UnsupportedTokenTypeError
|
||||
"""
|
||||
auth = request.headers.get("Authorization")
|
||||
if not auth:
|
||||
raise MissingAuthorizationError(
|
||||
self._default_auth_type, self._default_realm
|
||||
)
|
||||
|
||||
# https://tools.ietf.org/html/rfc6749#section-7.1
|
||||
token_parts = auth.split(None, 1)
|
||||
if len(token_parts) != 2:
|
||||
raise UnsupportedTokenTypeError(
|
||||
self._default_auth_type, self._default_realm
|
||||
)
|
||||
|
||||
token_type, token_string = token_parts
|
||||
validator = self.get_token_validator(token_type)
|
||||
return validator, token_string
|
||||
|
||||
def validate_request(self, scopes, request, **kwargs):
|
||||
"""Validate the request and return a token."""
|
||||
validator, token_string = self.parse_request_authorization(request)
|
||||
validator.validate_request(request)
|
||||
token = validator.authenticate_token(token_string)
|
||||
validator.validate_token(token, scopes, request, **kwargs)
|
||||
return token
|
||||
@@ -0,0 +1,30 @@
|
||||
from .endpoint import Endpoint
|
||||
|
||||
|
||||
class TokenEndpoint(Endpoint):
|
||||
"""Base class for token-based endpoints (revocation, introspection).
|
||||
|
||||
Subclasses must implement :meth:`authenticate_token` and
|
||||
:meth:`create_endpoint_response`.
|
||||
"""
|
||||
|
||||
#: Supported token types
|
||||
SUPPORTED_TOKEN_TYPES = ("access_token", "refresh_token")
|
||||
#: Allowed client authenticate methods
|
||||
CLIENT_AUTH_METHODS = ["client_secret_basic"]
|
||||
|
||||
def authenticate_endpoint_client(self, request):
|
||||
"""Authenticate client for endpoint with ``CLIENT_AUTH_METHODS``."""
|
||||
client = self.server.authenticate_client(
|
||||
request, self.CLIENT_AUTH_METHODS, self.ENDPOINT_NAME
|
||||
)
|
||||
request.client = client
|
||||
return client
|
||||
|
||||
def authenticate_token(self, request, client):
|
||||
"""Authenticate and return the token. Subclasses must implement this."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_endpoint_response(self, request):
|
||||
"""Process the request and return response. Subclasses must implement this."""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,42 @@
|
||||
import base64
|
||||
import binascii
|
||||
from urllib.parse import unquote
|
||||
|
||||
from authlib.common.encoding import to_unicode
|
||||
|
||||
|
||||
def list_to_scope(scope):
|
||||
"""Convert a list of scopes to a space separated string."""
|
||||
if isinstance(scope, (set, tuple, list)):
|
||||
return " ".join([to_unicode(s) for s in scope])
|
||||
if scope is None:
|
||||
return scope
|
||||
return to_unicode(scope)
|
||||
|
||||
|
||||
def scope_to_list(scope):
|
||||
"""Convert a space separated string to a list of scopes."""
|
||||
if isinstance(scope, (tuple, list, set)):
|
||||
return [to_unicode(s) for s in scope]
|
||||
elif scope is None:
|
||||
return None
|
||||
return scope.strip().split()
|
||||
|
||||
|
||||
def extract_basic_authorization(headers):
|
||||
auth = headers.get("Authorization")
|
||||
if not auth or " " not in auth:
|
||||
return None, None
|
||||
|
||||
auth_type, auth_token = auth.split(None, 1)
|
||||
if auth_type.lower() != "basic":
|
||||
return None, None
|
||||
|
||||
try:
|
||||
query = to_unicode(base64.b64decode(auth_token))
|
||||
except (binascii.Error, TypeError):
|
||||
return None, None
|
||||
if ":" in query:
|
||||
username, password = query.split(":", 1)
|
||||
return unquote(username), unquote(password)
|
||||
return query, None
|
||||
@@ -0,0 +1,35 @@
|
||||
import time
|
||||
|
||||
|
||||
class OAuth2Token(dict):
|
||||
def __init__(self, params):
|
||||
if params.get("expires_at") is not None:
|
||||
try:
|
||||
params["expires_at"] = int(params["expires_at"])
|
||||
except ValueError:
|
||||
# If expires_at is not parseable, fall back to expires_in if available
|
||||
# Otherwise leave expires_at untouched
|
||||
if params.get("expires_in"):
|
||||
params["expires_at"] = int(time.time()) + int(params["expires_in"])
|
||||
|
||||
elif params.get("expires_in"):
|
||||
params["expires_at"] = int(time.time()) + int(params["expires_in"])
|
||||
|
||||
super().__init__(params)
|
||||
|
||||
def is_expired(self, leeway=60):
|
||||
expires_at = self.get("expires_at")
|
||||
if expires_at is None:
|
||||
return None
|
||||
# Only check expiration if expires_at is an integer
|
||||
if not isinstance(expires_at, int):
|
||||
return None
|
||||
# small timedelta to consider token as expired before it actually expires
|
||||
expiration_threshold = expires_at - leeway
|
||||
return expiration_threshold < time.time()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, token):
|
||||
if isinstance(token, dict) and not isinstance(token, cls):
|
||||
token = cls(token)
|
||||
return token
|
||||
Reference in New Issue
Block a user