venv added, updated
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
authlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of
|
||||
The OAuth 2.0 Authorization Framework.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749
|
||||
"""
|
||||
|
||||
from .requests import OAuth2Request, JsonRequest
|
||||
from .wrappers import OAuth2Token
|
||||
from .errors import (
|
||||
OAuth2Error,
|
||||
AccessDeniedError,
|
||||
MissingAuthorizationError,
|
||||
InvalidGrantError,
|
||||
InvalidClientError,
|
||||
InvalidRequestError,
|
||||
InvalidScopeError,
|
||||
InsecureTransportError,
|
||||
UnauthorizedClientError,
|
||||
UnsupportedResponseTypeError,
|
||||
UnsupportedGrantTypeError,
|
||||
UnsupportedTokenTypeError,
|
||||
# exceptions for clients
|
||||
MissingCodeException,
|
||||
MissingTokenException,
|
||||
MissingTokenTypeException,
|
||||
MismatchingStateException,
|
||||
)
|
||||
from .models import ClientMixin, AuthorizationCodeMixin, TokenMixin
|
||||
from .authenticate_client import ClientAuthentication
|
||||
from .authorization_server import AuthorizationServer
|
||||
from .resource_protector import ResourceProtector, TokenValidator
|
||||
from .token_endpoint import TokenEndpoint
|
||||
from .grants import (
|
||||
BaseGrant,
|
||||
AuthorizationEndpointMixin,
|
||||
TokenEndpointMixin,
|
||||
AuthorizationCodeGrant,
|
||||
ImplicitGrant,
|
||||
ResourceOwnerPasswordCredentialsGrant,
|
||||
ClientCredentialsGrant,
|
||||
RefreshTokenGrant,
|
||||
)
|
||||
from .util import scope_to_list, list_to_scope
|
||||
|
||||
__all__ = [
|
||||
'OAuth2Token',
|
||||
'OAuth2Request', 'JsonRequest',
|
||||
'OAuth2Error',
|
||||
'AccessDeniedError',
|
||||
'MissingAuthorizationError',
|
||||
'InvalidGrantError',
|
||||
'InvalidClientError',
|
||||
'InvalidRequestError',
|
||||
'InvalidScopeError',
|
||||
'InsecureTransportError',
|
||||
'UnauthorizedClientError',
|
||||
'UnsupportedResponseTypeError',
|
||||
'UnsupportedGrantTypeError',
|
||||
'UnsupportedTokenTypeError',
|
||||
'MissingCodeException',
|
||||
'MissingTokenException',
|
||||
'MissingTokenTypeException',
|
||||
'MismatchingStateException',
|
||||
'ClientMixin', 'AuthorizationCodeMixin', 'TokenMixin',
|
||||
'ClientAuthentication',
|
||||
'AuthorizationServer',
|
||||
'ResourceProtector',
|
||||
'TokenValidator',
|
||||
'TokenEndpoint',
|
||||
'BaseGrant',
|
||||
'AuthorizationEndpointMixin',
|
||||
'TokenEndpointMixin',
|
||||
'AuthorizationCodeGrant',
|
||||
'ImplicitGrant',
|
||||
'ResourceOwnerPasswordCredentialsGrant',
|
||||
'ClientCredentialsGrant',
|
||||
'RefreshTokenGrant',
|
||||
'scope_to_list', 'list_to_scope',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
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(state=request.state, status_code=401)
|
||||
raise InvalidClientError(state=request.state)
|
||||
|
||||
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, request.state, 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, request.state)
|
||||
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.client_id
|
||||
if client_id and not request.data.get('client_secret'):
|
||||
client = _validate_client(query_client, client_id, request.state)
|
||||
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, state=None, status_code=400):
|
||||
if client_id is None:
|
||||
raise InvalidClientError(state=state, status_code=status_code)
|
||||
|
||||
client = query_client(client_id)
|
||||
if not client:
|
||||
raise InvalidClientError(state=state, status_code=status_code)
|
||||
|
||||
return client
|
||||
@@ -0,0 +1,302 @@
|
||||
from authlib.common.errors import ContinueIteration
|
||||
from .authenticate_client import ClientAuthentication
|
||||
from .requests import OAuth2Request, JsonRequest
|
||||
from .errors import (
|
||||
OAuth2Error,
|
||||
InvalidScopeError,
|
||||
UnsupportedResponseTypeError,
|
||||
UnsupportedGrantTypeError,
|
||||
)
|
||||
from .util import scope_to_list
|
||||
|
||||
|
||||
class AuthorizationServer:
|
||||
"""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):
|
||||
self.scopes_supported = scopes_supported
|
||||
self._token_generators = {}
|
||||
self._client_auth = None
|
||||
self._authorization_grants = []
|
||||
self._token_grants = []
|
||||
self._endpoints = {}
|
||||
|
||||
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 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, state=None):
|
||||
"""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(state=state)
|
||||
|
||||
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):
|
||||
"""Add extra endpoint to authorization server. e.g.
|
||||
RevocationEndpoint::
|
||||
|
||||
authorization_server.register_endpoint(RevocationEndpoint)
|
||||
|
||||
:param endpoint_cls: A 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)
|
||||
|
||||
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)
|
||||
raise UnsupportedResponseTypeError(request.response_type)
|
||||
|
||||
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)
|
||||
request.user = end_user
|
||||
|
||||
grant = self.get_authorization_grant(request)
|
||||
grant.validate_no_multiple_request_parameter(request)
|
||||
grant.validate_consent_request()
|
||||
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.grant_type)
|
||||
|
||||
def create_endpoint_response(self, name, request=None):
|
||||
"""Validate endpoint request and create endpoint response.
|
||||
|
||||
:param name: Endpoint name
|
||||
:param request: HTTP request instance.
|
||||
:return: Response
|
||||
"""
|
||||
if name not in self._endpoints:
|
||||
raise RuntimeError(f'There is no "{name}" endpoint.')
|
||||
|
||||
endpoints = self._endpoints[name]
|
||||
for endpoint in endpoints:
|
||||
request = endpoint.create_endpoint_request(request)
|
||||
try:
|
||||
return self.handle_response(*endpoint(request))
|
||||
except ContinueIteration:
|
||||
continue
|
||||
except OAuth2Error as error:
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
def create_authorization_response(self, request=None, grant_user=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)
|
||||
|
||||
try:
|
||||
grant = self.get_authorization_grant(request)
|
||||
except UnsupportedResponseTypeError as error:
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
try:
|
||||
redirect_uri = grant.validate_authorization_request()
|
||||
args = grant.create_authorization_response(redirect_uri, grant_user)
|
||||
return self.handle_response(*args)
|
||||
except OAuth2Error as error:
|
||||
return self.handle_error_response(request, error)
|
||||
|
||||
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,233 @@
|
||||
"""
|
||||
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.oauth2.base import OAuth2Error
|
||||
from authlib.common.security import is_secure_transport
|
||||
|
||||
__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):
|
||||
super().__init__()
|
||||
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,37 @@
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
from .base import BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin
|
||||
from .authorization_code import AuthorizationCodeGrant
|
||||
from .implicit import ImplicitGrant
|
||||
from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
|
||||
from .client_credentials import ClientCredentialsGrant
|
||||
from .refresh_token import RefreshTokenGrant
|
||||
|
||||
__all__ = [
|
||||
'BaseGrant', 'AuthorizationEndpointMixin', 'TokenEndpointMixin',
|
||||
'AuthorizationCodeGrant', 'ImplicitGrant',
|
||||
'ResourceOwnerPasswordCredentialsGrant',
|
||||
'ClientCredentialsGrant', 'RefreshTokenGrant',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,378 @@
|
||||
import logging
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
from authlib.common.security import generate_token
|
||||
from .base import BaseGrant, AuthorizationEndpointMixin, TokenEndpointMixin
|
||||
from ..errors import (
|
||||
OAuth2Error,
|
||||
UnauthorizedClientError,
|
||||
InvalidClientError,
|
||||
InvalidGrantError,
|
||||
InvalidRequestError,
|
||||
AccessDeniedError,
|
||||
)
|
||||
|
||||
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(state=self.request.state, 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.state:
|
||||
params.append(('state', self.request.state))
|
||||
uri = add_params_to_uri(redirect_uri, params)
|
||||
headers = [('Location', uri)]
|
||||
return 302, '', headers
|
||||
|
||||
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.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
|
||||
self.execute_hook('after_validate_token_request')
|
||||
|
||||
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.execute_hook('process_token', 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.redirect_uri,
|
||||
scope=request.scope,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
item.save()
|
||||
"""
|
||||
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.client_id
|
||||
log.debug('Validate authorization request of %r', client_id)
|
||||
|
||||
if client_id is None:
|
||||
raise InvalidClientError(state=request.state)
|
||||
|
||||
client = grant.server.query_client(client_id)
|
||||
if not client:
|
||||
raise InvalidClientError(state=request.state)
|
||||
|
||||
redirect_uri = grant.validate_authorization_redirect_uri(request, client)
|
||||
response_type = request.response_type
|
||||
if not client.check_response_type(response_type):
|
||||
raise UnauthorizedClientError(
|
||||
f'The client is not authorized to use "response_type={response_type}"',
|
||||
state=grant.request.state,
|
||||
redirect_uri=redirect_uri,
|
||||
)
|
||||
|
||||
try:
|
||||
grant.request.client = client
|
||||
grant.validate_requested_scope()
|
||||
grant.execute_hook('after_validate_authorization_request')
|
||||
except OAuth2Error as error:
|
||||
error.redirect_uri = redirect_uri
|
||||
raise error
|
||||
return redirect_uri
|
||||
@@ -0,0 +1,162 @@
|
||||
from authlib.consts import default_json_headers
|
||||
from authlib.common.urls import urlparse
|
||||
from ..requests import OAuth2Request
|
||||
from ..errors import InvalidRequestError
|
||||
|
||||
|
||||
class BaseGrant:
|
||||
#: 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):
|
||||
self.prompt = None
|
||||
self.redirect_uri = None
|
||||
self.request = request
|
||||
self.server = server
|
||||
self._hooks = {
|
||||
'after_validate_authorization_request': set(),
|
||||
'after_validate_consent_request': set(),
|
||||
'after_validate_token_request': set(),
|
||||
'process_token': set(),
|
||||
}
|
||||
|
||||
@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.scope
|
||||
state = self.request.state
|
||||
return self.server.validate_requested_scope(scope, state)
|
||||
|
||||
def register_hook(self, hook_type, hook):
|
||||
if hook_type not in self._hooks:
|
||||
raise ValueError('Hook type %s is not in %s.',
|
||||
hook_type, self._hooks)
|
||||
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)
|
||||
|
||||
|
||||
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.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.response_type in cls.RESPONSE_TYPES
|
||||
|
||||
@staticmethod
|
||||
def validate_authorization_redirect_uri(request: OAuth2Request, client):
|
||||
if request.redirect_uri:
|
||||
if not client.check_redirect_uri(request.redirect_uri):
|
||||
raise InvalidRequestError(
|
||||
f'Redirect URI {request.redirect_uri} is not supported by client.',
|
||||
state=request.state)
|
||||
return request.redirect_uri
|
||||
else:
|
||||
redirect_uri = client.get_default_redirect_uri()
|
||||
if not redirect_uri:
|
||||
raise InvalidRequestError(
|
||||
'Missing "redirect_uri" in request.',
|
||||
state=request.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.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.state)
|
||||
|
||||
def validate_consent_request(self):
|
||||
redirect_uri = self.validate_authorization_request()
|
||||
self.execute_hook('after_validate_consent_request', redirect_uri)
|
||||
self.redirect_uri = redirect_uri
|
||||
|
||||
def validate_authorization_request(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_authorization_response(self, redirect_uri: str, grant_user):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,102 @@
|
||||
import logging
|
||||
from .base import BaseGrant, TokenEndpointMixin
|
||||
from ..errors import UnauthorizedClientError
|
||||
|
||||
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()
|
||||
|
||||
self.request.client = client
|
||||
self.validate_requested_scope()
|
||||
|
||||
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.scope, include_refresh_token=False)
|
||||
log.debug('Issue token %r to %r', token, self.client)
|
||||
self.save_token(token)
|
||||
self.execute_hook('process_token', self, token=token)
|
||||
return 200, token, self.TOKEN_RESPONSE_HEADER
|
||||
@@ -0,0 +1,229 @@
|
||||
import logging
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
from .base import BaseGrant, AuthorizationEndpointMixin
|
||||
from ..errors import (
|
||||
OAuth2Error,
|
||||
UnauthorizedClientError,
|
||||
AccessDeniedError,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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.response_type
|
||||
if not client.check_response_type(response_type):
|
||||
raise UnauthorizedClientError(
|
||||
'The client is not authorized to use '
|
||||
'"response_type={}"'.format(response_type),
|
||||
state=self.request.state,
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=True,
|
||||
)
|
||||
|
||||
try:
|
||||
self.request.client = client
|
||||
self.validate_requested_scope()
|
||||
self.execute_hook('after_validate_authorization_request')
|
||||
except OAuth2Error as error:
|
||||
error.redirect_uri = redirect_uri
|
||||
error.redirect_fragment = True
|
||||
raise error
|
||||
return redirect_uri
|
||||
|
||||
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.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)
|
||||
self.execute_hook('process_token', 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(
|
||||
state=state,
|
||||
redirect_uri=redirect_uri,
|
||||
redirect_fragment=True
|
||||
)
|
||||
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
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 .base import BaseGrant, TokenEndpointMixin
|
||||
from ..util import scope_to_list
|
||||
from ..errors import (
|
||||
InvalidRequestError,
|
||||
InvalidScopeError,
|
||||
InvalidGrantError,
|
||||
UnauthorizedClientError,
|
||||
)
|
||||
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()
|
||||
|
||||
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.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
|
||||
|
||||
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.execute_hook('process_token', 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.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()
|
||||
@@ -0,0 +1,154 @@
|
||||
import logging
|
||||
from .base import BaseGrant, TokenEndpointMixin
|
||||
from ..errors import (
|
||||
UnauthorizedClientError,
|
||||
InvalidRequestError,
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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.scope
|
||||
token = self.generate_token(user=user, scope=scope)
|
||||
log.debug('Issue token %r to %r', token, self.client)
|
||||
self.save_token(token)
|
||||
self.execute_hook('process_token', 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,228 @@
|
||||
"""
|
||||
authlib.oauth2.rfc6749.models
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module defines how to construct Client, AuthorizationCode and Token.
|
||||
"""
|
||||
from authlib.deprecate import deprecate
|
||||
|
||||
|
||||
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_token_endpoint_auth_method(self, method):
|
||||
deprecate('Please implement ``check_endpoint_auth_method`` instead.')
|
||||
return self.check_endpoint_auth_method(method, 'token')
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,214 @@
|
||||
from authlib.common.urls import (
|
||||
urlparse,
|
||||
add_params_to_uri,
|
||||
add_params_to_qs,
|
||||
)
|
||||
from authlib.common.encoding import to_unicode
|
||||
from .errors import (
|
||||
MissingCodeException,
|
||||
MissingTokenException,
|
||||
MissingTokenTypeException,
|
||||
MismatchingStateException,
|
||||
)
|
||||
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 in kwargs:
|
||||
if kwargs[k] is not None:
|
||||
params.append((to_unicode(k), kwargs[k]))
|
||||
|
||||
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 'code' not in kwargs:
|
||||
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,103 @@
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict
|
||||
|
||||
from authlib.common.encoding import json_loads
|
||||
from authlib.common.urls import urlparse, url_decode
|
||||
from .errors import InsecureTransportError
|
||||
|
||||
|
||||
class OAuth2Request:
|
||||
def __init__(self, method: str, uri: str, body=None, headers=None):
|
||||
InsecureTransportError.check(uri)
|
||||
#: HTTP method
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.body = body
|
||||
#: HTTP headers
|
||||
self.headers = headers or {}
|
||||
|
||||
self.client = None
|
||||
self.auth_method = None
|
||||
self.user = None
|
||||
self.authorization_code = None
|
||||
self.refresh_token = None
|
||||
self.credential = None
|
||||
|
||||
self._parsed_query = None
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
if self._parsed_query is None:
|
||||
self._parsed_query = url_decode(urlparse.urlparse(self.uri).query)
|
||||
return dict(self._parsed_query)
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
return self.body or {}
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
data = {}
|
||||
data.update(self.args)
|
||||
data.update(self.form)
|
||||
return data
|
||||
|
||||
@property
|
||||
def datalist(self) -> DefaultDict[str, list]:
|
||||
""" Return all the data in query parameters and the body of the request as a dictionary with all the values
|
||||
in lists. """
|
||||
if self._parsed_query is None:
|
||||
self._parsed_query = url_decode(urlparse.urlparse(self.uri).query)
|
||||
values = defaultdict(list)
|
||||
for k, v in self._parsed_query:
|
||||
values[k].append(v)
|
||||
for k, v in self.form.items():
|
||||
values[k].append(v)
|
||||
return values
|
||||
|
||||
@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.form.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 JsonRequest:
|
||||
def __init__(self, method, uri, body=None, headers=None):
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.body = body
|
||||
self.headers = headers or {}
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return json_loads(self.body)
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
authlib.oauth2.rfc6749.resource_protector
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Implementation of Accessing Protected Resources per `Section 7`_.
|
||||
|
||||
.. _`Section 7`: https://tools.ietf.org/html/rfc6749#section-7
|
||||
"""
|
||||
from .util import scope_to_list
|
||||
from .errors import MissingAuthorizationError, UnsupportedTokenTypeError
|
||||
|
||||
|
||||
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,32 @@
|
||||
class TokenEndpoint:
|
||||
#: Endpoint name to be registered
|
||||
ENDPOINT_NAME = None
|
||||
#: Supported token types
|
||||
SUPPORTED_TOKEN_TYPES = ('access_token', 'refresh_token')
|
||||
#: Allowed client authenticate methods
|
||||
CLIENT_AUTH_METHODS = ['client_secret_basic']
|
||||
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
|
||||
def __call__(self, request):
|
||||
# make it callable for authorization server
|
||||
# ``create_endpoint_response``
|
||||
return self.create_endpoint_response(request)
|
||||
|
||||
def create_endpoint_request(self, request):
|
||||
return self.server.create_oauth2_request(request)
|
||||
|
||||
def authenticate_endpoint_client(self, request):
|
||||
"""Authentication 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):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_endpoint_response(self, request):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,41 @@
|
||||
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,25 @@
|
||||
import time
|
||||
|
||||
|
||||
class OAuth2Token(dict):
|
||||
def __init__(self, params):
|
||||
if params.get('expires_at'):
|
||||
params['expires_at'] = int(params['expires_at'])
|
||||
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 not expires_at:
|
||||
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