From b3edfa0d87d48ec9e31b17baeee59887cb9c05f8 Mon Sep 17 00:00:00 2001 From: AdeHub Date: Sat, 24 Aug 2024 16:33:52 +1200 Subject: [PATCH] update requests_oauthlib --- lib/requests_oauthlib/__init__.py | 3 +- .../compliance_fixes/__init__.py | 5 +- .../compliance_fixes/douban.py | 4 +- .../compliance_fixes/ebay.py | 22 ++++++ .../compliance_fixes/facebook.py | 10 +-- .../compliance_fixes/fitbit.py | 4 +- .../compliance_fixes/instagram.py | 5 +- .../compliance_fixes/linkedin.py | 21 ------ .../compliance_fixes/mailchimp.py | 6 +- .../compliance_fixes/plentymarkets.py | 4 +- .../compliance_fixes/slack.py | 5 +- .../compliance_fixes/weibo.py | 4 +- lib/requests_oauthlib/oauth1_auth.py | 13 ++-- lib/requests_oauthlib/oauth1_session.py | 15 ++-- lib/requests_oauthlib/oauth2_auth.py | 1 - lib/requests_oauthlib/oauth2_session.py | 71 ++++++++++++++++--- 16 files changed, 107 insertions(+), 86 deletions(-) create mode 100644 lib/requests_oauthlib/compliance_fixes/ebay.py delete mode 100644 lib/requests_oauthlib/compliance_fixes/linkedin.py diff --git a/lib/requests_oauthlib/__init__.py b/lib/requests_oauthlib/__init__.py index a4e03a4e..865d72fb 100644 --- a/lib/requests_oauthlib/__init__.py +++ b/lib/requests_oauthlib/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 import logging from .oauth1_auth import OAuth1 @@ -5,7 +6,7 @@ from .oauth1_session import OAuth1Session from .oauth2_auth import OAuth2 from .oauth2_session import OAuth2Session, TokenUpdated -__version__ = "1.3.0" +__version__ = "2.0.0" import requests diff --git a/lib/requests_oauthlib/compliance_fixes/__init__.py b/lib/requests_oauthlib/compliance_fixes/__init__.py index 02fa5120..8815ea0b 100644 --- a/lib/requests_oauthlib/compliance_fixes/__init__.py +++ b/lib/requests_oauthlib/compliance_fixes/__init__.py @@ -1,10 +1,9 @@ -from __future__ import absolute_import - +# ruff: noqa: F401 from .facebook import facebook_compliance_fix from .fitbit import fitbit_compliance_fix -from .linkedin import linkedin_compliance_fix from .slack import slack_compliance_fix from .instagram import instagram_compliance_fix from .mailchimp import mailchimp_compliance_fix from .weibo import weibo_compliance_fix from .plentymarkets import plentymarkets_compliance_fix +from .ebay import ebay_compliance_fix diff --git a/lib/requests_oauthlib/compliance_fixes/douban.py b/lib/requests_oauthlib/compliance_fixes/douban.py index ecc57b08..c8b99c72 100644 --- a/lib/requests_oauthlib/compliance_fixes/douban.py +++ b/lib/requests_oauthlib/compliance_fixes/douban.py @@ -1,14 +1,12 @@ import json -from oauthlib.common import to_unicode - def douban_compliance_fix(session): def fix_token_type(r): token = json.loads(r.text) token.setdefault("token_type", "Bearer") fixed_token = json.dumps(token) - r._content = to_unicode(fixed_token).encode("utf-8") + r._content = fixed_token.encode() return r session._client_default_token_placement = "query" diff --git a/lib/requests_oauthlib/compliance_fixes/ebay.py b/lib/requests_oauthlib/compliance_fixes/ebay.py new file mode 100644 index 00000000..ef33f391 --- /dev/null +++ b/lib/requests_oauthlib/compliance_fixes/ebay.py @@ -0,0 +1,22 @@ +import json + + +def ebay_compliance_fix(session): + def _compliance_fix(response): + token = json.loads(response.text) + + # eBay responds with non-compliant token types. + # https://developer.ebay.com/api-docs/static/oauth-client-credentials-grant.html + # https://developer.ebay.com/api-docs/static/oauth-auth-code-grant-request.html + # Modify these to be "Bearer". + if token.get("token_type") in ["Application Access Token", "User Access Token"]: + token["token_type"] = "Bearer" + fixed_token = json.dumps(token) + response._content = fixed_token.encode() + + return response + + session.register_compliance_hook("access_token_response", _compliance_fix) + session.register_compliance_hook("refresh_token_response", _compliance_fix) + + return session diff --git a/lib/requests_oauthlib/compliance_fixes/facebook.py b/lib/requests_oauthlib/compliance_fixes/facebook.py index 90e79212..f44558a8 100644 --- a/lib/requests_oauthlib/compliance_fixes/facebook.py +++ b/lib/requests_oauthlib/compliance_fixes/facebook.py @@ -1,11 +1,5 @@ from json import dumps - -try: - from urlparse import parse_qsl -except ImportError: - from urllib.parse import parse_qsl - -from oauthlib.common import to_unicode +from urllib.parse import parse_qsl def facebook_compliance_fix(session): @@ -26,7 +20,7 @@ def facebook_compliance_fix(session): if expires is not None: token["expires_in"] = expires token["token_type"] = "Bearer" - r._content = to_unicode(dumps(token)).encode("UTF-8") + r._content = dumps(token).encode() return r session.register_compliance_hook("access_token_response", _compliance_fix) diff --git a/lib/requests_oauthlib/compliance_fixes/fitbit.py b/lib/requests_oauthlib/compliance_fixes/fitbit.py index 7e627024..aacc68bf 100644 --- a/lib/requests_oauthlib/compliance_fixes/fitbit.py +++ b/lib/requests_oauthlib/compliance_fixes/fitbit.py @@ -8,8 +8,6 @@ MissingTokenError. from json import loads, dumps -from oauthlib.common import to_unicode - def fitbit_compliance_fix(session): def _missing_error(r): @@ -17,7 +15,7 @@ def fitbit_compliance_fix(session): if "errors" in token: # Set the error to the first one we have token["error"] = token["errors"][0]["errorType"] - r._content = to_unicode(dumps(token)).encode("UTF-8") + r._content = dumps(token).encode() return r session.register_compliance_hook("access_token_response", _missing_error) diff --git a/lib/requests_oauthlib/compliance_fixes/instagram.py b/lib/requests_oauthlib/compliance_fixes/instagram.py index 4e07fe08..7d5a2ad4 100644 --- a/lib/requests_oauthlib/compliance_fixes/instagram.py +++ b/lib/requests_oauthlib/compliance_fixes/instagram.py @@ -1,7 +1,4 @@ -try: - from urlparse import urlparse, parse_qs -except ImportError: - from urllib.parse import urlparse, parse_qs +from urllib.parse import urlparse, parse_qs from oauthlib.common import add_params_to_uri diff --git a/lib/requests_oauthlib/compliance_fixes/linkedin.py b/lib/requests_oauthlib/compliance_fixes/linkedin.py deleted file mode 100644 index cd5b4ace..00000000 --- a/lib/requests_oauthlib/compliance_fixes/linkedin.py +++ /dev/null @@ -1,21 +0,0 @@ -from json import loads, dumps - -from oauthlib.common import add_params_to_uri, to_unicode - - -def linkedin_compliance_fix(session): - def _missing_token_type(r): - token = loads(r.text) - token["token_type"] = "Bearer" - r._content = to_unicode(dumps(token)).encode("UTF-8") - return r - - def _non_compliant_param_name(url, headers, data): - token = [("oauth2_access_token", session.access_token)] - url = add_params_to_uri(url, token) - return url, headers, data - - session._client.default_token_placement = "query" - session.register_compliance_hook("access_token_response", _missing_token_type) - session.register_compliance_hook("protected_request", _non_compliant_param_name) - return session diff --git a/lib/requests_oauthlib/compliance_fixes/mailchimp.py b/lib/requests_oauthlib/compliance_fixes/mailchimp.py index c69ce9fd..0d602659 100644 --- a/lib/requests_oauthlib/compliance_fixes/mailchimp.py +++ b/lib/requests_oauthlib/compliance_fixes/mailchimp.py @@ -1,21 +1,19 @@ import json -from oauthlib.common import to_unicode - def mailchimp_compliance_fix(session): def _null_scope(r): token = json.loads(r.text) if "scope" in token and token["scope"] is None: token.pop("scope") - r._content = to_unicode(json.dumps(token)).encode("utf-8") + r._content = json.dumps(token).encode() return r def _non_zero_expiration(r): token = json.loads(r.text) if "expires_in" in token and token["expires_in"] == 0: token["expires_in"] = 3600 - r._content = to_unicode(json.dumps(token)).encode("utf-8") + r._content = json.dumps(token).encode() return r session.register_compliance_hook("access_token_response", _null_scope) diff --git a/lib/requests_oauthlib/compliance_fixes/plentymarkets.py b/lib/requests_oauthlib/compliance_fixes/plentymarkets.py index 9f605f05..859f0566 100644 --- a/lib/requests_oauthlib/compliance_fixes/plentymarkets.py +++ b/lib/requests_oauthlib/compliance_fixes/plentymarkets.py @@ -1,8 +1,6 @@ from json import dumps, loads import re -from oauthlib.common import to_unicode - def plentymarkets_compliance_fix(session): def _to_snake_case(n): @@ -22,7 +20,7 @@ def plentymarkets_compliance_fix(session): for k, v in token.items(): fixed_token[_to_snake_case(k)] = v - r._content = to_unicode(dumps(fixed_token)).encode("UTF-8") + r._content = dumps(fixed_token).encode() return r session.register_compliance_hook("access_token_response", _compliance_fix) diff --git a/lib/requests_oauthlib/compliance_fixes/slack.py b/lib/requests_oauthlib/compliance_fixes/slack.py index 3f574b03..9095a470 100644 --- a/lib/requests_oauthlib/compliance_fixes/slack.py +++ b/lib/requests_oauthlib/compliance_fixes/slack.py @@ -1,7 +1,4 @@ -try: - from urlparse import urlparse, parse_qs -except ImportError: - from urllib.parse import urlparse, parse_qs +from urllib.parse import urlparse, parse_qs from oauthlib.common import add_params_to_uri diff --git a/lib/requests_oauthlib/compliance_fixes/weibo.py b/lib/requests_oauthlib/compliance_fixes/weibo.py index 6733abeb..f1623fd6 100644 --- a/lib/requests_oauthlib/compliance_fixes/weibo.py +++ b/lib/requests_oauthlib/compliance_fixes/weibo.py @@ -1,13 +1,11 @@ from json import loads, dumps -from oauthlib.common import to_unicode - def weibo_compliance_fix(session): def _missing_token_type(r): token = loads(r.text) token["token_type"] = "Bearer" - r._content = to_unicode(dumps(token)).encode("UTF-8") + r._content = dumps(token).encode() return r session._client.default_token_placement = "query" diff --git a/lib/requests_oauthlib/oauth1_auth.py b/lib/requests_oauthlib/oauth1_auth.py index cfbbd590..f8c0bd6e 100644 --- a/lib/requests_oauthlib/oauth1_auth.py +++ b/lib/requests_oauthlib/oauth1_auth.py @@ -1,20 +1,15 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals - import logging from oauthlib.common import extract_params from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER from oauthlib.oauth1 import SIGNATURE_TYPE_BODY -from requests.compat import is_py3 from requests.utils import to_native_string from requests.auth import AuthBase CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded" CONTENT_TYPE_MULTI_PART = "multipart/form-data" -if is_py3: - unicode = str log = logging.getLogger(__name__) @@ -83,7 +78,7 @@ class OAuth1(AuthBase): or self.client.signature_type == SIGNATURE_TYPE_BODY ): content_type = CONTENT_TYPE_FORM_URLENCODED - if not isinstance(content_type, unicode): + if not isinstance(content_type, str): content_type = content_type.decode("utf-8") is_form_encoded = CONTENT_TYPE_FORM_URLENCODED in content_type @@ -96,17 +91,17 @@ class OAuth1(AuthBase): if is_form_encoded: r.headers["Content-Type"] = CONTENT_TYPE_FORM_URLENCODED r.url, headers, r.body = self.client.sign( - unicode(r.url), unicode(r.method), r.body or "", r.headers + str(r.url), str(r.method), r.body or "", r.headers ) elif self.force_include_body: # To allow custom clients to work on non form encoded bodies. r.url, headers, r.body = self.client.sign( - unicode(r.url), unicode(r.method), r.body or "", r.headers + str(r.url), str(r.method), r.body or "", r.headers ) else: # Omit body data in the signing of non form-encoded requests r.url, headers, _ = self.client.sign( - unicode(r.url), unicode(r.method), None, r.headers + str(r.url), str(r.method), None, r.headers ) r.prepare_headers(headers) diff --git a/lib/requests_oauthlib/oauth1_session.py b/lib/requests_oauthlib/oauth1_session.py index aa17f28f..7625c808 100644 --- a/lib/requests_oauthlib/oauth1_session.py +++ b/lib/requests_oauthlib/oauth1_session.py @@ -1,9 +1,4 @@ -from __future__ import unicode_literals - -try: - from urlparse import urlparse -except ImportError: - from urllib.parse import urlparse +from urllib.parse import urlparse import logging @@ -85,7 +80,7 @@ class OAuth1Session(requests.Session): 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback' >>> >>> # Third step. Fetch the access token - >>> redirect_response = raw_input('Paste the full redirect URL here.') + >>> redirect_response = input('Paste the full redirect URL here.') >>> oauth_session.parse_authorization_response(redirect_response) { 'oauth_token: 'kjerht2309u', @@ -258,7 +253,7 @@ class OAuth1Session(requests.Session): return add_params_to_uri(url, kwargs.items()) def fetch_request_token(self, url, realm=None, **request_kwargs): - r"""Fetch a request token. + """Fetch a request token. This is the first step in the OAuth 1 workflow. A request token is obtained by making a signed post request to url. The token is then @@ -267,8 +262,8 @@ class OAuth1Session(requests.Session): :param url: The request token endpoint URL. :param realm: A list of realms to request access to. - :param \*\*request_kwargs: Optional arguments passed to ''post'' - function in ''requests.Session'' + :param request_kwargs: Optional arguments passed to ''post'' + function in ''requests.Session'' :returns: The response in dict format. Note that a previously set callback_uri will be reset for your diff --git a/lib/requests_oauthlib/oauth2_auth.py b/lib/requests_oauthlib/oauth2_auth.py index b880f72f..f19f52ac 100644 --- a/lib/requests_oauthlib/oauth2_auth.py +++ b/lib/requests_oauthlib/oauth2_auth.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError from oauthlib.oauth2 import is_secure_transport from requests.auth import AuthBase diff --git a/lib/requests_oauthlib/oauth2_session.py b/lib/requests_oauthlib/oauth2_session.py index eea4ac6f..93cc4d7b 100644 --- a/lib/requests_oauthlib/oauth2_session.py +++ b/lib/requests_oauthlib/oauth2_session.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging from oauthlib.common import generate_token, urldecode @@ -46,6 +44,7 @@ class OAuth2Session(requests.Session): token=None, state=None, token_updater=None, + pkce=None, **kwargs ): """Construct a new OAuth 2 client session. @@ -72,18 +71,23 @@ class OAuth2Session(requests.Session): set a TokenUpdated warning will be raised when a token has been refreshed. This warning will carry the token in its token argument. + :param pkce: Set "S256" or "plain" to enable PKCE. Default is disabled. :param kwargs: Arguments to pass to the Session constructor. """ super(OAuth2Session, self).__init__(**kwargs) self._client = client or WebApplicationClient(client_id, token=token) self.token = token or {} - self.scope = scope + self._scope = scope self.redirect_uri = redirect_uri self.state = state or generate_token self._state = state self.auto_refresh_url = auto_refresh_url self.auto_refresh_kwargs = auto_refresh_kwargs or {} self.token_updater = token_updater + self._pkce = pkce + + if self._pkce not in ["S256", "plain", None]: + raise AttributeError("Wrong value for {}(.., pkce={})".format(self.__class__, self._pkce)) # Ensure that requests doesn't do any automatic auth. See #278. # The default behavior can be re-enabled by setting auth to None. @@ -95,8 +99,24 @@ class OAuth2Session(requests.Session): "access_token_response": set(), "refresh_token_response": set(), "protected_request": set(), + "refresh_token_request": set(), + "access_token_request": set(), } + @property + def scope(self): + """By default the scope from the client is used, except if overridden""" + if self._scope is not None: + return self._scope + elif self._client is not None: + return self._client.scope + else: + return None + + @scope.setter + def scope(self, scope): + self._scope = scope + def new_state(self): """Generates a state string to be used in authorizations.""" try: @@ -161,6 +181,13 @@ class OAuth2Session(requests.Session): :return: authorization_url, state """ state = state or self.new_state() + if self._pkce: + self._code_verifier = self._client.create_code_verifier(43) + kwargs["code_challenge_method"] = self._pkce + kwargs["code_challenge"] = self._client.create_code_challenge( + code_verifier=self._code_verifier, + code_challenge_method=self._pkce + ) return ( self._client.prepare_request_uri( url, @@ -185,10 +212,11 @@ class OAuth2Session(requests.Session): force_querystring=False, timeout=None, headers=None, - verify=True, + verify=None, proxies=None, include_client_id=None, client_secret=None, + cert=None, **kwargs ): """Generic method for fetching an access token from the token endpoint. @@ -229,6 +257,10 @@ class OAuth2Session(requests.Session): `auth` tuple. If the value is `None`, it will be omitted from the request, however if the value is an empty string, an empty string will be sent. + :param cert: Client certificate to send for OAuth 2.0 Mutual-TLS Client + Authentication (draft-ietf-oauth-mtls). Can either be the + path of a file containing the private key and certificate or + a tuple of two filenames for certificate and key. :param kwargs: Extra parameters to include in the token request. :return: A token dict """ @@ -247,6 +279,13 @@ class OAuth2Session(requests.Session): "Please supply either code or " "authorization_response parameters." ) + if self._pkce: + if self._code_verifier is None: + raise ValueError( + "Code verifier is not found, authorization URL must be generated before" + ) + kwargs["code_verifier"] = self._code_verifier + # Earlier versions of this library build an HTTPBasicAuth header out of # `username` and `password`. The RFC states, however these attributes # must be in the request body and not the header. @@ -320,7 +359,7 @@ class OAuth2Session(requests.Session): headers = headers or { "Accept": "application/json", - "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + "Content-Type": "application/x-www-form-urlencoded", } self.token = {} request_kwargs = {} @@ -333,6 +372,12 @@ class OAuth2Session(requests.Session): else: raise ValueError("The method kwarg must be POST or GET.") + for hook in self.compliance_hook["access_token_request"]: + log.debug("Invoking access_token_request hook %s.", hook) + token_url, headers, request_kwargs = hook( + token_url, headers, request_kwargs + ) + r = self.request( method=method, url=token_url, @@ -341,6 +386,7 @@ class OAuth2Session(requests.Session): auth=auth, verify=verify, proxies=proxies, + cert=cert, **request_kwargs ) @@ -382,7 +428,7 @@ class OAuth2Session(requests.Session): auth=None, timeout=None, headers=None, - verify=True, + verify=None, proxies=None, **kwargs ): @@ -420,9 +466,13 @@ class OAuth2Session(requests.Session): if headers is None: headers = { "Accept": "application/json", - "Content-Type": ("application/x-www-form-urlencoded;charset=UTF-8"), + "Content-Type": ("application/x-www-form-urlencoded"), } + for hook in self.compliance_hook["refresh_token_request"]: + log.debug("Invoking refresh_token_request hook %s.", hook) + token_url, headers, body = hook(token_url, headers, body) + r = self.post( token_url, data=dict(urldecode(body)), @@ -444,7 +494,7 @@ class OAuth2Session(requests.Session): r = hook(r) self.token = self._client.parse_request_body_response(r.text, scope=self.scope) - if not "refresh_token" in self.token: + if "refresh_token" not in self.token: log.debug("No new refresh token given. Re-using old.") self.token["refresh_token"] = refresh_token return self.token @@ -458,6 +508,7 @@ class OAuth2Session(requests.Session): withhold_token=False, client_id=None, client_secret=None, + files=None, **kwargs ): """Intercept all requests and add the OAuth 2 token if present.""" @@ -513,7 +564,7 @@ class OAuth2Session(requests.Session): log.debug("Supplying headers %s and data %s", headers, data) log.debug("Passing through key word arguments %s.", kwargs) return super(OAuth2Session, self).request( - method, url, headers=headers, data=data, **kwargs + method, url, headers=headers, data=data, files=files, **kwargs ) def register_compliance_hook(self, hook_type, hook): @@ -523,6 +574,8 @@ class OAuth2Session(requests.Session): access_token_response invoked before token parsing. refresh_token_response invoked before refresh token parsing. protected_request invoked before making a request. + access_token_request invoked before making a token fetch request. + refresh_token_request invoked before making a refresh request. If you find a new hook is needed please send a GitHub PR request or open an issue.