venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 deletions

View File

@@ -0,0 +1,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',
]

View File

@@ -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

View File

@@ -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

View File

@@ -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.'

View File

@@ -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',
]

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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