647 lines
25 KiB
Python
647 lines
25 KiB
Python
import hashlib
|
|
|
|
__all__ = [
|
|
'BadRequestException',
|
|
'BadStateException',
|
|
'CsrfException',
|
|
'DropboxOAuth2Flow',
|
|
'DropboxOAuth2FlowNoRedirect',
|
|
'NotApprovedException',
|
|
'OAuth2FlowNoRedirectResult',
|
|
'OAuth2FlowResult',
|
|
'ProviderException',
|
|
]
|
|
|
|
import base64
|
|
import os
|
|
import six
|
|
import urllib
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
|
|
from .session import (
|
|
API_HOST,
|
|
WEB_HOST,
|
|
pinned_session,
|
|
DEFAULT_TIMEOUT,
|
|
)
|
|
|
|
if six.PY3:
|
|
url_path_quote = urllib.parse.quote # pylint: disable=no-member,useless-suppression
|
|
url_encode = urllib.parse.urlencode # pylint: disable=no-member,useless-suppression
|
|
else:
|
|
url_path_quote = urllib.quote # pylint: disable=no-member,useless-suppression
|
|
url_encode = urllib.urlencode # pylint: disable=no-member,useless-suppression
|
|
|
|
TOKEN_ACCESS_TYPES = ['offline', 'online', 'legacy']
|
|
INCLUDE_GRANTED_SCOPES_TYPES = ['user', 'team']
|
|
PKCE_VERIFIER_LENGTH = 128
|
|
|
|
class OAuth2FlowNoRedirectResult(object):
|
|
"""
|
|
Authorization information for an OAuth2Flow performed with no redirect.
|
|
"""
|
|
|
|
def __init__(self, access_token, account_id, user_id, refresh_token, expiration, scope):
|
|
"""
|
|
Args:
|
|
access_token (str): Token to be used to authenticate later
|
|
requests.
|
|
refresh_token (str): Token to be used to acquire new access token
|
|
when existing one expires
|
|
expiration (int, datetime): Either the number of seconds from now that the token expires
|
|
in or the datetime at which the token expires
|
|
account_id (str): The Dropbox user's account ID.
|
|
user_id (str): Deprecated (use account_id instead).
|
|
refresh_token (str): Token to be used to acquire new access token
|
|
when existing one expires
|
|
expiration (int, datetime): Either the number of seconds from now that the token expires
|
|
in or the datetime at which the token expires
|
|
scope (list): list of scopes to request in base oauth flow.
|
|
"""
|
|
self.access_token = access_token
|
|
if not expiration:
|
|
self.expires_at = None
|
|
elif isinstance(expiration, datetime):
|
|
self.expires_at = expiration
|
|
else:
|
|
self.expires_at = datetime.utcnow() + timedelta(seconds=int(expiration))
|
|
self.refresh_token = refresh_token
|
|
self.account_id = account_id
|
|
self.user_id = user_id
|
|
self.scope = scope
|
|
|
|
def __repr__(self):
|
|
return 'OAuth2FlowNoRedirectResult(%s, %s, %s, %s, %s, %s)' % (
|
|
self.access_token,
|
|
self.account_id,
|
|
self.user_id,
|
|
self.refresh_token,
|
|
self.expires_at,
|
|
self.scope,
|
|
)
|
|
|
|
|
|
class OAuth2FlowResult(OAuth2FlowNoRedirectResult):
|
|
"""
|
|
Authorization information for an OAuth2Flow with redirect.
|
|
"""
|
|
|
|
def __init__(self, access_token, account_id, user_id, url_state, refresh_token,
|
|
expires_in, scope):
|
|
"""
|
|
Same as OAuth2FlowNoRedirectResult but with url_state.
|
|
|
|
Args:
|
|
url_state (str): The url state that was set by
|
|
:meth:`DropboxOAuth2Flow.start`.
|
|
"""
|
|
super(OAuth2FlowResult, self).__init__(
|
|
access_token=access_token,
|
|
account_id=account_id,
|
|
user_id=user_id,
|
|
refresh_token=refresh_token,
|
|
expiration=expires_in,
|
|
scope=scope)
|
|
self.url_state = url_state
|
|
|
|
@classmethod
|
|
def from_no_redirect_result(cls, result, url_state):
|
|
assert isinstance(result, OAuth2FlowNoRedirectResult)
|
|
return cls(result.access_token, result.account_id, result.user_id,
|
|
url_state, result.refresh_token, result.expires_at, result.scope)
|
|
|
|
def __repr__(self):
|
|
return 'OAuth2FlowResult(%s, %s, %s, %s, %s, %s, %s, %s, %s)' % (
|
|
self.access_token,
|
|
self.refresh_token,
|
|
self.expires_at,
|
|
self.account_id,
|
|
self.user_id,
|
|
self.scope,
|
|
self.url_state,
|
|
self.refresh_token,
|
|
self.expires_at,
|
|
)
|
|
|
|
|
|
class DropboxOAuth2FlowBase(object):
|
|
|
|
def __init__(self, consumer_key, consumer_secret=None, locale=None, token_access_type='legacy',
|
|
scope=None, include_granted_scopes=None, use_pkce=False, timeout=DEFAULT_TIMEOUT):
|
|
if scope is not None and (len(scope) == 0 or not isinstance(scope, list)):
|
|
raise BadInputException("Scope list must be of type list")
|
|
if token_access_type is not None and token_access_type not in TOKEN_ACCESS_TYPES:
|
|
raise BadInputException("Token access type must be from the following enum: {}".format(
|
|
TOKEN_ACCESS_TYPES))
|
|
if not (use_pkce or consumer_secret):
|
|
raise BadInputException("Must pass in either consumer secret or use PKCE")
|
|
if include_granted_scopes and not scope:
|
|
raise BadInputException("Must pass in scope to pass include_granted_scopes")
|
|
|
|
self.consumer_key = consumer_key
|
|
self.consumer_secret = consumer_secret
|
|
self.locale = locale
|
|
self.token_access_type = token_access_type
|
|
self.requests_session = pinned_session()
|
|
self.scope = scope
|
|
self.include_granted_scopes = include_granted_scopes
|
|
self._timeout = timeout
|
|
|
|
if use_pkce:
|
|
self.code_verifier = _generate_pkce_code_verifier()
|
|
self.code_challenge = _generate_pkce_code_challenge(self.code_verifier)
|
|
else:
|
|
self.code_verifier = None
|
|
self.code_challenge = None
|
|
|
|
def _get_authorize_url(self, redirect_uri, state, token_access_type, scope=None,
|
|
include_granted_scopes=None, code_challenge=None):
|
|
params = dict(response_type='code',
|
|
client_id=self.consumer_key)
|
|
if redirect_uri is not None:
|
|
params['redirect_uri'] = redirect_uri
|
|
if state is not None:
|
|
params['state'] = state
|
|
if token_access_type is not None:
|
|
assert token_access_type in TOKEN_ACCESS_TYPES
|
|
if token_access_type != 'legacy':
|
|
params['token_access_type'] = token_access_type
|
|
if code_challenge:
|
|
params['code_challenge'] = code_challenge
|
|
params['code_challenge_method'] = 'S256'
|
|
|
|
if scope is not None:
|
|
params['scope'] = " ".join(scope)
|
|
if include_granted_scopes is not None:
|
|
assert include_granted_scopes in INCLUDE_GRANTED_SCOPES_TYPES
|
|
params['include_granted_scopes'] = include_granted_scopes
|
|
|
|
return self.build_url('/oauth2/authorize', params, WEB_HOST)
|
|
|
|
def _finish(self, code, redirect_uri, code_verifier):
|
|
url = self.build_url('/oauth2/token')
|
|
params = {'grant_type': 'authorization_code',
|
|
'code': code,
|
|
'client_id': self.consumer_key,
|
|
}
|
|
if code_verifier:
|
|
params['code_verifier'] = code_verifier
|
|
else:
|
|
params['client_secret'] = self.consumer_secret
|
|
if self.locale is not None:
|
|
params['locale'] = self.locale
|
|
if redirect_uri is not None:
|
|
params['redirect_uri'] = redirect_uri
|
|
|
|
resp = self.requests_session.post(url, data=params, timeout=self._timeout)
|
|
resp.raise_for_status()
|
|
|
|
d = resp.json()
|
|
|
|
if 'team_id' in d:
|
|
account_id = d['team_id']
|
|
else:
|
|
account_id = d['account_id']
|
|
|
|
access_token = d['access_token']
|
|
|
|
if 'refresh_token' in d:
|
|
refresh_token = d['refresh_token']
|
|
else:
|
|
refresh_token = ""
|
|
|
|
if 'expires_in' in d:
|
|
expires_in = d['expires_in']
|
|
else:
|
|
expires_in = None
|
|
|
|
if 'scope' in d:
|
|
scope = d['scope']
|
|
else:
|
|
scope = None
|
|
|
|
uid = d['uid']
|
|
|
|
return OAuth2FlowNoRedirectResult(
|
|
access_token,
|
|
account_id,
|
|
uid,
|
|
refresh_token,
|
|
expires_in,
|
|
scope)
|
|
|
|
def build_path(self, target, params=None):
|
|
"""Build the path component for an API URL.
|
|
|
|
This method urlencodes the parameters, adds them
|
|
to the end of the target url, and puts a marker for the API
|
|
version in front.
|
|
|
|
:param str target: A target url (e.g. '/files') to build upon.
|
|
:param dict params: Optional dictionary of parameters (name to value).
|
|
:return: The path and parameters components of an API URL.
|
|
:rtype: str
|
|
"""
|
|
if six.PY2 and isinstance(target, six.text_type):
|
|
target = target.encode('utf8')
|
|
|
|
target_path = url_path_quote(target)
|
|
|
|
params = params or {}
|
|
params = params.copy()
|
|
|
|
if self.locale:
|
|
params['locale'] = self.locale
|
|
|
|
if params:
|
|
query_string = _params_to_urlencoded(params)
|
|
return "%s?%s" % (target_path, query_string)
|
|
else:
|
|
return target_path
|
|
|
|
def build_url(self, target, params=None, host=API_HOST):
|
|
"""Build an API URL.
|
|
|
|
This method adds scheme and hostname to the path
|
|
returned from build_path.
|
|
|
|
:param str target: A target url (e.g. '/files') to build upon.
|
|
:param dict params: Optional dictionary of parameters (name to value).
|
|
:return: The full API URL.
|
|
:rtype: str
|
|
"""
|
|
return "https://%s%s" % (host, self.build_path(target, params))
|
|
|
|
|
|
class DropboxOAuth2FlowNoRedirect(DropboxOAuth2FlowBase):
|
|
"""
|
|
OAuth 2 authorization helper for apps that can't provide a redirect URI
|
|
(such as the command-line example apps).
|
|
|
|
See examples under example/oauth
|
|
|
|
"""
|
|
|
|
def __init__(self, consumer_key, consumer_secret=None, locale=None, token_access_type='legacy',
|
|
scope=None, include_granted_scopes=None, use_pkce=False, timeout=DEFAULT_TIMEOUT): # noqa: E501;
|
|
"""
|
|
Construct an instance.
|
|
|
|
Parameters
|
|
:param str consumer_key: Your API app's "app key".
|
|
:param str consumer_secret: Your API app's "app secret".
|
|
:param str locale: The locale of the user of your application. For
|
|
example "en" or "en_US". Some API calls return localized data and
|
|
error messages; this setting tells the server which locale to use.
|
|
By default, the server uses "en_US".
|
|
:param str token_access_type: the type of token to be requested.
|
|
From the following enum:
|
|
legacy - creates one long-lived token with no expiration
|
|
online - create one short-lived token with an expiration
|
|
offline - create one short-lived token with an expiration with a refresh token
|
|
:param list scope: list of scopes to request in base oauth flow. If left blank,
|
|
will default to all scopes for app
|
|
:param str include_granted_scopes: which scopes to include from previous grants
|
|
From the following enum:
|
|
user - include user scopes in the grant
|
|
team - include team scopes in the grant
|
|
Note: if this user has never linked the app, include_granted_scopes must be None
|
|
:param bool use_pkce: Whether or not to use Sha256 based PKCE. PKCE should be only use on
|
|
client apps which doesn't call your server. It is less secure than non-PKCE flow but
|
|
can be used if you are unable to safely retrieve your app secret
|
|
:param Optional[float] timeout: Maximum duration in seconds that
|
|
client will wait for any single packet from the
|
|
server. After the timeout the client will give up on
|
|
connection. If `None`, client will wait forever. Defaults
|
|
to 100 seconds.
|
|
"""
|
|
super(DropboxOAuth2FlowNoRedirect, self).__init__(
|
|
consumer_key=consumer_key,
|
|
consumer_secret=consumer_secret,
|
|
locale=locale,
|
|
token_access_type=token_access_type,
|
|
scope=scope,
|
|
include_granted_scopes=include_granted_scopes,
|
|
use_pkce=use_pkce,
|
|
timeout=timeout
|
|
)
|
|
|
|
def start(self):
|
|
"""
|
|
Starts the OAuth 2 authorization process.
|
|
|
|
:return: The URL for a page on Dropbox's website. This page will let
|
|
the user "approve" your app, which gives your app permission to
|
|
access the user's Dropbox account. Tell the user to visit this URL
|
|
and approve your app.
|
|
"""
|
|
return self._get_authorize_url(None, None, self.token_access_type,
|
|
scope=self.scope,
|
|
include_granted_scopes=self.include_granted_scopes,
|
|
code_challenge=self.code_challenge)
|
|
|
|
def finish(self, code):
|
|
"""
|
|
If the user approves your app, they will be presented with an
|
|
"authorization code". Have the user copy/paste that authorization code
|
|
into your app and then call this method to get an access token.
|
|
|
|
:param str code: The authorization code shown to the user when they
|
|
approved your app.
|
|
:rtype: OAuth2FlowNoRedirectResult
|
|
:raises: The same exceptions as :meth:`DropboxOAuth2Flow.finish()`.
|
|
"""
|
|
return self._finish(code, None, self.code_verifier)
|
|
|
|
|
|
class DropboxOAuth2Flow(DropboxOAuth2FlowBase):
|
|
"""
|
|
OAuth 2 authorization helper. Use this for web apps.
|
|
|
|
OAuth 2 has a two-step authorization process. The first step is having the
|
|
user authorize your app. The second involves getting an OAuth 2 access
|
|
token from Dropbox.
|
|
|
|
See examples under example/oauth
|
|
|
|
"""
|
|
|
|
def __init__(self, consumer_key, redirect_uri, session,
|
|
csrf_token_session_key, consumer_secret=None, locale=None,
|
|
token_access_type='legacy', scope=None,
|
|
include_granted_scopes=None, use_pkce=False, timeout=DEFAULT_TIMEOUT):
|
|
"""
|
|
Construct an instance.
|
|
|
|
:param str consumer_key: Your API app's "app key".
|
|
:param str consumer_secret: Your API app's "app secret".
|
|
:param str redirect_uri: The URI that the Dropbox server will redirect
|
|
the user to after the user finishes authorizing your app. This URI
|
|
must be HTTPS-based and pre-registered with the Dropbox servers,
|
|
though localhost URIs are allowed without pre-registration and can
|
|
be either HTTP or HTTPS.
|
|
:param dict session: A dict-like object that represents the current
|
|
user's web session (will be used to save the CSRF token).
|
|
:param str csrf_token_session_key: The key to use when storing the CSRF
|
|
token in the session (for example: "dropbox-auth-csrf-token").
|
|
:param str locale: The locale of the user of your application. For
|
|
example "en" or "en_US". Some API calls return localized data and
|
|
error messages; this setting tells the server which locale to use.
|
|
By default, the server uses "en_US".
|
|
:param str token_access_type: the type of token to be requested.
|
|
From the following enum:
|
|
legacy - creates one long-lived token with no expiration
|
|
online - create one short-lived token with an expiration
|
|
offline - create one short-lived token with an expiration with a refresh token
|
|
:param list scope: list of scopes to request in base oauth flow. If left blank,
|
|
will default to all scopes for app
|
|
:param str include_granted_scopes: which scopes to include from previous grants
|
|
From the following enum:
|
|
user - include user scopes in the grant
|
|
team - include team scopes in the grant
|
|
Note: if this user has never linked the app, include_granted_scopes must be None
|
|
:param bool use_pkce: Whether or not to use Sha256 based PKCE
|
|
:param Optional[float] timeout: Maximum duration in seconds that
|
|
client will wait for any single packet from the
|
|
server. After the timeout the client will give up on
|
|
connection. If `None`, client will wait forever. Defaults
|
|
to 100 seconds.
|
|
"""
|
|
|
|
super(DropboxOAuth2Flow, self).__init__(
|
|
consumer_key=consumer_key,
|
|
consumer_secret=consumer_secret,
|
|
locale=locale,
|
|
token_access_type=token_access_type,
|
|
scope=scope,
|
|
include_granted_scopes=include_granted_scopes,
|
|
use_pkce=use_pkce,
|
|
timeout=timeout
|
|
)
|
|
self.redirect_uri = redirect_uri
|
|
self.session = session
|
|
self.csrf_token_session_key = csrf_token_session_key
|
|
|
|
def start(self, url_state=None):
|
|
"""
|
|
Starts the OAuth 2 authorization process.
|
|
|
|
This function builds an "authorization URL". You should redirect your
|
|
user's browser to this URL, which will give them an opportunity to
|
|
grant your app access to their Dropbox account. When the user
|
|
completes this process, they will be automatically redirected to the
|
|
``redirect_uri`` you passed in to the constructor.
|
|
|
|
This function will also save a CSRF token to
|
|
``session[csrf_token_session_key]`` (as provided to the constructor).
|
|
This CSRF token will be checked on :meth:`finish()` to prevent request
|
|
forgery.
|
|
|
|
:param str url_state: Any data that you would like to keep in the URL
|
|
through the authorization process. This exact value will be
|
|
returned to you by :meth:`finish()`.
|
|
:return: The URL for a page on Dropbox's website. This page will let
|
|
the user "approve" your app, which gives your app permission to
|
|
access the user's Dropbox account. Tell the user to visit this URL
|
|
and approve your app.
|
|
"""
|
|
csrf_token = base64.urlsafe_b64encode(os.urandom(16)).decode('ascii')
|
|
state = csrf_token
|
|
if url_state is not None:
|
|
state += "|" + url_state
|
|
self.session[self.csrf_token_session_key] = csrf_token
|
|
|
|
return self._get_authorize_url(self.redirect_uri, state, self.token_access_type,
|
|
scope=self.scope,
|
|
include_granted_scopes=self.include_granted_scopes,
|
|
code_challenge=self.code_challenge)
|
|
|
|
def finish(self, query_params):
|
|
"""
|
|
Call this after the user has visited the authorize URL (see
|
|
:meth:`start()`), approved your app and was redirected to your redirect
|
|
URI.
|
|
|
|
:param dict query_params: The query parameters on the GET request to
|
|
your redirect URI.
|
|
:rtype: OAuth2FlowResult
|
|
:raises: :class:`BadRequestException` If the redirect URL was missing
|
|
parameters or if the given parameters were not valid.
|
|
:raises: :class:`BadStateException` If there's no CSRF token in the
|
|
session.
|
|
:raises: :class:`CsrfException` If the ``state`` query parameter
|
|
doesn't contain the CSRF token from the user's session.
|
|
:raises: :class:`NotApprovedException` If the user chose not to
|
|
approve your app.
|
|
:raises: :class:`ProviderException` If Dropbox redirected to your
|
|
redirect URI with some unexpected error identifier and error message.
|
|
"""
|
|
# Check well-formedness of request.
|
|
|
|
state = query_params.get('state')
|
|
if state is None:
|
|
raise BadRequestException("Missing query parameter 'state'.")
|
|
|
|
error = query_params.get('error')
|
|
error_description = query_params.get('error_description')
|
|
code = query_params.get('code')
|
|
|
|
if error is not None and code is not None:
|
|
raise BadRequestException(
|
|
"Query parameters 'code' and 'error' are both set; "
|
|
"only one must be set.")
|
|
if error is None and code is None:
|
|
raise BadRequestException(
|
|
"Neither query parameter 'code' or 'error' is set.")
|
|
|
|
# Check CSRF token
|
|
|
|
if self.csrf_token_session_key not in self.session:
|
|
raise BadStateException('Missing CSRF token in session.')
|
|
csrf_token_from_session = self.session[self.csrf_token_session_key]
|
|
if len(csrf_token_from_session) <= 20:
|
|
raise AssertionError('CSRF token unexpectedly short: %r' %
|
|
csrf_token_from_session)
|
|
|
|
split_pos = state.find('|')
|
|
if split_pos < 0:
|
|
given_csrf_token = state
|
|
url_state = None
|
|
else:
|
|
given_csrf_token = state[0:split_pos]
|
|
url_state = state[split_pos + 1:]
|
|
|
|
if not _safe_equals(csrf_token_from_session, given_csrf_token):
|
|
raise CsrfException('expected %r, got %r' %
|
|
(csrf_token_from_session, given_csrf_token))
|
|
|
|
del self.session[self.csrf_token_session_key]
|
|
|
|
# Check for error identifier
|
|
|
|
if error is not None:
|
|
if error == 'access_denied':
|
|
# The user clicked "Deny"
|
|
if error_description is None:
|
|
raise NotApprovedException(
|
|
'No additional description from Dropbox')
|
|
else:
|
|
raise NotApprovedException(
|
|
'Additional description from Dropbox: %s' %
|
|
error_description)
|
|
else:
|
|
# All other errors
|
|
full_message = error
|
|
if error_description is not None:
|
|
full_message += ": " + error_description
|
|
raise ProviderException(full_message)
|
|
|
|
# If everything went ok, make the network call to get an access token.
|
|
|
|
no_redirect_result = self._finish(code, self.redirect_uri, self.code_verifier)
|
|
return OAuth2FlowResult.from_no_redirect_result(
|
|
no_redirect_result, url_state)
|
|
|
|
|
|
class BadRequestException(Exception):
|
|
"""
|
|
Thrown if the redirect URL was missing parameters or if the
|
|
given parameters were not valid.
|
|
|
|
The recommended action is to show an HTTP 400 error page.
|
|
"""
|
|
pass
|
|
|
|
|
|
class BadStateException(Exception):
|
|
"""
|
|
Thrown if all the parameters are correct, but there's no CSRF token in the
|
|
session. This probably means that the session expired.
|
|
|
|
The recommended action is to redirect the user's browser to try the
|
|
approval process again.
|
|
"""
|
|
pass
|
|
|
|
|
|
class CsrfException(Exception):
|
|
"""
|
|
Thrown if the given 'state' parameter doesn't contain the CSRF token from
|
|
the user's session. This is blocked to prevent CSRF attacks.
|
|
|
|
The recommended action is to respond with an HTTP 403 error page.
|
|
"""
|
|
pass
|
|
|
|
|
|
class NotApprovedException(Exception):
|
|
"""
|
|
The user chose not to approve your app.
|
|
"""
|
|
pass
|
|
|
|
|
|
class ProviderException(Exception):
|
|
"""
|
|
Dropbox redirected to your redirect URI with some unexpected error
|
|
identifier and error message.
|
|
|
|
The recommended action is to log the error, tell the user something went
|
|
wrong, and let them try again.
|
|
"""
|
|
pass
|
|
|
|
|
|
class BadInputException(Exception):
|
|
"""
|
|
Thrown if incorrect types/values are used
|
|
|
|
This should only ever be thrown during testing, app should have validation of input prior to
|
|
reaching this point
|
|
"""
|
|
pass
|
|
|
|
|
|
def _safe_equals(a, b):
|
|
if len(a) != len(b):
|
|
return False
|
|
res = 0
|
|
for ca, cb in zip(a, b):
|
|
res |= ord(ca) ^ ord(cb)
|
|
return res == 0
|
|
|
|
|
|
def _params_to_urlencoded(params):
|
|
"""
|
|
Returns a application/x-www-form-urlencoded ``str`` representing the
|
|
key/value pairs in ``params``.
|
|
|
|
Keys are values are ``str()``'d before calling ``urllib.urlencode``, with
|
|
the exception of unicode objects which are utf8-encoded.
|
|
"""
|
|
def encode(o):
|
|
if isinstance(o, six.binary_type):
|
|
return o
|
|
else:
|
|
if isinstance(o, six.text_type):
|
|
return o.encode('utf-8')
|
|
else:
|
|
return str(o).encode('utf-8')
|
|
|
|
utf8_params = {encode(k): encode(v) for k, v in six.iteritems(params)}
|
|
return url_encode(utf8_params)
|
|
|
|
def _generate_pkce_code_verifier():
|
|
code_verifier = base64.urlsafe_b64encode(os.urandom(PKCE_VERIFIER_LENGTH)).decode('utf-8')
|
|
code_verifier = re.sub('[^a-zA-Z0-9]+', '', code_verifier)
|
|
if len(code_verifier) > PKCE_VERIFIER_LENGTH:
|
|
code_verifier = code_verifier[:128]
|
|
return code_verifier
|
|
|
|
def _generate_pkce_code_challenge(code_verifier):
|
|
code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
|
code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8')
|
|
code_challenge = code_challenge.replace('=', '')
|
|
return code_challenge
|