mirror of https://github.com/morpheus65535/bazarr
500 lines
20 KiB
Python
500 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
oauthlib.oauth2.rfc6749
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
This module is an implementation of various logic needed
|
|
for consuming OAuth 2.0 RFC6749.
|
|
"""
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
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(object):
|
|
|
|
"""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`.
|
|
|
|
"""
|
|
|
|
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,
|
|
**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`.
|
|
"""
|
|
|
|
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 = 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."""
|
|
|
|
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 = dict(
|
|
(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 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
|
|
self.scope = scope or self.scope
|
|
auth_url = self.prepare_request_uri(
|
|
authorization_url, redirect_uri=self.redirect_url,
|
|
scope=self.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 successfull
|
|
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 body: Request body (URL encoded string).
|
|
|
|
: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: Request body (URL encoded string).
|
|
|
|
:param scope: List of scopes to request. Must be equal to
|
|
or a subset of the scopes granted when obtaining the refresh
|
|
token.
|
|
|
|
: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()
|
|
|
|
self.scope = scope or self.scope
|
|
body = self.prepare_refresh_body(body=body,
|
|
refresh_token=refresh_token, scope=self.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 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: http
|
|
|
|
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.
|
|
:return: Dictionary of token parameters.
|
|
:raises: Warning if scope has changed. 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
|
|
"""
|
|
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:
|
|
|
|
grant_type
|
|
REQUIRED. Value MUST be set to "refresh_token".
|
|
refresh_token
|
|
REQUIRED. The refresh token issued to the client.
|
|
scope
|
|
OPTIONAL. The scope of the access request as described by
|
|
Section 3.3. The requested scope MUST NOT include any scope
|
|
not originally granted by the resource owner, and if omitted is
|
|
treated as equal to the scope originally granted by the
|
|
resource owner.
|
|
"""
|
|
refresh_token = refresh_token or self.refresh_token
|
|
return prepare_token_request('refresh_token', 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 _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.
|
|
"""
|
|
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:
|
|
self._expires_at = int(response.get('expires_at'))
|
|
|
|
if 'mac_key' in response:
|
|
self.mac_key = response.get('mac_key')
|
|
|
|
if 'mac_algorithm' in response:
|
|
self.mac_algorithm = response.get('mac_algorithm')
|