mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 16:51:09 -05:00
first commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
oauthlib.oauth2
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is a wrapper for the most recent implementation of OAuth 2.0 Client
|
||||
and Server classes.
|
||||
"""
|
||||
from .rfc6749.clients import (
|
||||
BackendApplicationClient, Client, LegacyApplicationClient,
|
||||
MobileApplicationClient, ServiceApplicationClient, WebApplicationClient,
|
||||
)
|
||||
from .rfc6749.endpoints import (
|
||||
AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint,
|
||||
LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer,
|
||||
ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint,
|
||||
WebApplicationServer,
|
||||
)
|
||||
from .rfc6749.errors import (
|
||||
AccessDeniedError, FatalClientError, InsecureTransportError,
|
||||
InvalidClientError, InvalidClientIdError, InvalidGrantError,
|
||||
InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError,
|
||||
InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError,
|
||||
MissingClientIdError, MissingCodeError, MissingRedirectURIError,
|
||||
MissingResponseTypeError, MissingTokenError, MissingTokenTypeError,
|
||||
OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError,
|
||||
UnauthorizedClientError, UnsupportedGrantTypeError,
|
||||
UnsupportedResponseTypeError, UnsupportedTokenTypeError,
|
||||
)
|
||||
from .rfc6749.grant_types import (
|
||||
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
|
||||
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
|
||||
)
|
||||
from .rfc6749.request_validator import RequestValidator
|
||||
from .rfc6749.tokens import BearerToken, OAuth2Token
|
||||
from .rfc6749.utils import is_secure_transport
|
||||
from .rfc8628.clients import DeviceClient
|
||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import functools
|
||||
import logging
|
||||
|
||||
from .endpoints.base import BaseEndpoint, catch_errors_and_unavailability
|
||||
from .errors import (
|
||||
FatalClientError, OAuth2Error, ServerError, TemporarilyUnavailableError,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
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,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming OAuth 2.0 RFC6749.
|
||||
"""
|
||||
from .backend_application import BackendApplicationClient
|
||||
from .base import AUTH_HEADER, BODY, URI_QUERY, Client
|
||||
from .legacy_application import LegacyApplicationClient
|
||||
from .mobile_application import MobileApplicationClient
|
||||
from .service_application import ServiceApplicationClient
|
||||
from .web_application import WebApplicationClient
|
||||
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,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
from ..parameters import prepare_token_request
|
||||
from .base import Client
|
||||
|
||||
|
||||
class BackendApplicationClient(Client):
|
||||
|
||||
"""A public client utilizing the client credentials grant workflow.
|
||||
|
||||
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 which has been previously
|
||||
arranged with the authorization server (the method of which is beyond
|
||||
the scope of this specification).
|
||||
|
||||
The client credentials grant type MUST only be used by confidential
|
||||
clients.
|
||||
|
||||
Since the client authentication is used as the authorization grant,
|
||||
no additional authorization request is needed.
|
||||
"""
|
||||
|
||||
grant_type = 'client_credentials'
|
||||
|
||||
def prepare_request_body(self, body='', scope=None,
|
||||
include_client_id=False, **kwargs):
|
||||
"""Add the client credentials to the request body.
|
||||
|
||||
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`_ in the HTTP request entity-body:
|
||||
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
:param scope: The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
|
||||
:param include_client_id: `True` to send the `client_id` in the
|
||||
body of the upstream request. This is required
|
||||
if the client is not authenticating with the
|
||||
authorization server as described in
|
||||
`Section 3.2.1`_. False otherwise (default).
|
||||
:type include_client_id: Boolean
|
||||
|
||||
:param kwargs: Extra credentials to include in the token request.
|
||||
|
||||
The client MUST authenticate with the authorization server as
|
||||
described in `Section 3.2.1`_.
|
||||
|
||||
The prepared body will include all provided credentials as well as
|
||||
the ``grant_type`` parameter set to ``client_credentials``::
|
||||
|
||||
>>> from oauthlib.oauth2 import BackendApplicationClient
|
||||
>>> client = BackendApplicationClient('your_id')
|
||||
>>> client.prepare_request_body(scope=['hello', 'world'])
|
||||
'grant_type=client_credentials&scope=hello+world'
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
"""
|
||||
kwargs['client_id'] = self.client_id
|
||||
kwargs['include_client_id'] = include_client_id
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_token_request(self.grant_type, body=body,
|
||||
scope=scope, **kwargs)
|
||||
@@ -0,0 +1,604 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from oauthlib.common import generate_token
|
||||
from oauthlib.oauth2.rfc6749 import tokens
|
||||
from oauthlib.oauth2.rfc6749.errors import (
|
||||
InsecureTransportError, TokenExpiredError,
|
||||
)
|
||||
from oauthlib.oauth2.rfc6749.parameters import (
|
||||
parse_token_response, prepare_token_request,
|
||||
prepare_token_revocation_request,
|
||||
)
|
||||
from oauthlib.oauth2.rfc6749.utils import is_secure_transport
|
||||
|
||||
AUTH_HEADER = 'auth_header'
|
||||
URI_QUERY = 'query'
|
||||
BODY = 'body'
|
||||
|
||||
FORM_ENC_HEADERS = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
|
||||
class Client:
|
||||
"""Base OAuth2 client responsible for access token management.
|
||||
|
||||
This class also acts as a generic interface providing methods common to all
|
||||
client types such as ``prepare_authorization_request`` and
|
||||
``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
|
||||
the recommended way of interacting with clients (as opposed to the abstract
|
||||
prepare uri/body/etc methods). They are recommended over the older set
|
||||
because they are easier to use (more consistent) and add a few additional
|
||||
security checks, such as HTTPS and state checking.
|
||||
|
||||
Some of these methods require further implementation only provided by the
|
||||
specific purpose clients such as
|
||||
:py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
|
||||
seek to use the client class matching the OAuth workflow you need. For
|
||||
Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
|
||||
|
||||
"""
|
||||
refresh_token_key = 'refresh_token'
|
||||
|
||||
def __init__(self, client_id,
|
||||
default_token_placement=AUTH_HEADER,
|
||||
token_type='Bearer',
|
||||
access_token=None,
|
||||
refresh_token=None,
|
||||
mac_key=None,
|
||||
mac_algorithm=None,
|
||||
token=None,
|
||||
scope=None,
|
||||
state=None,
|
||||
redirect_url=None,
|
||||
state_generator=generate_token,
|
||||
code_verifier=None,
|
||||
code_challenge=None,
|
||||
code_challenge_method=None,
|
||||
**kwargs):
|
||||
"""Initialize a client with commonly used attributes.
|
||||
|
||||
:param client_id: Client identifier given by the OAuth provider upon
|
||||
registration.
|
||||
|
||||
:param default_token_placement: Tokens can be supplied in the Authorization
|
||||
header (default), the URL query component (``query``) or the request
|
||||
body (``body``).
|
||||
|
||||
:param token_type: OAuth 2 token type. Defaults to Bearer. Change this
|
||||
if you specify the ``access_token`` parameter and know it is of a
|
||||
different token type, such as a MAC, JWT or SAML token. Can
|
||||
also be supplied as ``token_type`` inside the ``token`` dict parameter.
|
||||
|
||||
:param access_token: An access token (string) used to authenticate
|
||||
requests to protected resources. Can also be supplied inside the
|
||||
``token`` dict parameter.
|
||||
|
||||
:param refresh_token: A refresh token (string) used to refresh expired
|
||||
tokens. Can also be supplied inside the ``token`` dict parameter.
|
||||
|
||||
:param mac_key: Encryption key used with MAC tokens.
|
||||
|
||||
:param mac_algorithm: Hashing algorithm for MAC tokens.
|
||||
|
||||
:param token: A dict of token attributes such as ``access_token``,
|
||||
``token_type`` and ``expires_at``.
|
||||
|
||||
:param scope: A list of default scopes to request authorization for.
|
||||
|
||||
:param state: A CSRF protection string used during authorization.
|
||||
|
||||
:param redirect_url: The redirection endpoint on the client side to which
|
||||
the user returns after authorization.
|
||||
|
||||
:param state_generator: A no argument state generation callable. Defaults
|
||||
to :py:meth:`oauthlib.common.generate_token`.
|
||||
|
||||
:param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
|
||||
authorization request to the token request.
|
||||
|
||||
:param code_challenge: PKCE parameter. A challenge derived from the code verifier that is sent in the
|
||||
authorization request, to be verified against later.
|
||||
|
||||
:param code_challenge_method: PKCE parameter. A method that was used to derive code challenge.
|
||||
Defaults to "plain" if not present in the request.
|
||||
"""
|
||||
|
||||
self.client_id = client_id
|
||||
self.default_token_placement = default_token_placement
|
||||
self.token_type = token_type
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
self.mac_key = mac_key
|
||||
self.mac_algorithm = mac_algorithm
|
||||
self.token = token or {}
|
||||
self.scope = scope
|
||||
self.state_generator = state_generator
|
||||
self.state = state
|
||||
self.redirect_url = redirect_url
|
||||
self.code_verifier = code_verifier
|
||||
self.code_challenge = code_challenge
|
||||
self.code_challenge_method = code_challenge_method
|
||||
self.code = None
|
||||
self.expires_in = None
|
||||
self._expires_at = None
|
||||
self.populate_token_attributes(self.token)
|
||||
|
||||
@property
|
||||
def token_types(self):
|
||||
"""Supported token types and their respective methods
|
||||
|
||||
Additional tokens can be supported by extending this dictionary.
|
||||
|
||||
The Bearer token spec is stable and safe to use.
|
||||
|
||||
The MAC token spec is not yet stable and support for MAC tokens
|
||||
is experimental and currently matching version 00 of the spec.
|
||||
"""
|
||||
return {
|
||||
'Bearer': self._add_bearer_token,
|
||||
'MAC': self._add_mac_token
|
||||
}
|
||||
|
||||
def prepare_request_uri(self, *args, **kwargs):
|
||||
"""Abstract method used to create request URIs."""
|
||||
raise NotImplementedError("Must be implemented by inheriting classes.")
|
||||
|
||||
def prepare_request_body(self, *args, **kwargs):
|
||||
"""Abstract method used to create request bodies."""
|
||||
raise NotImplementedError("Must be implemented by inheriting classes.")
|
||||
|
||||
def parse_request_uri_response(self, *args, **kwargs):
|
||||
"""Abstract method used to parse redirection responses."""
|
||||
raise NotImplementedError("Must be implemented by inheriting classes.")
|
||||
|
||||
def add_token(self, uri, http_method='GET', body=None, headers=None,
|
||||
token_placement=None, **kwargs):
|
||||
"""Add token to the request uri, body or authorization header.
|
||||
|
||||
The access token type provides the client with the information
|
||||
required to successfully utilize the access token to make a protected
|
||||
resource request (along with type-specific attributes). The client
|
||||
MUST NOT use an access token if it does not understand the token
|
||||
type.
|
||||
|
||||
For example, the "bearer" token type defined in
|
||||
[`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
|
||||
token string in the request:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /resource/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Authorization: Bearer mF_9.B5f-4.1JqM
|
||||
|
||||
while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
|
||||
utilized by issuing a MAC key together with the access token which is
|
||||
used to sign certain components of the HTTP requests:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /resource/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Authorization: MAC id="h480djs93hd8",
|
||||
nonce="274312:dj83hs9s",
|
||||
mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
|
||||
|
||||
.. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2
|
||||
.. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2
|
||||
"""
|
||||
if not is_secure_transport(uri):
|
||||
raise InsecureTransportError()
|
||||
|
||||
token_placement = token_placement or self.default_token_placement
|
||||
|
||||
case_insensitive_token_types = {
|
||||
k.lower(): v for k, v in self.token_types.items()}
|
||||
if not self.token_type.lower() in case_insensitive_token_types:
|
||||
raise ValueError("Unsupported token type: %s" % self.token_type)
|
||||
|
||||
if not (self.access_token or self.token.get('access_token')):
|
||||
raise ValueError("Missing access token.")
|
||||
|
||||
if self._expires_at and self._expires_at < time.time():
|
||||
raise TokenExpiredError()
|
||||
|
||||
return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
|
||||
headers, token_placement, **kwargs)
|
||||
|
||||
def prepare_authorization_request(self, authorization_url, state=None,
|
||||
redirect_url=None, scope=None, **kwargs):
|
||||
"""Prepare the authorization request.
|
||||
|
||||
This is the first step in many OAuth flows in which the user is
|
||||
redirected to a certain authorization URL. This method adds
|
||||
required parameters to the authorization URL.
|
||||
|
||||
:param authorization_url: Provider authorization endpoint URL.
|
||||
:param state: CSRF protection string. Will be automatically created if
|
||||
not provided. The generated state is available via the ``state``
|
||||
attribute. Clients should verify that the state is unchanged and
|
||||
present in the authorization response. This verification is done
|
||||
automatically if using the ``authorization_response`` parameter
|
||||
with ``prepare_token_request``.
|
||||
:param redirect_url: Redirect URL to which the user will be returned
|
||||
after authorization. Must be provided unless previously setup with
|
||||
the provider. If provided then it must also be provided in the
|
||||
token request.
|
||||
:param scope: List of scopes to request. Must be equal to
|
||||
or a subset of the scopes granted when obtaining the refresh
|
||||
token. If none is provided, the ones provided in the constructor are
|
||||
used.
|
||||
:param kwargs: Additional parameters to included in the request.
|
||||
:returns: The prepared request tuple with (url, headers, body).
|
||||
"""
|
||||
if not is_secure_transport(authorization_url):
|
||||
raise InsecureTransportError()
|
||||
|
||||
self.state = state or self.state_generator()
|
||||
self.redirect_url = redirect_url or self.redirect_url
|
||||
# do not assign scope to self automatically anymore
|
||||
scope = self.scope if scope is None else scope
|
||||
auth_url = self.prepare_request_uri(
|
||||
authorization_url, redirect_uri=self.redirect_url,
|
||||
scope=scope, state=self.state, **kwargs)
|
||||
return auth_url, FORM_ENC_HEADERS, ''
|
||||
|
||||
def prepare_token_request(self, token_url, authorization_response=None,
|
||||
redirect_url=None, state=None, body='', **kwargs):
|
||||
"""Prepare a token creation request.
|
||||
|
||||
Note that these requests usually require client authentication, either
|
||||
by including client_id or a set of provider specific authentication
|
||||
credentials.
|
||||
|
||||
:param token_url: Provider token creation endpoint URL.
|
||||
:param authorization_response: The full redirection URL string, i.e.
|
||||
the location to which the user was redirected after successful
|
||||
authorization. Used to mine credentials needed to obtain a token
|
||||
in this step, such as authorization code.
|
||||
:param redirect_url: The redirect_url supplied with the authorization
|
||||
request (if there was one).
|
||||
:param state:
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
:param kwargs: Additional parameters to included in the request.
|
||||
:returns: The prepared request tuple with (url, headers, body).
|
||||
"""
|
||||
if not is_secure_transport(token_url):
|
||||
raise InsecureTransportError()
|
||||
|
||||
state = state or self.state
|
||||
if authorization_response:
|
||||
self.parse_request_uri_response(
|
||||
authorization_response, state=state)
|
||||
self.redirect_url = redirect_url or self.redirect_url
|
||||
body = self.prepare_request_body(body=body,
|
||||
redirect_uri=self.redirect_url, **kwargs)
|
||||
|
||||
return token_url, FORM_ENC_HEADERS, body
|
||||
|
||||
def prepare_refresh_token_request(self, token_url, refresh_token=None,
|
||||
body='', scope=None, **kwargs):
|
||||
"""Prepare an access token refresh request.
|
||||
|
||||
Expired access tokens can be replaced by new access tokens without
|
||||
going through the OAuth dance if the client obtained a refresh token.
|
||||
This refresh token and authentication credentials can be used to
|
||||
obtain a new access token, and possibly a new refresh token.
|
||||
|
||||
:param token_url: Provider token refresh endpoint URL.
|
||||
:param refresh_token: Refresh token string.
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
:param scope: List of scopes to request. Must be equal to
|
||||
or a subset of the scopes granted when obtaining the refresh
|
||||
token. If none is provided, the ones provided in the constructor are
|
||||
used.
|
||||
:param kwargs: Additional parameters to included in the request.
|
||||
:returns: The prepared request tuple with (url, headers, body).
|
||||
"""
|
||||
if not is_secure_transport(token_url):
|
||||
raise InsecureTransportError()
|
||||
|
||||
# do not assign scope to self automatically anymore
|
||||
scope = self.scope if scope is None else scope
|
||||
body = self.prepare_refresh_body(body=body,
|
||||
refresh_token=refresh_token, scope=scope, **kwargs)
|
||||
return token_url, FORM_ENC_HEADERS, body
|
||||
|
||||
def prepare_token_revocation_request(self, revocation_url, token,
|
||||
token_type_hint="access_token", body='', callback=None, **kwargs):
|
||||
"""Prepare a token revocation request.
|
||||
|
||||
:param revocation_url: Provider token revocation endpoint URL.
|
||||
:param token: The access or refresh token to be revoked (string).
|
||||
:param token_type_hint: ``"access_token"`` (default) or
|
||||
``"refresh_token"``. This is optional and if you wish to not pass it you
|
||||
must provide ``token_type_hint=None``.
|
||||
:param body:
|
||||
:param callback: A jsonp callback such as ``package.callback`` to be invoked
|
||||
upon receiving the response. Not that it should not include a () suffix.
|
||||
:param kwargs: Additional parameters to included in the request.
|
||||
:returns: The prepared request tuple with (url, headers, body).
|
||||
|
||||
Note that JSONP request may use GET requests as the parameters will
|
||||
be added to the request URL query as opposed to the request body.
|
||||
|
||||
An example of a revocation request
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /revoke HTTP/1.1
|
||||
Host: server.example.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
|
||||
token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
|
||||
|
||||
An example of a jsonp revocation request
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
|
||||
Host: server.example.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||
|
||||
and an error response
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
package.myCallback({"error":"unsupported_token_type"});
|
||||
|
||||
Note that these requests usually require client credentials, client_id in
|
||||
the case for public clients and provider specific authentication
|
||||
credentials for confidential clients.
|
||||
"""
|
||||
if not is_secure_transport(revocation_url):
|
||||
raise InsecureTransportError()
|
||||
|
||||
return prepare_token_revocation_request(revocation_url, token,
|
||||
token_type_hint=token_type_hint, body=body, callback=callback,
|
||||
**kwargs)
|
||||
|
||||
def parse_request_body_response(self, body, scope=None, **kwargs):
|
||||
"""Parse the JSON response body.
|
||||
|
||||
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`_.
|
||||
|
||||
:param body: The response body from the token request.
|
||||
:param scope: Scopes originally requested. If none is provided, the ones
|
||||
provided in the constructor are used.
|
||||
:return: Dictionary of token parameters.
|
||||
:raises: Warning if scope has changed. :py:class:`oauthlib.oauth2.errors.OAuth2Error`
|
||||
if response is invalid.
|
||||
|
||||
These response are json encoded and could easily be parsed without
|
||||
the assistance of OAuthLib. However, there are a few subtle issues
|
||||
to be aware of regarding the response which are helpfully addressed
|
||||
through the raising of various errors.
|
||||
|
||||
A successful response should always contain
|
||||
|
||||
**access_token**
|
||||
The access token issued by the authorization server. Often
|
||||
a random string.
|
||||
|
||||
**token_type**
|
||||
The type of the token issued as described in `Section 7.1`_.
|
||||
Commonly ``Bearer``.
|
||||
|
||||
While it is not mandated it is recommended that the provider include
|
||||
|
||||
**expires_in**
|
||||
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**
|
||||
Providers may supply this in all responses but are required to only
|
||||
if it has changed since the authorization request.
|
||||
|
||||
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
|
||||
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
|
||||
"""
|
||||
scope = self.scope if scope is None else scope
|
||||
self.token = parse_token_response(body, scope=scope)
|
||||
self.populate_token_attributes(self.token)
|
||||
return self.token
|
||||
|
||||
def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
|
||||
"""Prepare an access token request, using a refresh token.
|
||||
|
||||
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 in the HTTP request entity-body:
|
||||
|
||||
:param refresh_token: REQUIRED. The refresh token issued to the client.
|
||||
:param 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. Note that if none is provided, the ones provided
|
||||
in the constructor are used if any.
|
||||
"""
|
||||
refresh_token = refresh_token or self.refresh_token
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_token_request(self.refresh_token_key, body=body, scope=scope,
|
||||
refresh_token=refresh_token, **kwargs)
|
||||
|
||||
def _add_bearer_token(self, uri, http_method='GET', body=None,
|
||||
headers=None, token_placement=None):
|
||||
"""Add a bearer token to the request uri, body or authorization header."""
|
||||
if token_placement == AUTH_HEADER:
|
||||
headers = tokens.prepare_bearer_headers(self.access_token, headers)
|
||||
|
||||
elif token_placement == URI_QUERY:
|
||||
uri = tokens.prepare_bearer_uri(self.access_token, uri)
|
||||
|
||||
elif token_placement == BODY:
|
||||
body = tokens.prepare_bearer_body(self.access_token, body)
|
||||
|
||||
else:
|
||||
raise ValueError("Invalid token placement.")
|
||||
return uri, headers, body
|
||||
|
||||
def create_code_verifier(self, length):
|
||||
"""Create PKCE **code_verifier** used in computing **code_challenge**.
|
||||
See `RFC7636 Section 4.1`_
|
||||
|
||||
:param length: REQUIRED. The length of the code_verifier.
|
||||
|
||||
The client first creates a code verifier, "code_verifier", for each
|
||||
OAuth 2.0 [RFC6749] Authorization Request, in the following manner:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
code_verifier = high-entropy cryptographic random STRING using the
|
||||
unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
|
||||
from Section 2.3 of [RFC3986], with a minimum length of 43 characters
|
||||
and a maximum length of 128 characters.
|
||||
|
||||
.. _`RFC7636 Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1
|
||||
"""
|
||||
code_verifier = None
|
||||
|
||||
if not length >= 43:
|
||||
raise ValueError("Length must be greater than or equal to 43")
|
||||
|
||||
if not length <= 128:
|
||||
raise ValueError("Length must be less than or equal to 128")
|
||||
|
||||
allowed_characters = re.compile('^[A-Zaa-z0-9-._~]')
|
||||
code_verifier = secrets.token_urlsafe(length)
|
||||
|
||||
if not re.search(allowed_characters, code_verifier):
|
||||
raise ValueError("code_verifier contains invalid characters")
|
||||
|
||||
self.code_verifier = code_verifier
|
||||
|
||||
return code_verifier
|
||||
|
||||
def create_code_challenge(self, code_verifier, code_challenge_method=None):
|
||||
"""Create PKCE **code_challenge** derived from the **code_verifier**.
|
||||
See `RFC7636 Section 4.2`_
|
||||
|
||||
:param code_verifier: REQUIRED. The **code_verifier** generated from `create_code_verifier()`.
|
||||
:param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable values include `S256`. DEFAULT is `plain`.
|
||||
|
||||
The client then creates a code challenge derived from the code
|
||||
verifier by using one of the following transformations on the code
|
||||
verifier::
|
||||
|
||||
plain
|
||||
code_challenge = code_verifier
|
||||
S256
|
||||
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
|
||||
|
||||
If the client is capable of using `S256`, it MUST use `S256`, as
|
||||
`S256` is Mandatory To Implement (MTI) on the server. Clients are
|
||||
permitted to use `plain` only if they cannot support `S256` for some
|
||||
technical reason and know via out-of-band configuration that the
|
||||
server supports `plain`.
|
||||
|
||||
The plain transformation is for compatibility with existing
|
||||
deployments and for constrained environments that can't use the S256 transformation.
|
||||
|
||||
.. _`RFC7636 Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
"""
|
||||
code_challenge = None
|
||||
|
||||
if code_verifier == None:
|
||||
raise ValueError("Invalid code_verifier")
|
||||
|
||||
if code_challenge_method == None:
|
||||
code_challenge_method = "plain"
|
||||
self.code_challenge_method = code_challenge_method
|
||||
code_challenge = code_verifier
|
||||
self.code_challenge = code_challenge
|
||||
|
||||
if code_challenge_method == "S256":
|
||||
h = hashlib.sha256()
|
||||
h.update(code_verifier.encode(encoding='ascii'))
|
||||
sha256_val = h.digest()
|
||||
code_challenge = bytes.decode(base64.urlsafe_b64encode(sha256_val))
|
||||
# replace '+' with '-', '/' with '_', and remove trailing '='
|
||||
code_challenge = code_challenge.replace("+", "-").replace("/", "_").replace("=", "")
|
||||
self.code_challenge = code_challenge
|
||||
|
||||
return code_challenge
|
||||
|
||||
def _add_mac_token(self, uri, http_method='GET', body=None,
|
||||
headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
|
||||
"""Add a MAC token to the request authorization header.
|
||||
|
||||
Warning: MAC token support is experimental as the spec is not yet stable.
|
||||
"""
|
||||
if token_placement != AUTH_HEADER:
|
||||
raise ValueError("Invalid token placement.")
|
||||
|
||||
headers = tokens.prepare_mac_header(self.access_token, uri,
|
||||
self.mac_key, http_method, headers=headers, body=body, ext=ext,
|
||||
hash_algorithm=self.mac_algorithm, **kwargs)
|
||||
return uri, headers, body
|
||||
|
||||
def _populate_attributes(self, response):
|
||||
warnings.warn("Please switch to the public method "
|
||||
"populate_token_attributes.", DeprecationWarning)
|
||||
return self.populate_token_attributes(response)
|
||||
|
||||
def populate_code_attributes(self, response):
|
||||
"""Add attributes from an auth code response to self."""
|
||||
|
||||
if 'code' in response:
|
||||
self.code = response.get('code')
|
||||
|
||||
def populate_token_attributes(self, response):
|
||||
"""Add attributes from a token exchange response to self."""
|
||||
|
||||
if 'access_token' in response:
|
||||
self.access_token = response.get('access_token')
|
||||
|
||||
if 'refresh_token' in response:
|
||||
self.refresh_token = response.get('refresh_token')
|
||||
|
||||
if 'token_type' in response:
|
||||
self.token_type = response.get('token_type')
|
||||
|
||||
if 'expires_in' in response:
|
||||
self.expires_in = response.get('expires_in')
|
||||
self._expires_at = time.time() + int(self.expires_in)
|
||||
|
||||
if 'expires_at' in response:
|
||||
try:
|
||||
self._expires_at = int(response.get('expires_at'))
|
||||
except:
|
||||
self._expires_at = None
|
||||
|
||||
if 'mac_key' in response:
|
||||
self.mac_key = response.get('mac_key')
|
||||
|
||||
if 'mac_algorithm' in response:
|
||||
self.mac_algorithm = response.get('mac_algorithm')
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
from ..parameters import prepare_token_request
|
||||
from .base import Client
|
||||
|
||||
|
||||
class LegacyApplicationClient(Client):
|
||||
|
||||
"""A public client using the resource owner password and username directly.
|
||||
|
||||
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.
|
||||
|
||||
The 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.
|
||||
|
||||
The method through which the client obtains the resource owner
|
||||
credentials is beyond the scope of this specification. The client
|
||||
MUST discard the credentials once an access token has been obtained.
|
||||
"""
|
||||
|
||||
grant_type = 'password'
|
||||
|
||||
def __init__(self, client_id, **kwargs):
|
||||
super().__init__(client_id, **kwargs)
|
||||
|
||||
def prepare_request_body(self, username, password, body='', scope=None,
|
||||
include_client_id=False, **kwargs):
|
||||
"""Add the resource owner password and username to the request body.
|
||||
|
||||
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`_ in the HTTP request entity-body:
|
||||
|
||||
:param username: The resource owner username.
|
||||
:param password: The resource owner password.
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
:param scope: The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
:param include_client_id: `True` to send the `client_id` in the
|
||||
body of the upstream request. This is required
|
||||
if the client is not authenticating with the
|
||||
authorization server as described in
|
||||
`Section 3.2.1`_. False otherwise (default).
|
||||
:type include_client_id: Boolean
|
||||
:param kwargs: Extra credentials to include in the token request.
|
||||
|
||||
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`_.
|
||||
|
||||
The prepared body will include all provided credentials as well as
|
||||
the ``grant_type`` parameter set to ``password``::
|
||||
|
||||
>>> from oauthlib.oauth2 import LegacyApplicationClient
|
||||
>>> client = LegacyApplicationClient('your_id')
|
||||
>>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world'])
|
||||
'grant_type=password&username=foo&scope=hello+world&password=bar'
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
"""
|
||||
kwargs['client_id'] = self.client_id
|
||||
kwargs['include_client_id'] = include_client_id
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_token_request(self.grant_type, body=body, username=username,
|
||||
[PASSWORD-REMOVED], scope=scope, **kwargs)
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
from ..parameters import parse_implicit_response, prepare_grant_uri
|
||||
from .base import Client
|
||||
|
||||
|
||||
class MobileApplicationClient(Client):
|
||||
|
||||
"""A public client utilizing the implicit code grant workflow.
|
||||
|
||||
A user-agent-based application is a public client in which the
|
||||
client code is downloaded from a web server and executes within a
|
||||
user-agent (e.g. web browser) on the device used by the resource
|
||||
owner. Protocol data and credentials are easily accessible (and
|
||||
often visible) to the resource owner. Since such applications
|
||||
reside within the user-agent, they can make seamless use of the
|
||||
user-agent capabilities when requesting authorization.
|
||||
|
||||
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.
|
||||
|
||||
As 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 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.
|
||||
"""
|
||||
|
||||
response_type = 'token'
|
||||
|
||||
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
|
||||
state=None, **kwargs):
|
||||
"""Prepare the implicit 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, per `Appendix B`_:
|
||||
|
||||
:param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
|
||||
and it should have been registered with the OAuth
|
||||
provider prior to use. As described in `Section 3.1.2`_.
|
||||
|
||||
:param scope: OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3`_. These may be any string but are commonly
|
||||
URIs or various categories such as ``videos`` or ``documents``.
|
||||
|
||||
:param 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`_.
|
||||
|
||||
:param kwargs: Extra arguments to include in the request URI.
|
||||
|
||||
In addition to supplied parameters, OAuthLib will append the ``client_id``
|
||||
that was provided in the constructor as well as the mandatory ``response_type``
|
||||
argument, set to ``token``::
|
||||
|
||||
>>> from oauthlib.oauth2 import MobileApplicationClient
|
||||
>>> client = MobileApplicationClient('your_id')
|
||||
>>> client.prepare_request_uri('https://example.com')
|
||||
'https://example.com?client_id=your_id&response_type=token'
|
||||
>>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
|
||||
'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
|
||||
>>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
|
||||
'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures'
|
||||
>>> client.prepare_request_uri('https://example.com', foo='bar')
|
||||
'https://example.com?client_id=your_id&response_type=token&foo=bar'
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
.. _`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
|
||||
"""
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_grant_uri(uri, self.client_id, self.response_type,
|
||||
redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
|
||||
|
||||
def parse_request_uri_response(self, uri, state=None, scope=None):
|
||||
"""Parse the response URI fragment.
|
||||
|
||||
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:
|
||||
|
||||
:param uri: The callback URI that resulted from the user being redirected
|
||||
back from the provider to you, the client.
|
||||
:param state: The state provided in the authorization request.
|
||||
:param scope: The scopes provided in the authorization request.
|
||||
:return: Dictionary of token parameters.
|
||||
:raises: OAuth2Error if response is invalid.
|
||||
|
||||
A successful response should always contain
|
||||
|
||||
**access_token**
|
||||
The access token issued by the authorization server. Often
|
||||
a random string.
|
||||
|
||||
**token_type**
|
||||
The type of the token issued as described in `Section 7.1`_.
|
||||
Commonly ``Bearer``.
|
||||
|
||||
**state**
|
||||
If you provided the state parameter in the authorization phase, then
|
||||
the provider is required to include that exact state value in the
|
||||
response.
|
||||
|
||||
While it is not mandated it is recommended that the provider include
|
||||
|
||||
**expires_in**
|
||||
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**
|
||||
Providers may supply this in all responses but are required to only
|
||||
if it has changed since the authorization request.
|
||||
|
||||
A few example responses can be seen below::
|
||||
|
||||
>>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world'
|
||||
>>> from oauthlib.oauth2 import MobileApplicationClient
|
||||
>>> client = MobileApplicationClient('your_id')
|
||||
>>> client.parse_request_uri_response(response_uri)
|
||||
{
|
||||
'access_token': 'sdlfkj452',
|
||||
'token_type': 'Bearer',
|
||||
'state': 'ss345asyht',
|
||||
'scope': [u'hello', u'world']
|
||||
}
|
||||
>>> client.parse_request_uri_response(response_uri, state='other')
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response
|
||||
**scope**
|
||||
File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response
|
||||
raise ValueError("Mismatching or missing state in params.")
|
||||
ValueError: Mismatching or missing state in params.
|
||||
>>> def alert_scope_changed(message, old, new):
|
||||
... print(message, old, new)
|
||||
...
|
||||
>>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
|
||||
>>> client.parse_request_body_response(response_body, scope=['other'])
|
||||
('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
|
||||
|
||||
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
"""
|
||||
scope = self.scope if scope is None else scope
|
||||
self.token = parse_implicit_response(uri, state=state, scope=scope)
|
||||
self.populate_token_attributes(self.token)
|
||||
return self.token
|
||||
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import time
|
||||
|
||||
from oauthlib.common import to_unicode
|
||||
|
||||
from ..parameters import prepare_token_request
|
||||
from .base import Client
|
||||
|
||||
|
||||
class ServiceApplicationClient(Client):
|
||||
"""A public client utilizing the JWT bearer grant.
|
||||
|
||||
JWT bearer tokes can be used to request an access token when a client
|
||||
wishes to utilize an existing trust relationship, expressed through the
|
||||
semantics of (and digital signature or keyed message digest calculated
|
||||
over) the JWT, without a direct user approval step at the authorization
|
||||
server.
|
||||
|
||||
This grant type does not involve an authorization step. It may be
|
||||
used by both public and confidential clients.
|
||||
"""
|
||||
|
||||
grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
||||
|
||||
def __init__(self, client_id, private_key=None, subject=None, issuer=None,
|
||||
audience=None, **kwargs):
|
||||
"""Initialize a JWT client with defaults for implicit use later.
|
||||
|
||||
:param client_id: Client identifier given by the OAuth provider upon
|
||||
registration.
|
||||
|
||||
:param private_key: Private key used for signing and encrypting.
|
||||
Must be given as a string.
|
||||
|
||||
:param subject: The principal that is the subject of the JWT, i.e.
|
||||
which user is the token requested on behalf of.
|
||||
For example, ``foo@example.com.
|
||||
|
||||
:param issuer: The JWT MUST contain an "iss" (issuer) claim that
|
||||
contains a unique identifier for the entity that issued
|
||||
the JWT. For example, ``your-client@provider.com``.
|
||||
|
||||
:param audience: A value identifying the authorization server as an
|
||||
intended audience, e.g.
|
||||
``https://provider.com/oauth2/token``.
|
||||
|
||||
:param kwargs: Additional arguments to pass to base client, such as
|
||||
state and token. See ``Client.__init__.__doc__`` for
|
||||
details.
|
||||
"""
|
||||
super().__init__(client_id, **kwargs)
|
||||
self.private_key = private_key
|
||||
self.subject = subject
|
||||
self.issuer = issuer
|
||||
self.audience = audience
|
||||
|
||||
def prepare_request_body(self,
|
||||
private_key=None,
|
||||
subject=None,
|
||||
issuer=None,
|
||||
audience=None,
|
||||
expires_at=None,
|
||||
issued_at=None,
|
||||
extra_claims=None,
|
||||
body='',
|
||||
scope=None,
|
||||
include_client_id=False,
|
||||
**kwargs):
|
||||
"""Create and add a JWT assertion to the request body.
|
||||
|
||||
:param private_key: Private key used for signing and encrypting.
|
||||
Must be given as a string.
|
||||
|
||||
:param subject: (sub) The principal that is the subject of the JWT,
|
||||
i.e. which user is the token requested on behalf of.
|
||||
For example, ``foo@example.com.
|
||||
|
||||
:param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that
|
||||
contains a unique identifier for the entity that issued
|
||||
the JWT. For example, ``your-client@provider.com``.
|
||||
|
||||
:param audience: (aud) A value identifying the authorization server as an
|
||||
intended audience, e.g.
|
||||
``https://provider.com/oauth2/token``.
|
||||
|
||||
:param expires_at: A unix expiration timestamp for the JWT. Defaults
|
||||
to an hour from now, i.e. ``time.time() + 3600``.
|
||||
|
||||
:param issued_at: A unix timestamp of when the JWT was created.
|
||||
Defaults to now, i.e. ``time.time()``.
|
||||
|
||||
:param extra_claims: A dict of additional claims to include in the JWT.
|
||||
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
|
||||
:param scope: The scope of the access request.
|
||||
|
||||
:param include_client_id: `True` to send the `client_id` in the
|
||||
body of the upstream request. This is required
|
||||
if the client is not authenticating with the
|
||||
authorization server as described in
|
||||
`Section 3.2.1`_. False otherwise (default).
|
||||
:type include_client_id: Boolean
|
||||
|
||||
:param not_before: A unix timestamp after which the JWT may be used.
|
||||
Not included unless provided. *
|
||||
|
||||
:param jwt_id: A unique JWT token identifier. Not included unless
|
||||
provided. *
|
||||
|
||||
:param kwargs: Extra credentials to include in the token request.
|
||||
|
||||
Parameters marked with a `*` above are not explicit arguments in the
|
||||
function signature, but are specially documented arguments for items
|
||||
appearing in the generic `**kwargs` keyworded input.
|
||||
|
||||
The "scope" parameter may be used, as defined in the Assertion
|
||||
Framework for OAuth 2.0 Client Authentication and Authorization Grants
|
||||
[I-D.ietf-oauth-assertions] specification, to indicate the requested
|
||||
scope.
|
||||
|
||||
Authentication of the client is optional, as described in
|
||||
`Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the
|
||||
"client_id" is only needed when a form of client authentication that
|
||||
relies on the parameter is used.
|
||||
|
||||
The following non-normative example demonstrates an Access Token
|
||||
Request with a JWT as an authorization grant (with extra line breaks
|
||||
for display purposes only):
|
||||
|
||||
.. code-block: http
|
||||
|
||||
POST /token.oauth2 HTTP/1.1
|
||||
Host: as.example.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
|
||||
&assertion=eyJhbGciOiJFUzI1NiJ9.
|
||||
eyJpc3Mi[...omitted for brevity...].
|
||||
J9l-ZhwP[...omitted for brevity...]
|
||||
|
||||
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
"""
|
||||
import jwt
|
||||
|
||||
key = private_key or self.private_key
|
||||
if not key:
|
||||
raise ValueError('An encryption key must be supplied to make JWT'
|
||||
' token requests.')
|
||||
claim = {
|
||||
'iss': issuer or self.issuer,
|
||||
'aud': audience or self.audience,
|
||||
'sub': subject or self.subject,
|
||||
'exp': int(expires_at or time.time() + 3600),
|
||||
'iat': int(issued_at or time.time()),
|
||||
}
|
||||
|
||||
for attr in ('iss', 'aud', 'sub'):
|
||||
if claim[attr] is None:
|
||||
raise ValueError(
|
||||
'Claim must include %s but none was given.' % attr)
|
||||
|
||||
if 'not_before' in kwargs:
|
||||
claim['nbf'] = kwargs.pop('not_before')
|
||||
|
||||
if 'jwt_id' in kwargs:
|
||||
claim['jti'] = kwargs.pop('jwt_id')
|
||||
|
||||
claim.update(extra_claims or {})
|
||||
|
||||
assertion = jwt.encode(claim, key, 'RS256')
|
||||
assertion = to_unicode(assertion)
|
||||
|
||||
kwargs['client_id'] = self.client_id
|
||||
kwargs['include_client_id'] = include_client_id
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_token_request(self.grant_type,
|
||||
body=body,
|
||||
assertion=assertion,
|
||||
scope=scope,
|
||||
**kwargs)
|
||||
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
from ..parameters import (
|
||||
parse_authorization_code_response, prepare_grant_uri,
|
||||
prepare_token_request,
|
||||
)
|
||||
from .base import Client
|
||||
|
||||
|
||||
class WebApplicationClient(Client):
|
||||
|
||||
"""A client utilizing the authorization code grant workflow.
|
||||
|
||||
A web application is a confidential client running on a web
|
||||
server. Resource owners access the client via an HTML user
|
||||
interface rendered in a user-agent on the device used by the
|
||||
resource owner. The client credentials as well as any access
|
||||
token issued to the client are stored on the web server and are
|
||||
not exposed to or accessible by the resource owner.
|
||||
|
||||
The authorization code grant type is used to obtain both access
|
||||
tokens and refresh tokens and is optimized for confidential clients.
|
||||
As 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.
|
||||
"""
|
||||
|
||||
grant_type = 'authorization_code'
|
||||
|
||||
def __init__(self, client_id, code=None, **kwargs):
|
||||
super().__init__(client_id, **kwargs)
|
||||
self.code = code
|
||||
|
||||
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
|
||||
state=None, code_challenge=None, code_challenge_method='plain', **kwargs):
|
||||
"""Prepare the authorization code 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, per `Appendix B`_:
|
||||
|
||||
:param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
|
||||
and it should have been registered with the OAuth
|
||||
provider prior to use. As described in `Section 3.1.2`_.
|
||||
|
||||
:param scope: OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3`_. These may be any string but are commonly
|
||||
URIs or various categories such as ``videos`` or ``documents``.
|
||||
|
||||
:param 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`_.
|
||||
|
||||
:param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced.
|
||||
A challenge derived from the code_verifier that is sent in the
|
||||
authorization request, to be verified against later.
|
||||
|
||||
:param code_challenge_method: OPTIONAL. PKCE parameter. A method that was used to derive code challenge.
|
||||
Defaults to "plain" if not present in the request.
|
||||
|
||||
:param kwargs: Extra arguments to include in the request URI.
|
||||
|
||||
In addition to supplied parameters, OAuthLib will append the ``client_id``
|
||||
that was provided in the constructor as well as the mandatory ``response_type``
|
||||
argument, set to ``code``::
|
||||
|
||||
>>> from oauthlib.oauth2 import WebApplicationClient
|
||||
>>> client = WebApplicationClient('your_id')
|
||||
>>> client.prepare_request_uri('https://example.com')
|
||||
'https://example.com?client_id=your_id&response_type=code'
|
||||
>>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
|
||||
'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
|
||||
>>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
|
||||
'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures'
|
||||
>>> client.prepare_request_uri('https://example.com', code_challenge='kjasBS523KdkAILD2k78NdcJSk2k3KHG6')
|
||||
'https://example.com?client_id=your_id&response_type=code&code_[AWS-SECRET-REMOVED]HG6'
|
||||
>>> client.prepare_request_uri('https://example.com', code_challenge_method='S256')
|
||||
'https://example.com?client_id=your_id&response_type=code&code_challenge_method=S256'
|
||||
>>> client.prepare_request_uri('https://example.com', foo='bar')
|
||||
'https://example.com?client_id=your_id&response_type=code&foo=bar'
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
.. _`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
|
||||
"""
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_grant_uri(uri, self.client_id, 'code',
|
||||
redirect_uri=redirect_uri, scope=scope, state=state, code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method, **kwargs)
|
||||
|
||||
def prepare_request_body(self, code=None, redirect_uri=None, body='',
|
||||
include_client_id=True, code_verifier=None, **kwargs):
|
||||
"""Prepare the access token request body.
|
||||
|
||||
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 code: REQUIRED. The authorization code received from the
|
||||
authorization server.
|
||||
|
||||
:param 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.
|
||||
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
|
||||
:param include_client_id: `True` (default) to send the `client_id` in the
|
||||
body of the upstream request. This is required
|
||||
if the client is not authenticating with the
|
||||
authorization server as described in `Section 3.2.1`_.
|
||||
:type include_client_id: Boolean
|
||||
|
||||
:param code_verifier: OPTIONAL. A cryptographically random string that is used to correlate the
|
||||
authorization request to the token request.
|
||||
|
||||
:param kwargs: Extra parameters to include in the token request.
|
||||
|
||||
In addition OAuthLib will add the ``grant_type`` parameter set to
|
||||
``authorization_code``.
|
||||
|
||||
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`_::
|
||||
|
||||
>>> from oauthlib.oauth2 import WebApplicationClient
|
||||
>>> client = WebApplicationClient('your_id')
|
||||
>>> client.prepare_request_body(code='sh35ksdf09sf')
|
||||
'grant_type=authorization_code&code=sh35ksdf09sf'
|
||||
>>> client.prepare_request_body(code_verifier='KB46DCKJ873NCGXK5GD682NHDKK34GR')
|
||||
'grant_type=authorization_code&code_[AWS-SECRET-REMOVED]'
|
||||
>>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
|
||||
'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
|
||||
|
||||
`Section 3.2.1` also states:
|
||||
In the "authorization_code" "grant_type" request to the token
|
||||
endpoint, an unauthenticated client MUST send its "client_id" to
|
||||
prevent itself from inadvertently accepting a code intended for a
|
||||
client with a different "client_id". This protects the client from
|
||||
substitution of the authentication code. (It provides no additional
|
||||
security for the protected resource.)
|
||||
|
||||
.. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
|
||||
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
"""
|
||||
code = code or self.code
|
||||
if 'client_id' in kwargs:
|
||||
warnings.warn("`client_id` has been deprecated in favor of "
|
||||
"`include_client_id`, a boolean value which will "
|
||||
"include the already configured `self.client_id`.",
|
||||
DeprecationWarning)
|
||||
if kwargs['client_id'] != self.client_id:
|
||||
raise ValueError("`client_id` was supplied as an argument, but "
|
||||
"it does not match `self.client_id`")
|
||||
|
||||
kwargs['client_id'] = self.client_id
|
||||
kwargs['include_client_id'] = include_client_id
|
||||
return prepare_token_request(self.grant_type, code=code, body=body,
|
||||
redirect_uri=redirect_uri, code_verifier=code_verifier, **kwargs)
|
||||
|
||||
def parse_request_uri_response(self, uri, state=None):
|
||||
"""Parse the URI query for code and state.
|
||||
|
||||
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:
|
||||
|
||||
:param uri: The callback URI that resulted from the user being redirected
|
||||
back from the provider to you, the client.
|
||||
:param state: The state provided in the authorization request.
|
||||
|
||||
**code**
|
||||
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**
|
||||
If the "state" parameter was present in the authorization request.
|
||||
|
||||
This method is mainly intended to enforce strict state checking with
|
||||
the added benefit of easily extracting parameters from the URI::
|
||||
|
||||
>>> from oauthlib.oauth2 import WebApplicationClient
|
||||
>>> client = WebApplicationClient('your_id')
|
||||
>>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45'
|
||||
>>> client.parse_request_uri_response(uri, state='sfetw45')
|
||||
{'state': 'sfetw45', 'code': 'sdfkjh345'}
|
||||
>>> client.parse_request_uri_response(uri, state='other')
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response
|
||||
back from the provider to you, the client.
|
||||
File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response
|
||||
raise MismatchingStateError()
|
||||
oauthlib.oauth2.rfc6749.errors.MismatchingStateError
|
||||
"""
|
||||
response = parse_authorization_code_response(uri, state=state)
|
||||
self.populate_code_attributes(response)
|
||||
return response
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
from .authorization import AuthorizationEndpoint
|
||||
from .introspect import IntrospectEndpoint
|
||||
from .metadata import MetadataEndpoint
|
||||
from .pre_configured import (
|
||||
BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
|
||||
Server, WebApplicationServer,
|
||||
)
|
||||
from .resource import ResourceEndpoint
|
||||
from .revocation import RevocationEndpoint
|
||||
from .token import TokenEndpoint
|
||||
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,114 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib.common import Request
|
||||
from oauthlib.oauth2.rfc6749 import utils
|
||||
|
||||
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthorizationEndpoint(BaseEndpoint):
|
||||
|
||||
"""Authorization endpoint - used by the client to obtain authorization
|
||||
from the resource owner via user-agent redirection.
|
||||
|
||||
The authorization endpoint is used to interact with the resource
|
||||
owner and obtain an authorization grant. The authorization server
|
||||
MUST first verify the identity of the resource owner. The way in
|
||||
which the authorization server authenticates the resource owner (e.g.
|
||||
username and password login, session cookies) is beyond the scope of
|
||||
this specification.
|
||||
|
||||
The endpoint URI MAY include an "application/x-www-form-urlencoded"
|
||||
formatted (per `Appendix B`_) query component,
|
||||
which MUST be retained when adding additional query parameters. The
|
||||
endpoint URI MUST NOT include a fragment component::
|
||||
|
||||
https://example.com/path?query=component # OK
|
||||
https://example.com/path?query=component#fragment # Not OK
|
||||
|
||||
Since requests to the authorization endpoint result in user
|
||||
authentication and the transmission of clear-text credentials (in the
|
||||
HTTP response), the authorization server MUST require the use of TLS
|
||||
as described in Section 1.6 when sending requests to the
|
||||
authorization endpoint::
|
||||
|
||||
# We will deny any request which URI schema is not with https
|
||||
|
||||
The authorization server MUST support the use of the HTTP "GET"
|
||||
method [RFC2616] for the authorization endpoint, and MAY support the
|
||||
use of the "POST" method as well::
|
||||
|
||||
# HTTP method is currently not enforced
|
||||
|
||||
Parameters sent without a value MUST be treated as if they were
|
||||
omitted from the request. The authorization server MUST ignore
|
||||
unrecognized request parameters. Request and response parameters
|
||||
MUST NOT be included more than once::
|
||||
|
||||
# Enforced through the design of oauthlib.common.Request
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
"""
|
||||
|
||||
def __init__(self, default_response_type, default_token_type,
|
||||
response_types):
|
||||
BaseEndpoint.__init__(self)
|
||||
self._response_types = response_types
|
||||
self._default_response_type = default_response_type
|
||||
self._default_token_type = default_token_type
|
||||
|
||||
@property
|
||||
def response_types(self):
|
||||
return self._response_types
|
||||
|
||||
@property
|
||||
def default_response_type(self):
|
||||
return self._default_response_type
|
||||
|
||||
@property
|
||||
def default_response_type_handler(self):
|
||||
return self.response_types.get(self.default_response_type)
|
||||
|
||||
@property
|
||||
def default_token_type(self):
|
||||
return self._default_token_type
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def create_authorization_response(self, uri, http_method='GET', body=None,
|
||||
headers=None, scopes=None, credentials=None):
|
||||
"""Extract response_type and route to the designated handler."""
|
||||
request = Request(
|
||||
uri, http_method=http_method, body=body, headers=headers)
|
||||
request.scopes = scopes
|
||||
# TODO: decide whether this should be a required argument
|
||||
request.user = None # TODO: explain this in docs
|
||||
for k, v in (credentials or {}).items():
|
||||
setattr(request, k, v)
|
||||
response_type_handler = self.response_types.get(
|
||||
request.response_type, self.default_response_type_handler)
|
||||
log.debug('Dispatching response_type %s request to %r.',
|
||||
request.response_type, response_type_handler)
|
||||
return response_type_handler.create_authorization_response(
|
||||
request, self.default_token_type)
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def validate_authorization_request(self, uri, http_method='GET', body=None,
|
||||
headers=None):
|
||||
"""Extract response_type and route to the designated handler."""
|
||||
request = Request(
|
||||
uri, http_method=http_method, body=body, headers=headers)
|
||||
|
||||
request.scopes = utils.scope_to_list(request.scope)
|
||||
|
||||
response_type_handler = self.response_types.get(
|
||||
request.response_type, self.default_response_type_handler)
|
||||
return response_type_handler.validate_authorization_request(request)
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import functools
|
||||
import logging
|
||||
|
||||
from ..errors import (
|
||||
FatalClientError, InvalidClientError, InvalidRequestError, OAuth2Error,
|
||||
ServerError, TemporarilyUnavailableError, UnsupportedTokenTypeError,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseEndpoint:
|
||||
|
||||
def __init__(self):
|
||||
self._available = True
|
||||
self._catch_errors = False
|
||||
self._valid_request_methods = None
|
||||
|
||||
@property
|
||||
def valid_request_methods(self):
|
||||
return self._valid_request_methods
|
||||
|
||||
@valid_request_methods.setter
|
||||
def valid_request_methods(self, valid_request_methods):
|
||||
if valid_request_methods is not None:
|
||||
valid_request_methods = [x.upper() for x in valid_request_methods]
|
||||
self._valid_request_methods = valid_request_methods
|
||||
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._available
|
||||
|
||||
@available.setter
|
||||
def available(self, available):
|
||||
self._available = available
|
||||
|
||||
@property
|
||||
def catch_errors(self):
|
||||
return self._catch_errors
|
||||
|
||||
@catch_errors.setter
|
||||
def catch_errors(self, catch_errors):
|
||||
self._catch_errors = catch_errors
|
||||
|
||||
def _raise_on_missing_token(self, request):
|
||||
"""Raise error on missing token."""
|
||||
if not request.token:
|
||||
raise InvalidRequestError(request=request,
|
||||
description='Missing token parameter.')
|
||||
def _raise_on_invalid_client(self, request):
|
||||
"""Raise on failed client authentication."""
|
||||
if self.request_validator.client_authentication_required(request):
|
||||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise InvalidClientError(request=request)
|
||||
elif not self.request_validator.authenticate_client_id(request.client_id, request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise InvalidClientError(request=request)
|
||||
|
||||
def _raise_on_unsupported_token(self, request):
|
||||
"""Raise on unsupported tokens."""
|
||||
if (request.token_type_hint and
|
||||
request.token_type_hint in self.valid_token_types and
|
||||
request.token_type_hint not in self.supported_token_types):
|
||||
raise UnsupportedTokenTypeError(request=request)
|
||||
|
||||
def _raise_on_bad_method(self, request):
|
||||
if self.valid_request_methods is None:
|
||||
raise ValueError('Configure "valid_request_methods" property first')
|
||||
if request.http_method.upper() not in self.valid_request_methods:
|
||||
raise InvalidRequestError(request=request,
|
||||
description=('Unsupported request method %s' % request.http_method.upper()))
|
||||
|
||||
def _raise_on_bad_post_request(self, request):
|
||||
"""Raise if invalid POST request received
|
||||
"""
|
||||
if request.http_method.upper() == 'POST':
|
||||
query_params = request.uri_query or ""
|
||||
if query_params:
|
||||
raise InvalidRequestError(request=request,
|
||||
description=('URL query parameters are not allowed'))
|
||||
|
||||
def catch_errors_and_unavailability(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(endpoint, uri, *args, **kwargs):
|
||||
if not endpoint.available:
|
||||
e = TemporarilyUnavailableError()
|
||||
log.info('Endpoint unavailable, ignoring request %s.' % uri)
|
||||
return {}, e.json, 503
|
||||
|
||||
if endpoint.catch_errors:
|
||||
try:
|
||||
return f(endpoint, uri, *args, **kwargs)
|
||||
except OAuth2Error:
|
||||
raise
|
||||
except FatalClientError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error = ServerError()
|
||||
log.warning(
|
||||
'Exception caught while processing request, %s.' % e)
|
||||
return {}, error.json, 500
|
||||
else:
|
||||
return f(endpoint, uri, *args, **kwargs)
|
||||
return wrapper
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.endpoint.introspect
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An implementation of the OAuth 2.0 `Token Introspection`.
|
||||
|
||||
.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from oauthlib.common import Request
|
||||
|
||||
from ..errors import OAuth2Error
|
||||
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntrospectEndpoint(BaseEndpoint):
|
||||
|
||||
"""Introspect token endpoint.
|
||||
|
||||
This endpoint defines a method to query an OAuth 2.0 authorization
|
||||
server to determine the active state of an OAuth 2.0 token and to
|
||||
determine meta-information about this token. OAuth 2.0 deployments
|
||||
can use this method to convey information about the authorization
|
||||
context of the token from the authorization server to the protected
|
||||
resource.
|
||||
|
||||
To prevent the values of access tokens from leaking into
|
||||
server-side logs via query parameters, an authorization server
|
||||
offering token introspection MAY disallow the use of HTTP GET on
|
||||
the introspection endpoint and instead require the HTTP POST method
|
||||
to be used at the introspection endpoint.
|
||||
"""
|
||||
|
||||
valid_token_types = ('access_token', 'refresh_token')
|
||||
valid_request_methods = ('POST',)
|
||||
|
||||
def __init__(self, request_validator, supported_token_types=None):
|
||||
BaseEndpoint.__init__(self)
|
||||
self.request_validator = request_validator
|
||||
self.supported_token_types = (
|
||||
supported_token_types or self.valid_token_types)
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def create_introspect_response(self, uri, http_method='POST', body=None,
|
||||
headers=None):
|
||||
"""Create introspect valid or invalid response
|
||||
|
||||
If the authorization server is unable to determine the state
|
||||
of the token without additional information, it SHOULD return
|
||||
an introspection response indicating the token is not active
|
||||
as described in Section 2.2.
|
||||
"""
|
||||
resp_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
'Pragma': 'no-cache',
|
||||
}
|
||||
request = Request(uri, http_method, body, headers)
|
||||
try:
|
||||
self.validate_introspect_request(request)
|
||||
log.debug('Token introspect valid for %r.', request)
|
||||
except OAuth2Error as e:
|
||||
log.debug('Client error during validation of %r. %r.', request, e)
|
||||
resp_headers.update(e.headers)
|
||||
return resp_headers, e.json, e.status_code
|
||||
|
||||
claims = self.request_validator.introspect_token(
|
||||
request.token,
|
||||
request.token_type_hint,
|
||||
request
|
||||
)
|
||||
if claims is None:
|
||||
return resp_headers, json.dumps(dict(active=False)), 200
|
||||
if "active" in claims:
|
||||
claims.pop("active")
|
||||
return resp_headers, json.dumps(dict(active=True, **claims)), 200
|
||||
|
||||
def validate_introspect_request(self, request):
|
||||
"""Ensure the request is valid.
|
||||
|
||||
The protected resource calls the introspection endpoint using
|
||||
an HTTP POST request with parameters sent as
|
||||
"application/x-www-form-urlencoded".
|
||||
|
||||
* token REQUIRED. The string value of the token.
|
||||
* token_type_hint OPTIONAL.
|
||||
|
||||
A hint about the type of the token submitted for
|
||||
introspection. The protected resource MAY pass this parameter to
|
||||
help the authorization server optimize the token lookup. If the
|
||||
server is unable to locate the token using the given hint, it MUST
|
||||
extend its search across all of its supported token types. An
|
||||
authorization server MAY ignore this parameter, particularly if it
|
||||
is able to detect the token type automatically.
|
||||
|
||||
* access_token: An Access Token as defined in [`RFC6749`], `section 1.4`_
|
||||
* refresh_token: A Refresh Token as defined in [`RFC6749`], `section 1.5`_
|
||||
|
||||
The introspection endpoint MAY accept other OPTIONAL
|
||||
parameters to provide further context to the query. For
|
||||
instance, an authorization server may desire to know the IP
|
||||
address of the client accessing the protected resource to
|
||||
determine if the correct client is likely to be presenting the
|
||||
token. The definition of this or any other parameters are
|
||||
outside the scope of this specification, to be defined by
|
||||
service documentation or extensions to this specification.
|
||||
|
||||
.. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
|
||||
.. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
|
||||
.. _`RFC6749`: http://tools.ietf.org/html/rfc6749
|
||||
"""
|
||||
self._raise_on_bad_method(request)
|
||||
self._raise_on_bad_post_request(request)
|
||||
self._raise_on_missing_token(request)
|
||||
self._raise_on_invalid_client(request)
|
||||
self._raise_on_unsupported_token(request)
|
||||
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.endpoint.metadata
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An implementation of the `OAuth 2.0 Authorization Server Metadata`.
|
||||
|
||||
.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .. import grant_types, utils
|
||||
from .authorization import AuthorizationEndpoint
|
||||
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||
from .introspect import IntrospectEndpoint
|
||||
from .revocation import RevocationEndpoint
|
||||
from .token import TokenEndpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetadataEndpoint(BaseEndpoint):
|
||||
|
||||
"""OAuth2.0 Authorization Server Metadata endpoint.
|
||||
|
||||
This specification generalizes the metadata format defined by
|
||||
`OpenID Connect Discovery 1.0` in a way that is compatible
|
||||
with OpenID Connect Discovery while being applicable to a wider set
|
||||
of OAuth 2.0 use cases. This is intentionally parallel to the way
|
||||
that OAuth 2.0 Dynamic Client Registration Protocol [`RFC7591`_]
|
||||
generalized the dynamic client registration mechanisms defined by
|
||||
OpenID Connect Dynamic Client Registration 1.0
|
||||
in a way that is compatible with it.
|
||||
|
||||
.. _`OpenID Connect Discovery 1.0`: https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
.. _`RFC7591`: https://tools.ietf.org/html/rfc7591
|
||||
"""
|
||||
|
||||
def __init__(self, endpoints, claims={}, raise_errors=True):
|
||||
assert isinstance(claims, dict)
|
||||
for endpoint in endpoints:
|
||||
assert isinstance(endpoint, BaseEndpoint)
|
||||
|
||||
BaseEndpoint.__init__(self)
|
||||
self.raise_errors = raise_errors
|
||||
self.endpoints = endpoints
|
||||
self.initial_claims = claims
|
||||
self.claims = self.validate_metadata_server()
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def create_metadata_response(self, uri, http_method='GET', body=None,
|
||||
headers=None):
|
||||
"""Create metadata response
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
return headers, json.dumps(self.claims), 200
|
||||
|
||||
def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False):
|
||||
if not self.raise_errors:
|
||||
return
|
||||
|
||||
if key not in array:
|
||||
if is_required:
|
||||
raise ValueError("key {} is a mandatory metadata.".format(key))
|
||||
|
||||
elif is_issuer:
|
||||
if not utils.is_secure_transport(array[key]):
|
||||
raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key]))
|
||||
if "?" in array[key] or "&" in array[key] or "#" in array[key]:
|
||||
raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key]))
|
||||
|
||||
elif is_url:
|
||||
if not array[key].startswith("http"):
|
||||
raise ValueError("key {}: {} must be an URL".format(key, array[key]))
|
||||
|
||||
elif is_list:
|
||||
if not isinstance(array[key], list):
|
||||
raise ValueError("key {}: {} must be an Array".format(key, array[key]))
|
||||
for elem in array[key]:
|
||||
if not isinstance(elem, str):
|
||||
raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem))
|
||||
|
||||
def validate_metadata_token(self, claims, endpoint):
|
||||
"""
|
||||
If the token endpoint is used in the grant type, the value of this
|
||||
parameter MUST be the same as the value of the "grant_type"
|
||||
parameter passed to the token endpoint defined in the grant type
|
||||
definition.
|
||||
"""
|
||||
self._grant_types.extend(endpoint._grant_types.keys())
|
||||
claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"])
|
||||
|
||||
self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True)
|
||||
self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True)
|
||||
self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True)
|
||||
|
||||
def validate_metadata_authorization(self, claims, endpoint):
|
||||
claims.setdefault("response_types_supported",
|
||||
list(filter(lambda x: x != "none", endpoint._response_types.keys())))
|
||||
claims.setdefault("response_modes_supported", ["query", "fragment"])
|
||||
|
||||
# The OAuth2.0 Implicit flow is defined as a "grant type" but it is not
|
||||
# using the "token" endpoint, as such, we have to add it explicitly to
|
||||
# the list of "grant_types_supported" when enabled.
|
||||
if "token" in claims["response_types_supported"]:
|
||||
self._grant_types.append("implicit")
|
||||
|
||||
self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True)
|
||||
self.validate_metadata(claims, "response_modes_supported", is_list=True)
|
||||
if "code" in claims["response_types_supported"]:
|
||||
code_grant = endpoint._response_types["code"]
|
||||
if not isinstance(code_grant, grant_types.AuthorizationCodeGrant) and hasattr(code_grant, "default_grant"):
|
||||
code_grant = code_grant.default_grant
|
||||
|
||||
claims.setdefault("code_challenge_methods_supported",
|
||||
list(code_grant._code_challenge_methods.keys()))
|
||||
self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True)
|
||||
self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True)
|
||||
|
||||
def validate_metadata_revocation(self, claims, endpoint):
|
||||
claims.setdefault("revocation_endpoint_auth_methods_supported",
|
||||
["client_secret_post", "client_secret_basic"])
|
||||
|
||||
self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True)
|
||||
self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True)
|
||||
self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True)
|
||||
|
||||
def validate_metadata_introspection(self, claims, endpoint):
|
||||
claims.setdefault("introspection_endpoint_auth_methods_supported",
|
||||
["client_secret_post", "client_secret_basic"])
|
||||
|
||||
self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True)
|
||||
self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True)
|
||||
self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True)
|
||||
|
||||
def validate_metadata_server(self):
|
||||
"""
|
||||
Authorization servers can have metadata describing their
|
||||
configuration. The following authorization server metadata values
|
||||
are used by this specification. More details can be found in
|
||||
`RFC8414 section 2`_ :
|
||||
|
||||
issuer
|
||||
REQUIRED
|
||||
|
||||
authorization_endpoint
|
||||
URL of the authorization server's authorization endpoint
|
||||
[`RFC6749#Authorization`_]. This is REQUIRED unless no grant types are supported
|
||||
that use the authorization endpoint.
|
||||
|
||||
token_endpoint
|
||||
URL of the authorization server's token endpoint [`RFC6749#Token`_]. This
|
||||
is REQUIRED unless only the implicit grant type is supported.
|
||||
|
||||
scopes_supported
|
||||
RECOMMENDED.
|
||||
|
||||
response_types_supported
|
||||
REQUIRED.
|
||||
|
||||
Other OPTIONAL fields:
|
||||
jwks_uri,
|
||||
registration_endpoint,
|
||||
response_modes_supported
|
||||
|
||||
grant_types_supported
|
||||
OPTIONAL. JSON array containing a list of the OAuth 2.0 grant
|
||||
type values that this authorization server supports. The array
|
||||
values used are the same as those used with the "grant_types"
|
||||
parameter defined by "OAuth 2.0 Dynamic Client Registration
|
||||
Protocol" [`RFC7591`_]. If omitted, the default value is
|
||||
"["authorization_code", "implicit"]".
|
||||
|
||||
token_endpoint_auth_methods_supported
|
||||
|
||||
token_endpoint_auth_signing_alg_values_supported
|
||||
|
||||
service_documentation
|
||||
|
||||
ui_locales_supported
|
||||
|
||||
op_policy_uri
|
||||
|
||||
op_tos_uri
|
||||
|
||||
revocation_endpoint
|
||||
|
||||
revocation_endpoint_auth_methods_supported
|
||||
|
||||
revocation_endpoint_auth_signing_alg_values_supported
|
||||
|
||||
introspection_endpoint
|
||||
|
||||
introspection_endpoint_auth_methods_supported
|
||||
|
||||
introspection_endpoint_auth_signing_alg_values_supported
|
||||
|
||||
code_challenge_methods_supported
|
||||
|
||||
Additional authorization server metadata parameters MAY also be used.
|
||||
Some are defined by other specifications, such as OpenID Connect
|
||||
Discovery 1.0 [`OpenID.Discovery`_].
|
||||
|
||||
.. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2
|
||||
.. _`RFC6749#Authorization`: https://tools.ietf.org/html/rfc6749#section-3.1
|
||||
.. _`RFC6749#Token`: https://tools.ietf.org/html/rfc6749#section-3.2
|
||||
.. _`RFC7591`: https://tools.ietf.org/html/rfc7591
|
||||
.. _`OpenID.Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
"""
|
||||
claims = copy.deepcopy(self.initial_claims)
|
||||
self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True)
|
||||
self.validate_metadata(claims, "jwks_uri", is_url=True)
|
||||
self.validate_metadata(claims, "scopes_supported", is_list=True)
|
||||
self.validate_metadata(claims, "service_documentation", is_url=True)
|
||||
self.validate_metadata(claims, "ui_locales_supported", is_list=True)
|
||||
self.validate_metadata(claims, "op_policy_uri", is_url=True)
|
||||
self.validate_metadata(claims, "op_tos_uri", is_url=True)
|
||||
|
||||
self._grant_types = []
|
||||
for endpoint in self.endpoints:
|
||||
if isinstance(endpoint, TokenEndpoint):
|
||||
self.validate_metadata_token(claims, endpoint)
|
||||
if isinstance(endpoint, AuthorizationEndpoint):
|
||||
self.validate_metadata_authorization(claims, endpoint)
|
||||
if isinstance(endpoint, RevocationEndpoint):
|
||||
self.validate_metadata_revocation(claims, endpoint)
|
||||
if isinstance(endpoint, IntrospectEndpoint):
|
||||
self.validate_metadata_introspection(claims, endpoint)
|
||||
|
||||
# "grant_types_supported" is a combination of all OAuth2 grant types
|
||||
# allowed in the current provider implementation.
|
||||
claims.setdefault("grant_types_supported", self._grant_types)
|
||||
self.validate_metadata(claims, "grant_types_supported", is_list=True)
|
||||
return claims
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.endpoints.pre_configured
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various endpoints needed
|
||||
for providing OAuth 2.0 RFC6749 servers.
|
||||
"""
|
||||
from ..grant_types import (
|
||||
AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
|
||||
RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
|
||||
)
|
||||
from ..tokens import BearerToken
|
||||
from .authorization import AuthorizationEndpoint
|
||||
from .introspect import IntrospectEndpoint
|
||||
from .resource import ResourceEndpoint
|
||||
from .revocation import RevocationEndpoint
|
||||
from .token import TokenEndpoint
|
||||
|
||||
|
||||
class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
"""An all-in-one endpoint featuring all four major grant types."""
|
||||
|
||||
def __init__(self, request_validator, token_expires_in=None,
|
||||
token_generator=None, refresh_token_generator=None,
|
||||
*args, **kwargs):
|
||||
"""Construct a new all-grants-in-one server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
oauthlib.oauth2.RequestValidator.
|
||||
:param token_expires_in: An int or a function to generate a token
|
||||
expiration offset (in seconds) given a
|
||||
oauthlib.common.Request object.
|
||||
:param token_generator: A function to generate a token from a request.
|
||||
:param refresh_token_generator: A function to generate a token from a
|
||||
request for the refresh token.
|
||||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.auth_grant = AuthorizationCodeGrant(request_validator)
|
||||
self.implicit_grant = ImplicitGrant(request_validator)
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||
request_validator)
|
||||
self.credentials_grant = ClientCredentialsGrant(request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||
response_types={
|
||||
'code': self.auth_grant,
|
||||
'token': self.implicit_grant,
|
||||
'none': self.auth_grant
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
|
||||
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||
grant_types={
|
||||
'authorization_code': self.auth_grant,
|
||||
'password': self.password_grant,
|
||||
'client_credentials': self.credentials_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
|
||||
|
||||
class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
"""Construct a new web application server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
oauthlib.oauth2.RequestValidator.
|
||||
:param token_expires_in: An int or a function to generate a token
|
||||
expiration offset (in seconds) given a
|
||||
oauthlib.common.Request object.
|
||||
:param token_generator: A function to generate a token from a request.
|
||||
:param refresh_token_generator: A function to generate a token from a
|
||||
request for the refresh token.
|
||||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.auth_grant = AuthorizationCodeGrant(request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||
response_types={'code': self.auth_grant},
|
||||
default_token_type=self.bearer)
|
||||
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||
grant_types={
|
||||
'authorization_code': self.auth_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
|
||||
|
||||
class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
"""An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
"""Construct a new implicit grant server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
oauthlib.oauth2.RequestValidator.
|
||||
:param token_expires_in: An int or a function to generate a token
|
||||
expiration offset (in seconds) given a
|
||||
oauthlib.common.Request object.
|
||||
:param token_generator: A function to generate a token from a request.
|
||||
:param refresh_token_generator: A function to generate a token from a
|
||||
request for the refresh token.
|
||||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.implicit_grant = ImplicitGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='token',
|
||||
response_types={
|
||||
'token': self.implicit_grant},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
IntrospectEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
|
||||
|
||||
class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
"""An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
"""Construct a resource owner password credentials grant server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
oauthlib.oauth2.RequestValidator.
|
||||
:param token_expires_in: An int or a function to generate a token
|
||||
expiration offset (in seconds) given a
|
||||
oauthlib.common.Request object.
|
||||
:param token_generator: A function to generate a token from a request.
|
||||
:param refresh_token_generator: A function to generate a token from a
|
||||
request for the refresh token.
|
||||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||
request_validator)
|
||||
self.refresh_grant = RefreshTokenGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
TokenEndpoint.__init__(self, default_grant_type='password',
|
||||
grant_types={
|
||||
'password': self.password_grant,
|
||||
'refresh_token': self.refresh_grant,
|
||||
},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
IntrospectEndpoint.__init__(self, request_validator)
|
||||
|
||||
|
||||
class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
|
||||
ResourceEndpoint, RevocationEndpoint):
|
||||
|
||||
"""An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
"""Construct a client credentials grant server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
oauthlib.oauth2.RequestValidator.
|
||||
:param token_expires_in: An int or a function to generate a token
|
||||
expiration offset (in seconds) given a
|
||||
oauthlib.common.Request object.
|
||||
:param token_generator: A function to generate a token from a request.
|
||||
:param refresh_token_generator: A function to generate a token from a
|
||||
request for the refresh token.
|
||||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
self.credentials_grant = ClientCredentialsGrant(request_validator)
|
||||
self.bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
TokenEndpoint.__init__(self, default_grant_type='client_credentials',
|
||||
grant_types={
|
||||
'client_credentials': self.credentials_grant},
|
||||
default_token_type=self.bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': self.bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
IntrospectEndpoint.__init__(self, request_validator,
|
||||
supported_token_types=['access_token'])
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib.common import Request
|
||||
|
||||
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResourceEndpoint(BaseEndpoint):
|
||||
|
||||
"""Authorizes access to protected resources.
|
||||
|
||||
The client accesses protected resources by presenting the access
|
||||
token to the resource server. The resource server MUST validate the
|
||||
access token and ensure that it has not expired and that its scope
|
||||
covers the requested resource. The methods used by the resource
|
||||
server to validate the access token (as well as any error responses)
|
||||
are beyond the scope of this specification but generally involve an
|
||||
interaction or coordination between the resource server and the
|
||||
authorization server::
|
||||
|
||||
# For most cases, returning a 403 should suffice.
|
||||
|
||||
The method in which the client utilizes the access token to
|
||||
authenticate with the resource server depends on the type of access
|
||||
token issued by the authorization server. Typically, it involves
|
||||
using the HTTP "Authorization" request header field [RFC2617] with an
|
||||
authentication scheme defined by the specification of the access
|
||||
token type used, such as [RFC6750]::
|
||||
|
||||
# Access tokens may also be provided in query and body
|
||||
https://example.com/protected?access_token=kjfch2345sdf # Query
|
||||
access_token=sdf23409df # Body
|
||||
"""
|
||||
|
||||
def __init__(self, default_token, token_types):
|
||||
BaseEndpoint.__init__(self)
|
||||
self._tokens = token_types
|
||||
self._default_token = default_token
|
||||
|
||||
@property
|
||||
def default_token(self):
|
||||
return self._default_token
|
||||
|
||||
@property
|
||||
def default_token_type_handler(self):
|
||||
return self.tokens.get(self.default_token)
|
||||
|
||||
@property
|
||||
def tokens(self):
|
||||
return self._tokens
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def verify_request(self, uri, http_method='GET', body=None, headers=None,
|
||||
scopes=None):
|
||||
"""Validate client, code etc, return body + headers"""
|
||||
request = Request(uri, http_method, body, headers)
|
||||
request.token_type = self.find_token_type(request)
|
||||
request.scopes = scopes
|
||||
token_type_handler = self.tokens.get(request.token_type,
|
||||
self.default_token_type_handler)
|
||||
log.debug('Dispatching token_type %s request to %r.',
|
||||
request.token_type, token_type_handler)
|
||||
return token_type_handler.validate_request(request), request
|
||||
|
||||
def find_token_type(self, request):
|
||||
"""Token type identification.
|
||||
|
||||
RFC 6749 does not provide a method for easily differentiating between
|
||||
different token types during protected resource access. We estimate
|
||||
the most likely token type (if any) by asking each known token type
|
||||
to give an estimation based on the request.
|
||||
"""
|
||||
estimates = sorted(((t.estimate_type(request), n)
|
||||
for n, t in self.tokens.items()), reverse=True)
|
||||
return estimates[0][1] if len(estimates) else None
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.endpoint.revocation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11).
|
||||
|
||||
.. _`Token Revocation`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib.common import Request
|
||||
|
||||
from ..errors import OAuth2Error
|
||||
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RevocationEndpoint(BaseEndpoint):
|
||||
|
||||
"""Token revocation endpoint.
|
||||
|
||||
Endpoint used by authenticated clients to revoke access and refresh tokens.
|
||||
Commonly this will be part of the Authorization Endpoint.
|
||||
"""
|
||||
|
||||
valid_token_types = ('access_token', 'refresh_token')
|
||||
valid_request_methods = ('POST',)
|
||||
|
||||
def __init__(self, request_validator, supported_token_types=None,
|
||||
enable_jsonp=False):
|
||||
BaseEndpoint.__init__(self)
|
||||
self.request_validator = request_validator
|
||||
self.supported_token_types = (
|
||||
supported_token_types or self.valid_token_types)
|
||||
self.enable_jsonp = enable_jsonp
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def create_revocation_response(self, uri, http_method='POST', body=None,
|
||||
headers=None):
|
||||
"""Revoke supplied access or refresh token.
|
||||
|
||||
|
||||
The authorization server responds with HTTP status code 200 if the
|
||||
token has been revoked successfully or if the client submitted an
|
||||
invalid token.
|
||||
|
||||
Note: invalid tokens do not cause an error response since the client
|
||||
cannot handle such an error in a reasonable way. Moreover, the purpose
|
||||
of the revocation request, invalidating the particular token, is
|
||||
already achieved.
|
||||
|
||||
The content of the response body is ignored by the client as all
|
||||
necessary information is conveyed in the response code.
|
||||
|
||||
An invalid token type hint value is ignored by the authorization server
|
||||
and does not influence the revocation response.
|
||||
"""
|
||||
resp_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
'Pragma': 'no-cache',
|
||||
}
|
||||
request = Request(
|
||||
uri, http_method=http_method, body=body, headers=headers)
|
||||
try:
|
||||
self.validate_revocation_request(request)
|
||||
log.debug('Token revocation valid for %r.', request)
|
||||
except OAuth2Error as e:
|
||||
log.debug('Client error during validation of %r. %r.', request, e)
|
||||
response_body = e.json
|
||||
if self.enable_jsonp and request.callback:
|
||||
response_body = '{}({});'.format(request.callback, response_body)
|
||||
resp_headers.update(e.headers)
|
||||
return resp_headers, response_body, e.status_code
|
||||
|
||||
self.request_validator.revoke_token(request.token,
|
||||
request.token_type_hint, request)
|
||||
|
||||
response_body = ''
|
||||
if self.enable_jsonp and request.callback:
|
||||
response_body = request.callback + '();'
|
||||
return {}, response_body, 200
|
||||
|
||||
def validate_revocation_request(self, request):
|
||||
"""Ensure the request is valid.
|
||||
|
||||
The client constructs the request by including the following parameters
|
||||
using the "application/x-www-form-urlencoded" format in the HTTP
|
||||
request entity-body:
|
||||
|
||||
token (REQUIRED). The token that the client wants to get revoked.
|
||||
|
||||
token_type_hint (OPTIONAL). A hint about the type of the token
|
||||
submitted for revocation. Clients MAY pass this parameter in order to
|
||||
help the authorization server to optimize the token lookup. If the
|
||||
server is unable to locate the token using the given hint, it MUST
|
||||
extend its search across all of its supported token types. An
|
||||
authorization server MAY ignore this parameter, particularly if it is
|
||||
able to detect the token type automatically. This specification
|
||||
defines two such values:
|
||||
|
||||
* access_token: An Access Token as defined in [RFC6749],
|
||||
`section 1.4`_
|
||||
|
||||
* refresh_token: A Refresh Token as defined in [RFC6749],
|
||||
`section 1.5`_
|
||||
|
||||
Specific implementations, profiles, and extensions of this
|
||||
specification MAY define other values for this parameter using
|
||||
the registry defined in `Section 4.1.2`_.
|
||||
|
||||
The client also includes its authentication credentials as described in
|
||||
`Section 2.3`_. of [`RFC6749`_].
|
||||
|
||||
.. _`section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
|
||||
.. _`section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
|
||||
.. _`section 2.3`: https://tools.ietf.org/html/rfc6749#section-2.3
|
||||
.. _`Section 4.1.2`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
|
||||
.. _`RFC6749`: https://tools.ietf.org/html/rfc6749
|
||||
"""
|
||||
self._raise_on_bad_method(request)
|
||||
self._raise_on_bad_post_request(request)
|
||||
self._raise_on_missing_token(request)
|
||||
self._raise_on_invalid_client(request)
|
||||
self._raise_on_unsupported_token(request)
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 RFC6749.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib.common import Request
|
||||
from oauthlib.oauth2.rfc6749 import utils
|
||||
|
||||
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TokenEndpoint(BaseEndpoint):
|
||||
|
||||
"""Token issuing endpoint.
|
||||
|
||||
The token endpoint is used by the client to obtain an access token by
|
||||
presenting its authorization grant or refresh token. The token
|
||||
endpoint is used with every authorization grant except for the
|
||||
implicit grant type (since an access token is issued directly).
|
||||
|
||||
The means through which the client obtains the location of the token
|
||||
endpoint are beyond the scope of this specification, but the location
|
||||
is typically provided in the service documentation.
|
||||
|
||||
The endpoint URI MAY include an "application/x-www-form-urlencoded"
|
||||
formatted (per `Appendix B`_) query component,
|
||||
which MUST be retained when adding additional query parameters. The
|
||||
endpoint URI MUST NOT include a fragment component::
|
||||
|
||||
https://example.com/path?query=component # OK
|
||||
https://example.com/path?query=component#fragment # Not OK
|
||||
|
||||
Since requests to the token endpoint result in the transmission of
|
||||
clear-text credentials (in the HTTP request and response), the
|
||||
authorization server MUST require the use of TLS as described in
|
||||
Section 1.6 when sending requests to the token endpoint::
|
||||
|
||||
# We will deny any request which URI schema is not with https
|
||||
|
||||
The client MUST use the HTTP "POST" method when making access token
|
||||
requests::
|
||||
|
||||
# HTTP method is currently not enforced
|
||||
|
||||
Parameters sent without a value MUST be treated as if they were
|
||||
omitted from the request. The authorization server MUST ignore
|
||||
unrecognized request parameters. Request and response parameters
|
||||
MUST NOT be included more than once::
|
||||
|
||||
# Delegated to each grant type.
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
"""
|
||||
|
||||
valid_request_methods = ('POST',)
|
||||
|
||||
def __init__(self, default_grant_type, default_token_type, grant_types):
|
||||
BaseEndpoint.__init__(self)
|
||||
self._grant_types = grant_types
|
||||
self._default_token_type = default_token_type
|
||||
self._default_grant_type = default_grant_type
|
||||
|
||||
@property
|
||||
def grant_types(self):
|
||||
return self._grant_types
|
||||
|
||||
@property
|
||||
def default_grant_type(self):
|
||||
return self._default_grant_type
|
||||
|
||||
@property
|
||||
def default_grant_type_handler(self):
|
||||
return self.grant_types.get(self.default_grant_type)
|
||||
|
||||
@property
|
||||
def default_token_type(self):
|
||||
return self._default_token_type
|
||||
|
||||
@catch_errors_and_unavailability
|
||||
def create_token_response(self, uri, http_method='POST', body=None,
|
||||
headers=None, credentials=None, grant_type_for_scope=None,
|
||||
claims=None):
|
||||
"""Extract grant_type and route to the designated handler."""
|
||||
request = Request(
|
||||
uri, http_method=http_method, body=body, headers=headers)
|
||||
self.validate_token_request(request)
|
||||
# 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant"
|
||||
# and "Client Credentials Grant" flows
|
||||
# https://tools.ietf.org/html/rfc6749#section-4.3.2
|
||||
# https://tools.ietf.org/html/rfc6749#section-4.4.2
|
||||
request.scopes = utils.scope_to_list(request.scope)
|
||||
|
||||
request.extra_credentials = credentials
|
||||
if grant_type_for_scope:
|
||||
request.grant_type = grant_type_for_scope
|
||||
|
||||
# OpenID Connect claims, if provided. The server using oauthlib might choose
|
||||
# to implement the claims parameter of the Authorization Request. In this case
|
||||
# it should retrieve those claims and pass them via the claims argument here,
|
||||
# as a dict.
|
||||
if claims:
|
||||
request.claims = claims
|
||||
|
||||
grant_type_handler = self.grant_types.get(request.grant_type,
|
||||
self.default_grant_type_handler)
|
||||
log.debug('Dispatching grant_type %s request to %r.',
|
||||
request.grant_type, grant_type_handler)
|
||||
return grant_type_handler.create_token_response(
|
||||
request, self.default_token_type)
|
||||
|
||||
def validate_token_request(self, request):
|
||||
self._raise_on_bad_method(request)
|
||||
self._raise_on_bad_post_request(request)
|
||||
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.errors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Error used both by OAuth 2 clients and providers to represent the spec
|
||||
defined error responses for all four core grant types.
|
||||
"""
|
||||
import json
|
||||
|
||||
from oauthlib.common import add_params_to_uri, urlencode
|
||||
|
||||
|
||||
class OAuth2Error(Exception):
|
||||
error = None
|
||||
status_code = 400
|
||||
description = ''
|
||||
|
||||
def __init__(self, description=None, uri=None, state=None,
|
||||
status_code=None, request=None):
|
||||
"""
|
||||
:param description: A human-readable ASCII [USASCII] text providing
|
||||
additional information, used to assist the client
|
||||
developer in understanding the error that occurred.
|
||||
Values for the "error_description" parameter
|
||||
MUST NOT include characters outside the set
|
||||
x20-21 / x23-5B / x5D-7E.
|
||||
|
||||
:param uri: 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.
|
||||
|
||||
:param state: A CSRF protection value received from the client.
|
||||
|
||||
:param status_code:
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
if description is not None:
|
||||
self.description = description
|
||||
|
||||
message = '({}) {}'.format(self.error, self.description)
|
||||
if request:
|
||||
message += ' ' + repr(request)
|
||||
super().__init__(message)
|
||||
|
||||
self.uri = uri
|
||||
self.state = state
|
||||
|
||||
if status_code:
|
||||
self.status_code = status_code
|
||||
|
||||
if request:
|
||||
self.redirect_uri = request.redirect_uri
|
||||
self.client_id = request.client_id
|
||||
self.scopes = request.scopes
|
||||
self.response_type = request.response_type
|
||||
self.response_mode = request.response_mode
|
||||
self.grant_type = request.grant_type
|
||||
if not state:
|
||||
self.state = request.state
|
||||
else:
|
||||
self.redirect_uri = None
|
||||
self.client_id = None
|
||||
self.scopes = None
|
||||
self.response_type = None
|
||||
self.response_mode = None
|
||||
self.grant_type = None
|
||||
|
||||
def in_uri(self, uri):
|
||||
fragment = self.response_mode == "fragment"
|
||||
return add_params_to_uri(uri, self.twotuples, fragment)
|
||||
|
||||
@property
|
||||
def twotuples(self):
|
||||
error = [('error', self.error)]
|
||||
if self.description:
|
||||
error.append(('error_description', self.description))
|
||||
if self.uri:
|
||||
error.append(('error_uri', self.uri))
|
||||
if self.state:
|
||||
error.append(('state', self.state))
|
||||
return error
|
||||
|
||||
@property
|
||||
def urlencoded(self):
|
||||
return urlencode(self.twotuples)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
return json.dumps(dict(self.twotuples))
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
if self.status_code == 401:
|
||||
"""
|
||||
https://tools.ietf.org/html/rfc6750#section-3
|
||||
|
||||
All challenges defined by this specification MUST use the auth-scheme
|
||||
value "Bearer". This scheme MUST be followed by one or more
|
||||
auth-param values.
|
||||
"""
|
||||
authvalues = ['error="{}"'.format(self.error)]
|
||||
if self.description:
|
||||
authvalues.append('error_description="{}"'.format(self.description))
|
||||
if self.uri:
|
||||
authvalues.append('error_uri="{}"'.format(self.uri))
|
||||
return {"WWW-Authenticate": "Bearer " + ", ".join(authvalues)}
|
||||
return {}
|
||||
|
||||
|
||||
class TokenExpiredError(OAuth2Error):
|
||||
error = 'token_expired'
|
||||
|
||||
|
||||
class InsecureTransportError(OAuth2Error):
|
||||
error = 'insecure_transport'
|
||||
description = 'OAuth 2 MUST utilize https.'
|
||||
|
||||
|
||||
class MismatchingStateError(OAuth2Error):
|
||||
error = 'mismatching_state'
|
||||
description = 'CSRF Warning! State not equal in request and response.'
|
||||
|
||||
|
||||
class MissingCodeError(OAuth2Error):
|
||||
error = 'missing_code'
|
||||
|
||||
|
||||
class MissingTokenError(OAuth2Error):
|
||||
error = 'missing_token'
|
||||
|
||||
|
||||
class MissingTokenTypeError(OAuth2Error):
|
||||
error = 'missing_token_type'
|
||||
|
||||
|
||||
class FatalClientError(OAuth2Error):
|
||||
"""
|
||||
Errors during authorization where user should not be redirected back.
|
||||
|
||||
If the request fails due to a missing, invalid, or mismatching
|
||||
redirection URI, or if the client identifier is missing or invalid,
|
||||
the authorization server SHOULD inform the resource owner of the
|
||||
error and MUST NOT automatically redirect the user-agent to the
|
||||
invalid redirection URI.
|
||||
|
||||
Instead the user should be informed of the error by the provider itself.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRequestFatalError(FatalClientError):
|
||||
"""
|
||||
For fatal errors, the request is missing a required parameter, includes
|
||||
an invalid parameter value, includes a parameter more than once, or is
|
||||
otherwise malformed.
|
||||
"""
|
||||
error = 'invalid_request'
|
||||
|
||||
|
||||
class InvalidRedirectURIError(InvalidRequestFatalError):
|
||||
description = 'Invalid redirect URI.'
|
||||
|
||||
|
||||
class MissingRedirectURIError(InvalidRequestFatalError):
|
||||
description = 'Missing redirect URI.'
|
||||
|
||||
|
||||
class MismatchingRedirectURIError(InvalidRequestFatalError):
|
||||
description = 'Mismatching redirect URI.'
|
||||
|
||||
|
||||
class InvalidClientIdError(InvalidRequestFatalError):
|
||||
description = 'Invalid client_id parameter value.'
|
||||
|
||||
|
||||
class MissingClientIdError(InvalidRequestFatalError):
|
||||
description = 'Missing client_id parameter.'
|
||||
|
||||
|
||||
class InvalidRequestError(OAuth2Error):
|
||||
"""
|
||||
The request is missing a required parameter, includes an invalid
|
||||
parameter value, includes a parameter more than once, or is
|
||||
otherwise malformed.
|
||||
"""
|
||||
error = 'invalid_request'
|
||||
|
||||
|
||||
class MissingResponseTypeError(InvalidRequestError):
|
||||
description = 'Missing response_type parameter.'
|
||||
|
||||
|
||||
class MissingCodeChallengeError(InvalidRequestError):
|
||||
"""
|
||||
If the server requires Proof Key for Code Exchange (PKCE) by OAuth
|
||||
public clients and the client does not send the "code_challenge" in
|
||||
the request, the authorization endpoint MUST return the authorization
|
||||
error response with the "error" value set to "invalid_request". The
|
||||
"error_description" or the response of "error_uri" SHOULD explain the
|
||||
nature of error, e.g., code challenge required.
|
||||
"""
|
||||
description = 'Code challenge required.'
|
||||
|
||||
|
||||
class MissingCodeVerifierError(InvalidRequestError):
|
||||
"""
|
||||
The request to the token endpoint, when PKCE is enabled, has
|
||||
the parameter `code_verifier` REQUIRED.
|
||||
"""
|
||||
description = 'Code verifier required.'
|
||||
|
||||
|
||||
class AccessDeniedError(OAuth2Error):
|
||||
"""
|
||||
The resource owner or authorization server denied the request.
|
||||
"""
|
||||
error = 'access_denied'
|
||||
|
||||
|
||||
class UnsupportedResponseTypeError(OAuth2Error):
|
||||
"""
|
||||
The authorization server does not support obtaining an authorization
|
||||
code using this method.
|
||||
"""
|
||||
error = 'unsupported_response_type'
|
||||
|
||||
|
||||
class UnsupportedCodeChallengeMethodError(InvalidRequestError):
|
||||
"""
|
||||
If the server supporting PKCE does not support the requested
|
||||
transformation, the authorization endpoint MUST return the
|
||||
authorization error response with "error" value set to
|
||||
"invalid_request". The "error_description" or the response of
|
||||
"error_uri" SHOULD explain the nature of error, e.g., transform
|
||||
algorithm not supported.
|
||||
"""
|
||||
description = 'Transform algorithm not supported.'
|
||||
|
||||
|
||||
class InvalidScopeError(OAuth2Error):
|
||||
"""
|
||||
The requested scope is invalid, unknown, or malformed, or
|
||||
exceeds the scope granted by the resource owner.
|
||||
|
||||
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
error = 'invalid_scope'
|
||||
|
||||
|
||||
class ServerError(OAuth2Error):
|
||||
"""
|
||||
The authorization server encountered an unexpected condition that
|
||||
prevented it from fulfilling the request. (This error code is needed
|
||||
because a 500 Internal Server Error HTTP status code cannot be returned
|
||||
to the client via a HTTP redirect.)
|
||||
"""
|
||||
error = 'server_error'
|
||||
|
||||
|
||||
class TemporarilyUnavailableError(OAuth2Error):
|
||||
"""
|
||||
The authorization server is currently unable to handle the request
|
||||
due to a temporary overloading or maintenance of the server.
|
||||
(This error code is needed because a 503 Service Unavailable HTTP
|
||||
status code cannot be returned to the client via a HTTP redirect.)
|
||||
"""
|
||||
error = 'temporarily_unavailable'
|
||||
|
||||
|
||||
class InvalidClientError(FatalClientError):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
error = 'invalid_client'
|
||||
status_code = 401
|
||||
|
||||
|
||||
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'
|
||||
status_code = 400
|
||||
|
||||
|
||||
class UnauthorizedClientError(OAuth2Error):
|
||||
"""
|
||||
The authenticated client is not authorized to use this authorization
|
||||
grant type.
|
||||
"""
|
||||
error = 'unauthorized_client'
|
||||
|
||||
|
||||
class UnsupportedGrantTypeError(OAuth2Error):
|
||||
"""
|
||||
The authorization grant type is not supported by the authorization
|
||||
server.
|
||||
"""
|
||||
error = 'unsupported_grant_type'
|
||||
|
||||
|
||||
class UnsupportedTokenTypeError(OAuth2Error):
|
||||
"""
|
||||
The authorization server does not support the hint of the
|
||||
presented token type. I.e. the client tried to revoke an access token
|
||||
on a server not supporting this feature.
|
||||
"""
|
||||
error = 'unsupported_token_type'
|
||||
|
||||
|
||||
class InvalidTokenError(OAuth2Error):
|
||||
"""
|
||||
The access token provided is expired, revoked, malformed, or
|
||||
invalid for other reasons. The resource SHOULD respond with
|
||||
the HTTP 401 (Unauthorized) status code. The client MAY
|
||||
request a new access token and retry the protected resource
|
||||
request.
|
||||
"""
|
||||
error = 'invalid_token'
|
||||
status_code = 401
|
||||
description = ("The access token provided is expired, revoked, malformed, "
|
||||
"or invalid for other reasons.")
|
||||
|
||||
|
||||
class InsufficientScopeError(OAuth2Error):
|
||||
"""
|
||||
The request requires higher privileges than provided by the
|
||||
access token. The resource server SHOULD respond with the HTTP
|
||||
403 (Forbidden) status code and MAY include the "scope"
|
||||
attribute with the scope necessary to access the protected
|
||||
resource.
|
||||
"""
|
||||
error = 'insufficient_scope'
|
||||
status_code = 403
|
||||
description = ("The request requires higher privileges than provided by "
|
||||
"the access token.")
|
||||
|
||||
|
||||
class ConsentRequired(OAuth2Error):
|
||||
"""
|
||||
The Authorization Server requires End-User consent.
|
||||
|
||||
This error MAY be returned when the prompt parameter value in the
|
||||
Authentication Request is none, but the Authentication Request cannot be
|
||||
completed without displaying a user interface for End-User consent.
|
||||
"""
|
||||
error = 'consent_required'
|
||||
|
||||
|
||||
class LoginRequired(OAuth2Error):
|
||||
"""
|
||||
The Authorization Server requires End-User authentication.
|
||||
|
||||
This error MAY be returned when the prompt parameter value in the
|
||||
Authentication Request is none, but the Authentication Request cannot be
|
||||
completed without displaying a user interface for End-User authentication.
|
||||
"""
|
||||
error = 'login_required'
|
||||
|
||||
|
||||
class CustomOAuth2Error(OAuth2Error):
|
||||
"""
|
||||
This error is a placeholder for all custom errors not described by the RFC.
|
||||
Some of the popular OAuth2 providers are using custom errors.
|
||||
"""
|
||||
def __init__(self, error, *args, **kwargs):
|
||||
self.error = error
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def raise_from_error(error, params=None):
|
||||
import inspect
|
||||
import sys
|
||||
kwargs = {
|
||||
'description': params.get('error_description'),
|
||||
'uri': params.get('error_uri'),
|
||||
'state': params.get('state')
|
||||
}
|
||||
for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
|
||||
if cls.error == error:
|
||||
raise cls(**kwargs)
|
||||
raise CustomOAuth2Error(error=error, **kwargs)
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
from .authorization_code import AuthorizationCodeGrant
|
||||
from .client_credentials import ClientCredentialsGrant
|
||||
from .implicit import ImplicitGrant
|
||||
from .refresh_token import RefreshTokenGrant
|
||||
from .resource_owner_password_credentials import (
|
||||
ResourceOwnerPasswordCredentialsGrant,
|
||||
)
|
||||
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,548 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
from oauthlib import common
|
||||
|
||||
from .. import errors
|
||||
from .base import GrantTypeBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def code_challenge_method_s256(verifier, challenge):
|
||||
"""
|
||||
If the "code_challenge_method" from `Section 4.3`_ was "S256", the
|
||||
received "code_verifier" is hashed by SHA-256, base64url-encoded, and
|
||||
then compared to the "code_challenge", i.e.:
|
||||
|
||||
BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
|
||||
|
||||
How to implement a base64url-encoding
|
||||
function without padding, based upon the standard base64-encoding
|
||||
function that uses padding.
|
||||
|
||||
To be concrete, example C# code implementing these functions is shown
|
||||
below. Similar code could be used in other languages.
|
||||
|
||||
static string base64urlencode(byte [] arg)
|
||||
{
|
||||
string s = Convert.ToBase64String(arg); // Regular base64 encoder
|
||||
s = s.Split('=')[0]; // Remove any trailing '='s
|
||||
s = s.Replace('+', '-'); // 62nd char of encoding
|
||||
s = s.Replace('/', '_'); // 63rd char of encoding
|
||||
return s;
|
||||
}
|
||||
|
||||
In python urlsafe_b64encode is already replacing '+' and '/', but preserve
|
||||
the trailing '='. So we have to remove it.
|
||||
|
||||
.. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
"""
|
||||
return base64.urlsafe_b64encode(
|
||||
hashlib.sha256(verifier.encode()).digest()
|
||||
).decode().rstrip('=') == challenge
|
||||
|
||||
|
||||
def code_challenge_method_plain(verifier, challenge):
|
||||
"""
|
||||
If the "code_challenge_method" from `Section 4.3`_ was "plain", they are
|
||||
compared directly, i.e.:
|
||||
|
||||
code_verifier == code_challenge.
|
||||
|
||||
.. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
|
||||
"""
|
||||
return verifier == challenge
|
||||
|
||||
|
||||
class AuthorizationCodeGrant(GrantTypeBase):
|
||||
|
||||
"""`Authorization Code Grant`_
|
||||
|
||||
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)
|
||||
|
||||
Note: The lines illustrating steps (A), (B), and (C) are broken into
|
||||
two parts as they pass through the user-agent.
|
||||
|
||||
Figure 3: Authorization Code Flow
|
||||
|
||||
The flow illustrated in Figure 3 includes the following steps:
|
||||
|
||||
(A) The client initiates the flow by directing the resource owner's
|
||||
user-agent to the authorization endpoint. The client includes
|
||||
its client identifier, requested scope, local state, and a
|
||||
redirection URI to which the authorization server will send the
|
||||
user-agent back once access is granted (or denied).
|
||||
|
||||
(B) The authorization server authenticates the resource owner (via
|
||||
the user-agent) and establishes whether the resource owner
|
||||
grants or denies the client's access request.
|
||||
|
||||
(C) Assuming the resource owner grants access, the authorization
|
||||
server redirects the user-agent back to the client using the
|
||||
redirection URI provided earlier (in the request or during
|
||||
client registration). The redirection URI includes an
|
||||
authorization code and any local state provided by the client
|
||||
earlier.
|
||||
|
||||
(D) The client requests an access token from the authorization
|
||||
server's token endpoint by including the authorization code
|
||||
received in the previous step. When making the request, the
|
||||
client authenticates with the authorization server. The client
|
||||
includes the redirection URI used to obtain the authorization
|
||||
code for verification.
|
||||
|
||||
(E) The authorization server authenticates the client, validates the
|
||||
authorization code, and ensures that the redirection URI
|
||||
received matches the URI used to redirect the client in
|
||||
step (C). If valid, the authorization server responds back with
|
||||
an access token and, optionally, a refresh token.
|
||||
|
||||
OAuth 2.0 public clients utilizing the Authorization Code Grant are
|
||||
susceptible to the authorization code interception attack.
|
||||
|
||||
A technique to mitigate against the threat through the use of Proof Key for Code
|
||||
Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib
|
||||
implementation.
|
||||
|
||||
.. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
|
||||
.. _`PKCE`: https://tools.ietf.org/html/rfc7636
|
||||
"""
|
||||
|
||||
default_response_mode = 'query'
|
||||
response_types = ['code']
|
||||
|
||||
# This dict below is private because as RFC mention it:
|
||||
# "S256" is Mandatory To Implement (MTI) on the server.
|
||||
#
|
||||
_code_challenge_methods = {
|
||||
'plain': code_challenge_method_plain,
|
||||
'S256': code_challenge_method_s256
|
||||
}
|
||||
|
||||
def create_authorization_code(self, request):
|
||||
"""
|
||||
Generates an authorization grant represented as a dictionary.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
grant = {'code': common.generate_token()}
|
||||
if hasattr(request, 'state') and request.state:
|
||||
grant['state'] = request.state
|
||||
log.debug('Created authorization code grant %r for request %r.',
|
||||
grant, request)
|
||||
return grant
|
||||
|
||||
def create_authorization_response(self, request, token_handler):
|
||||
"""
|
||||
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 `Appendix B`_:
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "code" for standard OAuth2
|
||||
authorization flow. For OpenID Connect it must be one of
|
||||
"code token", "code id_token", or "code token id_token" - we
|
||||
essentially test that "code" appears in the response_type.
|
||||
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.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
:returns: headers, body, status
|
||||
:raises: FatalClientError on invalid redirect URI or client id.
|
||||
|
||||
A few examples::
|
||||
|
||||
>>> from your_validator import your_validator
|
||||
>>> request = Request('https://example.com/authorize?client_id=valid'
|
||||
... '&redirect_uri=http%3A%2F%2Fclient.com%2F')
|
||||
>>> from oauthlib.common import Request
|
||||
>>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
|
||||
>>> token = BearerToken(your_validator)
|
||||
>>> grant = AuthorizationCodeGrant(your_validator)
|
||||
>>> request.scopes = ['authorized', 'in', 'some', 'form']
|
||||
>>> grant.create_authorization_response(request, token)
|
||||
(u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
|
||||
>>> request = Request('https://example.com/authorize?client_id=valid'
|
||||
... '&redirect_uri=http%3A%2F%2Fclient.com%2F'
|
||||
... '&response_type=code')
|
||||
>>> request.scopes = ['authorized', 'in', 'some', 'form']
|
||||
>>> grant.create_authorization_response(request, token)
|
||||
(u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
|
||||
>>> # If the client id or redirect uri fails validation
|
||||
>>> grant.create_authorization_response(request, token)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
|
||||
>>> grant.create_authorization_response(request, token)
|
||||
File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
|
||||
oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
.. _`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
|
||||
"""
|
||||
try:
|
||||
self.validate_authorization_request(request)
|
||||
log.debug('Pre resource owner authorization validation ok for %r.',
|
||||
request)
|
||||
|
||||
# If the request fails due to a missing, invalid, or mismatching
|
||||
# redirection URI, or if the client identifier is missing or invalid,
|
||||
# the authorization server SHOULD inform the resource owner of the
|
||||
# error and MUST NOT automatically redirect the user-agent to the
|
||||
# invalid redirection URI.
|
||||
except errors.FatalClientError as e:
|
||||
log.debug('Fatal client error during validation of %r. %r.',
|
||||
request, e)
|
||||
raise
|
||||
|
||||
# If the resource owner denies the access request or if the request
|
||||
# fails for reasons other than a missing or invalid redirection URI,
|
||||
# the authorization server informs the client by adding the following
|
||||
# parameters to the query component of the redirection URI using the
|
||||
# "application/x-www-form-urlencoded" format, per Appendix B:
|
||||
# https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
except errors.OAuth2Error as e:
|
||||
log.debug('Client error during validation of %r. %r.', request, e)
|
||||
request.redirect_uri = request.redirect_uri or self.error_uri
|
||||
redirect_uri = common.add_params_to_uri(
|
||||
request.redirect_uri, e.twotuples,
|
||||
fragment=request.response_mode == "fragment")
|
||||
return {'Location': redirect_uri}, None, 302
|
||||
|
||||
grant = self.create_authorization_code(request)
|
||||
for modifier in self._code_modifiers:
|
||||
grant = modifier(grant, token_handler, request)
|
||||
if 'access_token' in grant:
|
||||
self.request_validator.save_token(grant, request)
|
||||
log.debug('Saving grant %r for %r.', grant, request)
|
||||
self.request_validator.save_authorization_code(
|
||||
request.client_id, grant, request)
|
||||
return self.prepare_authorization_response(
|
||||
request, grant, {}, None, 302)
|
||||
|
||||
def create_token_response(self, request, token_handler):
|
||||
"""Validate the authorization code.
|
||||
|
||||
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.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
|
||||
"""
|
||||
headers = self._get_default_headers()
|
||||
try:
|
||||
self.validate_token_request(request)
|
||||
log.debug('Token request validation ok for %r.', request)
|
||||
except errors.OAuth2Error as e:
|
||||
log.debug('Client error during validation of %r. %r.', request, e)
|
||||
headers.update(e.headers)
|
||||
return headers, e.json, e.status_code
|
||||
|
||||
token = token_handler.create_token(request, refresh_token=self.refresh_token)
|
||||
|
||||
for modifier in self._token_modifiers:
|
||||
token = modifier(token, token_handler, request)
|
||||
|
||||
self.request_validator.save_token(token, request)
|
||||
self.request_validator.invalidate_authorization_code(
|
||||
request.client_id, request.code, request)
|
||||
headers.update(self._create_cors_headers(request))
|
||||
return headers, json.dumps(token), 200
|
||||
|
||||
def validate_authorization_request(self, request):
|
||||
"""Check the authorization request for normal and fatal errors.
|
||||
|
||||
A normal error could be a missing response_type parameter or the client
|
||||
attempting to access scope it is not allowed to ask authorization for.
|
||||
Normal errors can safely be included in the redirection URI and
|
||||
sent back to the client.
|
||||
|
||||
Fatal errors occur when the client_id or redirect_uri is invalid or
|
||||
missing. These must be caught by the provider and handled, how this
|
||||
is done is outside of the scope of OAuthLib but showing an error
|
||||
page describing the issue is a good idea.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
|
||||
# First check for fatal errors
|
||||
|
||||
# If the request fails due to a missing, invalid, or mismatching
|
||||
# redirection URI, or if the client identifier is missing or invalid,
|
||||
# the authorization server SHOULD inform the resource owner of the
|
||||
# error and MUST NOT automatically redirect the user-agent to the
|
||||
# invalid redirection URI.
|
||||
|
||||
# First check duplicate parameters
|
||||
for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
|
||||
try:
|
||||
duplicate_params = request.duplicate_params
|
||||
except ValueError:
|
||||
raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
|
||||
if param in duplicate_params:
|
||||
raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
|
||||
|
||||
# REQUIRED. The client identifier as described in Section 2.2.
|
||||
# https://tools.ietf.org/html/rfc6749#section-2.2
|
||||
if not request.client_id:
|
||||
raise errors.MissingClientIdError(request=request)
|
||||
|
||||
if not self.request_validator.validate_client_id(request.client_id, request):
|
||||
raise errors.InvalidClientIdError(request=request)
|
||||
|
||||
# OPTIONAL. As described in Section 3.1.2.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||
log.debug('Validating redirection uri %s for client %s.',
|
||||
request.redirect_uri, request.client_id)
|
||||
|
||||
# OPTIONAL. As described in Section 3.1.2.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||
self._handle_redirects(request)
|
||||
|
||||
# Then check for normal errors.
|
||||
|
||||
# If the resource owner denies the access request or if the request
|
||||
# fails for reasons other than a missing or invalid redirection URI,
|
||||
# the authorization server informs the client by adding the following
|
||||
# parameters to the query component of the redirection URI using the
|
||||
# "application/x-www-form-urlencoded" format, per Appendix B.
|
||||
# https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
|
||||
# Note that the correct parameters to be added are automatically
|
||||
# populated through the use of specific exceptions.
|
||||
|
||||
request_info = {}
|
||||
for validator in self.custom_validators.pre_auth:
|
||||
request_info.update(validator(request))
|
||||
|
||||
# REQUIRED.
|
||||
if request.response_type is None:
|
||||
raise errors.MissingResponseTypeError(request=request)
|
||||
# Value MUST be set to "code" or one of the OpenID authorization code including
|
||||
# response_types "code token", "code id_token", "code token id_token"
|
||||
elif not 'code' in request.response_type and request.response_type != 'none':
|
||||
raise errors.UnsupportedResponseTypeError(request=request)
|
||||
|
||||
if not self.request_validator.validate_response_type(request.client_id,
|
||||
request.response_type,
|
||||
request.client, request):
|
||||
|
||||
log.debug('Client %s is not authorized to use response_type %s.',
|
||||
request.client_id, request.response_type)
|
||||
raise errors.UnauthorizedClientError(request=request)
|
||||
|
||||
# OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
|
||||
# https://tools.ietf.org/html/rfc6749#section-4.4.1
|
||||
if self.request_validator.is_pkce_required(request.client_id, request) is True:
|
||||
if request.code_challenge is None:
|
||||
raise errors.MissingCodeChallengeError(request=request)
|
||||
|
||||
if request.code_challenge is not None:
|
||||
request_info["code_challenge"] = request.code_challenge
|
||||
|
||||
# OPTIONAL, defaults to "plain" if not present in the request.
|
||||
if request.code_challenge_method is None:
|
||||
request.code_challenge_method = "plain"
|
||||
|
||||
if request.code_challenge_method not in self._code_challenge_methods:
|
||||
raise errors.UnsupportedCodeChallengeMethodError(request=request)
|
||||
request_info["code_challenge_method"] = request.code_challenge_method
|
||||
|
||||
# OPTIONAL. The scope of the access request as described by Section 3.3
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
self.validate_scopes(request)
|
||||
|
||||
request_info.update({
|
||||
'client_id': request.client_id,
|
||||
'redirect_uri': request.redirect_uri,
|
||||
'response_type': request.response_type,
|
||||
'state': request.state,
|
||||
'request': request
|
||||
})
|
||||
|
||||
for validator in self.custom_validators.post_auth:
|
||||
request_info.update(validator(request))
|
||||
|
||||
return request.scopes, request_info
|
||||
|
||||
def validate_token_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
# REQUIRED. Value MUST be set to "authorization_code".
|
||||
if request.grant_type not in ('authorization_code', 'openid'):
|
||||
raise errors.UnsupportedGrantTypeError(request=request)
|
||||
|
||||
for validator in self.custom_validators.pre_token:
|
||||
validator(request)
|
||||
|
||||
if request.code is None:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing code parameter.', request=request)
|
||||
|
||||
for param in ('client_id', 'grant_type', 'redirect_uri'):
|
||||
if param in request.duplicate_params:
|
||||
raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
|
||||
request=request)
|
||||
|
||||
if self.request_validator.client_authentication_required(request):
|
||||
# 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.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
elif not self.request_validator.authenticate_client_id(request.client_id, request):
|
||||
# REQUIRED, if the client is not authenticating with the
|
||||
# authorization server as described in Section 3.2.1.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
|
||||
if not hasattr(request.client, 'client_id'):
|
||||
raise NotImplementedError('Authenticate client must set the '
|
||||
'request.client.client_id attribute '
|
||||
'in authenticate_client.')
|
||||
|
||||
request.client_id = request.client_id or request.client.client_id
|
||||
|
||||
# Ensure client is authorized use of this grant type
|
||||
self.validate_grant_type(request)
|
||||
|
||||
# REQUIRED. The authorization code received from the
|
||||
# authorization server.
|
||||
if not self.request_validator.validate_code(request.client_id,
|
||||
request.code, request.client, request):
|
||||
log.debug('Client, %r (%r), is not allowed access to scopes %r.',
|
||||
request.client_id, request.client, request.scopes)
|
||||
raise errors.InvalidGrantError(request=request)
|
||||
|
||||
# OPTIONAL. Validate PKCE code_verifier
|
||||
challenge = self.request_validator.get_code_challenge(request.code, request)
|
||||
|
||||
if challenge is not None:
|
||||
if request.code_verifier is None:
|
||||
raise errors.MissingCodeVerifierError(request=request)
|
||||
|
||||
challenge_method = self.request_validator.get_code_challenge_method(request.code, request)
|
||||
if challenge_method is None:
|
||||
raise errors.InvalidGrantError(request=request, description="Challenge method not found")
|
||||
|
||||
if challenge_method not in self._code_challenge_methods:
|
||||
raise errors.ServerError(
|
||||
description="code_challenge_method {} is not supported.".format(challenge_method),
|
||||
request=request
|
||||
)
|
||||
|
||||
if not self.validate_code_challenge(challenge,
|
||||
challenge_method,
|
||||
request.code_verifier):
|
||||
log.debug('request provided a invalid code_verifier.')
|
||||
raise errors.InvalidGrantError(request=request)
|
||||
elif self.request_validator.is_pkce_required(request.client_id, request) is True:
|
||||
if request.code_verifier is None:
|
||||
raise errors.MissingCodeVerifierError(request=request)
|
||||
raise errors.InvalidGrantError(request=request, description="Challenge not found")
|
||||
|
||||
for attr in ('user', 'scopes'):
|
||||
if getattr(request, attr, None) is None:
|
||||
log.debug('request.%s was not set on code validation.', attr)
|
||||
|
||||
# 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.
|
||||
if request.redirect_uri is None:
|
||||
request.using_default_redirect_uri = True
|
||||
request.redirect_uri = self.request_validator.get_default_redirect_uri(
|
||||
request.client_id, request)
|
||||
log.debug('Using default redirect_uri %s.', request.redirect_uri)
|
||||
if not request.redirect_uri:
|
||||
raise errors.MissingRedirectURIError(request=request)
|
||||
else:
|
||||
request.using_default_redirect_uri = False
|
||||
log.debug('Using provided redirect_uri %s', request.redirect_uri)
|
||||
|
||||
if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
|
||||
request.redirect_uri, request.client,
|
||||
request):
|
||||
log.debug('Redirect_uri (%r) invalid for client %r (%r).',
|
||||
request.redirect_uri, request.client_id, request.client)
|
||||
raise errors.MismatchingRedirectURIError(request=request)
|
||||
|
||||
for validator in self.custom_validators.post_token:
|
||||
validator(request)
|
||||
|
||||
def validate_code_challenge(self, challenge, challenge_method, verifier):
|
||||
if challenge_method in self._code_challenge_methods:
|
||||
return self._code_challenge_methods[challenge_method](verifier, challenge)
|
||||
raise NotImplementedError('Unknown challenge_method %s' % challenge_method)
|
||||
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import logging
|
||||
from itertools import chain
|
||||
|
||||
from oauthlib.common import add_params_to_uri
|
||||
from oauthlib.oauth2.rfc6749 import errors, utils
|
||||
from oauthlib.uri_validate import is_absolute_uri
|
||||
|
||||
from ..request_validator import RequestValidator
|
||||
from ..utils import is_secure_transport
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ValidatorsContainer:
|
||||
"""
|
||||
Container object for holding custom validator callables to be invoked
|
||||
as part of the grant type `validate_authorization_request()` or
|
||||
`validate_authorization_request()` methods on the various grant types.
|
||||
|
||||
Authorization validators must be callables that take a request object and
|
||||
return a dict, which may contain items to be added to the `request_info`
|
||||
returned from the grant_type after validation.
|
||||
|
||||
Token validators must be callables that take a request object and
|
||||
return None.
|
||||
|
||||
Both authorization validators and token validators may raise OAuth2
|
||||
exceptions if validation conditions fail.
|
||||
|
||||
Authorization validators added to `pre_auth` will be run BEFORE
|
||||
the standard validations (but after the critical ones that raise
|
||||
fatal errors) as part of `validate_authorization_request()`
|
||||
|
||||
Authorization validators added to `post_auth` will be run AFTER
|
||||
the standard validations as part of `validate_authorization_request()`
|
||||
|
||||
Token validators added to `pre_token` will be run BEFORE
|
||||
the standard validations as part of `validate_token_request()`
|
||||
|
||||
Token validators added to `post_token` will be run AFTER
|
||||
the standard validations as part of `validate_token_request()`
|
||||
|
||||
For example:
|
||||
|
||||
>>> def my_auth_validator(request):
|
||||
... return {'myval': True}
|
||||
>>> auth_code_grant = AuthorizationCodeGrant(request_validator)
|
||||
>>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator)
|
||||
>>> def my_token_validator(request):
|
||||
... if not request.everything_okay:
|
||||
... raise errors.OAuth2Error("uh-oh")
|
||||
>>> auth_code_grant.custom_validators.post_token.append(my_token_validator)
|
||||
"""
|
||||
|
||||
def __init__(self, post_auth, post_token,
|
||||
pre_auth, pre_token):
|
||||
self.pre_auth = pre_auth
|
||||
self.post_auth = post_auth
|
||||
self.pre_token = pre_token
|
||||
self.post_token = post_token
|
||||
|
||||
@property
|
||||
def all_pre(self):
|
||||
return chain(self.pre_auth, self.pre_token)
|
||||
|
||||
@property
|
||||
def all_post(self):
|
||||
return chain(self.post_auth, self.post_token)
|
||||
|
||||
|
||||
class GrantTypeBase:
|
||||
error_uri = None
|
||||
request_validator = None
|
||||
default_response_mode = 'fragment'
|
||||
refresh_token = True
|
||||
response_types = ['code']
|
||||
|
||||
def __init__(self, request_validator=None, **kwargs):
|
||||
self.request_validator = request_validator or RequestValidator()
|
||||
|
||||
# Transforms class variables into instance variables:
|
||||
self.response_types = self.response_types
|
||||
self.refresh_token = self.refresh_token
|
||||
self._setup_custom_validators(kwargs)
|
||||
self._code_modifiers = []
|
||||
self._token_modifiers = []
|
||||
|
||||
for kw, val in kwargs.items():
|
||||
setattr(self, kw, val)
|
||||
|
||||
def _setup_custom_validators(self, kwargs):
|
||||
post_auth = kwargs.get('post_auth', [])
|
||||
post_token = kwargs.get('post_token', [])
|
||||
pre_auth = kwargs.get('pre_auth', [])
|
||||
pre_token = kwargs.get('pre_token', [])
|
||||
if not hasattr(self, 'validate_authorization_request'):
|
||||
if post_auth or pre_auth:
|
||||
msg = ("{} does not support authorization validators. Use "
|
||||
"token validators instead.").format(self.__class__.__name__)
|
||||
raise ValueError(msg)
|
||||
# Using tuples here because they can't be appended to:
|
||||
post_auth, pre_auth = (), ()
|
||||
self.custom_validators = ValidatorsContainer(post_auth, post_token,
|
||||
pre_auth, pre_token)
|
||||
|
||||
def register_response_type(self, response_type):
|
||||
self.response_types.append(response_type)
|
||||
|
||||
def register_code_modifier(self, modifier):
|
||||
self._code_modifiers.append(modifier)
|
||||
|
||||
def register_token_modifier(self, modifier):
|
||||
self._token_modifiers.append(modifier)
|
||||
|
||||
def create_authorization_response(self, request, token_handler):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def create_token_response(self, request, token_handler):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def add_token(self, token, token_handler, request):
|
||||
"""
|
||||
:param token:
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
# Only add a hybrid access token on auth step if asked for
|
||||
if not request.response_type in ["token", "code token", "id_token token", "code id_token token"]:
|
||||
return token
|
||||
|
||||
token.update(token_handler.create_token(request, refresh_token=False))
|
||||
return token
|
||||
|
||||
def validate_grant_type(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
client_id = getattr(request, 'client_id', None)
|
||||
if not self.request_validator.validate_grant_type(client_id,
|
||||
request.grant_type, request.client, request):
|
||||
log.debug('Unauthorized from %r (%r) access to grant type %s.',
|
||||
request.client_id, request.client, request.grant_type)
|
||||
raise errors.UnauthorizedClientError(request=request)
|
||||
|
||||
def validate_scopes(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
if not request.scopes:
|
||||
request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list(
|
||||
self.request_validator.get_default_scopes(request.client_id, request))
|
||||
log.debug('Validating access to scopes %r for client %r (%r).',
|
||||
request.scopes, request.client_id, request.client)
|
||||
if not self.request_validator.validate_scopes(request.client_id,
|
||||
request.scopes, request.client, request):
|
||||
raise errors.InvalidScopeError(request=request)
|
||||
|
||||
def prepare_authorization_response(self, request, token, headers, body, status):
|
||||
"""Place token according to response mode.
|
||||
|
||||
Base classes can define a default response mode for their authorization
|
||||
response by overriding the static `default_response_mode` member.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token:
|
||||
:param headers:
|
||||
:param body:
|
||||
:param status:
|
||||
"""
|
||||
request.response_mode = request.response_mode or self.default_response_mode
|
||||
|
||||
if request.response_mode not in ('query', 'fragment'):
|
||||
log.debug('Overriding invalid response mode %s with %s',
|
||||
request.response_mode, self.default_response_mode)
|
||||
request.response_mode = self.default_response_mode
|
||||
|
||||
token_items = token.items()
|
||||
|
||||
if request.response_type == 'none':
|
||||
state = token.get('state', None)
|
||||
if state:
|
||||
token_items = [('state', state)]
|
||||
else:
|
||||
token_items = []
|
||||
|
||||
if request.response_mode == 'query':
|
||||
headers['Location'] = add_params_to_uri(
|
||||
request.redirect_uri, token_items, fragment=False)
|
||||
return headers, body, status
|
||||
|
||||
if request.response_mode == 'fragment':
|
||||
headers['Location'] = add_params_to_uri(
|
||||
request.redirect_uri, token_items, fragment=True)
|
||||
return headers, body, status
|
||||
|
||||
raise NotImplementedError(
|
||||
'Subclasses must set a valid default_response_mode')
|
||||
|
||||
def _get_default_headers(self):
|
||||
"""Create default headers for grant responses."""
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
'Pragma': 'no-cache',
|
||||
}
|
||||
|
||||
def _handle_redirects(self, request):
|
||||
if request.redirect_uri is not None:
|
||||
request.using_default_redirect_uri = False
|
||||
log.debug('Using provided redirect_uri %s', request.redirect_uri)
|
||||
if not is_absolute_uri(request.redirect_uri):
|
||||
raise errors.InvalidRedirectURIError(request=request)
|
||||
|
||||
# The authorization server MUST verify that the redirection URI
|
||||
# to which it will redirect the access token matches a
|
||||
# redirection URI registered by the client as described in
|
||||
# Section 3.1.2.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||
if not self.request_validator.validate_redirect_uri(
|
||||
request.client_id, request.redirect_uri, request):
|
||||
raise errors.MismatchingRedirectURIError(request=request)
|
||||
else:
|
||||
request.redirect_uri = self.request_validator.get_default_redirect_uri(
|
||||
request.client_id, request)
|
||||
request.using_default_redirect_uri = True
|
||||
log.debug('Using default redirect_uri %s.', request.redirect_uri)
|
||||
if not request.redirect_uri:
|
||||
raise errors.MissingRedirectURIError(request=request)
|
||||
if not is_absolute_uri(request.redirect_uri):
|
||||
raise errors.InvalidRedirectURIError(request=request)
|
||||
|
||||
def _create_cors_headers(self, request):
|
||||
"""If CORS is allowed, create the appropriate headers."""
|
||||
if 'origin' not in request.headers:
|
||||
return {}
|
||||
|
||||
origin = request.headers['origin']
|
||||
if not is_secure_transport(origin):
|
||||
log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
|
||||
return {}
|
||||
elif not self.request_validator.is_origin_allowed(
|
||||
request.client_id, origin, request):
|
||||
log.debug('Invalid origin "%s", CORS not allowed.', origin)
|
||||
return {}
|
||||
else:
|
||||
log.debug('Valid origin "%s", injecting CORS headers.', origin)
|
||||
return {'Access-Control-Allow-Origin': origin}
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .. import errors
|
||||
from .base import GrantTypeBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientCredentialsGrant(GrantTypeBase):
|
||||
|
||||
"""`Client Credentials Grant`_
|
||||
|
||||
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 method of which is beyond
|
||||
the scope of this specification).
|
||||
|
||||
The client credentials grant type MUST only be used by confidential
|
||||
clients::
|
||||
|
||||
+---------+ +---------------+
|
||||
: : : :
|
||||
: :>-- A - Client Authentication --->: Authorization :
|
||||
: Client : : Server :
|
||||
: :<-- B ---- Access Token ---------<: :
|
||||
: : : :
|
||||
+---------+ +---------------+
|
||||
|
||||
Figure 6: Client Credentials Flow
|
||||
|
||||
The flow illustrated in Figure 6 includes the following steps:
|
||||
|
||||
(A) The client authenticates with the authorization server and
|
||||
requests an access token from the token endpoint.
|
||||
|
||||
(B) The authorization server authenticates the client, and if valid,
|
||||
issues an access token.
|
||||
|
||||
.. _`Client Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.4
|
||||
"""
|
||||
|
||||
def create_token_response(self, request, token_handler):
|
||||
"""Return token or error in JSON format.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
|
||||
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`_.
|
||||
|
||||
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
|
||||
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
headers = self._get_default_headers()
|
||||
try:
|
||||
log.debug('Validating access token request, %r.', request)
|
||||
self.validate_token_request(request)
|
||||
except errors.OAuth2Error as e:
|
||||
log.debug('Client error in token request. %s.', e)
|
||||
headers.update(e.headers)
|
||||
return headers, e.json, e.status_code
|
||||
|
||||
token = token_handler.create_token(request, refresh_token=False)
|
||||
|
||||
for modifier in self._token_modifiers:
|
||||
token = modifier(token)
|
||||
|
||||
self.request_validator.save_token(token, request)
|
||||
|
||||
log.debug('Issuing token to client id %r (%r), %r.',
|
||||
request.client_id, request.client, token)
|
||||
return headers, json.dumps(token), 200
|
||||
|
||||
def validate_token_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
for validator in self.custom_validators.pre_token:
|
||||
validator(request)
|
||||
|
||||
if not getattr(request, 'grant_type', None):
|
||||
raise errors.InvalidRequestError('Request is missing grant type.',
|
||||
request=request)
|
||||
|
||||
if not request.grant_type == 'client_credentials':
|
||||
raise errors.UnsupportedGrantTypeError(request=request)
|
||||
|
||||
for param in ('grant_type', 'scope'):
|
||||
if param in request.duplicate_params:
|
||||
raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
|
||||
request=request)
|
||||
|
||||
log.debug('Authenticating client, %r.', request)
|
||||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
else:
|
||||
if not hasattr(request.client, 'client_id'):
|
||||
raise NotImplementedError('Authenticate client must set the '
|
||||
'request.client.client_id attribute '
|
||||
'in authenticate_client.')
|
||||
# Ensure client is authorized use of this grant type
|
||||
self.validate_grant_type(request)
|
||||
|
||||
request.client_id = request.client_id or request.client.client_id
|
||||
log.debug('Authorizing access to client %r.', request.client_id)
|
||||
self.validate_scopes(request)
|
||||
|
||||
for validator in self.custom_validators.post_token:
|
||||
validator(request)
|
||||
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib import common
|
||||
|
||||
from .. import errors
|
||||
from .base import GrantTypeBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImplicitGrant(GrantTypeBase):
|
||||
|
||||
"""`Implicit Grant`_
|
||||
|
||||
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.
|
||||
|
||||
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 |
|
||||
| |
|
||||
+---------+
|
||||
|
||||
Note: The lines illustrating steps (A) and (B) are broken into two
|
||||
parts as they pass through the user-agent.
|
||||
|
||||
Figure 4: Implicit Grant Flow
|
||||
|
||||
The flow illustrated in Figure 4 includes the following steps:
|
||||
|
||||
(A) The client initiates the flow by directing the resource owner's
|
||||
user-agent to the authorization endpoint. The client includes
|
||||
its client identifier, requested scope, local state, and a
|
||||
redirection URI to which the authorization server will send the
|
||||
user-agent back once access is granted (or denied).
|
||||
|
||||
(B) The authorization server authenticates the resource owner (via
|
||||
the user-agent) and establishes whether the resource owner
|
||||
grants or denies the client's access request.
|
||||
|
||||
(C) Assuming the resource owner grants access, the authorization
|
||||
server redirects the user-agent back to the client using the
|
||||
redirection URI provided earlier. The redirection URI includes
|
||||
the access token in the URI fragment.
|
||||
|
||||
(D) The user-agent follows the redirection instructions by making a
|
||||
request to the web-hosted client resource (which does not
|
||||
include the fragment per [RFC2616]). The user-agent retains the
|
||||
fragment information locally.
|
||||
|
||||
(E) The web-hosted client resource returns a web page (typically an
|
||||
HTML document with an embedded script) capable of accessing the
|
||||
full redirection URI including the fragment retained by the
|
||||
user-agent, and extracting the access token (and other
|
||||
parameters) contained in the fragment.
|
||||
|
||||
(F) The user-agent executes the script provided by the web-hosted
|
||||
client resource locally, which extracts the access token.
|
||||
|
||||
(G) The user-agent passes the access token to the client.
|
||||
|
||||
See `Section 10.3`_ and `Section 10.16`_ for important security considerations
|
||||
when using the implicit grant.
|
||||
|
||||
.. _`Implicit Grant`: https://tools.ietf.org/html/rfc6749#section-4.2
|
||||
.. _`Section 10.3`: https://tools.ietf.org/html/rfc6749#section-10.3
|
||||
.. _`Section 10.16`: https://tools.ietf.org/html/rfc6749#section-10.16
|
||||
"""
|
||||
|
||||
response_types = ['token']
|
||||
grant_allows_refresh_token = False
|
||||
|
||||
def create_authorization_response(self, request, token_handler):
|
||||
"""Create an authorization response.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
|
||||
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 `Appendix B`_:
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "token" for standard OAuth2 implicit flow
|
||||
or "id_token token" or just "id_token" for OIDC implicit flow
|
||||
|
||||
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 authorization server validates the request to ensure that all
|
||||
required parameters are present and valid. The authorization server
|
||||
MUST verify that the redirection URI to which it will redirect the
|
||||
access token matches a redirection URI registered by the client as
|
||||
described in `Section 3.1.2`_.
|
||||
|
||||
.. _`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
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
"""
|
||||
return self.create_token_response(request, token_handler)
|
||||
|
||||
def create_token_response(self, request, token_handler):
|
||||
"""Return token or error embedded in the URI fragment.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
|
||||
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
|
||||
`Appendix B`_:
|
||||
|
||||
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.
|
||||
|
||||
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
|
||||
"""
|
||||
try:
|
||||
self.validate_token_request(request)
|
||||
|
||||
# If the request fails due to a missing, invalid, or mismatching
|
||||
# redirection URI, or if the client identifier is missing or invalid,
|
||||
# the authorization server SHOULD inform the resource owner of the
|
||||
# error and MUST NOT automatically redirect the user-agent to the
|
||||
# invalid redirection URI.
|
||||
except errors.FatalClientError as e:
|
||||
log.debug('Fatal client error during validation of %r. %r.',
|
||||
request, e)
|
||||
raise
|
||||
|
||||
# If the resource owner denies the access request or if the request
|
||||
# fails for reasons other than a missing or invalid redirection URI,
|
||||
# the authorization server informs the client by adding the following
|
||||
# parameters to the fragment component of the redirection URI using the
|
||||
# "application/x-www-form-urlencoded" format, per Appendix B:
|
||||
# https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
except errors.OAuth2Error as e:
|
||||
log.debug('Client error during validation of %r. %r.', request, e)
|
||||
return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
|
||||
fragment=True)}, None, 302
|
||||
|
||||
# In OIDC implicit flow it is possible to have a request_type that does not include the access_token!
|
||||
# "id_token token" - return the access token and the id token
|
||||
# "id_token" - don't return the access token
|
||||
if "token" in request.response_type.split():
|
||||
token = token_handler.create_token(request, refresh_token=False)
|
||||
else:
|
||||
token = {}
|
||||
|
||||
if request.state is not None:
|
||||
token['state'] = request.state
|
||||
|
||||
for modifier in self._token_modifiers:
|
||||
token = modifier(token, token_handler, request)
|
||||
|
||||
# In OIDC implicit flow it is possible to have a request_type that does
|
||||
# not include the access_token! In this case there is no need to save a token.
|
||||
if "token" in request.response_type.split():
|
||||
self.request_validator.save_token(token, request)
|
||||
|
||||
return self.prepare_authorization_response(
|
||||
request, token, {}, None, 302)
|
||||
|
||||
def validate_authorization_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
return self.validate_token_request(request)
|
||||
|
||||
def validate_token_request(self, request):
|
||||
"""Check the token request for normal and fatal errors.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
This method is very similar to validate_authorization_request in
|
||||
the AuthorizationCodeGrant but differ in a few subtle areas.
|
||||
|
||||
A normal error could be a missing response_type parameter or the client
|
||||
attempting to access scope it is not allowed to ask authorization for.
|
||||
Normal errors can safely be included in the redirection URI and
|
||||
sent back to the client.
|
||||
|
||||
Fatal errors occur when the client_id or redirect_uri is invalid or
|
||||
missing. These must be caught by the provider and handled, how this
|
||||
is done is outside of the scope of OAuthLib but showing an error
|
||||
page describing the issue is a good idea.
|
||||
"""
|
||||
|
||||
# First check for fatal errors
|
||||
|
||||
# If the request fails due to a missing, invalid, or mismatching
|
||||
# redirection URI, or if the client identifier is missing or invalid,
|
||||
# the authorization server SHOULD inform the resource owner of the
|
||||
# error and MUST NOT automatically redirect the user-agent to the
|
||||
# invalid redirection URI.
|
||||
|
||||
# First check duplicate parameters
|
||||
for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
|
||||
try:
|
||||
duplicate_params = request.duplicate_params
|
||||
except ValueError:
|
||||
raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
|
||||
if param in duplicate_params:
|
||||
raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
|
||||
|
||||
# REQUIRED. The client identifier as described in Section 2.2.
|
||||
# https://tools.ietf.org/html/rfc6749#section-2.2
|
||||
if not request.client_id:
|
||||
raise errors.MissingClientIdError(request=request)
|
||||
|
||||
if not self.request_validator.validate_client_id(request.client_id, request):
|
||||
raise errors.InvalidClientIdError(request=request)
|
||||
|
||||
# OPTIONAL. As described in Section 3.1.2.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||
self._handle_redirects(request)
|
||||
|
||||
# Then check for normal errors.
|
||||
|
||||
request_info = self._run_custom_validators(request,
|
||||
self.custom_validators.all_pre)
|
||||
|
||||
# If the resource owner denies the access request or if the request
|
||||
# fails for reasons other than a missing or invalid redirection URI,
|
||||
# the authorization server informs the client by adding the following
|
||||
# parameters to the fragment component of the redirection URI using the
|
||||
# "application/x-www-form-urlencoded" format, per Appendix B.
|
||||
# https://tools.ietf.org/html/rfc6749#appendix-B
|
||||
|
||||
# Note that the correct parameters to be added are automatically
|
||||
# populated through the use of specific exceptions
|
||||
|
||||
# REQUIRED.
|
||||
if request.response_type is None:
|
||||
raise errors.MissingResponseTypeError(request=request)
|
||||
# Value MUST be one of our registered types: "token" by default or if using OIDC "id_token" or "id_token token"
|
||||
elif not set(request.response_type.split()).issubset(self.response_types):
|
||||
raise errors.UnsupportedResponseTypeError(request=request)
|
||||
|
||||
log.debug('Validating use of response_type token for client %r (%r).',
|
||||
request.client_id, request.client)
|
||||
if not self.request_validator.validate_response_type(request.client_id,
|
||||
request.response_type,
|
||||
request.client, request):
|
||||
|
||||
log.debug('Client %s is not authorized to use response_type %s.',
|
||||
request.client_id, request.response_type)
|
||||
raise errors.UnauthorizedClientError(request=request)
|
||||
|
||||
# OPTIONAL. The scope of the access request as described by Section 3.3
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
self.validate_scopes(request)
|
||||
|
||||
request_info.update({
|
||||
'client_id': request.client_id,
|
||||
'redirect_uri': request.redirect_uri,
|
||||
'response_type': request.response_type,
|
||||
'state': request.state,
|
||||
'request': request,
|
||||
})
|
||||
|
||||
request_info = self._run_custom_validators(
|
||||
request,
|
||||
self.custom_validators.all_post,
|
||||
request_info
|
||||
)
|
||||
|
||||
return request.scopes, request_info
|
||||
|
||||
def _run_custom_validators(self,
|
||||
request,
|
||||
validations,
|
||||
request_info=None):
|
||||
# Make a copy so we don't modify the existing request_info dict
|
||||
request_info = {} if request_info is None else request_info.copy()
|
||||
# For implicit grant, auth_validators and token_validators are
|
||||
# basically equivalent since the token is returned from the
|
||||
# authorization endpoint.
|
||||
for validator in validations:
|
||||
result = validator(request)
|
||||
if result is not None:
|
||||
request_info.update(result)
|
||||
return request_info
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .. import errors, utils
|
||||
from .base import GrantTypeBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RefreshTokenGrant(GrantTypeBase):
|
||||
|
||||
"""`Refresh token grant`_
|
||||
|
||||
.. _`Refresh token grant`: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
|
||||
def __init__(self, request_validator=None,
|
||||
issue_new_refresh_tokens=True,
|
||||
**kwargs):
|
||||
super().__init__(
|
||||
request_validator,
|
||||
issue_new_refresh_tokens=issue_new_refresh_tokens,
|
||||
**kwargs)
|
||||
|
||||
def create_token_response(self, request, token_handler):
|
||||
"""Create a new access token from a refresh_token.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
|
||||
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`_.
|
||||
|
||||
The authorization server MAY issue a new refresh token, in which case
|
||||
the client MUST discard the old refresh token and replace it with the
|
||||
new refresh token. The authorization server MAY revoke the old
|
||||
refresh token after issuing a new refresh token to the client. If a
|
||||
new refresh token is issued, the refresh token scope MUST be
|
||||
identical to that of the refresh token included by the client in the
|
||||
request.
|
||||
|
||||
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
|
||||
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
headers = self._get_default_headers()
|
||||
try:
|
||||
log.debug('Validating refresh token request, %r.', request)
|
||||
self.validate_token_request(request)
|
||||
except errors.OAuth2Error as e:
|
||||
log.debug('Client error in token request, %s.', e)
|
||||
headers.update(e.headers)
|
||||
return headers, e.json, e.status_code
|
||||
|
||||
token = token_handler.create_token(request,
|
||||
refresh_token=self.issue_new_refresh_tokens)
|
||||
|
||||
for modifier in self._token_modifiers:
|
||||
token = modifier(token, token_handler, request)
|
||||
|
||||
self.request_validator.save_token(token, request)
|
||||
|
||||
log.debug('Issuing new token to client id %r (%r), %r.',
|
||||
request.client_id, request.client, token)
|
||||
headers.update(self._create_cors_headers(request))
|
||||
return headers, json.dumps(token), 200
|
||||
|
||||
def validate_token_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
# REQUIRED. Value MUST be set to "refresh_token".
|
||||
if request.grant_type != 'refresh_token':
|
||||
raise errors.UnsupportedGrantTypeError(request=request)
|
||||
|
||||
for validator in self.custom_validators.pre_token:
|
||||
validator(request)
|
||||
|
||||
if request.refresh_token is None:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing refresh token parameter.',
|
||||
request=request)
|
||||
|
||||
# Because refresh tokens are typically long-lasting credentials used to
|
||||
# request additional access tokens, the refresh token is bound to the
|
||||
# client to which it was issued. 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.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
if self.request_validator.client_authentication_required(request):
|
||||
log.debug('Authenticating client, %r.', request)
|
||||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Invalid client (%r), denying access.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
elif not self.request_validator.authenticate_client_id(request.client_id, request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
|
||||
# Ensure client is authorized use of this grant type
|
||||
self.validate_grant_type(request)
|
||||
|
||||
# REQUIRED. The refresh token issued to the client.
|
||||
log.debug('Validating refresh token %s for client %r.',
|
||||
request.refresh_token, request.client)
|
||||
if not self.request_validator.validate_refresh_token(
|
||||
request.refresh_token, request.client, request):
|
||||
log.debug('Invalid refresh token, %s, for client %r.',
|
||||
request.refresh_token, request.client)
|
||||
raise errors.InvalidGrantError(request=request)
|
||||
|
||||
original_scopes = utils.scope_to_list(
|
||||
self.request_validator.get_original_scopes(
|
||||
request.refresh_token, request))
|
||||
|
||||
if request.scope:
|
||||
request.scopes = utils.scope_to_list(request.scope)
|
||||
if (not all(s in original_scopes for s in request.scopes)
|
||||
and not self.request_validator.is_within_original_scope(
|
||||
request.scopes, request.refresh_token, request)):
|
||||
log.debug('Refresh token %s lack requested scopes, %r.',
|
||||
request.refresh_token, request.scopes)
|
||||
raise errors.InvalidScopeError(request=request)
|
||||
else:
|
||||
request.scopes = original_scopes
|
||||
|
||||
for validator in self.custom_validators.post_token:
|
||||
validator(request)
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.grant_types
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .. import errors
|
||||
from .base import GrantTypeBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
|
||||
|
||||
"""`Resource Owner Password Credentials Grant`_
|
||||
|
||||
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) | |
|
||||
+---------+ +---------------+
|
||||
|
||||
Figure 5: Resource Owner Password Credentials Flow
|
||||
|
||||
The flow illustrated in Figure 5 includes the following steps:
|
||||
|
||||
(A) The resource owner provides the client with its username and
|
||||
password.
|
||||
|
||||
(B) The client requests an access token from the authorization
|
||||
server's token endpoint by including the credentials received
|
||||
from the resource owner. When making the request, the client
|
||||
authenticates with the authorization server.
|
||||
|
||||
(C) The authorization server authenticates the client and validates
|
||||
the resource owner credentials, and if valid, issues an access
|
||||
token.
|
||||
|
||||
.. _`Resource Owner Password Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.3
|
||||
"""
|
||||
|
||||
def create_token_response(self, request, token_handler):
|
||||
"""Return token or error in json format.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param token_handler: A token handler instance, for example of type
|
||||
oauthlib.oauth2.BearerToken.
|
||||
|
||||
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`_.
|
||||
|
||||
.. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
|
||||
.. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
headers = self._get_default_headers()
|
||||
try:
|
||||
if self.request_validator.client_authentication_required(request):
|
||||
log.debug('Authenticating client, %r.', request)
|
||||
if not self.request_validator.authenticate_client(request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
elif not self.request_validator.authenticate_client_id(request.client_id, request):
|
||||
log.debug('Client authentication failed, %r.', request)
|
||||
raise errors.InvalidClientError(request=request)
|
||||
log.debug('Validating access token request, %r.', request)
|
||||
self.validate_token_request(request)
|
||||
except errors.OAuth2Error as e:
|
||||
log.debug('Client error in token request, %s.', e)
|
||||
headers.update(e.headers)
|
||||
return headers, e.json, e.status_code
|
||||
|
||||
token = token_handler.create_token(request, self.refresh_token)
|
||||
|
||||
for modifier in self._token_modifiers:
|
||||
token = modifier(token)
|
||||
|
||||
self.request_validator.save_token(token, request)
|
||||
|
||||
log.debug('Issuing token %r to client id %r (%r) and username %s.',
|
||||
token, request.client_id, request.client, request.username)
|
||||
return headers, json.dumps(token), 200
|
||||
|
||||
def validate_token_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
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`_.
|
||||
|
||||
The authorization server MUST:
|
||||
|
||||
o require client authentication for confidential clients or for any
|
||||
client that was issued client credentials (or with other
|
||||
authentication requirements),
|
||||
|
||||
o authenticate the client if client authentication is included, and
|
||||
|
||||
o validate the resource owner password credentials using its
|
||||
existing password validation algorithm.
|
||||
|
||||
Since this access token request utilizes the resource owner's
|
||||
password, the authorization server MUST protect the endpoint against
|
||||
brute force attacks (e.g., using rate-limitation or generating
|
||||
alerts).
|
||||
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
.. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
|
||||
"""
|
||||
for validator in self.custom_validators.pre_token:
|
||||
validator(request)
|
||||
|
||||
for param in ('grant_type', 'username', 'password'):
|
||||
if not getattr(request, param, None):
|
||||
raise errors.InvalidRequestError(
|
||||
'Request is missing %s parameter.' % param, request=request)
|
||||
|
||||
for param in ('grant_type', 'username', 'password', 'scope'):
|
||||
if param in request.duplicate_params:
|
||||
raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
|
||||
|
||||
# This error should rarely (if ever) occur if requests are routed to
|
||||
# grant type handlers based on the grant_type parameter.
|
||||
if not request.grant_type == 'password':
|
||||
raise errors.UnsupportedGrantTypeError(request=request)
|
||||
|
||||
log.debug('Validating username %s.', request.username)
|
||||
if not self.request_validator.validate_user(request.username,
|
||||
request.password, request.client, request):
|
||||
raise errors.InvalidGrantError(
|
||||
'Invalid credentials given.', request=request)
|
||||
else:
|
||||
if not hasattr(request.client, 'client_id'):
|
||||
raise NotImplementedError(
|
||||
'Validate user must set the '
|
||||
'request.client.client_id attribute '
|
||||
'in authenticate_client.')
|
||||
log.debug('Authorizing access to user %r.', request.user)
|
||||
|
||||
# Ensure client is authorized use of this grant type
|
||||
self.validate_grant_type(request)
|
||||
|
||||
if request.client:
|
||||
request.client_id = request.client_id or request.client.client_id
|
||||
self.validate_scopes(request)
|
||||
|
||||
for validator in self.custom_validators.post_token:
|
||||
validator(request)
|
||||
@@ -0,0 +1,471 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.parameters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
|
||||
|
||||
.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.parse as urlparse
|
||||
|
||||
from oauthlib.common import add_params_to_qs, add_params_to_uri
|
||||
from oauthlib.signals import scope_changed
|
||||
|
||||
from .errors import (
|
||||
InsecureTransportError, MismatchingStateError, MissingCodeError,
|
||||
MissingTokenError, MissingTokenTypeError, raise_from_error,
|
||||
)
|
||||
from .tokens import OAuth2Token
|
||||
from .utils import is_secure_transport, list_to_scope, scope_to_list
|
||||
|
||||
|
||||
def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
|
||||
scope=None, state=None, code_challenge=None, code_challenge_method='plain', **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 as defined by
|
||||
[`W3C.REC-html401-19991224`_]:
|
||||
|
||||
:param uri:
|
||||
: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 code_challenge: PKCE parameter. A challenge derived from the
|
||||
code_verifier that is sent in the authorization
|
||||
request, to be verified against later.
|
||||
:param code_challenge_method: PKCE parameter. A method that was used to derive the
|
||||
code_challenge. Defaults to "plain" if not present in the request.
|
||||
:param kwargs: Extra arguments to embed in the grant/authorization URL.
|
||||
|
||||
An example of an authorization code grant authorization URL:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
|
||||
&code_[AWS-SECRET-REMOVED]HG6&code_challenge_method=S256
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
|
||||
Host: server.example.com
|
||||
|
||||
.. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
|
||||
.. _`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
|
||||
"""
|
||||
if not is_secure_transport(uri):
|
||||
raise InsecureTransportError()
|
||||
|
||||
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))
|
||||
if code_challenge is not None:
|
||||
params.append(('code_challenge', code_challenge))
|
||||
params.append(('code_challenge_method', code_challenge_method))
|
||||
|
||||
for k in kwargs:
|
||||
if kwargs[k]:
|
||||
params.append((str(k), kwargs[k]))
|
||||
|
||||
return add_params_to_uri(uri, params)
|
||||
|
||||
|
||||
def prepare_token_request(grant_type, body='', include_client_id=True, code_verifier=None, **kwargs):
|
||||
"""Prepare the access token request.
|
||||
|
||||
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 (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
|
||||
:param include_client_id: `True` (default) to send the `client_id` in the
|
||||
body of the upstream request. This is required
|
||||
if the client is not authenticating with the
|
||||
authorization server as described in
|
||||
`Section 3.2.1`_.
|
||||
:type include_client_id: Boolean
|
||||
|
||||
:param client_id: Unicode client identifier. Will only appear if
|
||||
`include_client_id` is True. *
|
||||
|
||||
:param client_secret: Unicode client secret. Will only appear if set to a
|
||||
value that is not `None`. Invoking this function with
|
||||
an empty string will send an empty `client_secret`
|
||||
value to the server. *
|
||||
|
||||
:param code: If using authorization_code grant, pass the previously
|
||||
obtained authorization code as the ``code`` argument. *
|
||||
|
||||
: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 code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
|
||||
authorization request to the token request.
|
||||
|
||||
:param kwargs: Extra arguments to embed in the request body.
|
||||
|
||||
Parameters marked with a `*` above are not explicit arguments in the
|
||||
function signature, but are specially documented arguments for items
|
||||
appearing in the generic `**kwargs` keyworded input.
|
||||
|
||||
An example of an authorization code token request body:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
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
|
||||
"""
|
||||
params = [('grant_type', grant_type)]
|
||||
|
||||
if 'scope' in kwargs:
|
||||
kwargs['scope'] = list_to_scope(kwargs['scope'])
|
||||
|
||||
# pull the `client_id` out of the kwargs.
|
||||
client_id = kwargs.pop('client_id', None)
|
||||
if include_client_id:
|
||||
if client_id is not None:
|
||||
params.append(('client_id', client_id))
|
||||
|
||||
# use code_verifier if code_challenge was passed in the authorization request
|
||||
if code_verifier is not None:
|
||||
params.append(('code_verifier', code_verifier))
|
||||
|
||||
# the kwargs iteration below only supports including boolean truth (truthy)
|
||||
# values, but some servers may require an empty string for `client_secret`
|
||||
client_secret = kwargs.pop('client_secret', None)
|
||||
if client_secret is not None:
|
||||
params.append(('client_secret', client_secret))
|
||||
|
||||
# this handles: `code`, `redirect_uri`, and other undocumented params
|
||||
for k in kwargs:
|
||||
if kwargs[k]:
|
||||
params.append((str(k), kwargs[k]))
|
||||
|
||||
return add_params_to_qs(body, params)
|
||||
|
||||
|
||||
def prepare_token_revocation_request(url, token, token_type_hint="access_token",
|
||||
callback=None, body='', **kwargs):
|
||||
"""Prepare a token revocation request.
|
||||
|
||||
The client constructs the request by including the following parameters
|
||||
using the ``application/x-www-form-urlencoded`` format in the HTTP request
|
||||
entity-body:
|
||||
|
||||
:param token: REQUIRED. The token that the client wants to get revoked.
|
||||
|
||||
:param token_type_hint: OPTIONAL. A hint about the type of the token
|
||||
submitted for revocation. Clients MAY pass this
|
||||
parameter in order to help the authorization server
|
||||
to optimize the token lookup. If the server is
|
||||
unable to locate the token using the given hint, it
|
||||
MUST extend its search across all of its supported
|
||||
token types. An authorization server MAY ignore
|
||||
this parameter, particularly if it is able to detect
|
||||
the token type automatically.
|
||||
|
||||
This specification defines two values for `token_type_hint`:
|
||||
|
||||
* access_token: An access token as defined in [RFC6749],
|
||||
`Section 1.4`_
|
||||
|
||||
* refresh_token: A refresh token as defined in [RFC6749],
|
||||
`Section 1.5`_
|
||||
|
||||
Specific implementations, profiles, and extensions of this
|
||||
specification MAY define other values for this parameter using the
|
||||
registry defined in `Section 4.1.2`_.
|
||||
|
||||
.. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
|
||||
.. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
|
||||
.. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2
|
||||
|
||||
"""
|
||||
if not is_secure_transport(url):
|
||||
raise InsecureTransportError()
|
||||
|
||||
params = [('token', token)]
|
||||
|
||||
if token_type_hint:
|
||||
params.append(('token_type_hint', token_type_hint))
|
||||
|
||||
for k in kwargs:
|
||||
if kwargs[k]:
|
||||
params.append((str(k), kwargs[k]))
|
||||
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
if callback:
|
||||
params.append(('callback', callback))
|
||||
return add_params_to_uri(url, params), headers, body
|
||||
else:
|
||||
return url, headers, 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
|
||||
|
||||
"""
|
||||
if not is_secure_transport(uri):
|
||||
raise InsecureTransportError()
|
||||
|
||||
query = urlparse.urlparse(uri).query
|
||||
params = dict(urlparse.parse_qsl(query))
|
||||
|
||||
if state and params.get('state', None) != state:
|
||||
raise MismatchingStateError()
|
||||
|
||||
if 'error' in params:
|
||||
raise_from_error(params.get('error'), params)
|
||||
|
||||
if not 'code' in params:
|
||||
raise MissingCodeError("Missing code parameter in response.")
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_implicit_response(uri, state=None, scope=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.
|
||||
|
||||
:param uri:
|
||||
:param state:
|
||||
:param scope:
|
||||
|
||||
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
|
||||
"""
|
||||
if not is_secure_transport(uri):
|
||||
raise InsecureTransportError()
|
||||
|
||||
fragment = urlparse.urlparse(uri).fragment
|
||||
params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
|
||||
|
||||
for key in ('expires_in',):
|
||||
if key in params: # cast things to int
|
||||
params[key] = int(params[key])
|
||||
|
||||
if 'scope' in params:
|
||||
params['scope'] = scope_to_list(params['scope'])
|
||||
|
||||
if 'expires_in' in params:
|
||||
params['expires_at'] = time.time() + int(params['expires_in'])
|
||||
|
||||
if state and params.get('state', None) != state:
|
||||
raise ValueError("Mismatching or missing state in params.")
|
||||
|
||||
params = OAuth2Token(params, old_scope=scope)
|
||||
validate_token_parameters(params)
|
||||
return params
|
||||
|
||||
|
||||
def parse_token_response(body, scope=None):
|
||||
"""Parse the JSON token response body into a dict.
|
||||
|
||||
The authorization server issues an access token and optional refresh
|
||||
token, and constructs the response by adding the following parameters
|
||||
to the entity body of the HTTP response with a 200 (OK) status code:
|
||||
|
||||
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.
|
||||
refresh_token
|
||||
OPTIONAL. The refresh token which can be used to obtain new
|
||||
access tokens using the same authorization grant as described
|
||||
in `Section 6`_.
|
||||
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`_.
|
||||
|
||||
The parameters are included in the entity body of the HTTP response
|
||||
using the "application/json" media type as defined by [`RFC4627`_]. The
|
||||
parameters are serialized into a JSON structure by adding each
|
||||
parameter at the highest structure level. Parameter names and string
|
||||
values are included as JSON strings. Numerical values are included
|
||||
as JSON numbers. The order of parameters does not matter and can
|
||||
vary.
|
||||
|
||||
:param body: The full json encoded response body.
|
||||
:param scope: The scope requested during authorization.
|
||||
|
||||
For example:
|
||||
|
||||
.. 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"
|
||||
}
|
||||
|
||||
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
|
||||
.. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
|
||||
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
.. _`RFC4627`: https://tools.ietf.org/html/rfc4627
|
||||
"""
|
||||
try:
|
||||
params = json.loads(body)
|
||||
except ValueError:
|
||||
|
||||
# Fall back to URL-encoded string, to support old implementations,
|
||||
# including (at time of writing) Facebook. See:
|
||||
# https://github.com/oauthlib/oauthlib/issues/267
|
||||
|
||||
params = dict(urlparse.parse_qsl(body))
|
||||
for key in ('expires_in',):
|
||||
if key in params: # cast things to int
|
||||
params[key] = int(params[key])
|
||||
|
||||
if 'scope' in params:
|
||||
params['scope'] = scope_to_list(params['scope'])
|
||||
|
||||
if 'expires_in' in params:
|
||||
if params['expires_in'] is None:
|
||||
params.pop('expires_in')
|
||||
else:
|
||||
params['expires_at'] = time.time() + int(params['expires_in'])
|
||||
|
||||
params = OAuth2Token(params, old_scope=scope)
|
||||
validate_token_parameters(params)
|
||||
return params
|
||||
|
||||
|
||||
def validate_token_parameters(params):
|
||||
"""Ensures token presence, token type, expiration and scope in params."""
|
||||
if 'error' in params:
|
||||
raise_from_error(params.get('error'), params)
|
||||
|
||||
if not 'access_token' in params:
|
||||
raise MissingTokenError(description="Missing access token parameter.")
|
||||
|
||||
if not 'token_type' in params:
|
||||
if os***REMOVED***iron.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
|
||||
raise MissingTokenTypeError()
|
||||
|
||||
# If the issued access token scope is different from the one requested by
|
||||
# the client, the authorization server MUST include the "scope" response
|
||||
# parameter to inform the client of the actual scope granted.
|
||||
# https://tools.ietf.org/html/rfc6749#section-3.3
|
||||
if params.scope_changed:
|
||||
message = 'Scope has changed from "{old}" to "{new}".'.format(
|
||||
old=params.old_scope, new=params.scope,
|
||||
)
|
||||
scope_changed.send(message=message, old=params.old_scopes, new=params.scopes)
|
||||
if not os***REMOVED***iron.get('OAUTHLIB_RELAX_TOKEN_SCOPE', None):
|
||||
w = Warning(message)
|
||||
w.token = params
|
||||
w.old_scope = params.old_scopes
|
||||
w.new_scope = params.scopes
|
||||
raise w
|
||||
@@ -0,0 +1,680 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.request_validator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
"""
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestValidator:
|
||||
|
||||
def client_authentication_required(self, request, *args, **kwargs):
|
||||
"""Determine if client authentication is required for current request.
|
||||
|
||||
According to the rfc6749, client authentication is required in the following cases:
|
||||
- Resource Owner Password Credentials Grant, when Client type is Confidential or when
|
||||
Client was issued client credentials or whenever Client provided client
|
||||
authentication, see `Section 4.3.2`_.
|
||||
- Authorization Code Grant, when Client type is Confidential or when Client was issued
|
||||
client credentials or whenever Client provided client authentication,
|
||||
see `Section 4.1.3`_.
|
||||
- Refresh Token Grant, when Client type is Confidential or when Client was issued
|
||||
client credentials or whenever Client provided client authentication, see
|
||||
`Section 6`_
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Refresh Token Grant
|
||||
|
||||
.. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2
|
||||
.. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
|
||||
.. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
return True
|
||||
|
||||
def authenticate_client(self, request, *args, **kwargs):
|
||||
"""Authenticate client through means outside the OAuth 2 spec.
|
||||
|
||||
Means of authentication is negotiated beforehand and may for example
|
||||
be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization
|
||||
header.
|
||||
|
||||
Headers may be accesses through request.headers and parameters found in
|
||||
both body and query can be obtained by direct attribute access, i.e.
|
||||
request.client_id for client_id in the URL query.
|
||||
|
||||
The authentication process is required to contain the identification of
|
||||
the client (i.e. search the database based on the client_id). In case the
|
||||
client doesn't exist based on the received client_id, this method has to
|
||||
return False and the HTTP response created by the library will contain
|
||||
'invalid_client' message.
|
||||
|
||||
After the client identification succeeds, this method needs to set the
|
||||
client on the request, i.e. request.client = client. A client object's
|
||||
class must contain the 'client_id' attribute and the 'client_id' must have
|
||||
a value.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Resource Owner Password Credentials Grant (may be disabled)
|
||||
- Client Credentials Grant
|
||||
- Refresh Token Grant
|
||||
|
||||
.. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def authenticate_client_id(self, client_id, request, *args, **kwargs):
|
||||
"""Ensure client_id belong to a non-confidential client.
|
||||
|
||||
A non-confidential client is one that is not required to authenticate
|
||||
through other means, such as using HTTP Basic.
|
||||
|
||||
Note, while not strictly necessary it can often be very convenient
|
||||
to set request.client to the client object associated with the
|
||||
given client_id.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request,
|
||||
*args, **kwargs):
|
||||
"""Ensure that the authorization process represented by this authorization
|
||||
code began with this 'redirect_uri'.
|
||||
|
||||
If the client specifies a redirect_uri when obtaining code then that
|
||||
redirect URI must be bound to the code and verified equal in this
|
||||
method, according to RFC 6749 section 4.1.3. Do not compare against
|
||||
the client's allowed redirect URIs, but against the URI used when the
|
||||
code was saved.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param code: Unicode authorization_code.
|
||||
:param redirect_uri: Unicode absolute URI.
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant (during token request)
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
||||
"""Get the default redirect URI for the client.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: The default redirect URI for the client
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def get_default_scopes(self, client_id, request, *args, **kwargs):
|
||||
"""Get the default scopes for the client.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: List of default scopes
|
||||
|
||||
Method is used by all core grant types:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Client Credentials grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||
"""Get the list of scopes associated with the refresh token.
|
||||
|
||||
:param refresh_token: Unicode refresh token.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: List of scopes.
|
||||
|
||||
Method is used by:
|
||||
- Refresh token grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def is_within_original_scope(self, request_scopes, refresh_token, request, *args, **kwargs):
|
||||
"""Check if requested scopes are within a scope of the refresh token.
|
||||
|
||||
When access tokens are refreshed the scope of the new token
|
||||
needs to be within the scope of the original token. This is
|
||||
ensured by checking that all requested scopes strings are on
|
||||
the list returned by the get_original_scopes. If this check
|
||||
fails, is_within_original_scope is called. The method can be
|
||||
used in situations where returning all valid scopes from the
|
||||
get_original_scopes is not practical.
|
||||
|
||||
:param request_scopes: A list of scopes that were requested by client.
|
||||
:param refresh_token: Unicode refresh_token.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Refresh token grant
|
||||
"""
|
||||
return False
|
||||
|
||||
def introspect_token(self, token, token_type_hint, request, *args, **kwargs):
|
||||
"""Introspect an access or refresh token.
|
||||
|
||||
Called once the introspect request is validated. This method should
|
||||
verify the *token* and either return a dictionary with the list of
|
||||
claims associated, or `None` in case the token is unknown.
|
||||
|
||||
Below the list of registered claims you should be interested in:
|
||||
|
||||
- scope : space-separated list of scopes
|
||||
- client_id : client identifier
|
||||
- username : human-readable identifier for the resource owner
|
||||
- token_type : type of the token
|
||||
- exp : integer timestamp indicating when this token will expire
|
||||
- iat : integer timestamp indicating when this token was issued
|
||||
- nbf : integer timestamp indicating when it can be "not-before" used
|
||||
- sub : subject of the token - identifier of the resource owner
|
||||
- aud : list of string identifiers representing the intended audience
|
||||
- iss : string representing issuer of this token
|
||||
- jti : string identifier for the token
|
||||
|
||||
Note that most of them are coming directly from JWT RFC. More details
|
||||
can be found in `Introspect Claims`_ or `JWT Claims`_.
|
||||
|
||||
The implementation can use *token_type_hint* to improve lookup
|
||||
efficiency, but must fallback to other types to be compliant with RFC.
|
||||
|
||||
The dict of claims is added to request.token after this method.
|
||||
|
||||
:param token: The token string.
|
||||
:param token_type_hint: access_token or refresh_token.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
Method is used by:
|
||||
- Introspect Endpoint (all grants are compatible)
|
||||
|
||||
.. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2
|
||||
.. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||
"""Invalidate an authorization code after use.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param code: The authorization code grant (request.code).
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
|
||||
"""Revoke an access or refresh token.
|
||||
|
||||
:param token: The token string.
|
||||
:param token_type_hint: access_token or refresh_token.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
Method is used by:
|
||||
- Revocation Endpoint
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def rotate_refresh_token(self, request):
|
||||
"""Determine whether to rotate the refresh token. Default, yes.
|
||||
|
||||
When access tokens are refreshed the old refresh token can be kept
|
||||
or replaced with a new one (rotated). Return True to rotate and
|
||||
and False for keeping original.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Refresh Token Grant
|
||||
"""
|
||||
return True
|
||||
|
||||
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||
"""Persist the authorization_code.
|
||||
|
||||
The code should at minimum be stored with:
|
||||
- the client_id (``client_id``)
|
||||
- the redirect URI used (``request.redirect_uri``)
|
||||
- a resource owner / user (``request.user``)
|
||||
- the authorized scopes (``request.scopes``)
|
||||
|
||||
To support PKCE, you MUST associate the code with:
|
||||
- Code Challenge (``request.code_challenge``) and
|
||||
- Code Challenge Method (``request.code_challenge_method``)
|
||||
|
||||
To support OIDC, you MUST associate the code with:
|
||||
- nonce, if present (``code["nonce"]``)
|
||||
|
||||
The ``code`` argument is actually a dictionary, containing at least a
|
||||
``code`` key with the actual authorization code:
|
||||
|
||||
``{'code': 'sdf345jsdf0934f'}``
|
||||
|
||||
It may also have a ``claims`` parameter which, when present, will be a dict
|
||||
deserialized from JSON as described at
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
|
||||
This value should be saved in this method and used again in ``.validate_code``.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param code: A dict of the authorization code grant and, optionally, state.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def save_token(self, token, request, *args, **kwargs):
|
||||
"""Persist the token with a token type specific method.
|
||||
|
||||
Currently, only save_bearer_token is supported.
|
||||
|
||||
:param token: A (Bearer) token dict.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
return self.save_bearer_token(token, request, *args, **kwargs)
|
||||
|
||||
def save_bearer_token(self, token, request, *args, **kwargs):
|
||||
"""Persist the Bearer token.
|
||||
|
||||
The Bearer token should at minimum be associated with:
|
||||
- a client and it's client_id, if available
|
||||
- a resource owner / user (request.user)
|
||||
- authorized scopes (request.scopes)
|
||||
- an expiration time
|
||||
- a refresh token, if issued
|
||||
- a claims document, if present in request.claims
|
||||
|
||||
The Bearer token dict may hold a number of items::
|
||||
|
||||
{
|
||||
'token_type': 'Bearer',
|
||||
'access_token': 'askfjh234as9sd8',
|
||||
'expires_in': 3600,
|
||||
'scope': 'string of space separated authorized scopes',
|
||||
'refresh_token': '23sdf876234', # if issued
|
||||
'state': 'given_by_client', # if supplied by client (implicit ONLY)
|
||||
}
|
||||
|
||||
Note that while "scope" is a string-separated list of authorized scopes,
|
||||
the original list is still available in request.scopes.
|
||||
|
||||
The token dict is passed as a reference so any changes made to the dictionary
|
||||
will go back to the user. If additional information must return to the client
|
||||
user, and it is only possible to get this information after writing the token
|
||||
to storage, it should be added to the token dictionary. If the token
|
||||
dictionary must be modified but the changes should not go back to the user,
|
||||
a copy of the dictionary must be made before making the changes.
|
||||
|
||||
Also note that if an Authorization Code grant request included a valid claims
|
||||
parameter (for OpenID Connect) then the request.claims property will contain
|
||||
the claims dict, which should be saved for later use when generating the
|
||||
id_token and/or UserInfo response content.
|
||||
|
||||
:param token: A Bearer token dict.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: The default redirect URI for the client
|
||||
|
||||
Method is used by all core grant types issuing Bearer tokens:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
- Resource Owner Password Credentials Grant (might not associate a client)
|
||||
- Client Credentials grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_bearer_token(self, token, scopes, request):
|
||||
"""Ensure the Bearer token is valid and authorized access to scopes.
|
||||
|
||||
:param token: A string of random characters.
|
||||
:param scopes: A list of scopes associated with the protected resource.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
A key to OAuth 2 security and restricting impact of leaked tokens is
|
||||
the short expiration time of tokens, *always ensure the token has not
|
||||
expired!*.
|
||||
|
||||
Two different approaches to scope validation:
|
||||
|
||||
1) all(scopes). The token must be authorized access to all scopes
|
||||
associated with the resource. For example, the
|
||||
token has access to ``read-only`` and ``images``,
|
||||
thus the client can view images but not upload new.
|
||||
Allows for fine grained access control through
|
||||
combining various scopes.
|
||||
|
||||
2) any(scopes). The token must be authorized access to one of the
|
||||
scopes associated with the resource. For example,
|
||||
token has access to ``read-only-images``.
|
||||
Allows for fine grained, although arguably less
|
||||
convenient, access control.
|
||||
|
||||
A powerful way to use scopes would mimic UNIX ACLs and see a scope
|
||||
as a group with certain privileges. For a restful API these might
|
||||
map to HTTP verbs instead of read, write and execute.
|
||||
|
||||
Note, the request.user attribute can be set to the resource owner
|
||||
associated with this token. Similarly the request.client and
|
||||
request.scopes attribute can be set to associated client object
|
||||
and authorized scopes. If you then use a decorator such as the
|
||||
one provided for django these attributes will be made available
|
||||
in all protected views as keyword arguments.
|
||||
|
||||
:param token: Unicode Bearer token
|
||||
:param scopes: List of scopes (defined by you)
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is indirectly used by all core Bearer token issuing grant types:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Client Credentials Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_client_id(self, client_id, request, *args, **kwargs):
|
||||
"""Ensure client_id belong to a valid and active client.
|
||||
|
||||
Note, while not strictly necessary it can often be very convenient
|
||||
to set request.client to the client object associated with the
|
||||
given client_id.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||
"""Verify that the authorization_code is valid and assigned to the given
|
||||
client.
|
||||
|
||||
Before returning true, set the following based on the information stored
|
||||
with the code in 'save_authorization_code':
|
||||
|
||||
- request.user
|
||||
- request.scopes
|
||||
- request.claims (if given)
|
||||
|
||||
OBS! The request.user attribute should be set to the resource owner
|
||||
associated with this authorization code. Similarly request.scopes
|
||||
must also be set.
|
||||
|
||||
The request.claims property, if it was given, should assigned a dict.
|
||||
|
||||
If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code')
|
||||
you MUST set the following based on the information stored:
|
||||
|
||||
- request.code_challenge
|
||||
- request.code_challenge_method
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param code: Unicode authorization code.
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
|
||||
"""Ensure client is authorized to use the grant_type requested.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param grant_type: Unicode grant type, i.e. authorization_code, password.
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Client Credentials Grant
|
||||
- Refresh Token Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
|
||||
"""Ensure client is authorized to redirect to the redirect_uri requested.
|
||||
|
||||
All clients should register the absolute URIs of all URIs they intend
|
||||
to redirect to. The registration is outside of the scope of oauthlib.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param redirect_uri: Unicode absolute URI.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
|
||||
"""Ensure the Bearer token is valid and authorized access to scopes.
|
||||
|
||||
OBS! The request.user attribute should be set to the resource owner
|
||||
associated with this refresh token.
|
||||
|
||||
:param refresh_token: Unicode refresh token.
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant (indirectly by issuing refresh tokens)
|
||||
- Resource Owner Password Credentials Grant (also indirectly)
|
||||
- Refresh Token Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
|
||||
"""Ensure client is authorized to use the response_type requested.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param response_type: Unicode response type, i.e. code, token.
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
||||
"""Ensure the client is authorized access to requested scopes.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param scopes: List of scopes (defined by you).
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by all core grant types:
|
||||
- Authorization Code Grant
|
||||
- Implicit Grant
|
||||
- Resource Owner Password Credentials Grant
|
||||
- Client Credentials Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_user(self, username, password, client, request, *args, **kwargs):
|
||||
"""Ensure the username and password is valid.
|
||||
|
||||
OBS! The validation should also set the user attribute of the request
|
||||
to a valid resource owner, i.e. request.user = username or similar. If
|
||||
not set you will be unable to associate a token with a user in the
|
||||
persistence method used (commonly, save_bearer_token).
|
||||
|
||||
:param username: Unicode username.
|
||||
:param password: Unicode password.
|
||||
:param client: Client object set by you, see ``.authenticate_client``.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Resource Owner Password Credentials Grant
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def is_pkce_required(self, client_id, request):
|
||||
"""Determine if current request requires PKCE. Default, False.
|
||||
This is called for both "authorization" and "token" requests.
|
||||
|
||||
Override this method by ``return True`` to enable PKCE for everyone.
|
||||
You might want to enable it only for public clients.
|
||||
Note that PKCE can also be used in addition of a client authentication.
|
||||
|
||||
OAuth 2.0 public clients utilizing the Authorization Code Grant are
|
||||
susceptible to the authorization code interception attack. This
|
||||
specification describes the attack as well as a technique to mitigate
|
||||
against the threat through the use of Proof Key for Code Exchange
|
||||
(PKCE, pronounced "pixy"). See `RFC7636`_.
|
||||
|
||||
:param client_id: Client identifier.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
|
||||
.. _`RFC7636`: https://tools.ietf.org/html/rfc7636
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_code_challenge(self, code, request):
|
||||
"""Is called for every "token" requests.
|
||||
|
||||
When the server issues the authorization code in the authorization
|
||||
response, it MUST associate the ``code_challenge`` and
|
||||
``code_challenge_method`` values with the authorization code so it can
|
||||
be verified later.
|
||||
|
||||
Typically, the ``code_challenge`` and ``code_challenge_method`` values
|
||||
are stored in encrypted form in the ``code`` itself but could
|
||||
alternatively be stored on the server associated with the code. The
|
||||
server MUST NOT include the ``code_challenge`` value in client requests
|
||||
in a form that other entities can extract.
|
||||
|
||||
Return the ``code_challenge`` associated to the code.
|
||||
If ``None`` is returned, code is considered to not be associated to any
|
||||
challenges.
|
||||
|
||||
:param code: Authorization code.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: code_challenge string
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant - when PKCE is active
|
||||
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_code_challenge_method(self, code, request):
|
||||
"""Is called during the "token" request processing, when a
|
||||
``code_verifier`` and a ``code_challenge`` has been provided.
|
||||
|
||||
See ``.get_code_challenge``.
|
||||
|
||||
Must return ``plain`` or ``S256``. You can return a custom value if you have
|
||||
implemented your own ``AuthorizationCodeGrant`` class.
|
||||
|
||||
:param code: Authorization code.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: code_challenge_method string
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant - when PKCE is active
|
||||
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def is_origin_allowed(self, client_id, origin, request, *args, **kwargs):
|
||||
"""Indicate if the given origin is allowed to access the token endpoint
|
||||
via Cross-Origin Resource Sharing (CORS). CORS is used by browser-based
|
||||
clients, such as Single-Page Applications, to perform the Authorization
|
||||
Code Grant.
|
||||
|
||||
(Note: If performing Authorization Code Grant via a public client such
|
||||
as a browser, you should use PKCE as well.)
|
||||
|
||||
If this method returns true, the appropriate CORS headers will be added
|
||||
to the response. By default this method always returns False, meaning
|
||||
CORS is disabled.
|
||||
|
||||
:param client_id: Unicode client identifier.
|
||||
:param redirect_uri: Unicode origin.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:rtype: bool
|
||||
|
||||
Method is used by:
|
||||
- Authorization Code Grant
|
||||
- Refresh Token Grant
|
||||
|
||||
"""
|
||||
return False
|
||||
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc6749.tokens
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains methods for adding two types of access tokens to requests.
|
||||
|
||||
- Bearer https://tools.ietf.org/html/rfc6750
|
||||
- MAC https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import warnings
|
||||
from binascii import b2a_base64
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from oauthlib import common
|
||||
from oauthlib.common import add_params_to_qs, add_params_to_uri
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
class OAuth2Token(dict):
|
||||
|
||||
def __init__(self, params, old_scope=None):
|
||||
super().__init__(params)
|
||||
self._new_scope = None
|
||||
if 'scope' in params and params['scope']:
|
||||
self._new_scope = set(utils.scope_to_list(params['scope']))
|
||||
if old_scope is not None:
|
||||
self._old_scope = set(utils.scope_to_list(old_scope))
|
||||
if self._new_scope is None:
|
||||
# the rfc says that if the scope hasn't changed, it's optional
|
||||
# in params so set the new scope to the old scope
|
||||
self._new_scope = self._old_scope
|
||||
else:
|
||||
self._old_scope = self._new_scope
|
||||
|
||||
@property
|
||||
def scope_changed(self):
|
||||
return self._new_scope != self._old_scope
|
||||
|
||||
@property
|
||||
def old_scope(self):
|
||||
return utils.list_to_scope(self._old_scope)
|
||||
|
||||
@property
|
||||
def old_scopes(self):
|
||||
return list(self._old_scope)
|
||||
|
||||
@property
|
||||
def scope(self):
|
||||
return utils.list_to_scope(self._new_scope)
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
return list(self._new_scope)
|
||||
|
||||
@property
|
||||
def missing_scopes(self):
|
||||
return list(self._old_scope - self._new_scope)
|
||||
|
||||
@property
|
||||
def additional_scopes(self):
|
||||
return list(self._new_scope - self._old_scope)
|
||||
|
||||
|
||||
def prepare_mac_header(token, uri, key, http_method,
|
||||
nonce=None,
|
||||
headers=None,
|
||||
body=None,
|
||||
ext='',
|
||||
hash_algorithm='hmac-sha-1',
|
||||
issue_time=None,
|
||||
draft=0):
|
||||
"""Add an `MAC Access Authentication`_ signature to headers.
|
||||
|
||||
Unlike OAuth 1, this HMAC signature does not require inclusion of the
|
||||
request payload/body, neither does it use a combination of client_secret
|
||||
and token_secret but rather a mac_key provided together with the access
|
||||
token.
|
||||
|
||||
Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
|
||||
`extension algorithms`_ are not supported.
|
||||
|
||||
Example MAC Authorization header, linebreaks added for clarity
|
||||
|
||||
Authorization: MAC id="h480djs93hd8",
|
||||
nonce="1336363200:dj83hs9s",
|
||||
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
|
||||
|
||||
.. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
|
||||
.. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
|
||||
|
||||
:param token:
|
||||
:param uri: Request URI.
|
||||
:param key: MAC given provided by token endpoint.
|
||||
:param http_method: HTTP Request method.
|
||||
:param nonce:
|
||||
:param headers: Request headers as a dictionary.
|
||||
:param body:
|
||||
:param ext:
|
||||
:param hash_algorithm: HMAC algorithm provided by token endpoint.
|
||||
:param issue_time: Time when the MAC credentials were issued (datetime).
|
||||
:param draft: MAC authentication specification version.
|
||||
:return: headers dictionary with the authorization field added.
|
||||
"""
|
||||
http_method = http_method.upper()
|
||||
host, port = utils.host_from_uri(uri)
|
||||
|
||||
if hash_algorithm.lower() == 'hmac-sha-1':
|
||||
h = hashlib.sha1
|
||||
elif hash_algorithm.lower() == 'hmac-sha-256':
|
||||
h = hashlib.sha256
|
||||
else:
|
||||
raise ValueError('unknown hash algorithm')
|
||||
|
||||
if draft == 0:
|
||||
nonce = nonce or '{}:{}'.format(utils.generate_age(issue_time),
|
||||
common.generate_nonce())
|
||||
else:
|
||||
ts = common.generate_timestamp()
|
||||
nonce = common.generate_nonce()
|
||||
|
||||
sch, net, path, par, query, fra = urlparse(uri)
|
||||
|
||||
if query:
|
||||
request_uri = path + '?' + query
|
||||
else:
|
||||
request_uri = path
|
||||
|
||||
# Hash the body/payload
|
||||
if body is not None and draft == 0:
|
||||
body = body.encode('utf-8')
|
||||
bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
|
||||
else:
|
||||
bodyhash = ''
|
||||
|
||||
# Create the normalized base string
|
||||
base = []
|
||||
if draft == 0:
|
||||
base.append(nonce)
|
||||
else:
|
||||
base.append(ts)
|
||||
base.append(nonce)
|
||||
base.append(http_method.upper())
|
||||
base.append(request_uri)
|
||||
base.append(host)
|
||||
base.append(port)
|
||||
if draft == 0:
|
||||
base.append(bodyhash)
|
||||
base.append(ext or '')
|
||||
base_string = '\n'.join(base) + '\n'
|
||||
|
||||
# hmac struggles with unicode strings - http://bugs.python.org/issue5285
|
||||
if isinstance(key, str):
|
||||
key = key.encode('utf-8')
|
||||
sign = hmac.new(key, base_string.encode('utf-8'), h)
|
||||
sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
|
||||
|
||||
header = []
|
||||
header.append('MAC id="%s"' % token)
|
||||
if draft != 0:
|
||||
header.append('ts="%s"' % ts)
|
||||
header.append('nonce="%s"' % nonce)
|
||||
if bodyhash:
|
||||
header.append('bodyhash="%s"' % bodyhash)
|
||||
if ext:
|
||||
header.append('ext="%s"' % ext)
|
||||
header.append('mac="%s"' % sign)
|
||||
|
||||
headers = headers or {}
|
||||
headers['Authorization'] = ', '.join(header)
|
||||
return headers
|
||||
|
||||
|
||||
def prepare_bearer_uri(token, uri):
|
||||
"""Add a `Bearer Token`_ to the request URI.
|
||||
Not recommended, use only if client can't use authorization header or body.
|
||||
|
||||
http://www.example.com/path?access_token=h480djs93hd8
|
||||
|
||||
.. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
|
||||
|
||||
:param token:
|
||||
:param uri:
|
||||
"""
|
||||
return add_params_to_uri(uri, [(('access_token', token))])
|
||||
|
||||
|
||||
def prepare_bearer_headers(token, headers=None):
|
||||
"""Add a `Bearer Token`_ to the request URI.
|
||||
Recommended method of passing bearer tokens.
|
||||
|
||||
Authorization: Bearer h480djs93hd8
|
||||
|
||||
.. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
|
||||
|
||||
:param token:
|
||||
:param headers:
|
||||
"""
|
||||
headers = headers or {}
|
||||
headers['Authorization'] = 'Bearer %s' % token
|
||||
return headers
|
||||
|
||||
|
||||
def prepare_bearer_body(token, body=''):
|
||||
"""Add a `Bearer Token`_ to the request body.
|
||||
|
||||
access_token=h480djs93hd8
|
||||
|
||||
.. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
|
||||
|
||||
:param token:
|
||||
:param body:
|
||||
"""
|
||||
return add_params_to_qs(body, [(('access_token', token))])
|
||||
|
||||
|
||||
def random_token_generator(request, refresh_token=False):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param refresh_token:
|
||||
"""
|
||||
return common.generate_token()
|
||||
|
||||
|
||||
def signed_token_generator(private_pem, **kwargs):
|
||||
"""
|
||||
:param private_pem:
|
||||
"""
|
||||
def signed_token_generator(request):
|
||||
request.claims = kwargs
|
||||
return common.generate_signed_token(private_pem, request)
|
||||
|
||||
return signed_token_generator
|
||||
|
||||
|
||||
def get_token_from_header(request):
|
||||
"""
|
||||
Helper function to extract a token from the request header.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:return: Return the token or None if the Authorization header is malformed.
|
||||
"""
|
||||
token = None
|
||||
|
||||
if 'Authorization' in request.headers:
|
||||
split_header = request.headers.get('Authorization').split()
|
||||
if len(split_header) == 2 and split_header[0].lower() == 'bearer':
|
||||
token = split_header[1]
|
||||
else:
|
||||
token = request.access_token
|
||||
|
||||
return token
|
||||
|
||||
|
||||
class TokenBase:
|
||||
__slots__ = ()
|
||||
|
||||
def __call__(self, request, refresh_token=False):
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def validate_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
def estimate_type(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
raise NotImplementedError('Subclasses must implement this method.')
|
||||
|
||||
|
||||
class BearerToken(TokenBase):
|
||||
__slots__ = (
|
||||
'request_validator', 'token_generator',
|
||||
'refresh_token_generator', 'expires_in'
|
||||
)
|
||||
|
||||
def __init__(self, request_validator=None, token_generator=None,
|
||||
expires_in=None, refresh_token_generator=None):
|
||||
self.request_validator = request_validator
|
||||
self.token_generator = token_generator or random_token_generator
|
||||
self.refresh_token_generator = (
|
||||
refresh_token_generator or self.token_generator
|
||||
)
|
||||
self.expires_in = expires_in or 3600
|
||||
|
||||
def create_token(self, request, refresh_token=False, **kwargs):
|
||||
"""
|
||||
Create a BearerToken, by default without refresh token.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param refresh_token:
|
||||
"""
|
||||
if "save_token" in kwargs:
|
||||
warnings.warn("`save_token` has been deprecated, it was not called internally."
|
||||
"If you do, call `request_validator.save_token()` instead.",
|
||||
DeprecationWarning)
|
||||
|
||||
if callable(self.expires_in):
|
||||
expires_in = self.expires_in(request)
|
||||
else:
|
||||
expires_in = self.expires_in
|
||||
|
||||
request.expires_in = expires_in
|
||||
|
||||
token = {
|
||||
'access_token': self.token_generator(request),
|
||||
'expires_in': expires_in,
|
||||
'token_type': 'Bearer',
|
||||
}
|
||||
|
||||
# If provided, include - this is optional in some cases https://tools.ietf.org/html/rfc6749#section-3.3 but
|
||||
# there is currently no mechanism to coordinate issuing a token for only a subset of the requested scopes so
|
||||
# all tokens issued are for the entire set of requested scopes.
|
||||
if request.scopes is not None:
|
||||
token['scope'] = ' '.join(request.scopes)
|
||||
|
||||
if refresh_token:
|
||||
if (request.refresh_token and
|
||||
not self.request_validator.rotate_refresh_token(request)):
|
||||
token['refresh_token'] = request.refresh_token
|
||||
else:
|
||||
token['refresh_token'] = self.refresh_token_generator(request)
|
||||
|
||||
token.update(request.extra_credentials or {})
|
||||
return OAuth2Token(token)
|
||||
|
||||
def validate_request(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
token = get_token_from_header(request)
|
||||
return self.request_validator.validate_bearer_token(
|
||||
token, request.scopes, request)
|
||||
|
||||
def estimate_type(self, request):
|
||||
"""
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
"""
|
||||
if request.headers.get('Authorization', '').split(' ')[0].lower() == 'bearer':
|
||||
return 9
|
||||
elif request.access_token is not None:
|
||||
return 5
|
||||
else:
|
||||
return 0
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
oauthlib.utils
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module contains utility methods used by various parts of the OAuth 2 spec.
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from oauthlib.common import urldecode
|
||||
|
||||
|
||||
def list_to_scope(scope):
|
||||
"""Convert a list of scopes to a space separated string."""
|
||||
if isinstance(scope, str) or scope is None:
|
||||
return scope
|
||||
elif isinstance(scope, (set, tuple, list)):
|
||||
return " ".join([str(s) for s in scope])
|
||||
else:
|
||||
raise ValueError("Invalid scope (%s), must be string, tuple, set, or list." % scope)
|
||||
|
||||
|
||||
def scope_to_list(scope):
|
||||
"""Convert a space separated string to a list of scopes."""
|
||||
if isinstance(scope, (tuple, list, set)):
|
||||
return [str(s) for s in scope]
|
||||
elif scope is None:
|
||||
return None
|
||||
else:
|
||||
return scope.strip().split(" ")
|
||||
|
||||
|
||||
def params_from_uri(uri):
|
||||
params = dict(urldecode(urlparse(uri).query))
|
||||
if 'scope' in params:
|
||||
params['scope'] = scope_to_list(params['scope'])
|
||||
return params
|
||||
|
||||
|
||||
def host_from_uri(uri):
|
||||
"""Extract hostname and port from URI.
|
||||
|
||||
Will use default port for HTTP and HTTPS if none is present in the URI.
|
||||
"""
|
||||
default_ports = {
|
||||
'HTTP': '80',
|
||||
'HTTPS': '443',
|
||||
}
|
||||
|
||||
sch, netloc, path, par, query, fra = urlparse(uri)
|
||||
if ':' in netloc:
|
||||
netloc, port = netloc.split(':', 1)
|
||||
else:
|
||||
port = default_ports.get(sch.upper())
|
||||
|
||||
return netloc, port
|
||||
|
||||
|
||||
def escape(u):
|
||||
"""Escape a string in an OAuth-compatible fashion.
|
||||
|
||||
TODO: verify whether this can in fact be used for OAuth 2
|
||||
|
||||
"""
|
||||
if not isinstance(u, str):
|
||||
raise ValueError('Only unicode objects are escapable.')
|
||||
return quote(u.encode('utf-8'), safe=b'~')
|
||||
|
||||
|
||||
def generate_age(issue_time):
|
||||
"""Generate a age parameter for MAC authentication draft 00."""
|
||||
td = datetime.datetime.now() - issue_time
|
||||
age = (td.microseconds + (td.seconds + td.days * 24 * 3600)
|
||||
* 10 ** 6) / 10 ** 6
|
||||
return str(age)
|
||||
|
||||
|
||||
def is_secure_transport(uri):
|
||||
"""Check if the uri is over ssl."""
|
||||
if os***REMOVED***iron.get('OAUTHLIB_INSECURE_TRANSPORT'):
|
||||
return True
|
||||
return uri.lower().startswith('https://')
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc8628
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
|
||||
"""
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc8628
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming OAuth 2.0 Device Authorization RFC8628.
|
||||
"""
|
||||
from .device import DeviceClient
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
oauthlib.oauth2.rfc8628
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for consuming and providing OAuth 2.0 Device Authorization RFC8628.
|
||||
"""
|
||||
from oauthlib.common import add_params_to_uri
|
||||
from oauthlib.oauth2 import BackendApplicationClient, Client
|
||||
from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
|
||||
from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
|
||||
from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
|
||||
|
||||
|
||||
class DeviceClient(Client):
|
||||
|
||||
"""A public client utilizing the device authorization workflow.
|
||||
|
||||
The client can request an access token using a device code and
|
||||
a public client id associated with the device code as defined
|
||||
in RFC8628.
|
||||
|
||||
The device authorization grant type can be used to obtain both
|
||||
access tokens and refresh tokens and is intended to be used in
|
||||
a scenario where the device being authorized does not have a
|
||||
user interface that is suitable for performing authentication.
|
||||
"""
|
||||
|
||||
grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
|
||||
|
||||
def __init__(self, client_id, **kwargs):
|
||||
super().__init__(client_id, **kwargs)
|
||||
self.client_secret = kwargs.get('client_secret')
|
||||
|
||||
def prepare_request_uri(self, uri, scope=None, **kwargs):
|
||||
if not is_secure_transport(uri):
|
||||
raise InsecureTransportError()
|
||||
|
||||
scope = self.scope if scope is None else scope
|
||||
params = [(('client_id', self.client_id)), (('grant_type', self.grant_type))]
|
||||
|
||||
if self.client_secret is not None:
|
||||
params.append(('client_secret', self.client_secret))
|
||||
|
||||
if scope:
|
||||
params.append(('scope', list_to_scope(scope)))
|
||||
|
||||
for k in kwargs:
|
||||
if kwargs[k]:
|
||||
params.append((str(k), kwargs[k]))
|
||||
|
||||
return add_params_to_uri(uri, params)
|
||||
|
||||
def prepare_request_body(self, device_code, body='', scope=None,
|
||||
include_client_id=False, **kwargs):
|
||||
"""Add device_code to request body
|
||||
|
||||
The client makes a request to the token endpoint by adding the
|
||||
device_code as a parameter using the
|
||||
"application/x-www-form-urlencoded" format to the HTTP request
|
||||
body.
|
||||
|
||||
:param body: Existing request body (URL encoded string) to embed parameters
|
||||
into. This may contain extra parameters. Default ''.
|
||||
:param scope: The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
|
||||
:param include_client_id: `True` to send the `client_id` in the
|
||||
body of the upstream request. This is required
|
||||
if the client is not authenticating with the
|
||||
authorization server as described in
|
||||
`Section 3.2.1`_. False otherwise (default).
|
||||
:type include_client_id: Boolean
|
||||
|
||||
:param kwargs: Extra credentials to include in the token request.
|
||||
|
||||
The prepared body will include all provided device_code as well as
|
||||
the ``grant_type`` parameter set to
|
||||
``urn:ietf:params:oauth:grant-type:device_code``::
|
||||
|
||||
>>> from oauthlib.oauth2 import DeviceClient
|
||||
>>> client = DeviceClient('your_id', 'your_code')
|
||||
>>> client.prepare_request_body(scope=['hello', 'world'])
|
||||
'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
|
||||
|
||||
.. _`Section 3.2.1`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1
|
||||
.. _`Section 3.3`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
|
||||
.. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
|
||||
"""
|
||||
|
||||
kwargs['client_id'] = self.client_id
|
||||
kwargs['include_client_id'] = include_client_id
|
||||
scope = self.scope if scope is None else scope
|
||||
return prepare_token_request(self.grant_type, body=body, device_code=device_code,
|
||||
scope=scope, **kwargs)
|
||||
Reference in New Issue
Block a user