diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2024-03-25 09:11:17 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2024-03-25 09:17:48 +0300 |
commit | 4624e4cfd95649270db02616edde8d0ca249b63d (patch) | |
tree | 1c8a43f50533ca759d137f258e42862e8cf5e80f /contrib/python/requests-oauthlib | |
parent | d2d971701bd8377ead5f973c96be81042774bd2a (diff) | |
download | ydb-4624e4cfd95649270db02616edde8d0ca249b63d.tar.gz |
Intermediate changes
Diffstat (limited to 'contrib/python/requests-oauthlib')
26 files changed, 366 insertions, 126 deletions
diff --git a/contrib/python/requests-oauthlib/.dist-info/METADATA b/contrib/python/requests-oauthlib/.dist-info/METADATA index 975ce567fc..1029e2ff65 100644 --- a/contrib/python/requests-oauthlib/.dist-info/METADATA +++ b/contrib/python/requests-oauthlib/.dist-info/METADATA @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: requests-oauthlib -Version: 1.3.1 +Version: 1.4.0 Summary: OAuthlib authentication support for Requests. Home-page: https://github.com/requests/requests-oauthlib Author: Kenneth Reitz Author-email: me@kennethreitz.com License: ISC -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English @@ -21,20 +20,23 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Description-Content-Type: text/x-rst License-File: LICENSE -Requires-Dist: oauthlib (>=3.0.0) -Requires-Dist: requests (>=2.0.0) +Requires-Dist: oauthlib >=3.0.0 +Requires-Dist: requests >=2.0.0 Provides-Extra: rsa -Requires-Dist: oauthlib[signedtoken] (>=3.0.0) ; extra == 'rsa' +Requires-Dist: oauthlib[signedtoken] >=3.0.0 ; extra == 'rsa' Requests-OAuthlib |build-status| |coverage-status| |docs| ========================================================= -This project provides first-class OAuth library support for `Requests <http://python-requests.org>`_. +This project provides first-class OAuth library support for `Requests <https://requests.readthedocs.io>`_. The OAuth 1 workflow -------------------- @@ -79,7 +81,7 @@ To install requests and requests_oauthlib you can use pip: .. code-block:: bash - $ pip install requests requests_oauthlib + pip install requests requests-oauthlib .. |build-status| image:: https://github.com/requests/requests-oauthlib/actions/workflows/run-tests.yml/badge.svg :target: https://github.com/requests/requests-oauthlib/actions @@ -94,10 +96,32 @@ To install requests and requests_oauthlib you can use pip: History ------- +v1.4.0 (27 Feb 2024) +++++++++++++++++++++++++ + +Full set of changes are in [github](https://github.com/requests/requests-oauthlib/milestone/4?closed=1). + +Additions & changes: + +- ``OAuth2Session`` now correctly uses the ``self.verify`` value if ``verify`` + is not overridden in ``fetch_token`` and ``refresh_token``. Fixes `#404 + <https://github.com/requests/requests-oauthlib/issues/404>`_. +- ``OAuth2Session`` constructor now uses its ``client.scope`` when a ``client`` + is provided and ``scope`` is not overridden. Fixes `#408 + <https://github.com/requests/requests-oauthlib/issues/408>`_ +- Add ``refresh_token_request`` and ``access_token_request`` compliance hooks +- Add PKCE support and Auth0 example +- Add support for Python 3.8-3.12 +- Remove support of Python 2.x, <3.7 +- Migrated to Github Action +- Updated dependencies +- Cleanup some docs and examples + v1.3.1 (21 January 2022) ++++++++++++++++++++++++ - Add initial support for OAuth Mutual TLS (draft-ietf-oauth-mtls) +- Removed outdated LinkedIn Compliance Fixes - Add eBay compliance fix - Add Spotify OAuth 2 Tutorial - Add support for python 3.8, 3.9 @@ -241,5 +265,3 @@ v0.4.0 (29 September 2013) - OAuth1 now updates r.headers instead of replacing it with non case insensitive dict - Remove last use of Response.content (in OAuth1Session). #44. - State param can now be supplied in OAuth2Session.authorize_url - - diff --git a/contrib/python/requests-oauthlib/AUTHORS.rst b/contrib/python/requests-oauthlib/AUTHORS.rst index c8fba5e997..0b2fb31615 100644 --- a/contrib/python/requests-oauthlib/AUTHORS.rst +++ b/contrib/python/requests-oauthlib/AUTHORS.rst @@ -1,13 +1,12 @@ -Requests-oauthlib is written and maintained by Kenneth Reitz and various -contributors: +Requests-oauthlib has been written by Kenneth Reitz, various contributors: -Development Lead +Initial Write-up ---------------- - Kenneth Reitz <me@kennethreitz.com> -Patches and Suggestions ------------------------ +Maintainers & Contributors +-------------------------- - Cory Benfield <cory@lukasa.co.uk> - Ib Lundgren <ib.lundgren@gmail.com> @@ -15,6 +14,7 @@ Patches and Suggestions - Imad Mouhtassem <mouhtasi@gmail.com> - Johan Euphrosine <proppy@google.com> - Johannes Spielmann <js@shezi.de> +- Jonathan Huot <jonathan.huot@gmail.com> - Martin Trigaux <me@mart-e.be> - Matt McClure <matt.mcclure@mapmyfitness.com> - Mikhail Sobolev <mss@mawhrin.net> @@ -22,4 +22,6 @@ Patches and Suggestions - Vinay Raikar <rockraikar@gmail.com> - kracekumar <me@kracekumar.com> - David Baumgold <david@davidbaumgold.com> +- Sylvain Marie <sylvain.marie@se.com> - Craig Anderson <craiga@craiga.id.au> +- Hugo van Kemenade <https://github.com/hugovk> diff --git a/contrib/python/requests-oauthlib/README.rst b/contrib/python/requests-oauthlib/README.rst index 9fd1bb9767..c1e99dd0e9 100644 --- a/contrib/python/requests-oauthlib/README.rst +++ b/contrib/python/requests-oauthlib/README.rst @@ -1,7 +1,7 @@ Requests-OAuthlib |build-status| |coverage-status| |docs| ========================================================= -This project provides first-class OAuth library support for `Requests <http://python-requests.org>`_. +This project provides first-class OAuth library support for `Requests <https://requests.readthedocs.io>`_. The OAuth 1 workflow -------------------- @@ -46,7 +46,7 @@ To install requests and requests_oauthlib you can use pip: .. code-block:: bash - $ pip install requests requests_oauthlib + pip install requests requests-oauthlib .. |build-status| image:: https://github.com/requests/requests-oauthlib/actions/workflows/run-tests.yml/badge.svg :target: https://github.com/requests/requests-oauthlib/actions diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/__init__.py b/contrib/python/requests-oauthlib/requests_oauthlib/__init__.py index 0d3e49f991..a71064cca0 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/__init__.py +++ b/contrib/python/requests-oauthlib/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.1" +__version__ = "1.4.0" import requests diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py index 0e8e3ac84f..8815ea0b8e 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py @@ -1,5 +1,4 @@ -from __future__ import absolute_import - +# ruff: noqa: F401 from .facebook import facebook_compliance_fix from .fitbit import fitbit_compliance_fix from .slack import slack_compliance_fix diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/douban.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/douban.py index ecc57b0818..c8b99c721e 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/douban.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py index 4aa423b3fe..ef33f39101 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py @@ -1,5 +1,4 @@ import json -from oauthlib.common import to_unicode def ebay_compliance_fix(session): @@ -13,7 +12,7 @@ def ebay_compliance_fix(session): if token.get("token_type") in ["Application Access Token", "User Access Token"]: token["token_type"] = "Bearer" fixed_token = json.dumps(token) - response._content = to_unicode(fixed_token).encode("utf-8") + response._content = fixed_token.encode() return response diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/facebook.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/facebook.py index 90e7921272..f44558a839 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/facebook.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/fitbit.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/fitbit.py index 7e62702401..aacc68bfbb 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/fitbit.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/instagram.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/instagram.py index 4e07fe08b5..7d5a2ad447 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/instagram.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/mailchimp.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/mailchimp.py index c69ce9fdae..0d602659c6 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/mailchimp.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/plentymarkets.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/plentymarkets.py index 9f605f058c..859f0566a5 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/plentymarkets.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/slack.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/slack.py index 3f574b03ad..9095a470cd 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/slack.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/weibo.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/weibo.py index 6733abeb15..f1623fd6d7 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/weibo.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_auth.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_auth.py index cfbbd5902c..f8c0bd6e74 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_auth.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_session.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_session.py index 88f2853ca0..7625c8084e 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_session.py +++ b/contrib/python/requests-oauthlib/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,7 +262,7 @@ 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'' + :param request_kwargs: Optional arguments passed to ''post'' function in ''requests.Session'' :returns: The response in dict format. diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_auth.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_auth.py index b880f72f58..f19f52ac90 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_auth.py +++ b/contrib/python/requests-oauthlib/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/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_session.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_session.py index db4468089b..93cc4d7bbd 100644 --- a/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_session.py +++ b/contrib/python/requests-oauthlib/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,7 +212,7 @@ class OAuth2Session(requests.Session): force_querystring=False, timeout=None, headers=None, - verify=True, + verify=None, proxies=None, include_client_id=None, client_secret=None, @@ -252,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. @@ -325,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 = {} @@ -338,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, @@ -388,7 +428,7 @@ class OAuth2Session(requests.Session): auth=None, timeout=None, headers=None, - verify=True, + verify=None, proxies=None, **kwargs ): @@ -426,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)), @@ -450,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 @@ -464,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.""" @@ -519,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): @@ -529,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. diff --git a/contrib/python/requests-oauthlib/tests/examples/base.py b/contrib/python/requests-oauthlib/tests/examples/base.py new file mode 100644 index 0000000000..2efa5dd746 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/examples/base.py @@ -0,0 +1,106 @@ +import os.path +import os +import subprocess +import shlex +import shutil +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + + +cwd = os.path.dirname(os.path.realpath(__file__)) + + +class Sample(): + def setUp(self): + super().setUp() + self.proc = None + self.outputs = [] + + def tearDown(self): + super().tearDown() + if self.proc is not None: + self.proc.stdin.close() + self.proc.stdout.close() + self.proc.kill() + + def replaceVariables(self, filein ,fileout, vars): + with open(filein, "rt") as fin: + with open(fileout, "wt") as fout: + for line in fin: + for k, v in vars.items(): + line = line.replace(k, v) + fout.write(line) + + def run_sample(self, filepath, variables): + inpath = os.path.join(cwd, "..", "..", "docs", "examples", filepath) + outpath = os.path.join(cwd, "tmp_{}".format(filepath)) + self.replaceVariables(inpath, outpath, variables) + + self.proc = subprocess.Popen( + [shutil.which("python"), + outpath], + text=True, bufsize=1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE + ) + + def write(self, string): + self.proc.stdin.write(string) + self.proc.stdin.flush() + + def wait_for_pattern(self, pattern): + try: + while True: + line = self.proc.stdout.readline() + self.outputs.append(line) + if pattern in line: + return line + except subprocess.TimeoutExpired: + self.assertTrue(False, "timeout when looking for output") + + def wait_for_end(self): + try: + outs, err = self.proc.communicate(timeout=10) + self.outputs += filter(lambda x: x != '', outs.split('\n')) + except subprocess.TimeoutExpired: + self.assertTrue(False, "timeout when looking for output") + return self.outputs[-1] + + + +class Browser(): + def setUp(self): + super().setUp() + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + self.driver = webdriver.Chrome(options=options) + self.user_username = os.environ.get("AUTH0_USERNAME") + self.user_password = os.environ.get("AUTH0_PASSWORD") + + if not self.user_username or not self.user_password: + self.skipTest("auth0 is not configured properly") + + def tearDown(self): + super().tearDown() + self.driver.quit() + + def authorize_auth0(self, authorize_url, expected_redirect_uri): + self.driver.get(authorize_url) + username = self.driver.find_element(By.ID, "username") + password = self.driver.find_element(By.ID, "password") + + wait = WebDriverWait(self.driver, timeout=2) + wait.until(lambda d : username.is_displayed()) + wait.until(lambda d : password.is_displayed()) + + username.clear() + username.send_keys(self.user_username) + password.send_keys(self.user_password) + username.send_keys(Keys.RETURN) + + wait.until(EC.url_contains(expected_redirect_uri)) + return self.driver.current_url + diff --git a/contrib/python/requests-oauthlib/tests/examples/test_native_spa_pkce_auth0.py b/contrib/python/requests-oauthlib/tests/examples/test_native_spa_pkce_auth0.py new file mode 100644 index 0000000000..6ff41e251c --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/examples/test_native_spa_pkce_auth0.py @@ -0,0 +1,39 @@ +import os +import unittest + +from . import base + +class TestNativeAuth0Test(base.Sample, base.Browser, unittest.TestCase): + def setUp(self): + super().setUp() + self.client_id = os.environ.get("AUTH0_PKCE_CLIENT_ID") + self.idp_domain = os.environ.get("AUTH0_DOMAIN") + + if not self.client_id or not self.idp_domain: + self.skipTest("native auth0 is not configured properly") + + def test_login(self): + # redirect_uri is http:// + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = "1" + + self.run_sample( + "native_spa_pkce_auth0.py", { + "OAUTH_CLIENT_ID": self.client_id, + "OAUTH_IDP_DOMAIN": self.idp_domain, + } + ) + authorize_url = self.wait_for_pattern("https://") + redirect_uri = self.authorize_auth0(authorize_url, "http://") + self.write(redirect_uri) + last_line = self.wait_for_end() + + import ast + response = ast.literal_eval(last_line) + self.assertIn("access_token", response) + self.assertIn("id_token", response) + self.assertIn("scope", response) + self.assertIn("openid", response["scope"]) + self.assertIn("expires_in", response) + self.assertIn("expires_at", response) + self.assertIn("token_type", response) + self.assertEqual("Bearer", response["token_type"]) diff --git a/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py b/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py index 5c90d52660..c5166bdb2f 100644 --- a/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py +++ b/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py @@ -1,14 +1,10 @@ -from __future__ import unicode_literals from unittest import TestCase import requests import requests_mock import time -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.oauth2.rfc6749.errors import InvalidGrantError from requests_oauthlib import OAuth2Session @@ -332,3 +328,60 @@ class EbayComplianceFixTest(TestCase): authorization_response="https://i.b/?code=hello", ) assert token["token_type"] == "Bearer" + + +def access_and_refresh_token_request_compliance_fix_test(session, client_secret): + def _non_compliant_header(url, headers, body): + headers["X-Client-Secret"] = client_secret + return url, headers, body + + session.register_compliance_hook("access_token_request", _non_compliant_header) + session.register_compliance_hook("refresh_token_request", _non_compliant_header) + return session + + +class RefreshTokenRequestComplianceFixTest(TestCase): + value_to_test_for = "value_to_test_for" + + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://example.com/token", + request_headers={"X-Client-Secret": self.value_to_test_for}, + json={ + "access_token": "this is the access token", + "expires_in": 7200, + "token_type": "Bearer", + }, + headers={"Content-Type": "application/json"}, + ) + mocker.post( + "https://example.com/refresh", + request_headers={"X-Client-Secret": self.value_to_test_for}, + json={ + "access_token": "this is the access token", + "expires_in": 7200, + "token_type": "Bearer", + }, + headers={"Content-Type": "application/json"}, + ) + mocker.start() + self.addCleanup(mocker.stop) + + session = OAuth2Session() + self.fixed_session = access_and_refresh_token_request_compliance_fix_test( + session, self.value_to_test_for + ) + + def test_access_token(self): + token = self.fixed_session.fetch_token( + "https://example.com/token", + authorization_response="https://i.b/?code=hello", + ) + assert token["token_type"] == "Bearer" + + def test_refresh_token(self): + token = self.fixed_session.refresh_token( + "https://example.com/refresh", + ) + assert token["token_type"] == "Bearer" diff --git a/contrib/python/requests-oauthlib/tests/test_core.py b/contrib/python/requests-oauthlib/tests/test_core.py index 6892e9f1ce..09cd0f0212 100644 --- a/contrib/python/requests-oauthlib/tests/test_core.py +++ b/contrib/python/requests-oauthlib/tests/test_core.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import requests import requests_oauthlib import oauthlib @@ -7,10 +6,7 @@ import os.path from io import StringIO import unittest -try: - import mock -except ImportError: - from unittest import mock +from unittest import mock @mock.patch("oauthlib.oauth1.rfc5849.generate_timestamp") diff --git a/contrib/python/requests-oauthlib/tests/test_oauth1_session.py b/contrib/python/requests-oauthlib/tests/test_oauth1_session.py index 1dd2b2f158..b3c8c70483 100644 --- a/contrib/python/requests-oauthlib/tests/test_oauth1_session.py +++ b/contrib/python/requests-oauthlib/tests/test_oauth1_session.py @@ -1,19 +1,13 @@ -from __future__ import unicode_literals, print_function import unittest -import sys import requests from io import StringIO +from unittest import mock from oauthlib.oauth1 import SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY from oauthlib.oauth1 import SIGNATURE_RSA, SIGNATURE_PLAINTEXT from requests_oauthlib import OAuth1Session try: - import mock -except ImportError: - from unittest import mock - -try: import cryptography except ImportError: cryptography = None @@ -23,11 +17,6 @@ try: except ImportError: jwt = None -if sys.version[0] == "3": - unicode_type = str -else: - unicode_type = unicode - TEST_RSA_KEY = ( "-----BEGIN RSA PRIVATE KEY-----\n" @@ -165,8 +154,8 @@ class OAuth1SessionTest(unittest.TestCase): self.assertEqual(resp["oauth_token"], "foo") self.assertEqual(resp["oauth_verifier"], "bar") for k, v in resp.items(): - self.assertIsInstance(k, unicode_type) - self.assertIsInstance(v, unicode_type) + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) def test_fetch_request_token(self): auth = OAuth1Session("foo") @@ -174,8 +163,8 @@ class OAuth1SessionTest(unittest.TestCase): resp = auth.fetch_request_token("https://example.com/token") self.assertEqual(resp["oauth_token"], "foo") for k, v in resp.items(): - self.assertIsInstance(k, unicode_type) - self.assertIsInstance(v, unicode_type) + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) def test_fetch_request_token_with_optional_arguments(self): auth = OAuth1Session("foo") @@ -185,8 +174,8 @@ class OAuth1SessionTest(unittest.TestCase): ) self.assertEqual(resp["oauth_token"], "foo") for k, v in resp.items(): - self.assertIsInstance(k, unicode_type) - self.assertIsInstance(v, unicode_type) + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) def test_fetch_access_token(self): auth = OAuth1Session("foo", verifier="bar") @@ -194,8 +183,8 @@ class OAuth1SessionTest(unittest.TestCase): resp = auth.fetch_access_token("https://example.com/token") self.assertEqual(resp["oauth_token"], "foo") for k, v in resp.items(): - self.assertIsInstance(k, unicode_type) - self.assertIsInstance(v, unicode_type) + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) def test_fetch_access_token_with_optional_arguments(self): auth = OAuth1Session("foo", verifier="bar") @@ -205,8 +194,8 @@ class OAuth1SessionTest(unittest.TestCase): ) self.assertEqual(resp["oauth_token"], "foo") for k, v in resp.items(): - self.assertIsInstance(k, unicode_type) - self.assertIsInstance(v, unicode_type) + self.assertIsInstance(k, str) + self.assertIsInstance(v, str) def _test_fetch_access_token_raises_error(self, auth): """Assert that an error is being raised whenever there's no verifier @@ -308,12 +297,6 @@ class OAuth1SessionTest(unittest.TestCase): generate_nonce.return_value = "abc" generate_timestamp.return_value = "123" - signature = ( - "OAuth " - 'oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", ' - 'oauth_signature_method="RSA-SHA1", oauth_consumer_key="foo", ' - 'oauth_verifier="bar", oauth_signature="{sig}"' - ).format(sig=TEST_RSA_OAUTH_SIGNATURE) sess = OAuth1Session( "key", "secret", diff --git a/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py b/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py index accb561ef6..69ed6f6647 100644 --- a/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py +++ b/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import unittest from oauthlib.oauth2 import WebApplicationClient, MobileApplicationClient diff --git a/contrib/python/requests-oauthlib/tests/test_oauth2_session.py b/contrib/python/requests-oauthlib/tests/test_oauth2_session.py index cfc6236855..7e3e63c57a 100644 --- a/contrib/python/requests-oauthlib/tests/test_oauth2_session.py +++ b/contrib/python/requests-oauthlib/tests/test_oauth2_session.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import json import time import tempfile @@ -8,10 +7,7 @@ from base64 import b64encode from copy import deepcopy from unittest import TestCase -try: - import mock -except ImportError: - from unittest import mock +from unittest import mock from oauthlib.common import urlencode from oauthlib.oauth2 import TokenExpiredError, OAuth2Error @@ -124,6 +120,27 @@ class OAuth2SessionTest(TestCase): self.assertIn(self.client_id, auth_url) self.assertIn("response_type=token", auth_url) + def test_pkce_authorization_url(self): + url = "https://example.com/authorize?foo=bar" + + web = WebApplicationClient(self.client_id) + s = OAuth2Session(client=web, pkce="S256") + auth_url, state = s.authorization_url(url) + self.assertIn(state, auth_url) + self.assertIn(self.client_id, auth_url) + self.assertIn("response_type=code", auth_url) + self.assertIn("code_challenge=", auth_url) + self.assertIn("code_challenge_method=S256", auth_url) + + mobile = MobileApplicationClient(self.client_id) + s = OAuth2Session(client=mobile, pkce="S256") + auth_url, state = s.authorization_url(url) + self.assertIn(state, auth_url) + self.assertIn(self.client_id, auth_url) + self.assertIn("response_type=token", auth_url) + self.assertIn("code_challenge=", auth_url) + self.assertIn("code_challenge_method=S256", auth_url) + @mock.patch("time.time", new=lambda: fake_time) def test_refresh_token_request(self): self.expired_token = dict(self.token) @@ -424,6 +441,16 @@ class OAuth2SessionTest(TestCase): authorization_response="https://i.b/no-state?code=abc", ) + @mock.patch("time.time", new=lambda: fake_time) + def test_pkce_web_app_fetch_token(self): + url = "https://example.com/token" + + web = WebApplicationClient(self.client_id, code=CODE) + sess = OAuth2Session(client=web, token=self.token, pkce="S256") + sess.send = fake_token(self.token) + sess._code_verifier = "foobar" + self.assertEqual(sess.fetch_token(url), self.token) + def test_client_id_proxy(self): sess = OAuth2Session("test-id") self.assertEqual(sess.client_id, "test-id") diff --git a/contrib/python/requests-oauthlib/ya.make b/contrib/python/requests-oauthlib/ya.make index 2145e60cc4..d1a60dc100 100644 --- a/contrib/python/requests-oauthlib/ya.make +++ b/contrib/python/requests-oauthlib/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(1.3.1) +VERSION(1.4.0) LICENSE(ISC) |