420 lines
16 KiB
Python
420 lines
16 KiB
Python
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
|