diff options
author | alexv-smirnov <alex@ydb.tech> | 2023-12-01 12:02:50 +0300 |
---|---|---|
committer | alexv-smirnov <alex@ydb.tech> | 2023-12-01 13:28:10 +0300 |
commit | 0e578a4c44d4abd539d9838347b9ebafaca41dfb (patch) | |
tree | a0c1969c37f818c830ebeff9c077eacf30be6ef8 /contrib/python/requests-oauthlib | |
parent | 84f2d3d4cc985e63217cff149bd2e6d67ae6fe22 (diff) | |
download | ydb-0e578a4c44d4abd539d9838347b9ebafaca41dfb.tar.gz |
Change "ya.make"
Diffstat (limited to 'contrib/python/requests-oauthlib')
29 files changed, 3202 insertions, 0 deletions
diff --git a/contrib/python/requests-oauthlib/.dist-info/METADATA b/contrib/python/requests-oauthlib/.dist-info/METADATA new file mode 100644 index 0000000000..975ce567fc --- /dev/null +++ b/contrib/python/requests-oauthlib/.dist-info/METADATA @@ -0,0 +1,245 @@ +Metadata-Version: 2.1 +Name: requests-oauthlib +Version: 1.3.1 +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 +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +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 :: 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) +Provides-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>`_. + +The OAuth 1 workflow +-------------------- + +OAuth 1 can seem overly complicated and it sure has its quirks. Luckily, +requests_oauthlib hides most of these and let you focus at the task at hand. + +Accessing protected resources using requests_oauthlib is as simple as: + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth1Session + >>> twitter = OAuth1Session('client_key', + client_secret='client_secret', + resource_owner_key='resource_owner_key', + resource_owner_secret='resource_owner_secret') + >>> url = 'https://api.twitter.com/1/account/settings.json' + >>> r = twitter.get(url) + +Before accessing resources you will need to obtain a few credentials from your +provider (e.g. Twitter) and authorization from the user for whom you wish to +retrieve resources for. You can read all about this in the full +`OAuth 1 workflow guide on RTD <https://requests-oauthlib.readthedocs.io/en/latest/oauth1_workflow.html>`_. + +The OAuth 2 workflow +-------------------- + +OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most +common being the Authorization Code Grant, also known as the WebApplication +flow. + +Fetching a protected resource after obtaining an access token can be extremely +simple. However, before accessing resources you will need to obtain a few +credentials from your provider (e.g. Google) and authorization from the user +for whom you wish to retrieve resources for. You can read all about this in the +full `OAuth 2 workflow guide on RTD <https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html>`_. + +Installation +------------- + +To install requests and requests_oauthlib you can use pip: + +.. code-block:: bash + + $ 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 +.. |coverage-status| image:: https://img.shields.io/coveralls/requests/requests-oauthlib.svg + :target: https://coveralls.io/r/requests/requests-oauthlib +.. |docs| image:: https://readthedocs.org/projects/requests-oauthlib/badge/ + :alt: Documentation Status + :scale: 100% + :target: https://requests-oauthlib.readthedocs.io/ + + +History +------- + +v1.3.1 (21 January 2022) +++++++++++++++++++++++++ + +- Add initial support for OAuth Mutual TLS (draft-ietf-oauth-mtls) +- Add eBay compliance fix +- Add Spotify OAuth 2 Tutorial +- Add support for python 3.8, 3.9 +- Fixed LinkedIn Compliance Fixes +- Fixed ReadTheDocs Documentation and sphinx errors +- Moved pipeline to GitHub Actions + +v1.3.0 (6 November 2019) +++++++++++++++++++++++++ + +- Instagram compliance fix +- Added ``force_querystring`` argument to fetch_token() method on OAuth2Session + +v1.2.0 (14 January 2019) +++++++++++++++++++++++++ + +- This project now depends on OAuthlib 3.0.0 and above. It does **not** support + versions of OAuthlib before 3.0.0. +- Updated oauth2 tests to use 'sess' for an OAuth2Session instance instead of `auth` + because OAuth2Session objects and methods acceept an `auth` paramether which is + typically an instance of `requests.auth.HTTPBasicAuth` +- `OAuth2Session.fetch_token` previously tried to guess how and where to provide + "client" and "user" credentials incorrectly. This was incompatible with some + OAuth servers and incompatible with breaking changes in oauthlib that seek to + correctly provide the `client_id`. The older implementation also did not raise + the correct exceptions when username and password are not present on Legacy + clients. +- Avoid automatic netrc authentication for OAuth2Session. + +v1.1.0 (9 January 2019) ++++++++++++++++++++++++ + +- Adjusted version specifier for ``oauthlib`` dependency: this project is + not yet compatible with ``oauthlib`` 3.0.0. +- Dropped dependency on ``nose``. +- Minor changes to clean up the code and make it more readable/maintainable. + +v1.0.0 (4 June 2018) +++++++++++++++++++++ + +- **Removed support for Python 2.6 and Python 3.3.** + This project now supports Python 2.7, and Python 3.4 and above. +- Added several examples to the documentation. +- Added plentymarkets compliance fix. +- Added a ``token`` property to OAuth1Session, to match the corresponding + ``token`` property on OAuth2Session. + +v0.8.0 (14 February 2017) ++++++++++++++++++++++++++ + +- Added Fitbit compliance fix. +- Fixed an issue where newlines in the response body for the access token + request would cause errors when trying to extract the token. +- Fixed an issue introduced in v0.7.0 where users passing ``auth`` to several + methods would encounter conflicts with the ``client_id`` and + ``client_secret``-derived auth. The user-supplied ``auth`` argument is now + used in preference to those options. + +v0.7.0 (22 September 2016) +++++++++++++++++++++++++++ + +- Allowed ``OAuth2Session.request`` to take the ``client_id`` and + ``client_secret`` parameters for the purposes of automatic token refresh, + which may need them. + +v0.6.2 (12 July 2016) ++++++++++++++++++++++ + +- Use ``client_id`` and ``client_secret`` for the Authorization header if + provided. +- Allow explicit bypass of the Authorization header by setting ``auth=False``. +- Pass through the ``proxies`` kwarg when refreshing tokens. +- Miscellaneous cleanups. + +v0.6.1 (19 February 2016) ++++++++++++++++++++++++++ + +- Fixed a bug when sending authorization in headers with no username and + password present. +- Make sure we clear the session token before obtaining a new one. +- Some improvements to the Slack compliance fix. +- Avoid timing problems around token refresh. +- Allow passing arbitrary arguments to requests when calling + ``fetch_request_token`` and ``fetch_access_token``. + +v0.6.0 (14 December 2015) ++++++++++++++++++++++++++ + +- Add compliance fix for Slack. +- Add compliance fix for Mailchimp. +- ``TokenRequestDenied`` exceptions now carry the entire response, not just the + status code. +- Pass through keyword arguments when refreshing tokens automatically. +- Send authorization in headers, not just body, to maximize compatibility. +- More getters/setters available for OAuth2 session client values. +- Allow sending custom headers when refreshing tokens, and set some defaults. + + +v0.5.0 (4 May 2015) ++++++++++++++++++++ +- Fix ``TypeError`` being raised instead of ``TokenMissing`` error. +- Raise requests exceptions on 4XX and 5XX responses in the OAuth2 flow. +- Avoid ``AttributeError`` when initializing the ``OAuth2Session`` class + without complete client information. + +v0.4.2 (16 October 2014) +++++++++++++++++++++++++ +- New ``authorized`` property on OAuth1Session and OAuth2Session, which allows + you to easily determine if the session is already authorized with OAuth tokens + or not. +- New ``TokenMissing`` and ``VerifierMissing`` exception classes for OAuth1Session: + this will make it easier to catch and identify these exceptions. + +v0.4.1 (6 June 2014) +++++++++++++++++++++ +- New install target ``[rsa]`` for people using OAuth1 RSA-SHA1 signature + method. +- Fixed bug in OAuth2 where supplied state param was not used in auth url. +- OAuth2 HTTPS checking can be disabled by setting environment variable + ``OAUTHLIB_INSECURE_TRANSPORT``. +- OAuth1 now re-authorize upon redirects. +- OAuth1 token fetching now raise a detailed error message when the + response body is incorrectly encoded or the request was denied. +- Added support for custom OAuth1 clients. +- OAuth2 compliance fix for Sina Weibo. +- Multiple fixes to facebook compliance fix. +- Compliance fixes now re-encode body properly as bytes in Python 3. +- Logging now properly done under ``requests_oauthlib`` namespace instead + of piggybacking on oauthlib namespace. +- Logging introduced for OAuth1 auth and session. + +v0.4.0 (29 September 2013) +++++++++++++++++++++++++++ +- OAuth1Session methods only return unicode strings. #55. +- Renamed requests_oauthlib.core to requests_oauthlib.oauth1_auth for consistency. #79. +- Added Facebook compliance fix and access_token_response hook to OAuth2Session. #63. +- Added LinkedIn compliance fix. +- Added refresh_token_response compliance hook, invoked before parsing the refresh token. +- Correctly limit compliance hooks to running only once! +- Content type guessing should only be done when no content type is given +- 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/.dist-info/top_level.txt b/contrib/python/requests-oauthlib/.dist-info/top_level.txt new file mode 100644 index 0000000000..55d4f9073f --- /dev/null +++ b/contrib/python/requests-oauthlib/.dist-info/top_level.txt @@ -0,0 +1 @@ +requests_oauthlib diff --git a/contrib/python/requests-oauthlib/AUTHORS.rst b/contrib/python/requests-oauthlib/AUTHORS.rst new file mode 100644 index 0000000000..c8fba5e997 --- /dev/null +++ b/contrib/python/requests-oauthlib/AUTHORS.rst @@ -0,0 +1,25 @@ +Requests-oauthlib is written and maintained by Kenneth Reitz and various +contributors: + +Development Lead +---------------- + +- Kenneth Reitz <me@kennethreitz.com> + +Patches and Suggestions +----------------------- + +- Cory Benfield <cory@lukasa.co.uk> +- Ib Lundgren <ib.lundgren@gmail.com> +- Devin Sevilla <dasevilla@gmail.com> +- Imad Mouhtassem <mouhtasi@gmail.com> +- Johan Euphrosine <proppy@google.com> +- Johannes Spielmann <js@shezi.de> +- Martin Trigaux <me@mart-e.be> +- Matt McClure <matt.mcclure@mapmyfitness.com> +- Mikhail Sobolev <mss@mawhrin.net> +- Paul Bonser <misterpib@gmail.com> +- Vinay Raikar <rockraikar@gmail.com> +- kracekumar <me@kracekumar.com> +- David Baumgold <david@davidbaumgold.com> +- Craig Anderson <craiga@craiga.id.au> diff --git a/contrib/python/requests-oauthlib/LICENSE b/contrib/python/requests-oauthlib/LICENSE new file mode 100644 index 0000000000..de09f408ce --- /dev/null +++ b/contrib/python/requests-oauthlib/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2014 Kenneth Reitz. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/contrib/python/requests-oauthlib/README.rst b/contrib/python/requests-oauthlib/README.rst new file mode 100644 index 0000000000..9fd1bb9767 --- /dev/null +++ b/contrib/python/requests-oauthlib/README.rst @@ -0,0 +1,58 @@ +Requests-OAuthlib |build-status| |coverage-status| |docs| +========================================================= + +This project provides first-class OAuth library support for `Requests <http://python-requests.org>`_. + +The OAuth 1 workflow +-------------------- + +OAuth 1 can seem overly complicated and it sure has its quirks. Luckily, +requests_oauthlib hides most of these and let you focus at the task at hand. + +Accessing protected resources using requests_oauthlib is as simple as: + +.. code-block:: pycon + + >>> from requests_oauthlib import OAuth1Session + >>> twitter = OAuth1Session('client_key', + client_secret='client_secret', + resource_owner_key='resource_owner_key', + resource_owner_secret='resource_owner_secret') + >>> url = 'https://api.twitter.com/1/account/settings.json' + >>> r = twitter.get(url) + +Before accessing resources you will need to obtain a few credentials from your +provider (e.g. Twitter) and authorization from the user for whom you wish to +retrieve resources for. You can read all about this in the full +`OAuth 1 workflow guide on RTD <https://requests-oauthlib.readthedocs.io/en/latest/oauth1_workflow.html>`_. + +The OAuth 2 workflow +-------------------- + +OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most +common being the Authorization Code Grant, also known as the WebApplication +flow. + +Fetching a protected resource after obtaining an access token can be extremely +simple. However, before accessing resources you will need to obtain a few +credentials from your provider (e.g. Google) and authorization from the user +for whom you wish to retrieve resources for. You can read all about this in the +full `OAuth 2 workflow guide on RTD <https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html>`_. + +Installation +------------- + +To install requests and requests_oauthlib you can use pip: + +.. code-block:: bash + + $ 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 +.. |coverage-status| image:: https://img.shields.io/coveralls/requests/requests-oauthlib.svg + :target: https://coveralls.io/r/requests/requests-oauthlib +.. |docs| image:: https://readthedocs.org/projects/requests-oauthlib/badge/ + :alt: Documentation Status + :scale: 100% + :target: https://requests-oauthlib.readthedocs.io/ diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/__init__.py b/contrib/python/requests-oauthlib/requests_oauthlib/__init__.py new file mode 100644 index 0000000000..0d3e49f991 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/__init__.py @@ -0,0 +1,19 @@ +import logging + +from .oauth1_auth import OAuth1 +from .oauth1_session import OAuth1Session +from .oauth2_auth import OAuth2 +from .oauth2_session import OAuth2Session, TokenUpdated + +__version__ = "1.3.1" + +import requests + +if requests.__version__ < "2.0.0": + msg = ( + "You are using requests version %s, which is older than " + "requests-oauthlib expects, please upgrade to 2.0.0 or later." + ) + raise Warning(msg % requests.__version__) + +logging.getLogger("requests_oauthlib").addHandler(logging.NullHandler()) diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py new file mode 100644 index 0000000000..0e8e3ac84f --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/__init__.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import + +from .facebook import facebook_compliance_fix +from .fitbit import fitbit_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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/douban.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/douban.py new file mode 100644 index 0000000000..ecc57b0818 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/douban.py @@ -0,0 +1,17 @@ +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") + return r + + session._client_default_token_placement = "query" + session.register_compliance_hook("access_token_response", fix_token_type) + + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py new file mode 100644 index 0000000000..4aa423b3fe --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/ebay.py @@ -0,0 +1,23 @@ +import json +from oauthlib.common import to_unicode + + +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 = to_unicode(fixed_token).encode("utf-8") + + 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/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/facebook.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/facebook.py new file mode 100644 index 0000000000..90e7921272 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/facebook.py @@ -0,0 +1,33 @@ +from json import dumps + +try: + from urlparse import parse_qsl +except ImportError: + from urllib.parse import parse_qsl + +from oauthlib.common import to_unicode + + +def facebook_compliance_fix(session): + def _compliance_fix(r): + # if Facebook claims to be sending us json, let's trust them. + if "application/json" in r.headers.get("content-type", {}): + return r + + # Facebook returns a content-type of text/plain when sending their + # x-www-form-urlencoded responses, along with a 200. If not, let's + # assume we're getting JSON and bail on the fix. + if "text/plain" in r.headers.get("content-type", {}) and r.status_code == 200: + token = dict(parse_qsl(r.text, keep_blank_values=True)) + else: + return r + + expires = token.get("expires") + if expires is not None: + token["expires_in"] = expires + token["token_type"] = "Bearer" + r._content = to_unicode(dumps(token)).encode("UTF-8") + return r + + session.register_compliance_hook("access_token_response", _compliance_fix) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/fitbit.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/fitbit.py new file mode 100644 index 0000000000..7e62702401 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/fitbit.py @@ -0,0 +1,25 @@ +""" +The Fitbit API breaks from the OAuth2 RFC standard by returning an "errors" +object list, rather than a single "error" string. This puts hooks in place so +that oauthlib can process an error in the results from access token and refresh +token responses. This is necessary to prevent getting the generic red herring +MissingTokenError. +""" + +from json import loads, dumps + +from oauthlib.common import to_unicode + + +def fitbit_compliance_fix(session): + def _missing_error(r): + token = loads(r.text) + 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") + return r + + session.register_compliance_hook("access_token_response", _missing_error) + session.register_compliance_hook("refresh_token_response", _missing_error) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/instagram.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/instagram.py new file mode 100644 index 0000000000..4e07fe08b5 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/instagram.py @@ -0,0 +1,26 @@ +try: + from urlparse import urlparse, parse_qs +except ImportError: + from urllib.parse import urlparse, parse_qs + +from oauthlib.common import add_params_to_uri + + +def instagram_compliance_fix(session): + def _non_compliant_param_name(url, headers, data): + # If the user has already specified the token in the URL + # then there's nothing to do. + # If the specified token is different from ``session.access_token``, + # we assume the user intends to override the access token. + url_query = dict(parse_qs(urlparse(url).query)) + token = url_query.get("access_token") + if token: + # Nothing to do, just return. + return url, headers, data + + token = [("access_token", session.access_token)] + url = add_params_to_uri(url, token) + return url, headers, data + + session.register_compliance_hook("protected_request", _non_compliant_param_name) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/mailchimp.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/mailchimp.py new file mode 100644 index 0000000000..c69ce9fdae --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/mailchimp.py @@ -0,0 +1,23 @@ +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") + 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") + return r + + session.register_compliance_hook("access_token_response", _null_scope) + session.register_compliance_hook("access_token_response", _non_zero_expiration) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/plentymarkets.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/plentymarkets.py new file mode 100644 index 0000000000..9f605f058c --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/plentymarkets.py @@ -0,0 +1,29 @@ +from json import dumps, loads +import re + +from oauthlib.common import to_unicode + + +def plentymarkets_compliance_fix(session): + def _to_snake_case(n): + return re.sub("(.)([A-Z][a-z]+)", r"\1_\2", n).lower() + + def _compliance_fix(r): + # Plenty returns the Token in CamelCase instead of _ + if ( + "application/json" in r.headers.get("content-type", {}) + and r.status_code == 200 + ): + token = loads(r.text) + else: + return r + + fixed_token = {} + for k, v in token.items(): + fixed_token[_to_snake_case(k)] = v + + r._content = to_unicode(dumps(fixed_token)).encode("UTF-8") + return r + + session.register_compliance_hook("access_token_response", _compliance_fix) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/slack.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/slack.py new file mode 100644 index 0000000000..3f574b03ad --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/slack.py @@ -0,0 +1,37 @@ +try: + from urlparse import urlparse, parse_qs +except ImportError: + from urllib.parse import urlparse, parse_qs + +from oauthlib.common import add_params_to_uri + + +def slack_compliance_fix(session): + def _non_compliant_param_name(url, headers, data): + # If the user has already specified the token, either in the URL + # or in a data dictionary, then there's nothing to do. + # If the specified token is different from ``session.access_token``, + # we assume the user intends to override the access token. + url_query = dict(parse_qs(urlparse(url).query)) + token = url_query.get("token") + if not token and isinstance(data, dict): + token = data.get("token") + + if token: + # Nothing to do, just return. + return url, headers, data + + if not data: + data = {"token": session.access_token} + elif isinstance(data, dict): + data["token"] = session.access_token + else: + # ``data`` is something other than a dict: maybe a stream, + # maybe a file object, maybe something else. We can't easily + # modify it, so we'll set the token by modifying the URL instead. + token = [("token", session.access_token)] + url = add_params_to_uri(url, token) + return url, headers, data + + session.register_compliance_hook("protected_request", _non_compliant_param_name) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/weibo.py b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/weibo.py new file mode 100644 index 0000000000..6733abeb15 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/compliance_fixes/weibo.py @@ -0,0 +1,15 @@ +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") + return r + + session._client.default_token_placement = "query" + session.register_compliance_hook("access_token_response", _missing_token_type) + return session diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_auth.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_auth.py new file mode 100644 index 0000000000..cfbbd5902c --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_auth.py @@ -0,0 +1,117 @@ +# -*- 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__) + +# OBS!: Correct signing of requests are conditional on invoking OAuth1 +# as the last step of preparing a request, or at least having the +# content-type set properly. +class OAuth1(AuthBase): + """Signs the request using OAuth 1 (RFC5849)""" + + client_class = Client + + def __init__( + self, + client_key, + client_secret=None, + resource_owner_key=None, + resource_owner_secret=None, + callback_uri=None, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_AUTH_HEADER, + rsa_key=None, + verifier=None, + decoding="utf-8", + client_class=None, + force_include_body=False, + **kwargs + ): + + try: + signature_type = signature_type.upper() + except AttributeError: + pass + + client_class = client_class or self.client_class + + self.force_include_body = force_include_body + + self.client = client_class( + client_key, + client_secret, + resource_owner_key, + resource_owner_secret, + callback_uri, + signature_method, + signature_type, + rsa_key, + verifier, + decoding=decoding, + **kwargs + ) + + def __call__(self, r): + """Add OAuth parameters to the request. + + Parameters may be included from the body if the content-type is + urlencoded, if no content type is set a guess is made. + """ + # Overwriting url is safe here as request will not modify it past + # this point. + log.debug("Signing request %s using client %s", r, self.client) + + content_type = r.headers.get("Content-Type", "") + if ( + not content_type + and extract_params(r.body) + or self.client.signature_type == SIGNATURE_TYPE_BODY + ): + content_type = CONTENT_TYPE_FORM_URLENCODED + if not isinstance(content_type, unicode): + content_type = content_type.decode("utf-8") + + is_form_encoded = CONTENT_TYPE_FORM_URLENCODED in content_type + + log.debug( + "Including body in call to sign: %s", + is_form_encoded or self.force_include_body, + ) + + 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 + ) + 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 + ) + 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 + ) + + r.prepare_headers(headers) + r.url = to_native_string(r.url) + log.debug("Updated url: %s", r.url) + log.debug("Updated headers: %s", headers) + log.debug("Updated body: %r", r.body) + return r diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_session.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_session.py new file mode 100644 index 0000000000..88f2853ca0 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/oauth1_session.py @@ -0,0 +1,400 @@ +from __future__ import unicode_literals + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse + +import logging + +from oauthlib.common import add_params_to_uri +from oauthlib.common import urldecode as _urldecode +from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER +import requests + +from . import OAuth1 + + +log = logging.getLogger(__name__) + + +def urldecode(body): + """Parse query or json to python dictionary""" + try: + return _urldecode(body) + except Exception: + import json + + return json.loads(body) + + +class TokenRequestDenied(ValueError): + def __init__(self, message, response): + super(TokenRequestDenied, self).__init__(message) + self.response = response + + @property + def status_code(self): + """For backwards-compatibility purposes""" + return self.response.status_code + + +class TokenMissing(ValueError): + def __init__(self, message, response): + super(TokenMissing, self).__init__(message) + self.response = response + + +class VerifierMissing(ValueError): + pass + + +class OAuth1Session(requests.Session): + """Request signing and convenience methods for the oauth dance. + + What is the difference between OAuth1Session and OAuth1? + + OAuth1Session actually uses OAuth1 internally and its purpose is to assist + in the OAuth workflow through convenience methods to prepare authorization + URLs and parse the various token and redirection responses. It also provide + rudimentary validation of responses. + + An example of the OAuth workflow using a basic CLI app and Twitter. + + >>> # Credentials obtained during the registration. + >>> client_key = 'client key' + >>> client_secret = 'secret' + >>> callback_uri = 'https://127.0.0.1/callback' + >>> + >>> # Endpoints found in the OAuth provider API documentation + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> authorization_url = 'https://api.twitter.com/oauth/authorize' + >>> access_token_url = 'https://api.twitter.com/oauth/access_token' + >>> + >>> oauth_session = OAuth1Session(client_key,client_secret=client_secret, callback_uri=callback_uri) + >>> + >>> # First step, fetch the request token. + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'kjerht2309u', + 'oauth_token_secret': 'lsdajfh923874', + } + >>> + >>> # Second step. Follow this link and authorize + >>> oauth_session.authorization_url(authorization_url) + '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.') + >>> oauth_session.parse_authorization_response(redirect_response) + { + 'oauth_token: 'kjerht2309u', + 'oauth_token_secret: 'lsdajfh923874', + 'oauth_verifier: 'w34o8967345', + } + >>> oauth_session.fetch_access_token(access_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + >>> # Done. You can now make OAuth requests. + >>> status_url = 'http://api.twitter.com/1/statuses/update.json' + >>> new_status = {'status': 'hello world!'} + >>> oauth_session.post(status_url, data=new_status) + <Response [200]> + """ + + def __init__( + self, + client_key, + client_secret=None, + resource_owner_key=None, + resource_owner_secret=None, + callback_uri=None, + signature_method=SIGNATURE_HMAC, + signature_type=SIGNATURE_TYPE_AUTH_HEADER, + rsa_key=None, + verifier=None, + client_class=None, + force_include_body=False, + **kwargs + ): + """Construct the OAuth 1 session. + + :param client_key: A client specific identifier. + :param client_secret: A client specific secret used to create HMAC and + plaintext signatures. + :param resource_owner_key: A resource owner key, also referred to as + request token or access token depending on + when in the workflow it is used. + :param resource_owner_secret: A resource owner secret obtained with + either a request or access token. Often + referred to as token secret. + :param callback_uri: The URL the user is redirect back to after + authorization. + :param signature_method: Signature methods determine how the OAuth + signature is created. The three options are + oauthlib.oauth1.SIGNATURE_HMAC (default), + oauthlib.oauth1.SIGNATURE_RSA and + oauthlib.oauth1.SIGNATURE_PLAIN. + :param signature_type: Signature type decides where the OAuth + parameters are added. Either in the + Authorization header (default) or to the URL + query parameters or the request body. Defined as + oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER, + oauthlib.oauth1.SIGNATURE_TYPE_QUERY and + oauthlib.oauth1.SIGNATURE_TYPE_BODY + respectively. + :param rsa_key: The private RSA key as a string. Can only be used with + signature_method=oauthlib.oauth1.SIGNATURE_RSA. + :param verifier: A verifier string to prove authorization was granted. + :param client_class: A subclass of `oauthlib.oauth1.Client` to use with + `requests_oauthlib.OAuth1` instead of the default + :param force_include_body: Always include the request body in the + signature creation. + :param **kwargs: Additional keyword arguments passed to `OAuth1` + """ + super(OAuth1Session, self).__init__() + self._client = OAuth1( + client_key, + client_secret=client_secret, + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret, + callback_uri=callback_uri, + signature_method=signature_method, + signature_type=signature_type, + rsa_key=rsa_key, + verifier=verifier, + client_class=client_class, + force_include_body=force_include_body, + **kwargs + ) + self.auth = self._client + + @property + def token(self): + oauth_token = self._client.client.resource_owner_key + oauth_token_secret = self._client.client.resource_owner_secret + oauth_verifier = self._client.client.verifier + + token_dict = {} + if oauth_token: + token_dict["oauth_token"] = oauth_token + if oauth_token_secret: + token_dict["oauth_token_secret"] = oauth_token_secret + if oauth_verifier: + token_dict["oauth_verifier"] = oauth_verifier + + return token_dict + + @token.setter + def token(self, value): + self._populate_attributes(value) + + @property + def authorized(self): + """Boolean that indicates whether this session has an OAuth token + or not. If `self.authorized` is True, you can reasonably expect + OAuth-protected requests to the resource to succeed. If + `self.authorized` is False, you need the user to go through the OAuth + authentication dance before OAuth-protected requests to the resource + will succeed. + """ + if self._client.client.signature_method == SIGNATURE_RSA: + # RSA only uses resource_owner_key + return bool(self._client.client.resource_owner_key) + else: + # other methods of authentication use all three pieces + return ( + bool(self._client.client.client_secret) + and bool(self._client.client.resource_owner_key) + and bool(self._client.client.resource_owner_secret) + ) + + def authorization_url(self, url, request_token=None, **kwargs): + """Create an authorization URL by appending request_token and optional + kwargs to url. + + This is the second step in the OAuth 1 workflow. The user should be + redirected to this authorization URL, grant access to you, and then + be redirected back to you. The redirection back can either be specified + during client registration or by supplying a callback URI per request. + + :param url: The authorization endpoint URL. + :param request_token: The previously obtained request token. + :param kwargs: Optional parameters to append to the URL. + :returns: The authorization URL with new parameters embedded. + + An example using a registered default callback URI. + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> authorization_url = 'https://api.twitter.com/oauth/authorize' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + >>> oauth_session.authorization_url(authorization_url) + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf' + >>> oauth_session.authorization_url(authorization_url, foo='bar') + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&foo=bar' + + An example using an explicit callback URI. + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> authorization_url = 'https://api.twitter.com/oauth/authorize' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret', callback_uri='https://127.0.0.1/callback') + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + >>> oauth_session.authorization_url(authorization_url) + 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback' + """ + kwargs["oauth_token"] = request_token or self._client.client.resource_owner_key + log.debug("Adding parameters %s to url %s", kwargs, url) + return add_params_to_uri(url, kwargs.items()) + + def fetch_request_token(self, url, realm=None, **request_kwargs): + r"""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 + parsed from the application/x-www-form-urlencoded response and ready + to be used to construct an authorization url. + + :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'' + :returns: The response in dict format. + + Note that a previously set callback_uri will be reset for your + convenience, or else signature creation will be incorrect on + consecutive requests. + + >>> request_token_url = 'https://api.twitter.com/oauth/request_token' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.fetch_request_token(request_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + """ + self._client.client.realm = " ".join(realm) if realm else None + token = self._fetch_token(url, **request_kwargs) + log.debug("Resetting callback_uri and realm (not needed in next phase).") + self._client.client.callback_uri = None + self._client.client.realm = None + return token + + def fetch_access_token(self, url, verifier=None, **request_kwargs): + """Fetch an access token. + + This is the final step in the OAuth 1 workflow. An access token is + obtained using all previously obtained credentials, including the + verifier from the authorization step. + + Note that a previously set verifier will be reset for your + convenience, or else signature creation will be incorrect on + consecutive requests. + + >>> access_token_url = 'https://api.twitter.com/oauth/access_token' + >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.parse_authorization_response(redirect_response) + { + 'oauth_token: 'kjerht2309u', + 'oauth_token_secret: 'lsdajfh923874', + 'oauth_verifier: 'w34o8967345', + } + >>> oauth_session.fetch_access_token(access_token_url) + { + 'oauth_token': 'sdf0o9823sjdfsdf', + 'oauth_token_secret': '2kjshdfp92i34asdasd', + } + """ + if verifier: + self._client.client.verifier = verifier + if not getattr(self._client.client, "verifier", None): + raise VerifierMissing("No client verifier has been set.") + token = self._fetch_token(url, **request_kwargs) + log.debug("Resetting verifier attribute, should not be used anymore.") + self._client.client.verifier = None + return token + + def parse_authorization_response(self, url): + """Extract parameters from the post authorization redirect response URL. + + :param url: The full URL that resulted from the user being redirected + back from the OAuth provider to you, the client. + :returns: A dict of parameters extracted from the URL. + + >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345' + >>> oauth_session = OAuth1Session('client-key', client_secret='secret') + >>> oauth_session.parse_authorization_response(redirect_response) + { + 'oauth_token: 'kjerht2309u', + 'oauth_token_secret: 'lsdajfh923874', + 'oauth_verifier: 'w34o8967345', + } + """ + log.debug("Parsing token from query part of url %s", url) + token = dict(urldecode(urlparse(url).query)) + log.debug("Updating internal client token attribute.") + self._populate_attributes(token) + self.token = token + return token + + def _populate_attributes(self, token): + if "oauth_token" in token: + self._client.client.resource_owner_key = token["oauth_token"] + else: + raise TokenMissing( + "Response does not contain a token: {resp}".format(resp=token), token + ) + if "oauth_token_secret" in token: + self._client.client.resource_owner_secret = token["oauth_token_secret"] + if "oauth_verifier" in token: + self._client.client.verifier = token["oauth_verifier"] + + def _fetch_token(self, url, **request_kwargs): + log.debug("Fetching token from %s using client %s", url, self._client.client) + r = self.post(url, **request_kwargs) + + if r.status_code >= 400: + error = "Token request failed with code %s, response was '%s'." + raise TokenRequestDenied(error % (r.status_code, r.text), r) + + log.debug('Decoding token from response "%s"', r.text) + try: + token = dict(urldecode(r.text.strip())) + except ValueError as e: + error = ( + "Unable to decode token from token response. " + "This is commonly caused by an unsuccessful request where" + " a non urlencoded error message is returned. " + "The decoding error was %s" + "" % e + ) + raise ValueError(error) + + log.debug("Obtained token %s", token) + log.debug("Updating internal client attributes from token data.") + self._populate_attributes(token) + self.token = token + return token + + def rebuild_auth(self, prepared_request, response): + """ + When being redirected we should always strip Authorization + header, since nonce may not be reused as per OAuth spec. + """ + if "Authorization" in prepared_request.headers: + # If we get redirected to a new host, we should strip out + # any authentication headers. + prepared_request.headers.pop("Authorization", True) + prepared_request.prepare_auth(self.auth) + return diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_auth.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_auth.py new file mode 100644 index 0000000000..b880f72f58 --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_auth.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals +from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError +from oauthlib.oauth2 import is_secure_transport +from requests.auth import AuthBase + + +class OAuth2(AuthBase): + """Adds proof of authorization (OAuth2 token) to the request.""" + + def __init__(self, client_id=None, client=None, token=None): + """Construct a new OAuth 2 authorization object. + + :param client_id: Client id obtained during registration + :param client: :class:`oauthlib.oauth2.Client` to be used. Default is + WebApplicationClient which is useful for any + hosted application but not mobile or desktop. + :param token: Token dictionary, must include access_token + and token_type. + """ + self._client = client or WebApplicationClient(client_id, token=token) + if token: + for k, v in token.items(): + setattr(self._client, k, v) + + def __call__(self, r): + """Append an OAuth 2 token to the request. + + Note that currently HTTPS is required for all requests. There may be + a token type that allows for plain HTTP in the future and then this + should be updated to allow plain HTTP on a white list basis. + """ + if not is_secure_transport(r.url): + raise InsecureTransportError() + r.url, r.headers, r.body = self._client.add_token( + r.url, http_method=r.method, body=r.body, headers=r.headers + ) + return r diff --git a/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_session.py b/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_session.py new file mode 100644 index 0000000000..db4468089b --- /dev/null +++ b/contrib/python/requests-oauthlib/requests_oauthlib/oauth2_session.py @@ -0,0 +1,540 @@ +from __future__ import unicode_literals + +import logging + +from oauthlib.common import generate_token, urldecode +from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError +from oauthlib.oauth2 import LegacyApplicationClient +from oauthlib.oauth2 import TokenExpiredError, is_secure_transport +import requests + +log = logging.getLogger(__name__) + + +class TokenUpdated(Warning): + def __init__(self, token): + super(TokenUpdated, self).__init__() + self.token = token + + +class OAuth2Session(requests.Session): + """Versatile OAuth 2 extension to :class:`requests.Session`. + + Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec + including the four core OAuth 2 grants. + + Can be used to create authorization urls, fetch tokens and access protected + resources using the :class:`requests.Session` interface you are used to. + + - :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant + - :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant + - :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant + - :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant + + Note that the only time you will be using Implicit Grant from python is if + you are driving a user agent able to obtain URL fragments. + """ + + def __init__( + self, + client_id=None, + client=None, + auto_refresh_url=None, + auto_refresh_kwargs=None, + scope=None, + redirect_uri=None, + token=None, + state=None, + token_updater=None, + **kwargs + ): + """Construct a new OAuth 2 client session. + + :param client_id: Client id obtained during registration + :param client: :class:`oauthlib.oauth2.Client` to be used. Default is + WebApplicationClient which is useful for any + hosted application but not mobile or desktop. + :param scope: List of scopes you wish to request access to + :param redirect_uri: Redirect URI you registered as callback + :param token: Token dictionary, must include access_token + and token_type. + :param state: State string used to prevent CSRF. This will be given + when creating the authorization url and must be supplied + when parsing the authorization response. + Can be either a string or a no argument callable. + :auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply + this if you wish the client to automatically refresh + your access tokens. + :auto_refresh_kwargs: Extra arguments to pass to the refresh token + endpoint. + :token_updater: Method with one argument, token, to be used to update + your token database on automatic token refresh. If not + set a TokenUpdated warning will be raised when a token + has been refreshed. This warning will carry the token + in its token argument. + :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.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 + + # Ensure that requests doesn't do any automatic auth. See #278. + # The default behavior can be re-enabled by setting auth to None. + self.auth = lambda r: r + + # Allow customizations for non compliant providers through various + # hooks to adjust requests and responses. + self.compliance_hook = { + "access_token_response": set(), + "refresh_token_response": set(), + "protected_request": set(), + } + + def new_state(self): + """Generates a state string to be used in authorizations.""" + try: + self._state = self.state() + log.debug("Generated new state %s.", self._state) + except TypeError: + self._state = self.state + log.debug("Re-using previously supplied state %s.", self._state) + return self._state + + @property + def client_id(self): + return getattr(self._client, "client_id", None) + + @client_id.setter + def client_id(self, value): + self._client.client_id = value + + @client_id.deleter + def client_id(self): + del self._client.client_id + + @property + def token(self): + return getattr(self._client, "token", None) + + @token.setter + def token(self, value): + self._client.token = value + self._client.populate_token_attributes(value) + + @property + def access_token(self): + return getattr(self._client, "access_token", None) + + @access_token.setter + def access_token(self, value): + self._client.access_token = value + + @access_token.deleter + def access_token(self): + del self._client.access_token + + @property + def authorized(self): + """Boolean that indicates whether this session has an OAuth token + or not. If `self.authorized` is True, you can reasonably expect + OAuth-protected requests to the resource to succeed. If + `self.authorized` is False, you need the user to go through the OAuth + authentication dance before OAuth-protected requests to the resource + will succeed. + """ + return bool(self.access_token) + + def authorization_url(self, url, state=None, **kwargs): + """Form an authorization URL. + + :param url: Authorization endpoint url, must be HTTPS. + :param state: An optional state string for CSRF protection. If not + given it will be generated for you. + :param kwargs: Extra parameters to include. + :return: authorization_url, state + """ + state = state or self.new_state() + return ( + self._client.prepare_request_uri( + url, + redirect_uri=self.redirect_uri, + scope=self.scope, + state=state, + **kwargs + ), + state, + ) + + def fetch_token( + self, + token_url, + code=None, + authorization_response=None, + body="", + auth=None, + username=None, + password=None, + method="POST", + force_querystring=False, + timeout=None, + headers=None, + verify=True, + proxies=None, + include_client_id=None, + client_secret=None, + cert=None, + **kwargs + ): + """Generic method for fetching an access token from the token endpoint. + + If you are using the MobileApplicationClient you will want to use + `token_from_fragment` instead of `fetch_token`. + + The current implementation enforces the RFC guidelines. + + :param token_url: Token endpoint URL, must use HTTPS. + :param code: Authorization code (used by WebApplicationClients). + :param authorization_response: Authorization response URL, the callback + URL of the request back to you. Used by + WebApplicationClients instead of code. + :param body: Optional application/x-www-form-urlencoded body to add the + include in the token request. Prefer kwargs over body. + :param auth: An auth tuple or method as accepted by `requests`. + :param username: Username required by LegacyApplicationClients to appear + in the request body. + :param password: Password required by LegacyApplicationClients to appear + in the request body. + :param method: The HTTP method used to make the request. Defaults + to POST, but may also be GET. Other methods should + be added as needed. + :param force_querystring: If True, force the request body to be sent + in the querystring instead. + :param timeout: Timeout of the request in seconds. + :param headers: Dict to default request headers with. + :param verify: Verify SSL certificate. + :param proxies: The `proxies` argument is passed onto `requests`. + :param include_client_id: Should the request body include the + `client_id` parameter. Default is `None`, + which will attempt to autodetect. This can be + forced to always include (True) or never + include (False). + :param client_secret: The `client_secret` paired to the `client_id`. + This is generally required unless provided in the + `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 + """ + if not is_secure_transport(token_url): + raise InsecureTransportError() + + if not code and authorization_response: + self._client.parse_request_uri_response( + authorization_response, state=self._state + ) + code = self._client.code + elif not code and isinstance(self._client, WebApplicationClient): + code = self._client.code + if not code: + raise ValueError( + "Please supply either code or " "authorization_response parameters." + ) + + # 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. + # If an upstream server is not spec compliant and requires them to + # appear as an Authorization header, supply an explicit `auth` header + # to this function. + # This check will allow for empty strings, but not `None`. + # + # References + # 4.3.2 - Resource Owner Password Credentials Grant + # https://tools.ietf.org/html/rfc6749#section-4.3.2 + + if isinstance(self._client, LegacyApplicationClient): + if username is None: + raise ValueError( + "`LegacyApplicationClient` requires both the " + "`username` and `password` parameters." + ) + if password is None: + raise ValueError( + "The required parameter `username` was supplied, " + "but `password` was not." + ) + + # merge username and password into kwargs for `prepare_request_body` + if username is not None: + kwargs["username"] = username + if password is not None: + kwargs["password"] = password + + # is an auth explicitly supplied? + if auth is not None: + # if we're dealing with the default of `include_client_id` (None): + # we will assume the `auth` argument is for an RFC compliant server + # and we should not send the `client_id` in the body. + # This approach allows us to still force the client_id by submitting + # `include_client_id=True` along with an `auth` object. + if include_client_id is None: + include_client_id = False + + # otherwise we may need to create an auth header + else: + # since we don't have an auth header, we MAY need to create one + # it is possible that we want to send the `client_id` in the body + # if so, `include_client_id` should be set to True + # otherwise, we will generate an auth header + if include_client_id is not True: + client_id = self.client_id + if client_id: + log.debug( + 'Encoding `client_id` "%s" with `client_secret` ' + "as Basic auth credentials.", + client_id, + ) + client_secret = client_secret if client_secret is not None else "" + auth = requests.auth.HTTPBasicAuth(client_id, client_secret) + + if include_client_id: + # this was pulled out of the params + # it needs to be passed into prepare_request_body + if client_secret is not None: + kwargs["client_secret"] = client_secret + + body = self._client.prepare_request_body( + code=code, + body=body, + redirect_uri=self.redirect_uri, + include_client_id=include_client_id, + **kwargs + ) + + headers = headers or { + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + } + self.token = {} + request_kwargs = {} + if method.upper() == "POST": + request_kwargs["params" if force_querystring else "data"] = dict( + urldecode(body) + ) + elif method.upper() == "GET": + request_kwargs["params"] = dict(urldecode(body)) + else: + raise ValueError("The method kwarg must be POST or GET.") + + r = self.request( + method=method, + url=token_url, + timeout=timeout, + headers=headers, + auth=auth, + verify=verify, + proxies=proxies, + cert=cert, + **request_kwargs + ) + + log.debug("Request to fetch token completed with status %s.", r.status_code) + log.debug("Request url was %s", r.request.url) + log.debug("Request headers were %s", r.request.headers) + log.debug("Request body was %s", r.request.body) + log.debug("Response headers were %s and content %s.", r.headers, r.text) + log.debug( + "Invoking %d token response hooks.", + len(self.compliance_hook["access_token_response"]), + ) + for hook in self.compliance_hook["access_token_response"]: + log.debug("Invoking hook %s.", hook) + r = hook(r) + + self._client.parse_request_body_response(r.text, scope=self.scope) + self.token = self._client.token + log.debug("Obtained token %s.", self.token) + return self.token + + def token_from_fragment(self, authorization_response): + """Parse token from the URI fragment, used by MobileApplicationClients. + + :param authorization_response: The full URL of the redirect back to you + :return: A token dict + """ + self._client.parse_request_uri_response( + authorization_response, state=self._state + ) + self.token = self._client.token + return self.token + + def refresh_token( + self, + token_url, + refresh_token=None, + body="", + auth=None, + timeout=None, + headers=None, + verify=True, + proxies=None, + **kwargs + ): + """Fetch a new access token using a refresh token. + + :param token_url: The token endpoint, must be HTTPS. + :param refresh_token: The refresh_token to use. + :param body: Optional application/x-www-form-urlencoded body to add the + include in the token request. Prefer kwargs over body. + :param auth: An auth tuple or method as accepted by `requests`. + :param timeout: Timeout of the request in seconds. + :param headers: A dict of headers to be used by `requests`. + :param verify: Verify SSL certificate. + :param proxies: The `proxies` argument will be passed to `requests`. + :param kwargs: Extra parameters to include in the token request. + :return: A token dict + """ + if not token_url: + raise ValueError("No token endpoint set for auto_refresh.") + + if not is_secure_transport(token_url): + raise InsecureTransportError() + + refresh_token = refresh_token or self.token.get("refresh_token") + + log.debug( + "Adding auto refresh key word arguments %s.", self.auto_refresh_kwargs + ) + kwargs.update(self.auto_refresh_kwargs) + body = self._client.prepare_refresh_body( + body=body, refresh_token=refresh_token, scope=self.scope, **kwargs + ) + log.debug("Prepared refresh token request body %s", body) + + if headers is None: + headers = { + "Accept": "application/json", + "Content-Type": ("application/x-www-form-urlencoded;charset=UTF-8"), + } + + r = self.post( + token_url, + data=dict(urldecode(body)), + auth=auth, + timeout=timeout, + headers=headers, + verify=verify, + withhold_token=True, + proxies=proxies, + ) + log.debug("Request to refresh token completed with status %s.", r.status_code) + log.debug("Response headers were %s and content %s.", r.headers, r.text) + log.debug( + "Invoking %d token response hooks.", + len(self.compliance_hook["refresh_token_response"]), + ) + for hook in self.compliance_hook["refresh_token_response"]: + log.debug("Invoking hook %s.", hook) + r = hook(r) + + self.token = self._client.parse_request_body_response(r.text, scope=self.scope) + if not "refresh_token" in self.token: + log.debug("No new refresh token given. Re-using old.") + self.token["refresh_token"] = refresh_token + return self.token + + def request( + self, + method, + url, + data=None, + headers=None, + withhold_token=False, + client_id=None, + client_secret=None, + **kwargs + ): + """Intercept all requests and add the OAuth 2 token if present.""" + if not is_secure_transport(url): + raise InsecureTransportError() + if self.token and not withhold_token: + log.debug( + "Invoking %d protected resource request hooks.", + len(self.compliance_hook["protected_request"]), + ) + for hook in self.compliance_hook["protected_request"]: + log.debug("Invoking hook %s.", hook) + url, headers, data = hook(url, headers, data) + + log.debug("Adding token %s to request.", self.token) + try: + url, headers, data = self._client.add_token( + url, http_method=method, body=data, headers=headers + ) + # Attempt to retrieve and save new access token if expired + except TokenExpiredError: + if self.auto_refresh_url: + log.debug( + "Auto refresh is set, attempting to refresh at %s.", + self.auto_refresh_url, + ) + + # We mustn't pass auth twice. + auth = kwargs.pop("auth", None) + if client_id and client_secret and (auth is None): + log.debug( + 'Encoding client_id "%s" with client_secret as Basic auth credentials.', + client_id, + ) + auth = requests.auth.HTTPBasicAuth(client_id, client_secret) + token = self.refresh_token( + self.auto_refresh_url, auth=auth, **kwargs + ) + if self.token_updater: + log.debug( + "Updating token to %s using %s.", token, self.token_updater + ) + self.token_updater(token) + url, headers, data = self._client.add_token( + url, http_method=method, body=data, headers=headers + ) + else: + raise TokenUpdated(token) + else: + raise + + log.debug("Requesting url %s using method %s.", url, method) + 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 + ) + + def register_compliance_hook(self, hook_type, hook): + """Register a hook for request/response tweaking. + + Available hooks are: + access_token_response invoked before token parsing. + refresh_token_response invoked before refresh token parsing. + protected_request invoked before making a request. + + If you find a new hook is needed please send a GitHub PR request + or open an issue. + """ + if hook_type not in self.compliance_hook: + raise ValueError( + "Hook type %s is not in %s.", hook_type, self.compliance_hook + ) + self.compliance_hook[hook_type].add(hook) diff --git a/contrib/python/requests-oauthlib/tests/__init__.py b/contrib/python/requests-oauthlib/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/__init__.py diff --git a/contrib/python/requests-oauthlib/tests/test.bin b/contrib/python/requests-oauthlib/tests/test.bin new file mode 100644 index 0000000000..b00d4f4796 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/test.bin @@ -0,0 +1 @@ +¥Æ
\ No newline at end of file diff --git a/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py b/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py new file mode 100644 index 0000000000..5c90d52660 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/test_compliance_fixes.py @@ -0,0 +1,334 @@ +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 oauthlib.oauth2.rfc6749.errors import InvalidGrantError +from requests_oauthlib import OAuth2Session +from requests_oauthlib.compliance_fixes import facebook_compliance_fix +from requests_oauthlib.compliance_fixes import fitbit_compliance_fix +from requests_oauthlib.compliance_fixes import mailchimp_compliance_fix +from requests_oauthlib.compliance_fixes import weibo_compliance_fix +from requests_oauthlib.compliance_fixes import slack_compliance_fix +from requests_oauthlib.compliance_fixes import instagram_compliance_fix +from requests_oauthlib.compliance_fixes import plentymarkets_compliance_fix +from requests_oauthlib.compliance_fixes import ebay_compliance_fix + + +class FacebookComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://graph.facebook.com/oauth/access_token", + text="access_token=urlencoded", + headers={"Content-Type": "text/plain"}, + ) + mocker.start() + self.addCleanup(mocker.stop) + + facebook = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = facebook_compliance_fix(facebook) + + def test_fetch_access_token(self): + token = self.session.fetch_token( + "https://graph.facebook.com/oauth/access_token", + client_secret="someclientsecret", + authorization_response="https://i.b/?code=hello", + ) + self.assertEqual(token, {"access_token": "urlencoded", "token_type": "Bearer"}) + + +class FitbitComplianceFixTest(TestCase): + def setUp(self): + self.mocker = requests_mock.Mocker() + self.mocker.post( + "https://api.fitbit.com/oauth2/token", + json={"errors": [{"errorType": "invalid_grant"}]}, + ) + self.mocker.start() + self.addCleanup(self.mocker.stop) + + fitbit = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = fitbit_compliance_fix(fitbit) + + def test_fetch_access_token(self): + self.assertRaises( + InvalidGrantError, + self.session.fetch_token, + "https://api.fitbit.com/oauth2/token", + client_secret="someclientsecret", + authorization_response="https://i.b/?code=hello", + ) + + self.mocker.post( + "https://api.fitbit.com/oauth2/token", json={"access_token": "fitbit"} + ) + + token = self.session.fetch_token( + "https://api.fitbit.com/oauth2/token", client_secret="good" + ) + + self.assertEqual(token, {"access_token": "fitbit"}) + + def test_refresh_token(self): + self.assertRaises( + InvalidGrantError, + self.session.refresh_token, + "https://api.fitbit.com/oauth2/token", + auth=requests.auth.HTTPBasicAuth("someclientid", "someclientsecret"), + ) + + self.mocker.post( + "https://api.fitbit.com/oauth2/token", + json={"access_token": "access", "refresh_token": "refresh"}, + ) + + token = self.session.refresh_token( + "https://api.fitbit.com/oauth2/token", + auth=requests.auth.HTTPBasicAuth("someclientid", "someclientsecret"), + ) + + self.assertEqual(token["access_token"], "access") + self.assertEqual(token["refresh_token"], "refresh") + + +class MailChimpComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://login.mailchimp.com/oauth2/token", + json={"access_token": "mailchimp", "expires_in": 0, "scope": None}, + ) + mocker.start() + self.addCleanup(mocker.stop) + + mailchimp = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = mailchimp_compliance_fix(mailchimp) + + def test_fetch_access_token(self): + token = self.session.fetch_token( + "https://login.mailchimp.com/oauth2/token", + client_secret="someclientsecret", + authorization_response="https://i.b/?code=hello", + ) + # Times should be close + approx_expires_at = time.time() + 3600 + actual_expires_at = token.pop("expires_at") + self.assertAlmostEqual(actual_expires_at, approx_expires_at, places=2) + + # Other token values exact + self.assertEqual(token, {"access_token": "mailchimp", "expires_in": 3600}) + + # And no scope at all + self.assertNotIn("scope", token) + + +class WeiboComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://api.weibo.com/oauth2/access_token", json={"access_token": "weibo"} + ) + mocker.start() + self.addCleanup(mocker.stop) + + weibo = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = weibo_compliance_fix(weibo) + + def test_fetch_access_token(self): + token = self.session.fetch_token( + "https://api.weibo.com/oauth2/access_token", + client_secret="someclientsecret", + authorization_response="https://i.b/?code=hello", + ) + self.assertEqual(token, {"access_token": "weibo", "token_type": "Bearer"}) + + +class SlackComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://slack.com/api/oauth.access", + json={"access_token": "xoxt-23984754863-2348975623103", "scope": "read"}, + ) + for method in ("GET", "POST"): + mocker.request( + method=method, + url="https://slack.com/api/auth.test", + json={ + "ok": True, + "url": "https://myteam.slack.com/", + "team": "My Team", + "user": "cal", + "team_id": "T12345", + "user_id": "U12345", + }, + ) + mocker.start() + self.addCleanup(mocker.stop) + + slack = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = slack_compliance_fix(slack) + + def test_protected_request(self): + self.session.token = {"access_token": "dummy-access-token"} + response = self.session.get("https://slack.com/api/auth.test") + url = response.request.url + query = parse_qs(urlparse(url).query) + self.assertNotIn("token", query) + body = response.request.body + data = parse_qs(body) + self.assertEqual(data["token"], ["dummy-access-token"]) + + def test_protected_request_override_token_get(self): + self.session.token = {"access_token": "dummy-access-token"} + response = self.session.get( + "https://slack.com/api/auth.test", data={"token": "different-token"} + ) + url = response.request.url + query = parse_qs(urlparse(url).query) + self.assertNotIn("token", query) + body = response.request.body + data = parse_qs(body) + self.assertEqual(data["token"], ["different-token"]) + + def test_protected_request_override_token_post(self): + self.session.token = {"access_token": "dummy-access-token"} + response = self.session.post( + "https://slack.com/api/auth.test", data={"token": "different-token"} + ) + url = response.request.url + query = parse_qs(urlparse(url).query) + self.assertNotIn("token", query) + body = response.request.body + data = parse_qs(body) + self.assertEqual(data["token"], ["different-token"]) + + def test_protected_request_override_token_url(self): + self.session.token = {"access_token": "dummy-access-token"} + response = self.session.get( + "https://slack.com/api/auth.test?token=different-token" + ) + url = response.request.url + query = parse_qs(urlparse(url).query) + self.assertEqual(query["token"], ["different-token"]) + self.assertIsNone(response.request.body) + + +class InstagramComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.request( + method="GET", + url="https://api.instagram.com/v1/users/self", + json={ + "data": { + "id": "1574083", + "username": "snoopdogg", + "full_name": "Snoop Dogg", + "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1574083_75sq_1295469061.jpg", + "bio": "This is my bio", + "website": "http://snoopdogg.com", + "is_business": False, + "counts": {"media": 1320, "follows": 420, "followed_by": 3410}, + } + }, + ) + mocker.start() + self.addCleanup(mocker.stop) + + instagram = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = instagram_compliance_fix(instagram) + + def test_protected_request(self): + self.session.token = {"access_token": "dummy-access-token"} + response = self.session.get("https://api.instagram.com/v1/users/self") + url = response.request.url + query = parse_qs(urlparse(url).query) + self.assertIn("access_token", query) + self.assertEqual(query["access_token"], ["dummy-access-token"]) + + def test_protected_request_dont_override(self): + """check that if the access_token param + already exist we don't override it""" + self.session.token = {"access_token": "dummy-access-token"} + response = self.session.get( + "https://api.instagram.com/v1/users/self?access_token=correct-access-token" + ) + url = response.request.url + query = parse_qs(urlparse(url).query) + self.assertIn("access_token", query) + self.assertEqual(query["access_token"], ["correct-access-token"]) + + +class PlentymarketsComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://shop.plentymarkets-cloud02.com", + json={ + "accessToken": "ecUN1r8KhJewMCdLAmpHOdZ4O0ofXKB9zf6CXK61", + "tokenType": "Bearer", + "expiresIn": 86400, + "refreshToken": "iG2kBGIjcXaRE4xmTVUnv7xwxX7XMcWCHqJmFaSX", + }, + headers={"Content-Type": "application/json"}, + ) + mocker.start() + self.addCleanup(mocker.stop) + + plentymarkets = OAuth2Session("someclientid", redirect_uri="https://i.b") + self.session = plentymarkets_compliance_fix(plentymarkets) + + def test_fetch_access_token(self): + token = self.session.fetch_token( + "https://shop.plentymarkets-cloud02.com", + authorization_response="https://i.b/?code=hello", + ) + + approx_expires_at = time.time() + 86400 + actual_expires_at = token.pop("expires_at") + self.assertAlmostEqual(actual_expires_at, approx_expires_at, places=2) + + self.assertEqual( + token, + { + "access_token": "ecUN1r8KhJewMCdLAmpHOdZ4O0ofXKB9zf6CXK61", + "expires_in": 86400, + "token_type": "Bearer", + "refresh_token": "iG2kBGIjcXaRE4xmTVUnv7xwxX7XMcWCHqJmFaSX", + }, + ) + + +class EbayComplianceFixTest(TestCase): + def setUp(self): + mocker = requests_mock.Mocker() + mocker.post( + "https://api.ebay.com/identity/v1/oauth2/token", + json={ + "access_token": "this is the access token", + "expires_in": 7200, + "token_type": "Application Access Token", + }, + headers={"Content-Type": "application/json"}, + ) + mocker.start() + self.addCleanup(mocker.stop) + + session = OAuth2Session() + self.fixed_session = ebay_compliance_fix(session) + + def test_fetch_access_token(self): + token = self.fixed_session.fetch_token( + "https://api.ebay.com/identity/v1/oauth2/token", + authorization_response="https://i.b/?code=hello", + ) + 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 new file mode 100644 index 0000000000..6892e9f1ce --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/test_core.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import requests +import requests_oauthlib +import oauthlib +import os.path +from io import StringIO +import unittest + +try: + import mock +except ImportError: + from unittest import mock + + +@mock.patch("oauthlib.oauth1.rfc5849.generate_timestamp") +@mock.patch("oauthlib.oauth1.rfc5849.generate_nonce") +class OAuth1Test(unittest.TestCase): + def testFormEncoded(self, generate_nonce, generate_timestamp): + """OAuth1 assumes form encoded if content type is not specified.""" + generate_nonce.return_value = "abc" + generate_timestamp.return_value = "1" + oauth = requests_oauthlib.OAuth1("client_key") + headers = {"Content-type": "application/x-www-form-urlencoded"} + r = requests.Request( + method="POST", + url="http://a.b/path?query=retain", + auth=oauth, + data="this=really&is=&+form=encoded", + headers=headers, + ) + a = r.prepare() + + self.assertEqual(a.url, "http://a.b/path?query=retain") + self.assertEqual(a.body, b"this=really&is=&+form=encoded") + self.assertEqual( + a.headers.get("Content-Type"), b"application/x-www-form-urlencoded" + ) + + # guess content-type + r = requests.Request( + method="POST", + url="http://a.b/path?query=retain", + auth=oauth, + data="this=really&is=&+form=encoded", + ) + b = r.prepare() + self.assertEqual(b.url, "http://a.b/path?query=retain") + self.assertEqual(b.body, b"this=really&is=&+form=encoded") + self.assertEqual( + b.headers.get("Content-Type"), b"application/x-www-form-urlencoded" + ) + + self.assertEqual(a.headers.get("Authorization"), b.headers.get("Authorization")) + + def testNonFormEncoded(self, generate_nonce, generate_timestamp): + """OAuth signature only depend on body if it is form encoded.""" + generate_nonce.return_value = "abc" + generate_timestamp.return_value = "1" + oauth = requests_oauthlib.OAuth1("client_key") + + r = requests.Request( + method="POST", + url="http://a.b/path?query=retain", + auth=oauth, + data="this really is not form encoded", + ) + a = r.prepare() + + r = requests.Request( + method="POST", url="http://a.b/path?query=retain", auth=oauth + ) + b = r.prepare() + + self.assertEqual(a.headers.get("Authorization"), b.headers.get("Authorization")) + + r = requests.Request( + method="POST", + url="http://a.b/path?query=retain", + auth=oauth, + files={"test": StringIO("hello")}, + ) + c = r.prepare() + + self.assertEqual(b.headers.get("Authorization"), c.headers.get("Authorization")) + + @unittest.skip("test uses real http://httpbin.org") + def testCanPostBinaryData(self, generate_nonce, generate_timestamp): + """ + Test we can post binary data. Should prevent regression of the + UnicodeDecodeError issue. + """ + generate_nonce.return_value = "abc" + generate_timestamp.return_value = "1" + oauth = requests_oauthlib.OAuth1("client_key") + import yatest.common + dirname = yatest.common.test_source_path() + fname = os.path.join(dirname, "test.bin") + + with open(fname, "rb") as f: + r = requests.post( + "http://httpbin.org/post", + data={"hi": "there"}, + files={"media": (os.path.basename(f.name), f)}, + headers={"content-type": "application/octet-stream"}, + auth=oauth, + ) + self.assertEqual(r.status_code, 200) + + @unittest.skip("test uses real http://httpbin.org") + def test_url_is_native_str(self, generate_nonce, generate_timestamp): + """ + Test that the URL is always a native string. + """ + generate_nonce.return_value = "abc" + generate_timestamp.return_value = "1" + oauth = requests_oauthlib.OAuth1("client_key") + + r = requests.get("http://httpbin.org/get", auth=oauth) + self.assertIsInstance(r.request.url, str) + + @unittest.skip("test uses real http://httpbin.org") + def test_content_type_override(self, generate_nonce, generate_timestamp): + """ + Content type should only be guessed if none is given. + """ + generate_nonce.return_value = "abc" + generate_timestamp.return_value = "1" + oauth = requests_oauthlib.OAuth1("client_key") + data = "a" + r = requests.post("http://httpbin.org/get", data=data, auth=oauth) + self.assertEqual( + r.request.headers.get("Content-Type"), b"application/x-www-form-urlencoded" + ) + r = requests.post( + "http://httpbin.org/get", + auth=oauth, + data=data, + headers={"Content-type": "application/json"}, + ) + self.assertEqual(r.request.headers.get("Content-Type"), b"application/json") + + def test_register_client_class(self, generate_timestamp, generate_nonce): + class ClientSubclass(oauthlib.oauth1.Client): + pass + + self.assertTrue(hasattr(requests_oauthlib.OAuth1, "client_class")) + + self.assertEqual(requests_oauthlib.OAuth1.client_class, oauthlib.oauth1.Client) + + normal = requests_oauthlib.OAuth1("client_key") + + self.assertIsInstance(normal.client, oauthlib.oauth1.Client) + self.assertNotIsInstance(normal.client, ClientSubclass) + + requests_oauthlib.OAuth1.client_class = ClientSubclass + + self.assertEqual(requests_oauthlib.OAuth1.client_class, ClientSubclass) + + custom = requests_oauthlib.OAuth1("client_key") + + self.assertIsInstance(custom.client, oauthlib.oauth1.Client) + self.assertIsInstance(custom.client, ClientSubclass) + + overridden = requests_oauthlib.OAuth1( + "client_key", client_class=oauthlib.oauth1.Client + ) + + self.assertIsInstance(overridden.client, oauthlib.oauth1.Client) + self.assertNotIsInstance(normal.client, ClientSubclass) diff --git a/contrib/python/requests-oauthlib/tests/test_oauth1_session.py b/contrib/python/requests-oauthlib/tests/test_oauth1_session.py new file mode 100644 index 0000000000..1dd2b2f158 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/test_oauth1_session.py @@ -0,0 +1,348 @@ +from __future__ import unicode_literals, print_function +import unittest +import sys +import requests +from io import StringIO + +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 + +try: + import jwt +except ImportError: + jwt = None + +if sys.version[0] == "3": + unicode_type = str +else: + unicode_type = unicode + + +TEST_RSA_KEY = ( + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEogIBAAKCAQEApF1JaMSN8TEsh4N4O/5SpEAVLivJyLH+Cgl3OQBPGgJkt8cg\n" + "49oasl+5iJS+VdrILxWM9/JCJyURpUuslX4Eb4eUBtQ0x5BaPa8+S2NLdGTaL7nB\n" + "OO8o8n0C5FEUU+qlEip79KE8aqOj+OC44VsIquSmOvWIQD26n3fCVlgwoRBD1gzz\n" + "sDOeaSyzpKrZR851Kh6rEmF2qjJ8jt6EkxMsRNACmBomzgA4M1TTsisSUO87444p\n" + "e35Z4/n5c735o2fZMrGgMwiJNh7rT8SYxtIkxngioiGnwkxGQxQ4NzPAHg+XSY0J\n" + "04pNm7KqTkgtxyrqOANJLIjXlR+U9SQ90NjHVQIDAQABAoIBABuBPOKaWcJt3yzC\n" + "NGGduoif7KtwSnEaUA+v69KPGa2Zju8uFHPssKD+4dZYRc2qMeunKJLpaGaSjnRh\n" + "yHyvvOBJCN1nr3lhz6gY5kzJTfwpUFXCOPJlGy4Q+2Xnp4YvcvYqQ9n5DVovDiZ8\n" + "vJOBn16xqpudMPLHIa7D5LJ8SY76HBjE+imTXw1EShdh5TOV9bmPFQqH6JFzowRH\n" + "hyH2DPHuyHJj6cl8FyqJw5lVWzG3n6Prvk7bYHsjmGjurN35UsumNAp6VouNyUP1\n" + "RAEcUJega49aIs6/FJ0ENJzQjlsAzVbTleHkpez2aIok+wsWJGJ4SVxAjADOWAaZ\n" + "uEJPc3UCgYEA1g4ZGrXOuo75p9/MRIepXGpBWxip4V7B9XmO9WzPCv8nMorJntWB\n" + "msYV1I01aITxadHatO4Gl2xLniNkDyrEQzJ7w38RQgsVK+CqbnC0K9N77QPbHeC1\n" + "YQd9RCNyUohOimKvb7jyv798FBU1GO5QI2eNgfnnfteSVXhD2iOoTOsCgYEAxJJ+\n" + "8toxJdnLa0uUsAbql6zeNXGbUBMzu3FomKlyuWuq841jS2kIalaO/TRj5hbnE45j\n" + "mCjeLgTVO6Ach3Wfk4zrqajqfFJ0zUg/Wexp49lC3RWiV4icBb85Q6bzeJD9Dn9v\n" + "hjpfWVkczf/NeA1fGH/pcgfkT6Dm706GFFttLL8CgYBl/HeXk1H47xAiHO4dJKnb\n" + "v0B+X8To/RXamF01r+8BpUoOubOQetdyX7ic+d6deuHu8i6LD/GSCeYJZYFR/KVg\n" + "AtiW757QYalnq3ZogkhFrVCZP8IRfTPOFBxp752TlyAcrSI7T9pQ47IBe4094KXM\n" + "CJWSfPgAJkOxd0iU0XJpmwKBgGfQxuMTgSlwYRKFlD1zKap5TdID8fbUbVnth0Q5\n" + "GbH7vwlp/qrxCdS/aj0n0irOpbOaW9ccnlrHiqY25VpVMLYIkt3DrDOEiNNx+KNR\n" + "TItdTwbcSiTYrS4L0/56ydM/H6bsfsXxRjI18hSJqMZiqXqS84OZz2aOn+h7HCzc\n" + "LEiZAoGASk20wFvilpRKHq79xxFWiDUPHi0x0pp82dYIEntGQkKUWkbSlhgf3MAi\n" + "5NEQTDmXdnB+rVeWIvEi+BXfdnNgdn8eC4zSdtF4sIAhYr5VWZo0WVWDhT7u2ccv\n" + "ZBFymiz8lo3gN57wGUCi9pbZqzV1+ZppX6YTNDdDCE0q+KO3Cec=\n" + "-----END RSA PRIVATE KEY-----" +) + +TEST_RSA_OAUTH_SIGNATURE = ( + "j8WF8PGjojT82aUDd2EL%2Bz7HCoHInFzWUpiEKMCy%2BJ2cYHWcBS7mXlmFDLgAKV0" + "P%2FyX4TrpXODYnJ6dRWdfghqwDpi%2FlQmB2jxCiGMdJoYxh3c5zDf26gEbGdP6D7O" + "Ssp5HUnzH6sNkmVjuE%2FxoJcHJdc23H6GhOs7VJ2LWNdbhKWP%2FMMlTrcoQDn8lz" + "%2Fb24WsJ6ae1txkUzpFOOlLM8aTdNtGL4OtsubOlRhNqnAFq93FyhXg0KjzUyIZzmMX" + "9Vx90jTks5QeBGYcLE0Op2iHb2u%2FO%2BEgdwFchgEwE5LgMUyHUI4F3Wglp28yHOAM" + "jPkI%2FkWMvpxtMrU3Z3KN31WQ%3D%3D" +) + + +class OAuth1SessionTest(unittest.TestCase): + def test_signature_types(self): + def verify_signature(getter): + def fake_send(r, **kwargs): + signature = getter(r) + if isinstance(signature, bytes): + signature = signature.decode("utf-8") + self.assertIn("oauth_signature", signature) + resp = mock.MagicMock(spec=requests.Response) + resp.cookies = [] + return resp + + return fake_send + + header = OAuth1Session("foo") + header.send = verify_signature(lambda r: r.headers["Authorization"]) + header.post("https://i.b") + + query = OAuth1Session("foo", signature_type=SIGNATURE_TYPE_QUERY) + query.send = verify_signature(lambda r: r.url) + query.post("https://i.b") + + body = OAuth1Session("foo", signature_type=SIGNATURE_TYPE_BODY) + headers = {"Content-Type": "application/x-www-form-urlencoded"} + body.send = verify_signature(lambda r: r.body) + body.post("https://i.b", headers=headers, data="") + + @mock.patch("oauthlib.oauth1.rfc5849.generate_timestamp") + @mock.patch("oauthlib.oauth1.rfc5849.generate_nonce") + def test_signature_methods(self, generate_nonce, generate_timestamp): + if not cryptography: + raise unittest.SkipTest("cryptography module is required") + if not jwt: + raise unittest.SkipTest("pyjwt module is required") + + 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="HMAC-SHA1", oauth_consumer_key="foo", oauth_signature="h2sRqLArjhlc5p3FTkuNogVHlKE%3D"' + auth = OAuth1Session("foo") + auth.send = self.verify_signature(signature) + auth.post("https://i.b") + + signature = 'OAuth oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_consumer_key="foo", oauth_signature="%26"' + auth = OAuth1Session("foo", signature_method=SIGNATURE_PLAINTEXT) + auth.send = self.verify_signature(signature) + auth.post("https://i.b") + + signature = ( + "OAuth " + 'oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", ' + 'oauth_signature_method="RSA-SHA1", oauth_consumer_key="foo", ' + 'oauth_signature="{sig}"' + ).format(sig=TEST_RSA_OAUTH_SIGNATURE) + auth = OAuth1Session( + "foo", signature_method=SIGNATURE_RSA, rsa_key=TEST_RSA_KEY + ) + auth.send = self.verify_signature(signature) + auth.post("https://i.b") + + @mock.patch("oauthlib.oauth1.rfc5849.generate_timestamp") + @mock.patch("oauthlib.oauth1.rfc5849.generate_nonce") + def test_binary_upload(self, generate_nonce, generate_timestamp): + generate_nonce.return_value = "abc" + generate_timestamp.return_value = "123" + fake_xml = StringIO("hello world") + headers = {"Content-Type": "application/xml"} + signature = 'OAuth oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", oauth_signature_method="HMAC-SHA1", oauth_consumer_key="foo", oauth_signature="h2sRqLArjhlc5p3FTkuNogVHlKE%3D"' + auth = OAuth1Session("foo") + auth.send = self.verify_signature(signature) + auth.post("https://i.b", headers=headers, files=[("fake", fake_xml)]) + + @mock.patch("oauthlib.oauth1.rfc5849.generate_timestamp") + @mock.patch("oauthlib.oauth1.rfc5849.generate_nonce") + def test_nonascii(self, generate_nonce, generate_timestamp): + 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="HMAC-SHA1", oauth_consumer_key="foo", oauth_signature="W0haoue5IZAZoaJiYCtfqwMf8x8%3D"' + auth = OAuth1Session("foo") + auth.send = self.verify_signature(signature) + auth.post("https://i.b?cjk=%E5%95%A6%E5%95%A6") + + def test_authorization_url(self): + auth = OAuth1Session("foo") + url = "https://example.comm/authorize" + token = "asluif023sf" + auth_url = auth.authorization_url(url, request_token=token) + self.assertEqual(auth_url, url + "?oauth_token=" + token) + + def test_parse_response_url(self): + url = "https://i.b/callback?oauth_token=foo&oauth_verifier=bar" + auth = OAuth1Session("foo") + resp = auth.parse_authorization_response(url) + 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) + + def test_fetch_request_token(self): + auth = OAuth1Session("foo") + auth.send = self.fake_body("oauth_token=foo") + 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) + + def test_fetch_request_token_with_optional_arguments(self): + auth = OAuth1Session("foo") + auth.send = self.fake_body("oauth_token=foo") + resp = auth.fetch_request_token( + "https://example.com/token", verify=False, stream=True + ) + self.assertEqual(resp["oauth_token"], "foo") + for k, v in resp.items(): + self.assertIsInstance(k, unicode_type) + self.assertIsInstance(v, unicode_type) + + def test_fetch_access_token(self): + auth = OAuth1Session("foo", verifier="bar") + auth.send = self.fake_body("oauth_token=foo") + 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) + + def test_fetch_access_token_with_optional_arguments(self): + auth = OAuth1Session("foo", verifier="bar") + auth.send = self.fake_body("oauth_token=foo") + resp = auth.fetch_access_token( + "https://example.com/token", verify=False, stream=True + ) + self.assertEqual(resp["oauth_token"], "foo") + for k, v in resp.items(): + self.assertIsInstance(k, unicode_type) + self.assertIsInstance(v, unicode_type) + + def _test_fetch_access_token_raises_error(self, auth): + """Assert that an error is being raised whenever there's no verifier + passed in to the client. + """ + auth.send = self.fake_body("oauth_token=foo") + with self.assertRaises(ValueError) as cm: + auth.fetch_access_token("https://example.com/token") + self.assertEqual("No client verifier has been set.", str(cm.exception)) + + def test_fetch_token_invalid_response(self): + auth = OAuth1Session("foo") + auth.send = self.fake_body("not valid urlencoded response!") + self.assertRaises( + ValueError, auth.fetch_request_token, "https://example.com/token" + ) + + for code in (400, 401, 403): + auth.send = self.fake_body("valid=response", code) + with self.assertRaises(ValueError) as cm: + auth.fetch_request_token("https://example.com/token") + self.assertEqual(cm.exception.status_code, code) + self.assertIsInstance(cm.exception.response, requests.Response) + + def test_fetch_access_token_missing_verifier(self): + self._test_fetch_access_token_raises_error(OAuth1Session("foo")) + + def test_fetch_access_token_has_verifier_is_none(self): + auth = OAuth1Session("foo") + del auth._client.client.verifier + self._test_fetch_access_token_raises_error(auth) + + def test_token_proxy_set(self): + token = { + "oauth_token": "fake-key", + "oauth_token_secret": "fake-secret", + "oauth_verifier": "fake-verifier", + } + sess = OAuth1Session("foo") + self.assertIsNone(sess._client.client.resource_owner_key) + self.assertIsNone(sess._client.client.resource_owner_secret) + self.assertIsNone(sess._client.client.verifier) + self.assertEqual(sess.token, {}) + + sess.token = token + self.assertEqual(sess._client.client.resource_owner_key, "fake-key") + self.assertEqual(sess._client.client.resource_owner_secret, "fake-secret") + self.assertEqual(sess._client.client.verifier, "fake-verifier") + + def test_token_proxy_get(self): + token = { + "oauth_token": "fake-key", + "oauth_token_secret": "fake-secret", + "oauth_verifier": "fake-verifier", + } + sess = OAuth1Session( + "foo", + resource_owner_key=token["oauth_token"], + resource_owner_secret=token["oauth_token_secret"], + verifier=token["oauth_verifier"], + ) + self.assertEqual(sess.token, token) + + sess._client.client.resource_owner_key = "different-key" + token["oauth_token"] = "different-key" + + self.assertEqual(sess.token, token) + + def test_authorized_false(self): + sess = OAuth1Session("foo") + self.assertIs(sess.authorized, False) + + def test_authorized_false_rsa(self): + signature = ( + "OAuth " + 'oauth_nonce="abc", oauth_timestamp="123", oauth_version="1.0", ' + 'oauth_signature_method="RSA-SHA1", oauth_consumer_key="foo", ' + 'oauth_signature="{sig}"' + ).format(sig=TEST_RSA_OAUTH_SIGNATURE) + sess = OAuth1Session( + "foo", signature_method=SIGNATURE_RSA, rsa_key=TEST_RSA_KEY + ) + sess.send = self.verify_signature(signature) + self.assertIs(sess.authorized, False) + + def test_authorized_true(self): + sess = OAuth1Session("key", "secret", verifier="bar") + sess.send = self.fake_body("oauth_token=foo&oauth_token_secret=bar") + sess.fetch_access_token("https://example.com/token") + self.assertIs(sess.authorized, True) + + @mock.patch("oauthlib.oauth1.rfc5849.generate_timestamp") + @mock.patch("oauthlib.oauth1.rfc5849.generate_nonce") + def test_authorized_true_rsa(self, generate_nonce, generate_timestamp): + if not cryptography: + raise unittest.SkipTest("cryptography module is required") + if not jwt: + raise unittest.SkipTest("pyjwt module is required") + + 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", + signature_method=SIGNATURE_RSA, + rsa_key=TEST_RSA_KEY, + verifier="bar", + ) + sess.send = self.fake_body("oauth_token=foo&oauth_token_secret=bar") + sess.fetch_access_token("https://example.com/token") + self.assertIs(sess.authorized, True) + + def verify_signature(self, signature): + def fake_send(r, **kwargs): + auth_header = r.headers["Authorization"] + if isinstance(auth_header, bytes): + auth_header = auth_header.decode("utf-8") + self.assertEqual(auth_header, signature) + resp = mock.MagicMock(spec=requests.Response) + resp.cookies = [] + return resp + + return fake_send + + def fake_body(self, body, status_code=200): + def fake_send(r, **kwargs): + resp = mock.MagicMock(spec=requests.Response) + resp.cookies = [] + resp.text = body + resp.status_code = status_code + return resp + + return fake_send diff --git a/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py b/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py new file mode 100644 index 0000000000..accb561ef6 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/test_oauth2_auth.py @@ -0,0 +1,54 @@ +from __future__ import unicode_literals +import unittest + +from oauthlib.oauth2 import WebApplicationClient, MobileApplicationClient +from oauthlib.oauth2 import LegacyApplicationClient, BackendApplicationClient +from requests import Request +from requests_oauthlib import OAuth2 + + +class OAuth2AuthTest(unittest.TestCase): + def setUp(self): + self.token = { + "token_type": "Bearer", + "access_token": "asdfoiw37850234lkjsdfsdf", + "expires_in": "3600", + } + self.client_id = "foo" + self.clients = [ + WebApplicationClient(self.client_id), + MobileApplicationClient(self.client_id), + LegacyApplicationClient(self.client_id), + BackendApplicationClient(self.client_id), + ] + + def test_add_token_to_url(self): + url = "https://example.com/resource?foo=bar" + new_url = url + "&access_token=" + self.token["access_token"] + for client in self.clients: + client.default_token_placement = "query" + auth = OAuth2(client=client, token=self.token) + r = Request("GET", url, auth=auth).prepare() + self.assertEqual(r.url, new_url) + + def test_add_token_to_headers(self): + token = "Bearer " + self.token["access_token"] + for client in self.clients: + auth = OAuth2(client=client, token=self.token) + r = Request("GET", "https://i.b", auth=auth).prepare() + self.assertEqual(r.headers["Authorization"], token) + + def test_add_token_to_body(self): + body = "foo=bar" + new_body = body + "&access_token=" + self.token["access_token"] + for client in self.clients: + client.default_token_placement = "body" + auth = OAuth2(client=client, token=self.token) + r = Request("GET", "https://i.b", data=body, auth=auth).prepare() + self.assertEqual(r.body, new_body) + + def test_add_nonexisting_token(self): + for client in self.clients: + auth = OAuth2(client=client) + r = Request("GET", "https://i.b", auth=auth) + self.assertRaises(ValueError, r.prepare) diff --git a/contrib/python/requests-oauthlib/tests/test_oauth2_session.py b/contrib/python/requests-oauthlib/tests/test_oauth2_session.py new file mode 100644 index 0000000000..cfc6236855 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/test_oauth2_session.py @@ -0,0 +1,527 @@ +from __future__ import unicode_literals +import json +import time +import tempfile +import shutil +import os +from base64 import b64encode +from copy import deepcopy +from unittest import TestCase + +try: + import mock +except ImportError: + from unittest import mock + +from oauthlib.common import urlencode +from oauthlib.oauth2 import TokenExpiredError, OAuth2Error +from oauthlib.oauth2 import MismatchingStateError +from oauthlib.oauth2 import WebApplicationClient, MobileApplicationClient +from oauthlib.oauth2 import LegacyApplicationClient, BackendApplicationClient +from requests_oauthlib import OAuth2Session, TokenUpdated +import requests + +from requests.auth import _basic_auth_str + + +fake_time = time.time() +CODE = "asdf345xdf" + + +def fake_token(token): + def fake_send(r, **kwargs): + resp = mock.MagicMock() + resp.text = json.dumps(token) + return resp + + return fake_send + + +class OAuth2SessionTest(TestCase): + def setUp(self): + self.token = { + "token_type": "Bearer", + "access_token": "asdfoiw37850234lkjsdfsdf", + "refresh_token": "sldvafkjw34509s8dfsdf", + "expires_in": 3600, + "expires_at": fake_time + 3600, + } + # use someclientid:someclientsecret to easily differentiate between client and user credentials + # these are the values used in oauthlib tests + self.client_id = "someclientid" + self.client_secret = "someclientsecret" + self.user_username = "user_username" + self.user_password = "user_password" + self.client_WebApplication = WebApplicationClient(self.client_id, code=CODE) + self.client_LegacyApplication = LegacyApplicationClient(self.client_id) + self.client_BackendApplication = BackendApplicationClient(self.client_id) + self.client_MobileApplication = MobileApplicationClient(self.client_id) + self.clients = [ + self.client_WebApplication, + self.client_LegacyApplication, + self.client_BackendApplication, + ] + self.all_clients = self.clients + [self.client_MobileApplication] + + def test_add_token(self): + token = "Bearer " + self.token["access_token"] + + def verifier(r, **kwargs): + auth_header = r.headers.get(str("Authorization"), None) + self.assertEqual(auth_header, token) + resp = mock.MagicMock() + resp.cookes = [] + return resp + + for client in self.all_clients: + sess = OAuth2Session(client=client, token=self.token) + sess.send = verifier + sess.get("https://i.b") + + def test_mtls(self): + cert = ( + "testsomething.example-client.pem", + "testsomething.example-client-key.pem", + ) + + def verifier(r, **kwargs): + self.assertIn("cert", kwargs) + self.assertEqual(cert, kwargs["cert"]) + self.assertIn("client_id=" + self.client_id, r.body) + resp = mock.MagicMock() + resp.text = json.dumps(self.token) + return resp + + for client in self.clients: + sess = OAuth2Session(client=client) + sess.send = verifier + + if isinstance(client, LegacyApplicationClient): + sess.fetch_token( + "https://i.b", + include_client_id=True, + cert=cert, + username="username1", + password="password1", + ) + else: + sess.fetch_token("https://i.b", include_client_id=True, cert=cert) + + def test_authorization_url(self): + url = "https://example.com/authorize?foo=bar" + + web = WebApplicationClient(self.client_id) + s = OAuth2Session(client=web) + 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) + + mobile = MobileApplicationClient(self.client_id) + s = OAuth2Session(client=mobile) + 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) + + @mock.patch("time.time", new=lambda: fake_time) + def test_refresh_token_request(self): + self.expired_token = dict(self.token) + self.expired_token["expires_in"] = "-1" + del self.expired_token["expires_at"] + + def fake_refresh(r, **kwargs): + if "/refresh" in r.url: + self.assertNotIn("Authorization", r.headers) + resp = mock.MagicMock() + resp.text = json.dumps(self.token) + return resp + + # No auto refresh setup + for client in self.clients: + sess = OAuth2Session(client=client, token=self.expired_token) + self.assertRaises(TokenExpiredError, sess.get, "https://i.b") + + # Auto refresh but no auto update + for client in self.clients: + sess = OAuth2Session( + client=client, + token=self.expired_token, + auto_refresh_url="https://i.b/refresh", + ) + sess.send = fake_refresh + self.assertRaises(TokenUpdated, sess.get, "https://i.b") + + # Auto refresh and auto update + def token_updater(token): + self.assertEqual(token, self.token) + + for client in self.clients: + sess = OAuth2Session( + client=client, + token=self.expired_token, + auto_refresh_url="https://i.b/refresh", + token_updater=token_updater, + ) + sess.send = fake_refresh + sess.get("https://i.b") + + def fake_refresh_with_auth(r, **kwargs): + if "/refresh" in r.url: + self.assertIn("Authorization", r.headers) + encoded = b64encode( + "{client_id}:{client_secret}".format( + client_id=self.client_id, client_secret=self.client_secret + ).encode("latin1") + ) + content = "Basic {encoded}".format(encoded=encoded.decode("latin1")) + self.assertEqual(r.headers["Authorization"], content) + resp = mock.MagicMock() + resp.text = json.dumps(self.token) + return resp + + for client in self.clients: + sess = OAuth2Session( + client=client, + token=self.expired_token, + auto_refresh_url="https://i.b/refresh", + token_updater=token_updater, + ) + sess.send = fake_refresh_with_auth + sess.get( + "https://i.b", + client_id=self.client_id, + client_secret=self.client_secret, + ) + + @mock.patch("time.time", new=lambda: fake_time) + def test_token_from_fragment(self): + mobile = MobileApplicationClient(self.client_id) + response_url = "https://i.b/callback#" + urlencode(self.token.items()) + sess = OAuth2Session(client=mobile) + self.assertEqual(sess.token_from_fragment(response_url), self.token) + + @mock.patch("time.time", new=lambda: fake_time) + def test_fetch_token(self): + url = "https://example.com/token" + + for client in self.clients: + sess = OAuth2Session(client=client, token=self.token) + sess.send = fake_token(self.token) + if isinstance(client, LegacyApplicationClient): + # this client requires a username+password + # if unset, an error will be raised + self.assertRaises(ValueError, sess.fetch_token, url) + self.assertRaises( + ValueError, sess.fetch_token, url, username="username1" + ) + self.assertRaises( + ValueError, sess.fetch_token, url, password="password1" + ) + # otherwise it will pass + self.assertEqual( + sess.fetch_token(url, username="username1", password="password1"), + self.token, + ) + else: + self.assertEqual(sess.fetch_token(url), self.token) + + error = {"error": "invalid_request"} + for client in self.clients: + sess = OAuth2Session(client=client, token=self.token) + sess.send = fake_token(error) + if isinstance(client, LegacyApplicationClient): + # this client requires a username+password + # if unset, an error will be raised + self.assertRaises(ValueError, sess.fetch_token, url) + self.assertRaises( + ValueError, sess.fetch_token, url, username="username1" + ) + self.assertRaises( + ValueError, sess.fetch_token, url, password="password1" + ) + # otherwise it will pass + self.assertRaises( + OAuth2Error, + sess.fetch_token, + url, + username="username1", + password="password1", + ) + else: + self.assertRaises(OAuth2Error, sess.fetch_token, url) + + # there are different scenarios in which the `client_id` can be specified + # reference `oauthlib.tests.oauth2.rfc6749.clients.test_web_application.WebApplicationClientTest.test_prepare_request_body` + # this only needs to test WebApplicationClient + client = self.client_WebApplication + client.tester = True + + # this should be a tuple of (r.url, r.body, r.headers.get('Authorization')) + _fetch_history = [] + + def fake_token_history(token): + def fake_send(r, **kwargs): + resp = mock.MagicMock() + resp.text = json.dumps(token) + _fetch_history.append( + (r.url, r.body, r.headers.get("Authorization", None)) + ) + return resp + + return fake_send + + sess = OAuth2Session(client=client, token=self.token) + sess.send = fake_token_history(self.token) + expected_auth_header = _basic_auth_str(self.client_id, self.client_secret) + + # scenario 1 - default request + # this should send the `client_id` in the headers, as that is recommended by the RFC + self.assertEqual( + sess.fetch_token(url, client_secret="someclientsecret"), self.token + ) + self.assertEqual(len(_fetch_history), 1) + self.assertNotIn( + "client_id", _fetch_history[0][1] + ) # no `client_id` in the body + self.assertNotIn( + "client_secret", _fetch_history[0][1] + ) # no `client_secret` in the body + self.assertEqual( + _fetch_history[0][2], expected_auth_header + ) # ensure a Basic Authorization header + + # scenario 2 - force the `client_id` into the body + self.assertEqual( + sess.fetch_token( + url, client_secret="someclientsecret", include_client_id=True + ), + self.token, + ) + self.assertEqual(len(_fetch_history), 2) + self.assertIn("client_id=%s" % self.client_id, _fetch_history[1][1]) + self.assertIn("client_secret=%s" % self.client_secret, _fetch_history[1][1]) + self.assertEqual( + _fetch_history[1][2], None + ) # ensure NO Basic Authorization header + + # scenario 3 - send in an auth object + auth = requests.auth.HTTPBasicAuth(self.client_id, self.client_secret) + self.assertEqual(sess.fetch_token(url, auth=auth), self.token) + self.assertEqual(len(_fetch_history), 3) + self.assertNotIn( + "client_id", _fetch_history[2][1] + ) # no `client_id` in the body + self.assertNotIn( + "client_secret", _fetch_history[2][1] + ) # no `client_secret` in the body + self.assertEqual( + _fetch_history[2][2], expected_auth_header + ) # ensure a Basic Authorization header + + # scenario 4 - send in a username/password combo + # this should send the `client_id` in the headers, like scenario 1 + self.assertEqual( + sess.fetch_token( + url, username=self.user_username, password=self.user_password + ), + self.token, + ) + self.assertEqual(len(_fetch_history), 4) + self.assertNotIn( + "client_id", _fetch_history[3][1] + ) # no `client_id` in the body + self.assertNotIn( + "client_secret", _fetch_history[3][1] + ) # no `client_secret` in the body + self.assertEqual( + _fetch_history[0][2], expected_auth_header + ) # ensure a Basic Authorization header + self.assertIn("username=%s" % self.user_username, _fetch_history[3][1]) + self.assertIn("password=%s" % self.user_password, _fetch_history[3][1]) + + # scenario 5 - send data in `params` and not in `data` for providers + # that expect data in URL + self.assertEqual( + sess.fetch_token(url, client_secret="somesecret", force_querystring=True), + self.token, + ) + self.assertIn("code=%s" % CODE, _fetch_history[4][0]) + + # some quick tests for valid ways of supporting `client_secret` + + # scenario 2b - force the `client_id` into the body; but the `client_secret` is `None` + self.assertEqual( + sess.fetch_token(url, client_secret=None, include_client_id=True), + self.token, + ) + self.assertEqual(len(_fetch_history), 6) + self.assertIn("client_id=%s" % self.client_id, _fetch_history[5][1]) + self.assertNotIn( + "client_secret=", _fetch_history[5][1] + ) # no `client_secret` in the body + self.assertEqual( + _fetch_history[5][2], None + ) # ensure NO Basic Authorization header + + # scenario 2c - force the `client_id` into the body; but the `client_secret` is an empty string + self.assertEqual( + sess.fetch_token(url, client_secret="", include_client_id=True), self.token + ) + self.assertEqual(len(_fetch_history), 7) + self.assertIn("client_id=%s" % self.client_id, _fetch_history[6][1]) + self.assertIn("client_secret=", _fetch_history[6][1]) + self.assertEqual( + _fetch_history[6][2], None + ) # ensure NO Basic Authorization header + + def test_cleans_previous_token_before_fetching_new_one(self): + """Makes sure the previous token is cleaned before fetching a new one. + + The reason behind it is that, if the previous token is expired, this + method shouldn't fail with a TokenExpiredError, since it's attempting + to get a new one (which shouldn't be expired). + + """ + new_token = deepcopy(self.token) + past = time.time() - 7200 + now = time.time() + self.token["expires_at"] = past + new_token["expires_at"] = now + 3600 + url = "https://example.com/token" + + with mock.patch("time.time", lambda: now): + for client in self.clients: + sess = OAuth2Session(client=client, token=self.token) + sess.send = fake_token(new_token) + if isinstance(client, LegacyApplicationClient): + # this client requires a username+password + # if unset, an error will be raised + self.assertRaises(ValueError, sess.fetch_token, url) + self.assertRaises( + ValueError, sess.fetch_token, url, username="username1" + ) + self.assertRaises( + ValueError, sess.fetch_token, url, password="password1" + ) + # otherwise it will pass + self.assertEqual( + sess.fetch_token( + url, username="username1", password="password1" + ), + new_token, + ) + else: + self.assertEqual(sess.fetch_token(url), new_token) + + def test_web_app_fetch_token(self): + # Ensure the state parameter is used, see issue #105. + client = OAuth2Session("someclientid", state="somestate") + self.assertRaises( + MismatchingStateError, + client.fetch_token, + "https://i.b/token", + authorization_response="https://i.b/no-state?code=abc", + ) + + def test_client_id_proxy(self): + sess = OAuth2Session("test-id") + self.assertEqual(sess.client_id, "test-id") + sess.client_id = "different-id" + self.assertEqual(sess.client_id, "different-id") + sess._client.client_id = "something-else" + self.assertEqual(sess.client_id, "something-else") + del sess.client_id + self.assertIsNone(sess.client_id) + + def test_access_token_proxy(self): + sess = OAuth2Session("test-id") + self.assertIsNone(sess.access_token) + sess.access_token = "test-token" + self.assertEqual(sess.access_token, "test-token") + sess._client.access_token = "different-token" + self.assertEqual(sess.access_token, "different-token") + del sess.access_token + self.assertIsNone(sess.access_token) + + def test_token_proxy(self): + token = {"access_token": "test-access"} + sess = OAuth2Session("test-id", token=token) + self.assertEqual(sess.access_token, "test-access") + self.assertEqual(sess.token, token) + token["access_token"] = "something-else" + sess.token = token + self.assertEqual(sess.access_token, "something-else") + self.assertEqual(sess.token, token) + sess._client.access_token = "different-token" + token["access_token"] = "different-token" + self.assertEqual(sess.access_token, "different-token") + self.assertEqual(sess.token, token) + # can't delete token attribute + with self.assertRaises(AttributeError): + del sess.token + + def test_authorized_false(self): + sess = OAuth2Session("someclientid") + self.assertFalse(sess.authorized) + + @mock.patch("time.time", new=lambda: fake_time) + def test_authorized_true(self): + def fake_token(token): + def fake_send(r, **kwargs): + resp = mock.MagicMock() + resp.text = json.dumps(token) + return resp + + return fake_send + + url = "https://example.com/token" + + for client in self.clients: + sess = OAuth2Session(client=client) + sess.send = fake_token(self.token) + self.assertFalse(sess.authorized) + if isinstance(client, LegacyApplicationClient): + # this client requires a username+password + # if unset, an error will be raised + self.assertRaises(ValueError, sess.fetch_token, url) + self.assertRaises( + ValueError, sess.fetch_token, url, username="username1" + ) + self.assertRaises( + ValueError, sess.fetch_token, url, password="password1" + ) + # otherwise it will pass + sess.fetch_token(url, username="username1", password="password1") + else: + sess.fetch_token(url) + self.assertTrue(sess.authorized) + + +class OAuth2SessionNetrcTest(OAuth2SessionTest): + """Ensure that there is no magic auth handling. + + By default, requests sessions have magic handling of netrc files, + which is undesirable for this library because it will take + precedence over manually set authentication headers. + """ + + def setUp(self): + # Set up a temporary home directory + self.homedir = tempfile.mkdtemp() + self.prehome = os.environ.get("HOME", None) + os.environ["HOME"] = self.homedir + + # Write a .netrc file that will cause problems + netrc_loc = os.path.expanduser("~/.netrc") + with open(netrc_loc, "w") as f: + f.write("machine i.b\n" " password abc123\n" " login spam@eggs.co\n") + + super(OAuth2SessionNetrcTest, self).setUp() + + def tearDown(self): + super(OAuth2SessionNetrcTest, self).tearDown() + + if self.prehome is not None: + os.environ["HOME"] = self.prehome + shutil.rmtree(self.homedir) diff --git a/contrib/python/requests-oauthlib/tests/ya.make b/contrib/python/requests-oauthlib/tests/ya.make new file mode 100644 index 0000000000..a8f7328ae7 --- /dev/null +++ b/contrib/python/requests-oauthlib/tests/ya.make @@ -0,0 +1,28 @@ +PY3TEST() + +PEERDIR( + contrib/python/requests-oauthlib + contrib/python/requests-mock +) + +# These tests use real http://httpbin.org that is why they are disabled: +# testCanPostBinaryData +# test_url_is_native_str +# test_content_type_override + +TEST_SRCS( + __init__.py + test_compliance_fixes.py + test_core.py + test_oauth1_session.py + test_oauth2_auth.py + test_oauth2_session.py +) + +DATA( + arcadia/contrib/python/requests-oauthlib/tests +) + +NO_LINT() + +END() diff --git a/contrib/python/requests-oauthlib/ya.make b/contrib/python/requests-oauthlib/ya.make new file mode 100644 index 0000000000..2145e60cc4 --- /dev/null +++ b/contrib/python/requests-oauthlib/ya.make @@ -0,0 +1,45 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(1.3.1) + +LICENSE(ISC) + +PEERDIR( + contrib/python/oauthlib + contrib/python/requests +) + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + requests_oauthlib/__init__.py + requests_oauthlib/compliance_fixes/__init__.py + requests_oauthlib/compliance_fixes/douban.py + requests_oauthlib/compliance_fixes/ebay.py + requests_oauthlib/compliance_fixes/facebook.py + requests_oauthlib/compliance_fixes/fitbit.py + requests_oauthlib/compliance_fixes/instagram.py + requests_oauthlib/compliance_fixes/mailchimp.py + requests_oauthlib/compliance_fixes/plentymarkets.py + requests_oauthlib/compliance_fixes/slack.py + requests_oauthlib/compliance_fixes/weibo.py + requests_oauthlib/oauth1_auth.py + requests_oauthlib/oauth1_session.py + requests_oauthlib/oauth2_auth.py + requests_oauthlib/oauth2_session.py +) + +RESOURCE_FILES( + PREFIX contrib/python/requests-oauthlib/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() + +RECURSE_FOR_TESTS( + tests +) |