aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/oauthlib
diff options
context:
space:
mode:
authoralexv-smirnov <alex@ydb.tech>2023-12-01 12:02:50 +0300
committeralexv-smirnov <alex@ydb.tech>2023-12-01 13:28:10 +0300
commit0e578a4c44d4abd539d9838347b9ebafaca41dfb (patch)
treea0c1969c37f818c830ebeff9c077eacf30be6ef8 /contrib/python/oauthlib
parent84f2d3d4cc985e63217cff149bd2e6d67ae6fe22 (diff)
downloadydb-0e578a4c44d4abd539d9838347b9ebafaca41dfb.tar.gz
Change "ya.make"
Diffstat (limited to 'contrib/python/oauthlib')
-rw-r--r--contrib/python/oauthlib/.dist-info/METADATA179
-rw-r--r--contrib/python/oauthlib/.dist-info/top_level.txt1
-rw-r--r--contrib/python/oauthlib/LICENSE27
-rw-r--r--contrib/python/oauthlib/README.rst137
-rw-r--r--contrib/python/oauthlib/oauthlib/__init__.py34
-rw-r--r--contrib/python/oauthlib/oauthlib/common.py432
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/__init__.py23
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/__init__.py365
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/__init__.py8
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/access_token.py215
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/authorization.py158
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/base.py244
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py14
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/request_token.py209
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/resource.py163
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/signature_only.py82
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/errors.py76
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/parameters.py133
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/request_validator.py849
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/signature.py852
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth1/rfc5849/utils.py83
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/__init__.py36
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/__init__.py16
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/__init__.py14
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/backend_application.py74
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/base.py604
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/legacy_application.py84
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/mobile_application.py174
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/service_application.py189
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/web_application.py222
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/__init__.py17
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/authorization.py114
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/base.py113
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/introspect.py120
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/metadata.py238
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py216
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/resource.py84
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/revocation.py126
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/token.py119
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/errors.py400
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/__init__.py11
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py548
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/base.py268
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py123
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/implicit.py376
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py136
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py199
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/parameters.py471
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/request_validator.py680
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/tokens.py356
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc6749/utils.py83
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc8628/__init__.py10
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/__init__.py8
-rw-r--r--contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/device.py95
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/__init__.py7
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/__init__.py0
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/__init__.py0
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/__init__.py9
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/pre_configured.py97
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/userinfo.py106
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/exceptions.py149
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/__init__.py13
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/authorization_code.py43
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/base.py326
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/dispatchers.py101
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/hybrid.py63
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/implicit.py51
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/refresh_token.py34
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/request_validator.py320
-rw-r--r--contrib/python/oauthlib/oauthlib/openid/connect/core/tokens.py48
-rw-r--r--contrib/python/oauthlib/oauthlib/signals.py40
-rw-r--r--contrib/python/oauthlib/oauthlib/uri_validate.py190
-rw-r--r--contrib/python/oauthlib/tests/__init__.py3
-rw-r--r--contrib/python/oauthlib/tests/oauth1/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_access_token.py91
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_authorization.py54
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_base.py406
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_request_token.py90
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_resource.py102
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_signature_only.py50
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/test_client.py269
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/test_parameters.py90
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/test_request_validator.py68
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/test_signatures.py896
-rw-r--r--contrib/python/oauthlib/tests/oauth1/rfc5849/test_utils.py138
-rw-r--r--contrib/python/oauthlib/tests/oauth2/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_backend_application.py86
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_base.py355
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_legacy_application.py140
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_mobile_application.py111
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_service_application.py185
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_web_application.py269
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py75
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_client_authentication.py162
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py128
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_error_responses.py491
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_extra_credentials.py69
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py168
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_metadata.py148
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py108
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py148
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_scope_handling.py193
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_utils.py11
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_authorization_code.py382
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_client_credentials.py76
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_implicit.py62
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_refresh_token.py211
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py156
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/test_parameters.py304
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/test_request_validator.py51
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/test_server.py391
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/test_tokens.py170
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc6749/test_utils.py100
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc8628/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc8628/clients/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/oauth2/rfc8628/clients/test_device.py63
-rw-r--r--contrib/python/oauthlib/tests/openid/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/endpoints/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_claims_handling.py107
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py78
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py67
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/__init__.py0
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_authorization_code.py200
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_base.py104
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_dispatchers.py122
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_hybrid.py102
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_implicit.py170
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_refresh_token.py105
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/test_request_validator.py50
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/test_server.py184
-rw-r--r--contrib/python/oauthlib/tests/openid/connect/core/test_tokens.py157
-rw-r--r--contrib/python/oauthlib/tests/test_common.py243
-rw-r--r--contrib/python/oauthlib/tests/test_uri_validate.py84
-rw-r--r--contrib/python/oauthlib/tests/unittest/__init__.py32
-rw-r--r--contrib/python/oauthlib/tests/ya.make88
-rw-r--r--contrib/python/oauthlib/ya.make93
144 files changed, 21261 insertions, 0 deletions
diff --git a/contrib/python/oauthlib/.dist-info/METADATA b/contrib/python/oauthlib/.dist-info/METADATA
new file mode 100644
index 0000000000..5bb339b0bd
--- /dev/null
+++ b/contrib/python/oauthlib/.dist-info/METADATA
@@ -0,0 +1,179 @@
+Metadata-Version: 2.1
+Name: oauthlib
+Version: 3.2.2
+Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic
+Home-page: https://github.com/oauthlib/oauthlib
+Author: The OAuthlib Community
+Author-email: idan@gazit.me
+Maintainer: Ib Lundgren
+Maintainer-email: ib.lundgren@gmail.com
+License: BSD
+Platform: any
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: MacOS
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: Implementation
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.6
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Provides-Extra: rsa
+Requires-Dist: cryptography (>=3.0.0) ; extra == 'rsa'
+Provides-Extra: signals
+Requires-Dist: blinker (>=1.4.0) ; extra == 'signals'
+Provides-Extra: signedtoken
+Requires-Dist: cryptography (>=3.0.0) ; extra == 'signedtoken'
+Requires-Dist: pyjwt (<3,>=2.0.0) ; extra == 'signedtoken'
+
+OAuthLib - Python Framework for OAuth1 & OAuth2
+===============================================
+
+*A generic, spec-compliant, thorough implementation of the OAuth request-signing
+logic for Python 3.6+.*
+
+.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master
+ :target: https://app.travis-ci.com/oauthlib/oauthlib
+ :alt: Travis
+.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
+ :target: https://coveralls.io/r/oauthlib/oauthlib
+ :alt: Coveralls
+.. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg
+ :target: https://pypi.org/project/oauthlib/
+ :alt: Download from PyPI
+.. image:: https://img.shields.io/pypi/l/oauthlib.svg
+ :target: https://pypi.org/project/oauthlib/
+ :alt: License
+.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib.svg?type=shield
+ :target: https://app.fossa.io/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib?ref=badge_shield
+ :alt: FOSSA Status
+.. image:: https://img.shields.io/readthedocs/oauthlib.svg
+ :target: https://oauthlib.readthedocs.io/en/latest/index.html
+ :alt: Read the Docs
+.. image:: https://badges.gitter.im/oauthlib/oauthlib.svg
+ :target: https://gitter.im/oauthlib/Lobby
+ :alt: Chat on Gitter
+
+
+.. image:: https://raw.githubusercontent.com/oauthlib/oauthlib/8d71b161fd145d11c40d55c9ab66ac134a303253/docs/logo/oauthlib-banner-700x192.png
+ :target: https://github.com/oauthlib/oauthlib/
+ :alt: OAuth + Python = OAuthlib Python Framework
+
+
+OAuth often seems complicated and difficult-to-implement. There are several
+prominent libraries for handling OAuth requests, but they all suffer from one or
+both of the following:
+
+1. They predate the `OAuth 1.0 spec`_, AKA RFC 5849.
+2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749.
+3. They assume the usage of a specific HTTP request library.
+
+.. _`OAuth 1.0 spec`: https://tools.ietf.org/html/rfc5849
+.. _`OAuth 2.0 spec`: https://tools.ietf.org/html/rfc6749
+
+OAuthLib is a framework which implements the logic of OAuth1 or OAuth2 without
+assuming a specific HTTP request object or web framework. Use it to graft OAuth
+client support onto your favorite HTTP library, or provide support onto your
+favourite web framework. If you're a maintainer of such a library, write a thin
+veneer on top of OAuthLib and get OAuth support for very little effort.
+
+
+Documentation
+--------------
+
+Full documentation is available on `Read the Docs`_. All contributions are very
+welcome! The documentation is still quite sparse, please open an issue for what
+you'd like to know, or discuss it in our `Gitter community`_, or even better, send a
+pull request!
+
+.. _`Gitter community`: https://gitter.im/oauthlib/Lobby
+.. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html
+
+Interested in making OAuth requests?
+------------------------------------
+
+Then you might be more interested in using `requests`_ which has OAuthLib
+powered OAuth support provided by the `requests-oauthlib`_ library.
+
+.. _`requests`: https://github.com/requests/requests
+.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
+
+Which web frameworks are supported?
+-----------------------------------
+
+The following packages provide OAuth support using OAuthLib.
+
+- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
+- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
+- For Pyramid there is `pyramid-oauthlib`_.
+- For Bottle there is `bottle-oauthlib`_.
+
+If you have written an OAuthLib package that supports your favorite framework,
+please open a Pull Request, updating the documentation.
+
+.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit
+.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib
+.. _`Django REST framework`: http://django-rest-framework.org
+.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
+.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
+.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
+
+Using OAuthLib? Please get in touch!
+------------------------------------
+Patching OAuth support onto an http request framework? Creating an OAuth
+provider extension for a web framework? Simply using OAuthLib to Get Things Done
+or to learn?
+
+No matter which we'd love to hear from you in our `Gitter community`_ or if you have
+anything in particular you would like to have, change or comment on don't
+hesitate for a second to send a pull request or open an issue. We might be quite
+busy and therefore slow to reply but we love feedback!
+
+Chances are you have run into something annoying that you wish there was
+documentation for, if you wish to gain eternal fame and glory, and a drink if we
+have the pleasure to run into each other, please send a docs pull request =)
+
+.. _`Gitter community`: https://gitter.im/oauthlib/Lobby
+
+License
+-------
+
+OAuthLib is yours to use and abuse according to the terms of the BSD license.
+Check the LICENSE file for full details.
+
+Credits
+-------
+
+OAuthLib has been started and maintained several years by Idan Gazit and other
+amazing `AUTHORS`_. Thanks to their wonderful work, the open-source `community`_
+creation has been possible and the project can stay active and reactive to users
+requests.
+
+
+.. _`AUTHORS`: https://github.com/oauthlib/oauthlib/blob/master/AUTHORS
+.. _`community`: https://github.com/oauthlib/
+
+Changelog
+---------
+
+*OAuthLib is in active development, with the core of both OAuth1 and OAuth2
+completed, for providers as well as clients.* See `supported features`_ for
+details.
+
+.. _`supported features`: https://oauthlib.readthedocs.io/en/latest/feature_matrix.html
+
+For a full changelog see ``CHANGELOG.rst``.
diff --git a/contrib/python/oauthlib/.dist-info/top_level.txt b/contrib/python/oauthlib/.dist-info/top_level.txt
new file mode 100644
index 0000000000..b5f3f0e345
--- /dev/null
+++ b/contrib/python/oauthlib/.dist-info/top_level.txt
@@ -0,0 +1 @@
+oauthlib
diff --git a/contrib/python/oauthlib/LICENSE b/contrib/python/oauthlib/LICENSE
new file mode 100644
index 0000000000..d5a9e9acd0
--- /dev/null
+++ b/contrib/python/oauthlib/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2019 The OAuthlib Community
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of this project nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/contrib/python/oauthlib/README.rst b/contrib/python/oauthlib/README.rst
new file mode 100644
index 0000000000..eb8c452d00
--- /dev/null
+++ b/contrib/python/oauthlib/README.rst
@@ -0,0 +1,137 @@
+OAuthLib - Python Framework for OAuth1 & OAuth2
+===============================================
+
+*A generic, spec-compliant, thorough implementation of the OAuth request-signing
+logic for Python 3.6+.*
+
+.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master
+ :target: https://app.travis-ci.com/oauthlib/oauthlib
+ :alt: Travis
+.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
+ :target: https://coveralls.io/r/oauthlib/oauthlib
+ :alt: Coveralls
+.. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg
+ :target: https://pypi.org/project/oauthlib/
+ :alt: Download from PyPI
+.. image:: https://img.shields.io/pypi/l/oauthlib.svg
+ :target: https://pypi.org/project/oauthlib/
+ :alt: License
+.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib.svg?type=shield
+ :target: https://app.fossa.io/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib?ref=badge_shield
+ :alt: FOSSA Status
+.. image:: https://img.shields.io/readthedocs/oauthlib.svg
+ :target: https://oauthlib.readthedocs.io/en/latest/index.html
+ :alt: Read the Docs
+.. image:: https://badges.gitter.im/oauthlib/oauthlib.svg
+ :target: https://gitter.im/oauthlib/Lobby
+ :alt: Chat on Gitter
+
+
+.. image:: https://raw.githubusercontent.com/oauthlib/oauthlib/8d71b161fd145d11c40d55c9ab66ac134a303253/docs/logo/oauthlib-banner-700x192.png
+ :target: https://github.com/oauthlib/oauthlib/
+ :alt: OAuth + Python = OAuthlib Python Framework
+
+
+OAuth often seems complicated and difficult-to-implement. There are several
+prominent libraries for handling OAuth requests, but they all suffer from one or
+both of the following:
+
+1. They predate the `OAuth 1.0 spec`_, AKA RFC 5849.
+2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749.
+3. They assume the usage of a specific HTTP request library.
+
+.. _`OAuth 1.0 spec`: https://tools.ietf.org/html/rfc5849
+.. _`OAuth 2.0 spec`: https://tools.ietf.org/html/rfc6749
+
+OAuthLib is a framework which implements the logic of OAuth1 or OAuth2 without
+assuming a specific HTTP request object or web framework. Use it to graft OAuth
+client support onto your favorite HTTP library, or provide support onto your
+favourite web framework. If you're a maintainer of such a library, write a thin
+veneer on top of OAuthLib and get OAuth support for very little effort.
+
+
+Documentation
+--------------
+
+Full documentation is available on `Read the Docs`_. All contributions are very
+welcome! The documentation is still quite sparse, please open an issue for what
+you'd like to know, or discuss it in our `Gitter community`_, or even better, send a
+pull request!
+
+.. _`Gitter community`: https://gitter.im/oauthlib/Lobby
+.. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html
+
+Interested in making OAuth requests?
+------------------------------------
+
+Then you might be more interested in using `requests`_ which has OAuthLib
+powered OAuth support provided by the `requests-oauthlib`_ library.
+
+.. _`requests`: https://github.com/requests/requests
+.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
+
+Which web frameworks are supported?
+-----------------------------------
+
+The following packages provide OAuth support using OAuthLib.
+
+- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
+- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
+- For Pyramid there is `pyramid-oauthlib`_.
+- For Bottle there is `bottle-oauthlib`_.
+
+If you have written an OAuthLib package that supports your favorite framework,
+please open a Pull Request, updating the documentation.
+
+.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit
+.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib
+.. _`Django REST framework`: http://django-rest-framework.org
+.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
+.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
+.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
+
+Using OAuthLib? Please get in touch!
+------------------------------------
+Patching OAuth support onto an http request framework? Creating an OAuth
+provider extension for a web framework? Simply using OAuthLib to Get Things Done
+or to learn?
+
+No matter which we'd love to hear from you in our `Gitter community`_ or if you have
+anything in particular you would like to have, change or comment on don't
+hesitate for a second to send a pull request or open an issue. We might be quite
+busy and therefore slow to reply but we love feedback!
+
+Chances are you have run into something annoying that you wish there was
+documentation for, if you wish to gain eternal fame and glory, and a drink if we
+have the pleasure to run into each other, please send a docs pull request =)
+
+.. _`Gitter community`: https://gitter.im/oauthlib/Lobby
+
+License
+-------
+
+OAuthLib is yours to use and abuse according to the terms of the BSD license.
+Check the LICENSE file for full details.
+
+Credits
+-------
+
+OAuthLib has been started and maintained several years by Idan Gazit and other
+amazing `AUTHORS`_. Thanks to their wonderful work, the open-source `community`_
+creation has been possible and the project can stay active and reactive to users
+requests.
+
+
+.. _`AUTHORS`: https://github.com/oauthlib/oauthlib/blob/master/AUTHORS
+.. _`community`: https://github.com/oauthlib/
+
+Changelog
+---------
+
+*OAuthLib is in active development, with the core of both OAuth1 and OAuth2
+completed, for providers as well as clients.* See `supported features`_ for
+details.
+
+.. _`supported features`: https://oauthlib.readthedocs.io/en/latest/feature_matrix.html
+
+For a full changelog see ``CHANGELOG.rst``.
diff --git a/contrib/python/oauthlib/oauthlib/__init__.py b/contrib/python/oauthlib/oauthlib/__init__.py
new file mode 100644
index 0000000000..d9a5e38ea0
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/__init__.py
@@ -0,0 +1,34 @@
+"""
+ oauthlib
+ ~~~~~~~~
+
+ A generic, spec-compliant, thorough implementation of the OAuth
+ request-signing logic.
+
+ :copyright: (c) 2019 by The OAuthlib Community
+ :license: BSD, see LICENSE for details.
+"""
+import logging
+from logging import NullHandler
+
+__author__ = 'The OAuthlib Community'
+__version__ = '3.2.2'
+
+logging.getLogger('oauthlib').addHandler(NullHandler())
+
+_DEBUG = False
+
+def set_debug(debug_val):
+ """Set value of debug flag
+
+ :param debug_val: Value to set. Must be a bool value.
+ """
+ global _DEBUG
+ _DEBUG = debug_val
+
+def get_debug():
+ """Get debug mode value.
+
+ :return: `True` if debug mode is on, `False` otherwise
+ """
+ return _DEBUG
diff --git a/contrib/python/oauthlib/oauthlib/common.py b/contrib/python/oauthlib/oauthlib/common.py
new file mode 100644
index 0000000000..395e75efc9
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/common.py
@@ -0,0 +1,432 @@
+"""
+oauthlib.common
+~~~~~~~~~~~~~~
+
+This module provides data structures and utilities common
+to all implementations of OAuth.
+"""
+import collections
+import datetime
+import logging
+import re
+import time
+import urllib.parse as urlparse
+from urllib.parse import (
+ quote as _quote, unquote as _unquote, urlencode as _urlencode,
+)
+
+from . import get_debug
+
+try:
+ from secrets import SystemRandom, randbits
+except ImportError:
+ from random import SystemRandom, getrandbits as randbits
+
+UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ '0123456789')
+
+CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
+ 'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
+
+SANITIZE_PATTERN = re.compile(r'([^&;]*(?:password|token)[^=]*=)[^&;]+', re.IGNORECASE)
+INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
+
+always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ 'abcdefghijklmnopqrstuvwxyz'
+ '0123456789' '_.-')
+
+log = logging.getLogger('oauthlib')
+
+
+# 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
+def quote(s, safe=b'/'):
+ s = s.encode('utf-8') if isinstance(s, str) else s
+ s = _quote(s, safe)
+ # PY3 always returns unicode. PY2 may return either, depending on whether
+ # it had to modify the string.
+ if isinstance(s, bytes):
+ s = s.decode('utf-8')
+ return s
+
+
+def unquote(s):
+ s = _unquote(s)
+ # PY3 always returns unicode. PY2 seems to always return what you give it,
+ # which differs from quote's behavior. Just to be safe, make sure it is
+ # unicode before we return.
+ if isinstance(s, bytes):
+ s = s.decode('utf-8')
+ return s
+
+
+def urlencode(params):
+ utf8_params = encode_params_utf8(params)
+ urlencoded = _urlencode(utf8_params)
+ if isinstance(urlencoded, str):
+ return urlencoded
+ else:
+ return urlencoded.decode("utf-8")
+
+
+def encode_params_utf8(params):
+ """Ensures that all parameters in a list of 2-element tuples are encoded to
+ bytestrings using UTF-8
+ """
+ encoded = []
+ for k, v in params:
+ encoded.append((
+ k.encode('utf-8') if isinstance(k, str) else k,
+ v.encode('utf-8') if isinstance(v, str) else v))
+ return encoded
+
+
+def decode_params_utf8(params):
+ """Ensures that all parameters in a list of 2-element tuples are decoded to
+ unicode using UTF-8.
+ """
+ decoded = []
+ for k, v in params:
+ decoded.append((
+ k.decode('utf-8') if isinstance(k, bytes) else k,
+ v.decode('utf-8') if isinstance(v, bytes) else v))
+ return decoded
+
+
+urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?\'$')
+
+
+def urldecode(query):
+ """Decode a query string in x-www-form-urlencoded format into a sequence
+ of two-element tuples.
+
+ Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
+ correct formatting of the query string by validation. If validation fails
+ a ValueError will be raised. urllib.parse_qsl will only raise errors if
+ any of name-value pairs omits the equals sign.
+ """
+ # Check if query contains invalid characters
+ if query and not set(query) <= urlencoded:
+ error = ("Error trying to decode a non urlencoded string. "
+ "Found invalid characters: %s "
+ "in the string: '%s'. "
+ "Please ensure the request/response body is "
+ "x-www-form-urlencoded.")
+ raise ValueError(error % (set(query) - urlencoded, query))
+
+ # Check for correctly hex encoded values using a regular expression
+ # All encoded values begin with % followed by two hex characters
+ # correct = %00, %A0, %0A, %FF
+ # invalid = %G0, %5H, %PO
+ if INVALID_HEX_PATTERN.search(query):
+ raise ValueError('Invalid hex encoding in query string.')
+
+ # We want to allow queries such as "c2" whereas urlparse.parse_qsl
+ # with the strict_parsing flag will not.
+ params = urlparse.parse_qsl(query, keep_blank_values=True)
+
+ # unicode all the things
+ return decode_params_utf8(params)
+
+
+def extract_params(raw):
+ """Extract parameters and return them as a list of 2-tuples.
+
+ Will successfully extract parameters from urlencoded query strings,
+ dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
+ empty list of parameters. Any other input will result in a return
+ value of None.
+ """
+ if isinstance(raw, (bytes, str)):
+ try:
+ params = urldecode(raw)
+ except ValueError:
+ params = None
+ elif hasattr(raw, '__iter__'):
+ try:
+ dict(raw)
+ except ValueError:
+ params = None
+ except TypeError:
+ params = None
+ else:
+ params = list(raw.items() if isinstance(raw, dict) else raw)
+ params = decode_params_utf8(params)
+ else:
+ params = None
+
+ return params
+
+
+def generate_nonce():
+ """Generate pseudorandom nonce that is unlikely to repeat.
+
+ Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
+ Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+ A random 64-bit number is appended to the epoch timestamp for both
+ randomness and to decrease the likelihood of collisions.
+
+ .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
+ """
+ return str(str(randbits(64)) + generate_timestamp())
+
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC).
+
+ Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
+ Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+ .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
+ """
+ return str(int(time.time()))
+
+
+def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
+ """Generates a non-guessable OAuth token
+
+ OAuth (1 and 2) does not specify the format of tokens except that they
+ should be strings of random characters. Tokens should not be guessable
+ and entropy when generating the random characters is important. Which is
+ why SystemRandom is used instead of the default random.choice method.
+ """
+ rand = SystemRandom()
+ return ''.join(rand.choice(chars) for x in range(length))
+
+
+def generate_signed_token(private_pem, request):
+ import jwt
+
+ now = datetime.datetime.utcnow()
+
+ claims = {
+ 'scope': request.scope,
+ 'exp': now + datetime.timedelta(seconds=request.expires_in)
+ }
+
+ claims.update(request.claims)
+
+ token = jwt.encode(claims, private_pem, 'RS256')
+ token = to_unicode(token, "UTF-8")
+
+ return token
+
+
+def verify_signed_token(public_pem, token):
+ import jwt
+
+ return jwt.decode(token, public_pem, algorithms=['RS256'])
+
+
+def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
+ """Generates an OAuth client_id
+
+ OAuth 2 specify the format of client_id in
+ https://tools.ietf.org/html/rfc6749#appendix-A.
+ """
+ return generate_token(length, chars)
+
+
+def add_params_to_qs(query, params):
+ """Extend a query with a list of two-tuples."""
+ if isinstance(params, dict):
+ params = params.items()
+ queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
+ queryparams.extend(params)
+ return urlencode(queryparams)
+
+
+def add_params_to_uri(uri, params, fragment=False):
+ """Add a list of two-tuples to the uri query components."""
+ sch, net, path, par, query, fra = urlparse.urlparse(uri)
+ if fragment:
+ fra = add_params_to_qs(fra, params)
+ else:
+ query = add_params_to_qs(query, params)
+ return urlparse.urlunparse((sch, net, path, par, query, fra))
+
+
+def safe_string_equals(a, b):
+ """ Near-constant time string comparison.
+
+ Used in order to avoid timing attacks on sensitive information such
+ as secret keys during request verification (`rootLabs`_).
+
+ .. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
+
+ """
+ if len(a) != len(b):
+ return False
+
+ result = 0
+ for x, y in zip(a, b):
+ result |= ord(x) ^ ord(y)
+ return result == 0
+
+
+def to_unicode(data, encoding='UTF-8'):
+ """Convert a number of different types of objects to unicode."""
+ if isinstance(data, str):
+ return data
+
+ if isinstance(data, bytes):
+ return str(data, encoding=encoding)
+
+ if hasattr(data, '__iter__'):
+ try:
+ dict(data)
+ except TypeError:
+ pass
+ except ValueError:
+ # Assume it's a one dimensional data structure
+ return (to_unicode(i, encoding) for i in data)
+ else:
+ # We support 2.6 which lacks dict comprehensions
+ if hasattr(data, 'items'):
+ data = data.items()
+ return {to_unicode(k, encoding): to_unicode(v, encoding) for k, v in data}
+
+ return data
+
+
+class CaseInsensitiveDict(dict):
+
+ """Basic case insensitive dict with strings only keys."""
+
+ proxy = {}
+
+ def __init__(self, data):
+ self.proxy = {k.lower(): k for k in data}
+ for k in data:
+ self[k] = data[k]
+
+ def __contains__(self, k):
+ return k.lower() in self.proxy
+
+ def __delitem__(self, k):
+ key = self.proxy[k.lower()]
+ super().__delitem__(key)
+ del self.proxy[k.lower()]
+
+ def __getitem__(self, k):
+ key = self.proxy[k.lower()]
+ return super().__getitem__(key)
+
+ def get(self, k, default=None):
+ return self[k] if k in self else default
+
+ def __setitem__(self, k, v):
+ super().__setitem__(k, v)
+ self.proxy[k.lower()] = k
+
+ def update(self, *args, **kwargs):
+ super().update(*args, **kwargs)
+ for k in dict(*args, **kwargs):
+ self.proxy[k.lower()] = k
+
+
+class Request:
+
+ """A malleable representation of a signable HTTP request.
+
+ Body argument may contain any data, but parameters will only be decoded if
+ they are one of:
+
+ * urlencoded query string
+ * dict
+ * list of 2-tuples
+
+ Anything else will be treated as raw body data to be passed through
+ unmolested.
+ """
+
+ def __init__(self, uri, http_method='GET', body=None, headers=None,
+ encoding='utf-8'):
+ # Convert to unicode using encoding if given, else assume unicode
+ encode = lambda x: to_unicode(x, encoding) if encoding else x
+
+ self.uri = encode(uri)
+ self.http_method = encode(http_method)
+ self.headers = CaseInsensitiveDict(encode(headers or {}))
+ self.body = encode(body)
+ self.decoded_body = extract_params(self.body)
+ self.oauth_params = []
+ self.validator_log = {}
+
+ self._params = {
+ "access_token": None,
+ "client": None,
+ "client_id": None,
+ "client_secret": None,
+ "code": None,
+ "code_challenge": None,
+ "code_challenge_method": None,
+ "code_verifier": None,
+ "extra_credentials": None,
+ "grant_type": None,
+ "redirect_uri": None,
+ "refresh_token": None,
+ "request_token": None,
+ "response_type": None,
+ "scope": None,
+ "scopes": None,
+ "state": None,
+ "token": None,
+ "user": None,
+ "token_type_hint": None,
+
+ # OpenID Connect
+ "response_mode": None,
+ "nonce": None,
+ "display": None,
+ "prompt": None,
+ "claims": None,
+ "max_age": None,
+ "ui_locales": None,
+ "id_token_hint": None,
+ "login_hint": None,
+ "acr_values": None
+ }
+ self._params.update(dict(urldecode(self.uri_query)))
+ self._params.update(dict(self.decoded_body or []))
+
+ def __getattr__(self, name):
+ if name in self._params:
+ return self._params[name]
+ else:
+ raise AttributeError(name)
+
+ def __repr__(self):
+ if not get_debug():
+ return "<oauthlib.Request SANITIZED>"
+ body = self.body
+ headers = self.headers.copy()
+ if body:
+ body = SANITIZE_PATTERN.sub('\1<SANITIZED>', str(body))
+ if 'Authorization' in headers:
+ headers['Authorization'] = '<SANITIZED>'
+ return '<oauthlib.Request url="{}", http_method="{}", headers="{}", body="{}">'.format(
+ self.uri, self.http_method, headers, body)
+
+ @property
+ def uri_query(self):
+ return urlparse.urlparse(self.uri).query
+
+ @property
+ def uri_query_params(self):
+ if not self.uri_query:
+ return []
+ return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
+ strict_parsing=True)
+
+ @property
+ def duplicate_params(self):
+ seen_keys = collections.defaultdict(int)
+ all_keys = (p[0]
+ for p in (self.decoded_body or []) + self.uri_query_params)
+ for k in all_keys:
+ seen_keys[k] += 1
+ return [k for k, c in seen_keys.items() if c > 1]
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/__init__.py b/contrib/python/oauthlib/oauthlib/oauth1/__init__.py
new file mode 100644
index 0000000000..9caf12a90d
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/__init__.py
@@ -0,0 +1,23 @@
+"""
+oauthlib.oauth1
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 1.0 Client
+and Server classes.
+"""
+from .rfc5849 import (
+ SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
+ SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA,
+ SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512,
+ SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY,
+ Client,
+)
+from .rfc5849.endpoints import (
+ AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
+ ResourceEndpoint, SignatureOnlyEndpoint, WebApplicationServer,
+)
+from .rfc5849.errors import (
+ InsecureTransportError, InvalidClientError, InvalidRequestError,
+ InvalidSignatureMethodError, OAuth1Error,
+)
+from .rfc5849.request_validator import RequestValidator
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/__init__.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/__init__.py
new file mode 100644
index 0000000000..c559251fed
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/__init__.py
@@ -0,0 +1,365 @@
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+
+It supports all three standard signature methods defined in RFC 5849:
+
+- HMAC-SHA1
+- RSA-SHA1
+- PLAINTEXT
+
+It also supports signature methods that are not defined in RFC 5849. These are
+based on the standard ones but replace SHA-1 with the more secure SHA-256:
+
+- HMAC-SHA256
+- RSA-SHA256
+
+"""
+import base64
+import hashlib
+import logging
+import urllib.parse as urlparse
+
+from oauthlib.common import (
+ Request, generate_nonce, generate_timestamp, to_unicode, urlencode,
+)
+
+from . import parameters, signature
+
+log = logging.getLogger(__name__)
+
+# Available signature methods
+#
+# Note: SIGNATURE_HMAC and SIGNATURE_RSA are kept for backward compatibility
+# with previous versions of this library, when it the only HMAC-based and
+# RSA-based signature methods were HMAC-SHA1 and RSA-SHA1. But now that it
+# supports other hashing algorithms besides SHA1, explicitly identifying which
+# hashing algorithm is being used is recommended.
+#
+# Note: if additional values are defined here, don't forget to update the
+# imports in "../__init__.py" so they are available outside this module.
+
+SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
+SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
+SIGNATURE_HMAC_SHA512 = "HMAC-SHA512"
+SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 # deprecated variable for HMAC-SHA1
+
+SIGNATURE_RSA_SHA1 = "RSA-SHA1"
+SIGNATURE_RSA_SHA256 = "RSA-SHA256"
+SIGNATURE_RSA_SHA512 = "RSA-SHA512"
+SIGNATURE_RSA = SIGNATURE_RSA_SHA1 # deprecated variable for RSA-SHA1
+
+SIGNATURE_PLAINTEXT = "PLAINTEXT"
+
+SIGNATURE_METHODS = (
+ SIGNATURE_HMAC_SHA1,
+ SIGNATURE_HMAC_SHA256,
+ SIGNATURE_HMAC_SHA512,
+ SIGNATURE_RSA_SHA1,
+ SIGNATURE_RSA_SHA256,
+ SIGNATURE_RSA_SHA512,
+ SIGNATURE_PLAINTEXT
+)
+
+SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
+SIGNATURE_TYPE_QUERY = 'QUERY'
+SIGNATURE_TYPE_BODY = 'BODY'
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+
+
+class Client:
+
+ """A client used to sign OAuth 1.0 RFC 5849 requests."""
+ SIGNATURE_METHODS = {
+ SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client,
+ SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client,
+ SIGNATURE_HMAC_SHA512: signature.sign_hmac_sha512_with_client,
+ SIGNATURE_RSA_SHA1: signature.sign_rsa_sha1_with_client,
+ SIGNATURE_RSA_SHA256: signature.sign_rsa_sha256_with_client,
+ SIGNATURE_RSA_SHA512: signature.sign_rsa_sha512_with_client,
+ SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
+ }
+
+ @classmethod
+ def register_signature_method(cls, method_name, method_callback):
+ cls.SIGNATURE_METHODS[method_name] = method_callback
+
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC_SHA1,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None, verifier=None, realm=None,
+ encoding='utf-8', decoding=None,
+ nonce=None, timestamp=None):
+ """Create an OAuth 1 client.
+
+ :param client_key: Client key (consumer key), mandatory.
+ :param resource_owner_key: Resource owner key (oauth token).
+ :param resource_owner_secret: Resource owner secret (oauth token secret).
+ :param callback_uri: Callback used when obtaining request token.
+ :param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
+ :param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
+ SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
+ depending on where you want to embed the oauth
+ credentials.
+ :param rsa_key: RSA key used with SIGNATURE_RSA.
+ :param verifier: Verifier used when obtaining an access token.
+ :param realm: Realm (scope) to which access is being requested.
+ :param encoding: If you provide non-unicode input you may use this
+ to have oauthlib automatically convert.
+ :param decoding: If you wish that the returned uri, headers and body
+ from sign be encoded back from unicode, then set
+ decoding to your preferred encoding, i.e. utf-8.
+ :param nonce: Use this nonce instead of generating one. (Mainly for testing)
+ :param timestamp: Use this timestamp instead of using current. (Mainly for testing)
+ """
+ # Convert to unicode using encoding if given, else assume unicode
+ encode = lambda x: to_unicode(x, encoding) if encoding else x
+
+ self.client_key = encode(client_key)
+ self.client_secret = encode(client_secret)
+ self.resource_owner_key = encode(resource_owner_key)
+ self.resource_owner_secret = encode(resource_owner_secret)
+ self.signature_method = encode(signature_method)
+ self.signature_type = encode(signature_type)
+ self.callback_uri = encode(callback_uri)
+ self.rsa_key = encode(rsa_key)
+ self.verifier = encode(verifier)
+ self.realm = encode(realm)
+ self.encoding = encode(encoding)
+ self.decoding = encode(decoding)
+ self.nonce = encode(nonce)
+ self.timestamp = encode(timestamp)
+
+ def __repr__(self):
+ attrs = vars(self).copy()
+ attrs['client_secret'] = '****' if attrs['client_secret'] else None
+ attrs['rsa_key'] = '****' if attrs['rsa_key'] else None
+ attrs[
+ 'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
+ attribute_str = ', '.join('{}={}'.format(k, v) for k, v in attrs.items())
+ return '<{} {}>'.format(self.__class__.__name__, attribute_str)
+
+ def get_oauth_signature(self, request):
+ """Get an OAuth signature to be used in signing a request
+
+ To satisfy `section 3.4.1.2`_ item 2, if the request argument's
+ headers dict attribute contains a Host item, its value will
+ replace any netloc part of the request argument's uri attribute
+ value.
+
+ .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ """
+ if self.signature_method == SIGNATURE_PLAINTEXT:
+ # fast-path
+ return signature.sign_plaintext(self.client_secret,
+ self.resource_owner_secret)
+
+ uri, headers, body = self._render(request)
+
+ collected_params = signature.collect_parameters(
+ uri_query=urlparse.urlparse(uri).query,
+ body=body,
+ headers=headers)
+ log.debug("Collected params: {}".format(collected_params))
+
+ normalized_params = signature.normalize_parameters(collected_params)
+ normalized_uri = signature.base_string_uri(uri, headers.get('Host', None))
+ log.debug("Normalized params: {}".format(normalized_params))
+ log.debug("Normalized URI: {}".format(normalized_uri))
+
+ base_string = signature.signature_base_string(request.http_method,
+ normalized_uri, normalized_params)
+
+ log.debug("Signing: signature base string: {}".format(base_string))
+
+ if self.signature_method not in self.SIGNATURE_METHODS:
+ raise ValueError('Invalid signature method.')
+
+ sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
+
+ log.debug("Signature: {}".format(sig))
+ return sig
+
+ def get_oauth_params(self, request):
+ """Get the basic OAuth parameters to be used in generating a signature.
+ """
+ nonce = (generate_nonce()
+ if self.nonce is None else self.nonce)
+ timestamp = (generate_timestamp()
+ if self.timestamp is None else self.timestamp)
+ params = [
+ ('oauth_nonce', nonce),
+ ('oauth_timestamp', timestamp),
+ ('oauth_version', '1.0'),
+ ('oauth_signature_method', self.signature_method),
+ ('oauth_consumer_key', self.client_key),
+ ]
+ if self.resource_owner_key:
+ params.append(('oauth_token', self.resource_owner_key))
+ if self.callback_uri:
+ params.append(('oauth_callback', self.callback_uri))
+ if self.verifier:
+ params.append(('oauth_verifier', self.verifier))
+
+ # providing body hash for requests other than x-www-form-urlencoded
+ # as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1
+ # 4.1.1. When to include the body hash
+ # * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
+ # * [...] SHOULD include the oauth_body_hash parameter on all other requests.
+ # Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2
+ # At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension.
+ content_type = request.headers.get('Content-Type', None)
+ content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
+ if request.body is not None and content_type_eligible:
+ params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))
+
+ return params
+
+ def _render(self, request, formencode=False, realm=None):
+ """Render a signed request according to signature type
+
+ Returns a 3-tuple containing the request URI, headers, and body.
+
+ If the formencode argument is True and the body contains parameters, it
+ is escaped and returned as a valid formencoded string.
+ """
+ # TODO what if there are body params on a header-type auth?
+ # TODO what if there are query params on a body-type auth?
+
+ uri, headers, body = request.uri, request.headers, request.body
+
+ # TODO: right now these prepare_* methods are very narrow in scope--they
+ # only affect their little thing. In some cases (for example, with
+ # header auth) it might be advantageous to allow these methods to touch
+ # other parts of the request, like the headers—so the prepare_headers
+ # method could also set the Content-Type header to x-www-form-urlencoded
+ # like the spec requires. This would be a fundamental change though, and
+ # I'm not sure how I feel about it.
+ if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
+ headers = parameters.prepare_headers(
+ request.oauth_params, request.headers, realm=realm)
+ elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
+ body = parameters.prepare_form_encoded_body(
+ request.oauth_params, request.decoded_body)
+ if formencode:
+ body = urlencode(body)
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ elif self.signature_type == SIGNATURE_TYPE_QUERY:
+ uri = parameters.prepare_request_uri_query(
+ request.oauth_params, request.uri)
+ else:
+ raise ValueError('Unknown signature type specified.')
+
+ return uri, headers, body
+
+ def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
+ """Sign a request
+
+ Signs an HTTP request with the specified parts.
+
+ Returns a 3-tuple of the signed request's URI, headers, and body.
+ Note that http_method is not returned as it is unaffected by the OAuth
+ signing process. Also worth noting is that duplicate parameters
+ will be included in the signature, regardless of where they are
+ specified (query, body).
+
+ The body argument may be a dict, a list of 2-tuples, or a formencoded
+ string. The Content-Type header must be 'application/x-www-form-urlencoded'
+ if it is present.
+
+ If the body argument is not one of the above, it will be returned
+ verbatim as it is unaffected by the OAuth signing process. Attempting to
+ sign a request with non-formencoded data using the OAuth body signature
+ type is invalid and will raise an exception.
+
+ If the body does contain parameters, it will be returned as a properly-
+ formatted formencoded string.
+
+ Body may not be included if the http_method is either GET or HEAD as
+ this changes the semantic meaning of the request.
+
+ All string data MUST be unicode or be encoded with the same encoding
+ scheme supplied to the Client constructor, default utf-8. This includes
+ strings inside body dicts, for example.
+ """
+ # normalize request data
+ request = Request(uri, http_method, body, headers,
+ encoding=self.encoding)
+
+ # sanity check
+ content_type = request.headers.get('Content-Type', None)
+ multipart = content_type and content_type.startswith('multipart/')
+ should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
+ has_params = request.decoded_body is not None
+ # 3.4.1.3.1. Parameter Sources
+ # [Parameters are collected from the HTTP request entity-body, but only
+ # if [...]:
+ # * The entity-body is single-part.
+ if multipart and has_params:
+ raise ValueError(
+ "Headers indicate a multipart body but body contains parameters.")
+ # * The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # [W3C.REC-html40-19980424].
+ elif should_have_params and not has_params:
+ raise ValueError(
+ "Headers indicate a formencoded body but body was not decodable.")
+ # * The HTTP request entity-header includes the "Content-Type"
+ # header field set to "application/x-www-form-urlencoded".
+ elif not should_have_params and has_params:
+ raise ValueError(
+ "Body contains parameters but Content-Type header was {} "
+ "instead of {}".format(content_type or "not set",
+ CONTENT_TYPE_FORM_URLENCODED))
+
+ # 3.5.2. Form-Encoded Body
+ # Protocol parameters can be transmitted in the HTTP request entity-
+ # body, but only if the following REQUIRED conditions are met:
+ # o The entity-body is single-part.
+ # o The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # [W3C.REC-html40-19980424].
+ # o The HTTP request entity-header includes the "Content-Type" header
+ # field set to "application/x-www-form-urlencoded".
+ elif self.signature_type == SIGNATURE_TYPE_BODY and not (
+ should_have_params and has_params and not multipart):
+ raise ValueError(
+ 'Body signatures may only be used with form-urlencoded content')
+
+ # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ # with the clause that parameters from body should only be included
+ # in non GET or HEAD requests. Extracting the request body parameters
+ # and including them in the signature base string would give semantic
+ # meaning to the body, which it should not have according to the
+ # HTTP 1.1 spec.
+ elif http_method.upper() in ('GET', 'HEAD') and has_params:
+ raise ValueError('GET/HEAD requests should not include body.')
+
+ # generate the basic OAuth parameters
+ request.oauth_params = self.get_oauth_params(request)
+
+ # generate the signature
+ request.oauth_params.append(
+ ('oauth_signature', self.get_oauth_signature(request)))
+
+ # render the signed request and return it
+ uri, headers, body = self._render(request, formencode=True,
+ realm=(realm or self.realm))
+
+ if self.decoding:
+ log.debug('Encoding URI, headers and body to %s.', self.decoding)
+ uri = uri.encode(self.decoding)
+ body = body.encode(self.decoding) if body else body
+ new_headers = {}
+ for k, v in headers.items():
+ new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
+ headers = new_headers
+ return uri, headers, body
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/__init__.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/__init__.py
new file mode 100644
index 0000000000..9f30389f23
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/__init__.py
@@ -0,0 +1,8 @@
+from .access_token import AccessTokenEndpoint
+from .authorization import AuthorizationEndpoint
+from .base import BaseEndpoint
+from .request_token import RequestTokenEndpoint
+from .resource import ResourceEndpoint
+from .signature_only import SignatureOnlyEndpoint
+
+from .pre_configured import WebApplicationServer # isort:skip
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/access_token.py
new file mode 100644
index 0000000000..13665db08f
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/access_token.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.access_token
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the access token provider logic of
+OAuth 1.0 RFC 5849. It validates the correctness of access token requests,
+creates and persists tokens as well as create the proper response to be
+returned to the client.
+"""
+import logging
+
+from oauthlib.common import urlencode
+
+from .. import errors
+from .base import BaseEndpoint
+
+log = logging.getLogger(__name__)
+
+
+class AccessTokenEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for providing OAuth 1 access tokens.
+
+ Typical use is to instantiate with a request validator and invoke the
+ ``create_access_token_response`` from a view function. The tuple returned
+ has all information necessary (body, status, headers) to quickly form
+ and return a proper response. See :doc:`/oauth1/validator` for details on which
+ validator methods to implement for this endpoint.
+ """
+
+ def create_access_token(self, request, credentials):
+ """Create and save a new access token.
+
+ Similar to OAuth 2, indication of granted scopes will be included as a
+ space separated list in ``oauth_authorized_realms``.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The token as an urlencoded string.
+ """
+ request.realms = self.request_validator.get_realms(
+ request.resource_owner_key, request)
+ token = {
+ 'oauth_token': self.token_generator(),
+ 'oauth_token_secret': self.token_generator(),
+ # Backport the authorized scopes indication used in OAuth2
+ 'oauth_authorized_realms': ' '.join(request.realms)
+ }
+ token.update(credentials)
+ self.request_validator.save_access_token(token, request)
+ return urlencode(token.items())
+
+ def create_access_token_response(self, uri, http_method='GET', body=None,
+ headers=None, credentials=None):
+ """Create an access token response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param credentials: A list of extra credentials to include in the token.
+ :returns: A tuple of 3 elements.
+ 1. A dict of headers to set on the response.
+ 2. The response body as a string.
+ 3. The response status code as an integer.
+
+ An example of a valid request::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import AccessTokenEndpoint
+ >>> endpoint = AccessTokenEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_access_token_response(
+ ... 'https://your.provider/access_token?foo=bar',
+ ... headers={
+ ... 'Authorization': 'OAuth oauth_token=234lsdkf....'
+ ... },
+ ... credentials={
+ ... 'my_specific': 'argument',
+ ... })
+ >>> h
+ {'Content-Type': 'application/x-www-form-urlencoded'}
+ >>> b
+ 'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_authorized_realms=movies+pics&my_specific=argument'
+ >>> s
+ 200
+
+ An response to invalid request would have a different body and status::
+
+ >>> b
+ 'error=invalid_request&description=missing+resource+owner+key'
+ >>> s
+ 400
+
+ The same goes for an an unauthorized request:
+
+ >>> b
+ ''
+ >>> s
+ 401
+ """
+ resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ valid, processed_request = self.validate_access_token_request(
+ request)
+ if valid:
+ token = self.create_access_token(request, credentials or {})
+ self.request_validator.invalidate_request_token(
+ request.client_key,
+ request.resource_owner_key,
+ request)
+ return resp_headers, token, 200
+ else:
+ return {}, None, 401
+ except errors.OAuth1Error as e:
+ return resp_headers, e.urlencoded, e.status_code
+
+ def validate_access_token_request(self, request):
+ """Validate an access token request.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :raises: OAuth1Error if the request is invalid.
+ :returns: A tuple of 2 elements.
+ 1. The validation result (True or False).
+ 2. The request object.
+ """
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+
+ if not request.resource_owner_key:
+ raise errors.InvalidRequestError(
+ description='Missing resource owner.')
+
+ if not self.request_validator.check_request_token(
+ request.resource_owner_key):
+ raise errors.InvalidRequestError(
+ description='Invalid resource owner key format.')
+
+ if not request.verifier:
+ raise errors.InvalidRequestError(
+ description='Missing verifier.')
+
+ if not self.request_validator.check_verifier(request.verifier):
+ raise errors.InvalidRequestError(
+ description='Invalid verifier format.')
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request,
+ request_token=request.resource_owner_key):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid or expired token.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy token is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable resource owner enumeration
+ valid_resource_owner = self.request_validator.validate_request_token(
+ request.client_key, request.resource_owner_key, request)
+ if not valid_resource_owner:
+ request.resource_owner_key = self.request_validator.dummy_request_token
+
+ # The server MUST verify (Section 3.2) the validity of the request,
+ # ensure that the resource owner has authorized the provisioning of
+ # token credentials to the client, and ensure that the temporary
+ # credentials have not expired or been used before. The server MUST
+ # also verify the verification code received from the client.
+ # .. _`Section 3.2`: https://tools.ietf.org/html/rfc5849#section-3.2
+ #
+ # Note that early exit would enable resource owner authorization
+ # verifier enumertion.
+ valid_verifier = self.request_validator.validate_verifier(
+ request.client_key,
+ request.resource_owner_key,
+ request.verifier,
+ request)
+
+ valid_signature = self._check_signature(request, is_token_request=True)
+
+ # log the results to the validator_log
+ # this lets us handle internal reporting and analysis
+ request.validator_log['client'] = valid_client
+ request.validator_log['resource_owner'] = valid_resource_owner
+ request.validator_log['verifier'] = valid_verifier
+ request.validator_log['signature'] = valid_signature
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_resource_owner, valid_verifier,
+ valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client:, %s", valid_client)
+ log.info("Valid token:, %s", valid_resource_owner)
+ log.info("Valid verifier:, %s", valid_verifier)
+ log.info("Valid signature:, %s", valid_signature)
+ return v, request
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/authorization.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/authorization.py
new file mode 100644
index 0000000000..00d9576b01
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/authorization.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.authorization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from urllib.parse import urlencode
+
+from oauthlib.common import add_params_to_uri
+
+from .. import errors
+from .base import BaseEndpoint
+
+
+class AuthorizationEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for letting authenticated users authorize access
+ to their protected resources to a client.
+
+ Typical use would be to have two views, one for displaying the authorization
+ form and one to process said form on submission.
+
+ The first view will want to utilize ``get_realms_and_credentials`` to fetch
+ requested realms and useful client credentials, such as name and
+ description, to be used when creating the authorization form.
+
+ During form processing you can use ``create_authorization_response`` to
+ validate the request, create a verifier as well as prepare the final
+ redirection URI used to send the user back to the client.
+
+ See :doc:`/oauth1/validator` for details on which validator methods to implement
+ for this endpoint.
+ """
+
+ def create_verifier(self, request, credentials):
+ """Create and save a new request token.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param credentials: A dict of extra token credentials.
+ :returns: The verifier as a dict.
+ """
+ verifier = {
+ 'oauth_token': request.resource_owner_key,
+ 'oauth_verifier': self.token_generator(),
+ }
+ verifier.update(credentials)
+ self.request_validator.save_verifier(
+ request.resource_owner_key, verifier, request)
+ return verifier
+
+ def create_authorization_response(self, uri, http_method='GET', body=None,
+ headers=None, realms=None, credentials=None):
+ """Create an authorization response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param credentials: A list of credentials to include in the verifier.
+ :returns: A tuple of 3 elements.
+ 1. A dict of headers to set on the response.
+ 2. The response body as a string.
+ 3. The response status code as an integer.
+
+ If the callback URI tied to the current token is "oob", a response with
+ a 200 status code will be returned. In this case, it may be desirable to
+ modify the response to better display the verifier to the client.
+
+ An example of an authorization request::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import AuthorizationEndpoint
+ >>> endpoint = AuthorizationEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_authorization_response(
+ ... 'https://your.provider/authorize?oauth_token=...',
+ ... credentials={
+ ... 'extra': 'argument',
+ ... })
+ >>> h
+ {'Location': 'https://the.client/callback?oauth_verifier=...&extra=argument'}
+ >>> b
+ None
+ >>> s
+ 302
+
+ An example of a request with an "oob" callback::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import AuthorizationEndpoint
+ >>> endpoint = AuthorizationEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_authorization_response(
+ ... 'https://your.provider/authorize?foo=bar',
+ ... credentials={
+ ... 'extra': 'argument',
+ ... })
+ >>> h
+ {'Content-Type': 'application/x-www-form-urlencoded'}
+ >>> b
+ 'oauth_verifier=...&extra=argument'
+ >>> s
+ 200
+ """
+ request = self._create_request(uri, http_method=http_method, body=body,
+ headers=headers)
+
+ if not request.resource_owner_key:
+ raise errors.InvalidRequestError(
+ 'Missing mandatory parameter oauth_token.')
+ if not self.request_validator.verify_request_token(
+ request.resource_owner_key, request):
+ raise errors.InvalidClientError()
+
+ request.realms = realms
+ if (request.realms and not self.request_validator.verify_realms(
+ request.resource_owner_key, request.realms, request)):
+ raise errors.InvalidRequestError(
+ description=('User granted access to realms outside of '
+ 'what the client may request.'))
+
+ verifier = self.create_verifier(request, credentials or {})
+ redirect_uri = self.request_validator.get_redirect_uri(
+ request.resource_owner_key, request)
+ if redirect_uri == 'oob':
+ response_headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'}
+ response_body = urlencode(verifier)
+ return response_headers, response_body, 200
+ else:
+ populated_redirect = add_params_to_uri(
+ redirect_uri, verifier.items())
+ return {'Location': populated_redirect}, None, 302
+
+ def get_realms_and_credentials(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Fetch realms and credentials for the presented request token.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :returns: A tuple of 2 elements.
+ 1. A list of request realms.
+ 2. A dict of credentials which may be useful in creating the
+ authorization form.
+ """
+ request = self._create_request(uri, http_method=http_method, body=body,
+ headers=headers)
+
+ if not self.request_validator.verify_request_token(
+ request.resource_owner_key, request):
+ raise errors.InvalidClientError()
+
+ realms = self.request_validator.get_realms(
+ request.resource_owner_key, request)
+ return realms, {'resource_owner_key': request.resource_owner_key}
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/base.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/base.py
new file mode 100644
index 0000000000..7831be7c5e
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/base.py
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.base
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+import time
+
+from oauthlib.common import CaseInsensitiveDict, Request, generate_token
+
+from .. import (
+ CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
+ SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1,
+ SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, SIGNATURE_TYPE_AUTH_HEADER,
+ SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, errors, signature, utils,
+)
+
+
+class BaseEndpoint:
+
+ def __init__(self, request_validator, token_generator=None):
+ self.request_validator = request_validator
+ self.token_generator = token_generator or generate_token
+
+ def _get_signature_type_and_params(self, request):
+ """Extracts parameters from query, headers and body. Signature type
+ is set to the source in which parameters were found.
+ """
+ # Per RFC5849, only the Authorization header may contain the 'realm'
+ # optional parameter.
+ header_params = signature.collect_parameters(headers=request.headers,
+ exclude_oauth_signature=False, with_realm=True)
+ body_params = signature.collect_parameters(body=request.body,
+ exclude_oauth_signature=False)
+ query_params = signature.collect_parameters(uri_query=request.uri_query,
+ exclude_oauth_signature=False)
+
+ params = []
+ params.extend(header_params)
+ params.extend(body_params)
+ params.extend(query_params)
+ signature_types_with_oauth_params = list(filter(lambda s: s[2], (
+ (SIGNATURE_TYPE_AUTH_HEADER, params,
+ utils.filter_oauth_params(header_params)),
+ (SIGNATURE_TYPE_BODY, params,
+ utils.filter_oauth_params(body_params)),
+ (SIGNATURE_TYPE_QUERY, params,
+ utils.filter_oauth_params(query_params))
+ )))
+
+ if len(signature_types_with_oauth_params) > 1:
+ found_types = [s[0] for s in signature_types_with_oauth_params]
+ raise errors.InvalidRequestError(
+ description=('oauth_ params must come from only 1 signature'
+ 'type but were found in %s',
+ ', '.join(found_types)))
+
+ try:
+ signature_type, params, oauth_params = signature_types_with_oauth_params[
+ 0]
+ except IndexError:
+ raise errors.InvalidRequestError(
+ description='Missing mandatory OAuth parameters.')
+
+ return signature_type, params, oauth_params
+
+ def _create_request(self, uri, http_method, body, headers):
+ # Only include body data from x-www-form-urlencoded requests
+ headers = CaseInsensitiveDict(headers or {})
+ if ("Content-Type" in headers and
+ CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
+ request = Request(uri, http_method, body, headers)
+ else:
+ request = Request(uri, http_method, '', headers)
+
+ signature_type, params, oauth_params = (
+ self._get_signature_type_and_params(request))
+
+ # The server SHOULD return a 400 (Bad Request) status code when
+ # receiving a request with duplicated protocol parameters.
+ if len(dict(oauth_params)) != len(oauth_params):
+ raise errors.InvalidRequestError(
+ description='Duplicate OAuth1 entries.')
+
+ oauth_params = dict(oauth_params)
+ request.signature = oauth_params.get('oauth_signature')
+ request.client_key = oauth_params.get('oauth_consumer_key')
+ request.resource_owner_key = oauth_params.get('oauth_token')
+ request.nonce = oauth_params.get('oauth_nonce')
+ request.timestamp = oauth_params.get('oauth_timestamp')
+ request.redirect_uri = oauth_params.get('oauth_callback')
+ request.verifier = oauth_params.get('oauth_verifier')
+ request.signature_method = oauth_params.get('oauth_signature_method')
+ request.realm = dict(params).get('realm')
+ request.oauth_params = oauth_params
+
+ # Parameters to Client depend on signature method which may vary
+ # for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters
+ request.params = [(k, v) for k, v in params if k != "oauth_signature"]
+
+ if 'realm' in request.headers.get('Authorization', ''):
+ request.params = [(k, v)
+ for k, v in request.params if k != "realm"]
+
+ return request
+
+ def _check_transport_security(self, request):
+ # TODO: move into oauthlib.common from oauth2.utils
+ if (self.request_validator.enforce_ssl and
+ not request.uri.lower().startswith("https://")):
+ raise errors.InsecureTransportError()
+
+ def _check_mandatory_parameters(self, request):
+ # The server SHOULD return a 400 (Bad Request) status code when
+ # receiving a request with missing parameters.
+ if not all((request.signature, request.client_key,
+ request.nonce, request.timestamp,
+ request.signature_method)):
+ raise errors.InvalidRequestError(
+ description='Missing mandatory OAuth parameters.')
+
+ # OAuth does not mandate a particular signature method, as each
+ # implementation can have its own unique requirements. Servers are
+ # free to implement and document their own custom methods.
+ # Recommending any particular method is beyond the scope of this
+ # specification. Implementers should review the Security
+ # Considerations section (`Section 4`_) before deciding on which
+ # method to support.
+ # .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4
+ if (not request.signature_method in
+ self.request_validator.allowed_signature_methods):
+ raise errors.InvalidSignatureMethodError(
+ description="Invalid signature, {} not in {!r}.".format(
+ request.signature_method,
+ self.request_validator.allowed_signature_methods))
+
+ # Servers receiving an authenticated request MUST validate it by:
+ # If the "oauth_version" parameter is present, ensuring its value is
+ # "1.0".
+ if ('oauth_version' in request.oauth_params and
+ request.oauth_params['oauth_version'] != '1.0'):
+ raise errors.InvalidRequestError(
+ description='Invalid OAuth version.')
+
+ # The timestamp value MUST be a positive integer. Unless otherwise
+ # specified by the server's documentation, the timestamp is expressed
+ # in the number of seconds since January 1, 1970 00:00:00 GMT.
+ if len(request.timestamp) != 10:
+ raise errors.InvalidRequestError(
+ description='Invalid timestamp size')
+
+ try:
+ ts = int(request.timestamp)
+
+ except ValueError:
+ raise errors.InvalidRequestError(
+ description='Timestamp must be an integer.')
+
+ else:
+ # To avoid the need to retain an infinite number of nonce values for
+ # future checks, servers MAY choose to restrict the time period after
+ # which a request with an old timestamp is rejected.
+ if abs(time.time() - ts) > self.request_validator.timestamp_lifetime:
+ raise errors.InvalidRequestError(
+ description=('Timestamp given is invalid, differ from '
+ 'allowed by over %s seconds.' % (
+ self.request_validator.timestamp_lifetime)))
+
+ # Provider specific validation of parameters, used to enforce
+ # restrictions such as character set and length.
+ if not self.request_validator.check_client_key(request.client_key):
+ raise errors.InvalidRequestError(
+ description='Invalid client key format.')
+
+ if not self.request_validator.check_nonce(request.nonce):
+ raise errors.InvalidRequestError(
+ description='Invalid nonce format.')
+
+ def _check_signature(self, request, is_token_request=False):
+ # ---- RSA Signature verification ----
+ if request.signature_method == SIGNATURE_RSA_SHA1 or \
+ request.signature_method == SIGNATURE_RSA_SHA256 or \
+ request.signature_method == SIGNATURE_RSA_SHA512:
+ # RSA-based signature method
+
+ # The server verifies the signature per `[RFC3447] section 8.2.2`_
+ # .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1
+
+ rsa_key = self.request_validator.get_rsa_key(
+ request.client_key, request)
+
+ if request.signature_method == SIGNATURE_RSA_SHA1:
+ valid_signature = signature.verify_rsa_sha1(request, rsa_key)
+ elif request.signature_method == SIGNATURE_RSA_SHA256:
+ valid_signature = signature.verify_rsa_sha256(request, rsa_key)
+ elif request.signature_method == SIGNATURE_RSA_SHA512:
+ valid_signature = signature.verify_rsa_sha512(request, rsa_key)
+ else:
+ valid_signature = False
+
+ # ---- HMAC or Plaintext Signature verification ----
+ else:
+ # Non-RSA based signature method
+
+ # Servers receiving an authenticated request MUST validate it by:
+ # Recalculating the request signature independently as described in
+ # `Section 3.4`_ and comparing it to the value received from the
+ # client via the "oauth_signature" parameter.
+ # .. _`Section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+
+ client_secret = self.request_validator.get_client_secret(
+ request.client_key, request)
+
+ resource_owner_secret = None
+ if request.resource_owner_key:
+ if is_token_request:
+ resource_owner_secret = \
+ self.request_validator.get_request_token_secret(
+ request.client_key, request.resource_owner_key,
+ request)
+ else:
+ resource_owner_secret = \
+ self.request_validator.get_access_token_secret(
+ request.client_key, request.resource_owner_key,
+ request)
+
+ if request.signature_method == SIGNATURE_HMAC_SHA1:
+ valid_signature = signature.verify_hmac_sha1(
+ request, client_secret, resource_owner_secret)
+ elif request.signature_method == SIGNATURE_HMAC_SHA256:
+ valid_signature = signature.verify_hmac_sha256(
+ request, client_secret, resource_owner_secret)
+ elif request.signature_method == SIGNATURE_HMAC_SHA512:
+ valid_signature = signature.verify_hmac_sha512(
+ request, client_secret, resource_owner_secret)
+ elif request.signature_method == SIGNATURE_PLAINTEXT:
+ valid_signature = signature.verify_plaintext(
+ request, client_secret, resource_owner_secret)
+ else:
+ valid_signature = False
+
+ return valid_signature
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py
new file mode 100644
index 0000000000..23e3cfc84e
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py
@@ -0,0 +1,14 @@
+from . import (
+ AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
+ ResourceEndpoint,
+)
+
+
+class WebApplicationServer(RequestTokenEndpoint, AuthorizationEndpoint,
+ AccessTokenEndpoint, ResourceEndpoint):
+
+ def __init__(self, request_validator):
+ RequestTokenEndpoint.__init__(self, request_validator)
+ AuthorizationEndpoint.__init__(self, request_validator)
+ AccessTokenEndpoint.__init__(self, request_validator)
+ ResourceEndpoint.__init__(self, request_validator)
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/request_token.py
new file mode 100644
index 0000000000..0323cfb845
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/request_token.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.request_token
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the request token provider logic of
+OAuth 1.0 RFC 5849. It validates the correctness of request token requests,
+creates and persists tokens as well as create the proper response to be
+returned to the client.
+"""
+import logging
+
+from oauthlib.common import urlencode
+
+from .. import errors
+from .base import BaseEndpoint
+
+log = logging.getLogger(__name__)
+
+
+class RequestTokenEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for providing OAuth 1 request tokens.
+
+ Typical use is to instantiate with a request validator and invoke the
+ ``create_request_token_response`` from a view function. The tuple returned
+ has all information necessary (body, status, headers) to quickly form
+ and return a proper response. See :doc:`/oauth1/validator` for details on which
+ validator methods to implement for this endpoint.
+ """
+
+ def create_request_token(self, request, credentials):
+ """Create and save a new request token.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param credentials: A dict of extra token credentials.
+ :returns: The token as an urlencoded string.
+ """
+ token = {
+ 'oauth_token': self.token_generator(),
+ 'oauth_token_secret': self.token_generator(),
+ 'oauth_callback_confirmed': 'true'
+ }
+ token.update(credentials)
+ self.request_validator.save_request_token(token, request)
+ return urlencode(token.items())
+
+ def create_request_token_response(self, uri, http_method='GET', body=None,
+ headers=None, credentials=None):
+ """Create a request token response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param credentials: A list of extra credentials to include in the token.
+ :returns: A tuple of 3 elements.
+ 1. A dict of headers to set on the response.
+ 2. The response body as a string.
+ 3. The response status code as an integer.
+
+ An example of a valid request::
+
+ >>> from your_validator import your_validator
+ >>> from oauthlib.oauth1 import RequestTokenEndpoint
+ >>> endpoint = RequestTokenEndpoint(your_validator)
+ >>> h, b, s = endpoint.create_request_token_response(
+ ... 'https://your.provider/request_token?foo=bar',
+ ... headers={
+ ... 'Authorization': 'OAuth realm=movies user, oauth_....'
+ ... },
+ ... credentials={
+ ... 'my_specific': 'argument',
+ ... })
+ >>> h
+ {'Content-Type': 'application/x-www-form-urlencoded'}
+ >>> b
+ 'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_callback_confirmed=true&my_specific=argument'
+ >>> s
+ 200
+
+ An response to invalid request would have a different body and status::
+
+ >>> b
+ 'error=invalid_request&description=missing+callback+uri'
+ >>> s
+ 400
+
+ The same goes for an an unauthorized request:
+
+ >>> b
+ ''
+ >>> s
+ 401
+ """
+ resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ valid, processed_request = self.validate_request_token_request(
+ request)
+ if valid:
+ token = self.create_request_token(request, credentials or {})
+ return resp_headers, token, 200
+ else:
+ return {}, None, 401
+ except errors.OAuth1Error as e:
+ return resp_headers, e.urlencoded, e.status_code
+
+ def validate_request_token_request(self, request):
+ """Validate a request token request.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :raises: OAuth1Error if the request is invalid.
+ :returns: A tuple of 2 elements.
+ 1. The validation result (True or False).
+ 2. The request object.
+ """
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+
+ if request.realm:
+ request.realms = request.realm.split(' ')
+ else:
+ request.realms = self.request_validator.get_default_realms(
+ request.client_key, request)
+ if not self.request_validator.check_realms(request.realms):
+ raise errors.InvalidRequestError(
+ description='Invalid realm {}. Allowed are {!r}.'.format(
+ request.realms, self.request_validator.realms))
+
+ if not request.redirect_uri:
+ raise errors.InvalidRequestError(
+ description='Missing callback URI.')
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request,
+ request_token=request.resource_owner_key):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ # Note that `realm`_ is only used in authorization headers and how
+ # it should be interpreted is not included in the OAuth spec.
+ # However they could be seen as a scope or realm to which the
+ # client has access and as such every client should be checked
+ # to ensure it is authorized access to that scope or realm.
+ # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2
+ #
+ # Note that early exit would enable client realm access enumeration.
+ #
+ # The require_realm indicates this is the first step in the OAuth
+ # workflow where a client requests access to a specific realm.
+ # This first step (obtaining request token) need not require a realm
+ # and can then be identified by checking the require_resource_owner
+ # flag and absence of realm.
+ #
+ # Clients obtaining an access token will not supply a realm and it will
+ # not be checked. Instead the previously requested realm should be
+ # transferred from the request token to the access token.
+ #
+ # Access to protected resources will always validate the realm but note
+ # that the realm is now tied to the access token and not provided by
+ # the client.
+ valid_realm = self.request_validator.validate_requested_realms(
+ request.client_key, request.realms, request)
+
+ # Callback is normally never required, except for requests for
+ # a Temporary Credential as described in `Section 2.1`_
+ # .._`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
+ valid_redirect = self.request_validator.validate_redirect_uri(
+ request.client_key, request.redirect_uri, request)
+ if not request.redirect_uri:
+ raise NotImplementedError('Redirect URI must either be provided '
+ 'or set to a default during validation.')
+
+ valid_signature = self._check_signature(request)
+
+ # log the results to the validator_log
+ # this lets us handle internal reporting and analysis
+ request.validator_log['client'] = valid_client
+ request.validator_log['realm'] = valid_realm
+ request.validator_log['callback'] = valid_redirect
+ request.validator_log['signature'] = valid_signature
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_realm, valid_redirect, valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client: %s.", valid_client)
+ log.info("Valid realm: %s.", valid_realm)
+ log.info("Valid callback: %s.", valid_redirect)
+ log.info("Valid signature: %s.", valid_signature)
+ return v, request
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/resource.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/resource.py
new file mode 100644
index 0000000000..8641152e4e
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/resource.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.resource
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the resource protection provider logic of
+OAuth 1.0 RFC 5849.
+"""
+import logging
+
+from .. import errors
+from .base import BaseEndpoint
+
+log = logging.getLogger(__name__)
+
+
+class ResourceEndpoint(BaseEndpoint):
+
+ """An endpoint responsible for protecting resources.
+
+ Typical use is to instantiate with a request validator and invoke the
+ ``validate_protected_resource_request`` in a decorator around a view
+ function. If the request is valid, invoke and return the response of the
+ view. If invalid create and return an error response directly from the
+ decorator.
+
+ See :doc:`/oauth1/validator` for details on which validator methods to implement
+ for this endpoint.
+
+ An example decorator::
+
+ from functools import wraps
+ from your_validator import your_validator
+ from oauthlib.oauth1 import ResourceEndpoint
+ endpoint = ResourceEndpoint(your_validator)
+
+ def require_oauth(realms=None):
+ def decorator(f):
+ @wraps(f)
+ def wrapper(request, *args, **kwargs):
+ v, r = provider.validate_protected_resource_request(
+ request.url,
+ http_method=request.method,
+ body=request.data,
+ headers=request.headers,
+ realms=realms or [])
+ if v:
+ return f(*args, **kwargs)
+ else:
+ return abort(403)
+ """
+
+ def validate_protected_resource_request(self, uri, http_method='GET',
+ body=None, headers=None, realms=None):
+ """Create a request token response, with a new request token if valid.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :param realms: A list of realms the resource is protected under.
+ This will be supplied to the ``validate_realms``
+ method of the request validator.
+ :returns: A tuple of 2 elements.
+ 1. True if valid, False otherwise.
+ 2. An oauthlib.common.Request object.
+ """
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ except errors.OAuth1Error:
+ return False, None
+
+ try:
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+ except errors.OAuth1Error:
+ return False, request
+
+ if not request.resource_owner_key:
+ return False, request
+
+ if not self.request_validator.check_access_token(
+ request.resource_owner_key):
+ return False, request
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request,
+ access_token=request.resource_owner_key):
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid or expired token.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy token is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable resource owner enumeration
+ valid_resource_owner = self.request_validator.validate_access_token(
+ request.client_key, request.resource_owner_key, request)
+ if not valid_resource_owner:
+ request.resource_owner_key = self.request_validator.dummy_access_token
+
+ # Note that `realm`_ is only used in authorization headers and how
+ # it should be interpreted is not included in the OAuth spec.
+ # However they could be seen as a scope or realm to which the
+ # client has access and as such every client should be checked
+ # to ensure it is authorized access to that scope or realm.
+ # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2
+ #
+ # Note that early exit would enable client realm access enumeration.
+ #
+ # The require_realm indicates this is the first step in the OAuth
+ # workflow where a client requests access to a specific realm.
+ # This first step (obtaining request token) need not require a realm
+ # and can then be identified by checking the require_resource_owner
+ # flag and absence of realm.
+ #
+ # Clients obtaining an access token will not supply a realm and it will
+ # not be checked. Instead the previously requested realm should be
+ # transferred from the request token to the access token.
+ #
+ # Access to protected resources will always validate the realm but note
+ # that the realm is now tied to the access token and not provided by
+ # the client.
+ valid_realm = self.request_validator.validate_realms(request.client_key,
+ request.resource_owner_key, request, uri=request.uri,
+ realms=realms)
+
+ valid_signature = self._check_signature(request)
+
+ # log the results to the validator_log
+ # this lets us handle internal reporting and analysis
+ request.validator_log['client'] = valid_client
+ request.validator_log['resource_owner'] = valid_resource_owner
+ request.validator_log['realm'] = valid_realm
+ request.validator_log['signature'] = valid_signature
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_resource_owner, valid_realm,
+ valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client: %s", valid_client)
+ log.info("Valid token: %s", valid_resource_owner)
+ log.info("Valid realm: %s", valid_realm)
+ log.info("Valid signature: %s", valid_signature)
+ return v, request
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/signature_only.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/signature_only.py
new file mode 100644
index 0000000000..d693ccb7f6
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/endpoints/signature_only.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.signature_only
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the signing logic of OAuth 1.0 RFC 5849.
+"""
+
+import logging
+
+from .. import errors
+from .base import BaseEndpoint
+
+log = logging.getLogger(__name__)
+
+
+class SignatureOnlyEndpoint(BaseEndpoint):
+
+ """An endpoint only responsible for verifying an oauth signature."""
+
+ def validate_request(self, uri, http_method='GET',
+ body=None, headers=None):
+ """Validate a signed OAuth request.
+
+ :param uri: The full URI of the token request.
+ :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+ :param body: The request body as a string.
+ :param headers: The request headers as a dict.
+ :returns: A tuple of 2 elements.
+ 1. True if valid, False otherwise.
+ 2. An oauthlib.common.Request object.
+ """
+ try:
+ request = self._create_request(uri, http_method, body, headers)
+ except errors.OAuth1Error as err:
+ log.info(
+ 'Exception caught while validating request, %s.' % err)
+ return False, None
+
+ try:
+ self._check_transport_security(request)
+ self._check_mandatory_parameters(request)
+ except errors.OAuth1Error as err:
+ log.info(
+ 'Exception caught while validating request, %s.' % err)
+ return False, request
+
+ if not self.request_validator.validate_timestamp_and_nonce(
+ request.client_key, request.timestamp, request.nonce, request):
+ log.debug('[Failure] verification failed: timestamp/nonce')
+ return False, request
+
+ # The server SHOULD return a 401 (Unauthorized) status code when
+ # receiving a request with invalid client credentials.
+ # Note: This is postponed in order to avoid timing attacks, instead
+ # a dummy client is assigned and used to maintain near constant
+ # time request verification.
+ #
+ # Note that early exit would enable client enumeration
+ valid_client = self.request_validator.validate_client_key(
+ request.client_key, request)
+ if not valid_client:
+ request.client_key = self.request_validator.dummy_client
+
+ valid_signature = self._check_signature(request)
+
+ # log the results to the validator_log
+ # this lets us handle internal reporting and analysis
+ request.validator_log['client'] = valid_client
+ request.validator_log['signature'] = valid_signature
+
+ # We delay checking validity until the very end, using dummy values for
+ # calculations and fetching secrets/keys to ensure the flow of every
+ # request remains almost identical regardless of whether valid values
+ # have been supplied. This ensures near constant time execution and
+ # prevents malicious users from guessing sensitive information
+ v = all((valid_client, valid_signature))
+ if not v:
+ log.info("[Failure] request verification failed.")
+ log.info("Valid client: %s", valid_client)
+ log.info("Valid signature: %s", valid_signature)
+ return v, request
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/errors.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/errors.py
new file mode 100644
index 0000000000..8774d40741
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/errors.py
@@ -0,0 +1,76 @@
+"""
+oauthlib.oauth1.rfc5849.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 1 clients and provicers to represent the spec
+defined error responses for all four core grant types.
+"""
+from oauthlib.common import add_params_to_uri, urlencode
+
+
+class OAuth1Error(Exception):
+ error = None
+ description = ''
+
+ def __init__(self, description=None, uri=None, status_code=400,
+ request=None):
+ """
+ description: A human-readable ASCII [USASCII] text providing
+ additional information, used to assist the client
+ developer in understanding the error that occurred.
+ Values for the "error_description" parameter MUST NOT
+ include characters outside the set
+ x20-21 / x23-5B / x5D-7E.
+
+ uri: A URI identifying a human-readable web page with information
+ about the error, used to provide the client developer with
+ additional information about the error. Values for the
+ "error_uri" parameter MUST conform to the URI- Reference
+ syntax, and thus MUST NOT include characters outside the set
+ x21 / x23-5B / x5D-7E.
+
+ state: A CSRF protection value received from the client.
+
+ request: Oauthlib Request object
+ """
+ self.description = description or self.description
+ message = '({}) {}'.format(self.error, self.description)
+ if request:
+ message += ' ' + repr(request)
+ super().__init__(message)
+
+ self.uri = uri
+ self.status_code = status_code
+
+ def in_uri(self, uri):
+ return add_params_to_uri(uri, self.twotuples)
+
+ @property
+ def twotuples(self):
+ error = [('error', self.error)]
+ if self.description:
+ error.append(('error_description', self.description))
+ if self.uri:
+ error.append(('error_uri', self.uri))
+ return error
+
+ @property
+ def urlencoded(self):
+ return urlencode(self.twotuples)
+
+
+class InsecureTransportError(OAuth1Error):
+ error = 'insecure_transport_protocol'
+ description = 'Only HTTPS connections are permitted.'
+
+
+class InvalidSignatureMethodError(OAuth1Error):
+ error = 'invalid_signature_method'
+
+
+class InvalidRequestError(OAuth1Error):
+ error = 'invalid_request'
+
+
+class InvalidClientError(OAuth1Error):
+ error = 'invalid_client'
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/parameters.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/parameters.py
new file mode 100644
index 0000000000..2163772df3
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/parameters.py
@@ -0,0 +1,133 @@
+"""
+oauthlib.parameters
+~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
+
+.. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5
+"""
+from urllib.parse import urlparse, urlunparse
+
+from oauthlib.common import extract_params, urlencode
+
+from . import utils
+
+
+# TODO: do we need filter_params now that oauth_params are handled by Request?
+# We can easily pass in just oauth protocol params.
+@utils.filter_params
+def prepare_headers(oauth_params, headers=None, realm=None):
+ """**Prepare the Authorization header.**
+ Per `section 3.5.1`_ of the spec.
+
+ Protocol parameters can be transmitted using the HTTP "Authorization"
+ header field as defined by `RFC2617`_ with the auth-scheme name set to
+ "OAuth" (case insensitive).
+
+ For example::
+
+ Authorization: OAuth realm="Example",
+ oauth_consumer_key="0685bd9184jfhq22",
+ oauth_token="ad180jjd733klru7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+ oauth_timestamp="137131200",
+ oauth_nonce="4572616e48616d6d65724c61686176",
+ oauth_version="1.0"
+
+
+ .. _`section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1
+ .. _`RFC2617`: https://tools.ietf.org/html/rfc2617
+ """
+ headers = headers or {}
+
+ # Protocol parameters SHALL be included in the "Authorization" header
+ # field as follows:
+ authorization_header_parameters_parts = []
+ for oauth_parameter_name, value in oauth_params:
+ # 1. Parameter names and values are encoded per Parameter Encoding
+ # (`Section 3.6`_)
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ escaped_name = utils.escape(oauth_parameter_name)
+ escaped_value = utils.escape(value)
+
+ # 2. Each parameter's name is immediately followed by an "=" character
+ # (ASCII code 61), a """ character (ASCII code 34), the parameter
+ # value (MAY be empty), and another """ character (ASCII code 34).
+ part = '{}="{}"'.format(escaped_name, escaped_value)
+
+ authorization_header_parameters_parts.append(part)
+
+ # 3. Parameters are separated by a "," character (ASCII code 44) and
+ # OPTIONAL linear whitespace per `RFC2617`_.
+ #
+ # .. _`RFC2617`: https://tools.ietf.org/html/rfc2617
+ authorization_header_parameters = ', '.join(
+ authorization_header_parameters_parts)
+
+ # 4. The OPTIONAL "realm" parameter MAY be added and interpreted per
+ # `RFC2617 section 1.2`_.
+ #
+ # .. _`RFC2617 section 1.2`: https://tools.ietf.org/html/rfc2617#section-1.2
+ if realm:
+ # NOTE: realm should *not* be escaped
+ authorization_header_parameters = ('realm="%s", ' % realm +
+ authorization_header_parameters)
+
+ # the auth-scheme name set to "OAuth" (case insensitive).
+ authorization_header = 'OAuth %s' % authorization_header_parameters
+
+ # contribute the Authorization header to the given headers
+ full_headers = {}
+ full_headers.update(headers)
+ full_headers['Authorization'] = authorization_header
+ return full_headers
+
+
+def _append_params(oauth_params, params):
+ """Append OAuth params to an existing set of parameters.
+
+ Both params and oauth_params is must be lists of 2-tuples.
+
+ Per `section 3.5.2`_ and `3.5.3`_ of the spec.
+
+ .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2
+ .. _`3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3
+
+ """
+ merged = list(params)
+ merged.extend(oauth_params)
+ # The request URI / entity-body MAY include other request-specific
+ # parameters, in which case, the protocol parameters SHOULD be appended
+ # following the request-specific parameters, properly separated by an "&"
+ # character (ASCII code 38)
+ merged.sort(key=lambda i: i[0].startswith('oauth_'))
+ return merged
+
+
+def prepare_form_encoded_body(oauth_params, body):
+ """Prepare the Form-Encoded Body.
+
+ Per `section 3.5.2`_ of the spec.
+
+ .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2
+
+ """
+ # append OAuth params to the existing body
+ return _append_params(oauth_params, body)
+
+
+def prepare_request_uri_query(oauth_params, uri):
+ """Prepare the Request URI Query.
+
+ Per `section 3.5.3`_ of the spec.
+
+ .. _`section 3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3
+
+ """
+ # append OAuth params to the existing set of query components
+ sch, net, path, par, query, fra = urlparse(uri)
+ query = urlencode(
+ _append_params(oauth_params, extract_params(query) or []))
+ return urlunparse((sch, net, path, par, query, fra))
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/request_validator.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/request_validator.py
new file mode 100644
index 0000000000..e937aabf40
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/request_validator.py
@@ -0,0 +1,849 @@
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from . import SIGNATURE_METHODS, utils
+
+
+class RequestValidator:
+
+ """A validator/datastore interaction base class for OAuth 1 providers.
+
+ OAuth providers should inherit from RequestValidator and implement the
+ methods and properties outlined below. Further details are provided in the
+ documentation for each method and property.
+
+ Methods used to check the format of input parameters. Common tests include
+ length, character set, membership, range or pattern. These tests are
+ referred to as `whitelisting or blacklisting`_. Whitelisting is better
+ but blacklisting can be useful to spot malicious activity.
+ The following have methods a default implementation:
+
+ - check_client_key
+ - check_request_token
+ - check_access_token
+ - check_nonce
+ - check_verifier
+ - check_realms
+
+ The methods above default to whitelist input parameters, checking that they
+ are alphanumerical and between a minimum and maximum length. Rather than
+ overloading the methods a few properties can be used to configure these
+ methods.
+
+ * @safe_characters -> (character set)
+ * @client_key_length -> (min, max)
+ * @request_token_length -> (min, max)
+ * @access_token_length -> (min, max)
+ * @nonce_length -> (min, max)
+ * @verifier_length -> (min, max)
+ * @realms -> [list, of, realms]
+
+ Methods used to validate/invalidate input parameters. These checks usually
+ hit either persistent or temporary storage such as databases or the
+ filesystem. See each methods documentation for detailed usage.
+ The following methods must be implemented:
+
+ - validate_client_key
+ - validate_request_token
+ - validate_access_token
+ - validate_timestamp_and_nonce
+ - validate_redirect_uri
+ - validate_requested_realms
+ - validate_realms
+ - validate_verifier
+ - invalidate_request_token
+
+ Methods used to retrieve sensitive information from storage.
+ The following methods must be implemented:
+
+ - get_client_secret
+ - get_request_token_secret
+ - get_access_token_secret
+ - get_rsa_key
+ - get_realms
+ - get_default_realms
+ - get_redirect_uri
+
+ Methods used to save credentials.
+ The following methods must be implemented:
+
+ - save_request_token
+ - save_verifier
+ - save_access_token
+
+ Methods used to verify input parameters. This methods are used during
+ authorizing request token by user (AuthorizationEndpoint), to check if
+ parameters are valid. During token authorization request is not signed,
+ thus 'validation' methods can not be used. The following methods must be
+ implemented:
+
+ - verify_realms
+ - verify_request_token
+
+ To prevent timing attacks it is necessary to not exit early even if the
+ client key or resource owner key is invalid. Instead dummy values should
+ be used during the remaining verification process. It is very important
+ that the dummy client and token are valid input parameters to the methods
+ get_client_secret, get_rsa_key and get_(access/request)_token_secret and
+ that the running time of those methods when given a dummy value remain
+ equivalent to the running time when given a valid client/resource owner.
+ The following properties must be implemented:
+
+ * @dummy_client
+ * @dummy_request_token
+ * @dummy_access_token
+
+ Example implementations have been provided, note that the database used is
+ a simple dictionary and serves only an illustrative purpose. Use whichever
+ database suits your project and how to access it is entirely up to you.
+ The methods are introduced in an order which should make understanding
+ their use more straightforward and as such it could be worth reading what
+ follows in chronological order.
+
+ .. _`whitelisting or blacklisting`: https://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
+ """
+
+ def __init__(self):
+ pass
+
+ @property
+ def allowed_signature_methods(self):
+ return SIGNATURE_METHODS
+
+ @property
+ def safe_characters(self):
+ return set(utils.UNICODE_ASCII_CHARACTER_SET)
+
+ @property
+ def client_key_length(self):
+ return 20, 30
+
+ @property
+ def request_token_length(self):
+ return 20, 30
+
+ @property
+ def access_token_length(self):
+ return 20, 30
+
+ @property
+ def timestamp_lifetime(self):
+ return 600
+
+ @property
+ def nonce_length(self):
+ return 20, 30
+
+ @property
+ def verifier_length(self):
+ return 20, 30
+
+ @property
+ def realms(self):
+ return []
+
+ @property
+ def enforce_ssl(self):
+ return True
+
+ def check_client_key(self, client_key):
+ """Check that the client key only contains safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.client_key_length
+ return (set(client_key) <= self.safe_characters and
+ lower <= len(client_key) <= upper)
+
+ def check_request_token(self, request_token):
+ """Checks that the request token contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.request_token_length
+ return (set(request_token) <= self.safe_characters and
+ lower <= len(request_token) <= upper)
+
+ def check_access_token(self, request_token):
+ """Checks that the token contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.access_token_length
+ return (set(request_token) <= self.safe_characters and
+ lower <= len(request_token) <= upper)
+
+ def check_nonce(self, nonce):
+ """Checks that the nonce only contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.nonce_length
+ return (set(nonce) <= self.safe_characters and
+ lower <= len(nonce) <= upper)
+
+ def check_verifier(self, verifier):
+ """Checks that the verifier contains only safe characters
+ and is no shorter than lower and no longer than upper.
+ """
+ lower, upper = self.verifier_length
+ return (set(verifier) <= self.safe_characters and
+ lower <= len(verifier) <= upper)
+
+ def check_realms(self, realms):
+ """Check that the realm is one of a set allowed realms."""
+ return all(r in self.realms for r in realms)
+
+ def _subclass_must_implement(self, fn):
+ """
+ Returns a NotImplementedError for a function that should be implemented.
+ :param fn: name of the function
+ """
+ m = "Missing function implementation in {}: {}".format(type(self), fn)
+ return NotImplementedError(m)
+
+ @property
+ def dummy_client(self):
+ """Dummy client used when an invalid client key is supplied.
+
+ :returns: The dummy client key string.
+
+ The dummy client should be associated with either a client secret,
+ a rsa key or both depending on which signature methods are supported.
+ Providers should make sure that
+
+ get_client_secret(dummy_client)
+ get_rsa_key(dummy_client)
+
+ return a valid secret or key for the dummy client.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise self._subclass_must_implement("dummy_client")
+
+ @property
+ def dummy_request_token(self):
+ """Dummy request token used when an invalid token was supplied.
+
+ :returns: The dummy request token string.
+
+ The dummy request token should be associated with a request token
+ secret such that get_request_token_secret(.., dummy_request_token)
+ returns a valid secret.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement("dummy_request_token")
+
+ @property
+ def dummy_access_token(self):
+ """Dummy access token used when an invalid token was supplied.
+
+ :returns: The dummy access token string.
+
+ The dummy access token should be associated with an access token
+ secret such that get_access_token_secret(.., dummy_access_token)
+ returns a valid secret.
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise self._subclass_must_implement("dummy_access_token")
+
+ def get_client_secret(self, client_key, request):
+ """Retrieves the client secret associated with the client key.
+
+ :param client_key: The client/consumer key.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The client secret as a string.
+
+ This method must allow the use of a dummy client_key value.
+ Fetching the secret using the dummy key must take the same amount of
+ time as fetching a secret for a valid client::
+
+ # Unlikely to be near constant time as it uses two database
+ # lookups for a valid client, and only one for an invalid.
+ from your_datastore import ClientSecret
+ if ClientSecret.has(client_key):
+ return ClientSecret.get(client_key)
+ else:
+ return 'dummy'
+
+ # Aim to mimic number of latency inducing operations no matter
+ # whether the client is valid or not.
+ from your_datastore import ClientSecret
+ return ClientSecret.get(client_key, 'dummy')
+
+ Note that the returned key must be in plaintext.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise self._subclass_must_implement('get_client_secret')
+
+ def get_request_token_secret(self, client_key, token, request):
+ """Retrieves the shared secret associated with the request token.
+
+ :param client_key: The client/consumer key.
+ :param token: The request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The token secret as a string.
+
+ This method must allow the use of a dummy values and the running time
+ must be roughly equivalent to that of the running time of valid values::
+
+ # Unlikely to be near constant time as it uses two database
+ # lookups for a valid client, and only one for an invalid.
+ from your_datastore import RequestTokenSecret
+ if RequestTokenSecret.has(client_key):
+ return RequestTokenSecret.get((client_key, request_token))
+ else:
+ return 'dummy'
+
+ # Aim to mimic number of latency inducing operations no matter
+ # whether the client is valid or not.
+ from your_datastore import RequestTokenSecret
+ return ClientSecret.get((client_key, request_token), 'dummy')
+
+ Note that the returned key must be in plaintext.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement('get_request_token_secret')
+
+ def get_access_token_secret(self, client_key, token, request):
+ """Retrieves the shared secret associated with the access token.
+
+ :param client_key: The client/consumer key.
+ :param token: The access token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The token secret as a string.
+
+ This method must allow the use of a dummy values and the running time
+ must be roughly equivalent to that of the running time of valid values::
+
+ # Unlikely to be near constant time as it uses two database
+ # lookups for a valid client, and only one for an invalid.
+ from your_datastore import AccessTokenSecret
+ if AccessTokenSecret.has(client_key):
+ return AccessTokenSecret.get((client_key, request_token))
+ else:
+ return 'dummy'
+
+ # Aim to mimic number of latency inducing operations no matter
+ # whether the client is valid or not.
+ from your_datastore import AccessTokenSecret
+ return ClientSecret.get((client_key, request_token), 'dummy')
+
+ Note that the returned key must be in plaintext.
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise self._subclass_must_implement("get_access_token_secret")
+
+ def get_default_realms(self, client_key, request):
+ """Get the default realms for a client.
+
+ :param client_key: The client/consumer key.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The list of default realms associated with the client.
+
+ The list of default realms will be set during client registration and
+ is outside the scope of OAuthLib.
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise self._subclass_must_implement("get_default_realms")
+
+ def get_realms(self, token, request):
+ """Get realms associated with a request token.
+
+ :param token: The request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The list of realms associated with the request token.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement("get_realms")
+
+ def get_redirect_uri(self, token, request):
+ """Get the redirect URI associated with a request token.
+
+ :param token: The request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The redirect URI associated with the request token.
+
+ It may be desirable to return a custom URI if the redirect is set to "oob".
+ In this case, the user will be redirected to the returned URI and at that
+ endpoint the verifier can be displayed.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise self._subclass_must_implement("get_redirect_uri")
+
+ def get_rsa_key(self, client_key, request):
+ """Retrieves a previously stored client provided RSA key.
+
+ :param client_key: The client/consumer key.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: The rsa public key as a string.
+
+ This method must allow the use of a dummy client_key value. Fetching
+ the rsa key using the dummy key must take the same amount of time
+ as fetching a key for a valid client. The dummy key must also be of
+ the same bit length as client keys.
+
+ Note that the key must be returned in plaintext.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise self._subclass_must_implement("get_rsa_key")
+
+ def invalidate_request_token(self, client_key, request_token, request):
+ """Invalidates a used request token.
+
+ :param client_key: The client/consumer key.
+ :param request_token: The request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: None
+
+ Per `Section 2.3`_ of the spec:
+
+ "The server MUST (...) ensure that the temporary
+ credentials have not expired or been used before."
+
+ .. _`Section 2.3`: https://tools.ietf.org/html/rfc5849#section-2.3
+
+ This method should ensure that provided token won't validate anymore.
+ It can be simply removing RequestToken from storage or setting
+ specific flag that makes it invalid (note that such flag should be
+ also validated during request token validation).
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement("invalidate_request_token")
+
+ def validate_client_key(self, client_key, request):
+ """Validates that supplied client key is a registered and valid client.
+
+ :param client_key: The client/consumer key.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ Note that if the dummy client is supplied it should validate in same
+ or nearly the same amount of time as a valid one.
+
+ Ensure latency inducing tasks are mimiced even for dummy clients.
+ For example, use::
+
+ from your_datastore import Client
+ try:
+ return Client.exists(client_key, access_token)
+ except DoesNotExist:
+ return False
+
+ Rather than::
+
+ from your_datastore import Client
+ if access_token == self.dummy_access_token:
+ return False
+ else:
+ return Client.exists(client_key, access_token)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise self._subclass_must_implement("validate_client_key")
+
+ def validate_request_token(self, client_key, token, request):
+ """Validates that supplied request token is registered and valid.
+
+ :param client_key: The client/consumer key.
+ :param token: The request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ Note that if the dummy request_token is supplied it should validate in
+ the same nearly the same amount of time as a valid one.
+
+ Ensure latency inducing tasks are mimiced even for dummy clients.
+ For example, use::
+
+ from your_datastore import RequestToken
+ try:
+ return RequestToken.exists(client_key, access_token)
+ except DoesNotExist:
+ return False
+
+ Rather than::
+
+ from your_datastore import RequestToken
+ if access_token == self.dummy_access_token:
+ return False
+ else:
+ return RequestToken.exists(client_key, access_token)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement("validate_request_token")
+
+ def validate_access_token(self, client_key, token, request):
+ """Validates that supplied access token is registered and valid.
+
+ :param client_key: The client/consumer key.
+ :param token: The access token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ Note that if the dummy access token is supplied it should validate in
+ the same or nearly the same amount of time as a valid one.
+
+ Ensure latency inducing tasks are mimiced even for dummy clients.
+ For example, use::
+
+ from your_datastore import AccessToken
+ try:
+ return AccessToken.exists(client_key, access_token)
+ except DoesNotExist:
+ return False
+
+ Rather than::
+
+ from your_datastore import AccessToken
+ if access_token == self.dummy_access_token:
+ return False
+ else:
+ return AccessToken.exists(client_key, access_token)
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise self._subclass_must_implement("validate_access_token")
+
+ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+ request, request_token=None, access_token=None):
+ """Validates that the nonce has not been used before.
+
+ :param client_key: The client/consumer key.
+ :param timestamp: The ``oauth_timestamp`` parameter.
+ :param nonce: The ``oauth_nonce`` parameter.
+ :param request_token: Request token string, if any.
+ :param access_token: Access token string, if any.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ Per `Section 3.3`_ of the spec.
+
+ "A nonce is a random string, uniquely generated by the client to allow
+ the server to verify that a request has never been made before and
+ helps prevent replay attacks when requests are made over a non-secure
+ channel. The nonce value MUST be unique across all requests with the
+ same timestamp, client credentials, and token combinations."
+
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
+
+ One of the first validation checks that will be made is for the validity
+ of the nonce and timestamp, which are associated with a client key and
+ possibly a token. If invalid then immediately fail the request
+ by returning False. If the nonce/timestamp pair has been used before and
+ you may just have detected a replay attack. Therefore it is an essential
+ part of OAuth security that you not allow nonce/timestamp reuse.
+ Note that this validation check is done before checking the validity of
+ the client and token.::
+
+ nonces_and_timestamps_database = [
+ (u'foo', 1234567890, u'rannoMstrInghere', u'bar')
+ ]
+
+ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+ request_token=None, access_token=None):
+
+ return ((client_key, timestamp, nonce, request_token or access_token)
+ not in self.nonces_and_timestamps_database)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ * RequestTokenEndpoint
+ * ResourceEndpoint
+ * SignatureOnlyEndpoint
+ """
+ raise self._subclass_must_implement("validate_timestamp_and_nonce")
+
+ def validate_redirect_uri(self, client_key, redirect_uri, request):
+ """Validates the client supplied redirection URI.
+
+ :param client_key: The client/consumer key.
+ :param redirect_uri: The URI the client which to redirect back to after
+ authorization is successful.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ It is highly recommended that OAuth providers require their clients
+ to register all redirection URIs prior to using them in requests and
+ register them as absolute URIs. See `CWE-601`_ for more information
+ about open redirection attacks.
+
+ By requiring registration of all redirection URIs it should be
+ straightforward for the provider to verify whether the supplied
+ redirect_uri is valid or not.
+
+ Alternatively per `Section 2.1`_ of the spec:
+
+ "If the client is unable to receive callbacks or a callback URI has
+ been established via other means, the parameter value MUST be set to
+ "oob" (case sensitive), to indicate an out-of-band configuration."
+
+ .. _`CWE-601`: http://cwe.mitre.org/top25/index.html#CWE-601
+ .. _`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise self._subclass_must_implement("validate_redirect_uri")
+
+ def validate_requested_realms(self, client_key, realms, request):
+ """Validates that the client may request access to the realm.
+
+ :param client_key: The client/consumer key.
+ :param realms: The list of realms that client is requesting access to.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ This method is invoked when obtaining a request token and should
+ tie a realm to the request token and after user authorization
+ this realm restriction should transfer to the access token.
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise self._subclass_must_implement("validate_requested_realms")
+
+ def validate_realms(self, client_key, token, request, uri=None,
+ realms=None):
+ """Validates access to the request realm.
+
+ :param client_key: The client/consumer key.
+ :param token: A request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param uri: The URI the realms is protecting.
+ :param realms: A list of realms that must have been granted to
+ the access token.
+ :returns: True or False
+
+ How providers choose to use the realm parameter is outside the OAuth
+ specification but it is commonly used to restrict access to a subset
+ of protected resources such as "photos".
+
+ realms is a convenience parameter which can be used to provide
+ a per view method pre-defined list of allowed realms.
+
+ Can be as simple as::
+
+ from your_datastore import RequestToken
+ request_token = RequestToken.get(token, None)
+
+ if not request_token:
+ return False
+ return set(request_token.realms).issuperset(set(realms))
+
+ This method is used by
+
+ * ResourceEndpoint
+ """
+ raise self._subclass_must_implement("validate_realms")
+
+ def validate_verifier(self, client_key, token, verifier, request):
+ """Validates a verification code.
+
+ :param client_key: The client/consumer key.
+ :param token: A request token string.
+ :param verifier: The authorization verifier string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ OAuth providers issue a verification code to clients after the
+ resource owner authorizes access. This code is used by the client to
+ obtain token credentials and the provider must verify that the
+ verifier is valid and associated with the client as well as the
+ resource owner.
+
+ Verifier validation should be done in near constant time
+ (to avoid verifier enumeration). To achieve this we need a
+ constant time string comparison which is provided by OAuthLib
+ in ``oauthlib.common.safe_string_equals``::
+
+ from your_datastore import Verifier
+ correct_verifier = Verifier.get(client_key, request_token)
+ from oauthlib.common import safe_string_equals
+ return safe_string_equals(verifier, correct_verifier)
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement("validate_verifier")
+
+ def verify_request_token(self, token, request):
+ """Verify that the given OAuth1 request token is valid.
+
+ :param token: A request token string.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ This method is used only in AuthorizationEndpoint to check whether the
+ oauth_token given in the authorization URL is valid or not.
+ This request is not signed and thus similar ``validate_request_token``
+ method can not be used.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise self._subclass_must_implement("verify_request_token")
+
+ def verify_realms(self, token, realms, request):
+ """Verify authorized realms to see if they match those given to token.
+
+ :param token: An access token string.
+ :param realms: A list of realms the client attempts to access.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :returns: True or False
+
+ This prevents the list of authorized realms sent by the client during
+ the authorization step to be altered to include realms outside what
+ was bound with the request token.
+
+ Can be as simple as::
+
+ valid_realms = self.get_realms(token)
+ return all((r in valid_realms for r in realms))
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise self._subclass_must_implement("verify_realms")
+
+ def save_access_token(self, token, request):
+ """Save an OAuth1 access token.
+
+ :param token: A dict with token credentials.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ The token dictionary will at minimum include
+
+ * ``oauth_token`` the access token string.
+ * ``oauth_token_secret`` the token specific secret used in signing.
+ * ``oauth_authorized_realms`` a space separated list of realms.
+
+ Client key can be obtained from ``request.client_key``.
+
+ The list of realms (not joined string) can be obtained from
+ ``request.realm``.
+
+ This method is used by
+
+ * AccessTokenEndpoint
+ """
+ raise self._subclass_must_implement("save_access_token")
+
+ def save_request_token(self, token, request):
+ """Save an OAuth1 request token.
+
+ :param token: A dict with token credentials.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ The token dictionary will at minimum include
+
+ * ``oauth_token`` the request token string.
+ * ``oauth_token_secret`` the token specific secret used in signing.
+ * ``oauth_callback_confirmed`` the string ``true``.
+
+ Client key can be obtained from ``request.client_key``.
+
+ This method is used by
+
+ * RequestTokenEndpoint
+ """
+ raise self._subclass_must_implement("save_request_token")
+
+ def save_verifier(self, token, verifier, request):
+ """Associate an authorization verifier with a request token.
+
+ :param token: A request token string.
+ :param verifier: A dictionary containing the oauth_verifier and
+ oauth_token
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ We need to associate verifiers with tokens for validation during the
+ access token request.
+
+ Note that unlike save_x_token token here is the ``oauth_token`` token
+ string from the request token saved previously.
+
+ This method is used by
+
+ * AuthorizationEndpoint
+ """
+ raise self._subclass_must_implement("save_verifier")
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/signature.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/signature.py
new file mode 100644
index 0000000000..9cb1a517ee
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/signature.py
@@ -0,0 +1,852 @@
+"""
+This module is an implementation of `section 3.4`_ of RFC 5849.
+
+**Usage**
+
+Steps for signing a request:
+
+1. Collect parameters from the request using ``collect_parameters``.
+2. Normalize those parameters using ``normalize_parameters``.
+3. Create the *base string URI* using ``base_string_uri``.
+4. Create the *signature base string* from the above three components
+ using ``signature_base_string``.
+5. Pass the *signature base string* and the client credentials to one of the
+ sign-with-client functions. The HMAC-based signing functions needs
+ client credentials with secrets. The RSA-based signing functions needs
+ client credentials with an RSA private key.
+
+To verify a request, pass the request and credentials to one of the verify
+functions. The HMAC-based signing functions needs the shared secrets. The
+RSA-based verify functions needs the RSA public key.
+
+**Scope**
+
+All of the functions in this module should be considered internal to OAuthLib,
+since they are not imported into the "oauthlib.oauth1" module. Programs using
+OAuthLib should not use directly invoke any of the functions in this module.
+
+**Deprecated functions**
+
+The "sign_" methods that are not "_with_client" have been deprecated. They may
+be removed in a future release. Since they are all internal functions, this
+should have no impact on properly behaving programs.
+
+.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+"""
+
+import binascii
+import hashlib
+import hmac
+import ipaddress
+import logging
+import urllib.parse as urlparse
+import warnings
+
+from oauthlib.common import extract_params, safe_string_equals, urldecode
+
+from . import utils
+
+log = logging.getLogger(__name__)
+
+
+# ==== Common functions ==========================================
+
+def signature_base_string(
+ http_method: str,
+ base_str_uri: str,
+ normalized_encoded_request_parameters: str) -> str:
+ """
+ Construct the signature base string.
+
+ The *signature base string* is the value that is calculated and signed by
+ the client. It is also independently calculated by the server to verify
+ the signature, and therefore must produce the exact same value at both
+ ends or the signature won't verify.
+
+ The rules for calculating the *signature base string* are defined in
+ section 3.4.1.1`_ of RFC 5849.
+
+ .. _`section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ """
+
+ # The signature base string is constructed by concatenating together,
+ # in order, the following HTTP request elements:
+
+ # 1. The HTTP request method in uppercase. For example: "HEAD",
+ # "GET", "POST", etc. If the request uses a custom HTTP method, it
+ # MUST be encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ base_string = utils.escape(http_method.upper())
+
+ # 2. An "&" character (ASCII code 38).
+ base_string += '&'
+
+ # 3. The base string URI from `Section 3.4.1.2`_, after being encoded
+ # (`Section 3.6`_).
+ #
+ # .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ base_string += utils.escape(base_str_uri)
+
+ # 4. An "&" character (ASCII code 38).
+ base_string += '&'
+
+ # 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
+ # being encoded (`Section 3.6`).
+ #
+ # .. _`Sec 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ base_string += utils.escape(normalized_encoded_request_parameters)
+
+ return base_string
+
+
+def base_string_uri(uri: str, host: str = None) -> str:
+ """
+ Calculates the _base string URI_.
+
+ The *base string URI* is one of the components that make up the
+ *signature base string*.
+
+ The ``host`` is optional. If provided, it is used to override any host and
+ port values in the ``uri``. The value for ``host`` is usually extracted from
+ the "Host" request header from the HTTP request. Its value may be just the
+ hostname, or the hostname followed by a colon and a TCP/IP port number
+ (hostname:port). If a value for the``host`` is provided but it does not
+ contain a port number, the default port number is used (i.e. if the ``uri``
+ contained a port number, it will be discarded).
+
+ The rules for calculating the *base string URI* are defined in
+ section 3.4.1.2`_ of RFC 5849.
+
+ .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
+
+ :param uri: URI
+ :param host: hostname with optional port number, separated by a colon
+ :return: base string URI
+ """
+
+ if not isinstance(uri, str):
+ raise ValueError('uri must be a string.')
+
+ # FIXME: urlparse does not support unicode
+ output = urlparse.urlparse(uri)
+ scheme = output.scheme
+ hostname = output.hostname
+ port = output.port
+ path = output.path
+ params = output.params
+
+ # The scheme, authority, and path of the request resource URI `RFC3986`
+ # are included by constructing an "http" or "https" URI representing
+ # the request resource (without the query or fragment) as follows:
+ #
+ # .. _`RFC3986`: https://tools.ietf.org/html/rfc3986
+
+ if not scheme:
+ raise ValueError('missing scheme')
+
+ # Per `RFC 2616 section 5.1.2`_:
+ #
+ # Note that the absolute path cannot be empty; if none is present in
+ # the original URI, it MUST be given as "/" (the server root).
+ #
+ # .. _`RFC 2616 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2
+ if not path:
+ path = '/'
+
+ # 1. The scheme and host MUST be in lowercase.
+ scheme = scheme.lower()
+ # Note: if ``host`` is used, it will be converted to lowercase below
+ if hostname is not None:
+ hostname = hostname.lower()
+
+ # 2. The host and port values MUST match the content of the HTTP
+ # request "Host" header field.
+ if host is not None:
+ # NOTE: override value in uri with provided host
+ # Host argument is equal to netloc. It means it's missing scheme.
+ # Add it back, before parsing.
+
+ host = host.lower()
+ host = f"{scheme}://{host}"
+ output = urlparse.urlparse(host)
+ hostname = output.hostname
+ port = output.port
+
+ # 3. The port MUST be included if it is not the default port for the
+ # scheme, and MUST be excluded if it is the default. Specifically,
+ # the port MUST be excluded when making an HTTP request `RFC2616`_
+ # to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
+ # All other non-default port numbers MUST be included.
+ #
+ # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616
+ # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818
+
+ if hostname is None:
+ raise ValueError('missing host')
+
+ # NOTE: Try guessing if we're dealing with IP or hostname
+ try:
+ hostname = ipaddress.ip_address(hostname)
+ except ValueError:
+ pass
+
+ if isinstance(hostname, ipaddress.IPv6Address):
+ hostname = f"[{hostname}]"
+ elif isinstance(hostname, ipaddress.IPv4Address):
+ hostname = f"{hostname}"
+
+ if port is not None and not (0 < port <= 65535):
+ raise ValueError('port out of range') # 16-bit unsigned ints
+ if (scheme, port) in (('http', 80), ('https', 443)):
+ netloc = hostname # default port for scheme: exclude port num
+ elif port:
+ netloc = f"{hostname}:{port}" # use hostname:port
+ else:
+ netloc = hostname
+
+ v = urlparse.urlunparse((scheme, netloc, path, params, '', ''))
+
+ # RFC 5849 does not specify which characters are encoded in the
+ # "base string URI", nor how they are encoded - which is very bad, since
+ # the signatures won't match if there are any differences. Fortunately,
+ # most URIs only use characters that are clearly not encoded (e.g. digits
+ # and A-Z, a-z), so have avoided any differences between implementations.
+ #
+ # The example from its section 3.4.1.2 illustrates that spaces in
+ # the path are percent encoded. But it provides no guidance as to what other
+ # characters (if any) must be encoded (nor how); nor if characters in the
+ # other components are to be encoded or not.
+ #
+ # This implementation **assumes** that **only** the space is percent-encoded
+ # and it is done to the entire value (not just to spaces in the path).
+ #
+ # This code may need to be changed if it is discovered that other characters
+ # are expected to be encoded.
+ #
+ # Note: the "base string URI" returned by this function will be encoded
+ # again before being concatenated into the "signature base string". So any
+ # spaces in the URI will actually appear in the "signature base string"
+ # as "%2520" (the "%20" further encoded according to section 3.6).
+
+ return v.replace(' ', '%20')
+
+
+def collect_parameters(uri_query='', body=None, headers=None,
+ exclude_oauth_signature=True, with_realm=False):
+ """
+ Gather the request parameters from all the parameter sources.
+
+ This function is used to extract all the parameters, which are then passed
+ to ``normalize_parameters`` to produce one of the components that make up
+ the *signature base string*.
+
+ Parameters starting with `oauth_` will be unescaped.
+
+ Body parameters must be supplied as a dict, a list of 2-tuples, or a
+ form encoded query string.
+
+ Headers must be supplied as a dict.
+
+ The rules where the parameters must be sourced from are defined in
+ `section 3.4.1.3.1`_ of RFC 5849.
+
+ .. _`Sec 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ """
+ if body is None:
+ body = []
+ headers = headers or {}
+ params = []
+
+ # The parameters from the following sources are collected into a single
+ # list of name/value pairs:
+
+ # * The query component of the HTTP request URI as defined by
+ # `RFC3986, Section 3.4`_. The query component is parsed into a list
+ # of name/value pairs by treating it as an
+ # "application/x-www-form-urlencoded" string, separating the names
+ # and values and decoding them as defined by W3C.REC-html40-19980424
+ # `W3C-HTML-4.0`_, Section 17.13.4.
+ #
+ # .. _`RFC3986, Sec 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4
+ # .. _`W3C-HTML-4.0`: https://www.w3.org/TR/1998/REC-html40-19980424/
+ if uri_query:
+ params.extend(urldecode(uri_query))
+
+ # * The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
+ # present. The header's content is parsed into a list of name/value
+ # pairs excluding the "realm" parameter if present. The parameter
+ # values are decoded as defined by `Section 3.5.1`_.
+ #
+ # .. _`Section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1
+ if headers:
+ headers_lower = {k.lower(): v for k, v in headers.items()}
+ authorization_header = headers_lower.get('authorization')
+ if authorization_header is not None:
+ params.extend([i for i in utils.parse_authorization_header(
+ authorization_header) if with_realm or i[0] != 'realm'])
+
+ # * The HTTP request entity-body, but only if all of the following
+ # conditions are met:
+ # * The entity-body is single-part.
+ #
+ # * The entity-body follows the encoding requirements of the
+ # "application/x-www-form-urlencoded" content-type as defined by
+ # W3C.REC-html40-19980424 `W3C-HTML-4.0`_.
+
+ # * The HTTP request entity-header includes the "Content-Type"
+ # header field set to "application/x-www-form-urlencoded".
+ #
+ # .. _`W3C-HTML-4.0`: https://www.w3.org/TR/1998/REC-html40-19980424/
+
+ # TODO: enforce header param inclusion conditions
+ bodyparams = extract_params(body) or []
+ params.extend(bodyparams)
+
+ # ensure all oauth params are unescaped
+ unescaped_params = []
+ for k, v in params:
+ if k.startswith('oauth_'):
+ v = utils.unescape(v)
+ unescaped_params.append((k, v))
+
+ # The "oauth_signature" parameter MUST be excluded from the signature
+ # base string if present.
+ if exclude_oauth_signature:
+ unescaped_params = list(filter(lambda i: i[0] != 'oauth_signature',
+ unescaped_params))
+
+ return unescaped_params
+
+
+def normalize_parameters(params) -> str:
+ """
+ Calculate the normalized request parameters.
+
+ The *normalized request parameters* is one of the components that make up
+ the *signature base string*.
+
+ The rules for parameter normalization are defined in `section 3.4.1.3.2`_ of
+ RFC 5849.
+
+ .. _`Sec 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ """
+
+ # The parameters collected in `Section 3.4.1.3`_ are normalized into a
+ # single string as follows:
+ #
+ # .. _`Section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+ # 1. First, the name and value of each parameter are encoded
+ # (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
+
+ # 2. The parameters are sorted by name, using ascending byte value
+ # ordering. If two or more parameters share the same name, they
+ # are sorted by their value.
+ key_values.sort()
+
+ # 3. The name of each parameter is concatenated to its corresponding
+ # value using an "=" character (ASCII code 61) as a separator, even
+ # if the value is empty.
+ parameter_parts = ['{}={}'.format(k, v) for k, v in key_values]
+
+ # 4. The sorted name/value pairs are concatenated together into a
+ # single string by using an "&" character (ASCII code 38) as
+ # separator.
+ return '&'.join(parameter_parts)
+
+
+# ==== Common functions for HMAC-based signature methods =========
+
+def _sign_hmac(hash_algorithm_name: str,
+ sig_base_str: str,
+ client_secret: str,
+ resource_owner_secret: str):
+ """
+ **HMAC-SHA256**
+
+ The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature
+ algorithm as defined in `RFC4634`_::
+
+ digest = HMAC-SHA256 (key, text)
+
+ Per `section 3.4.2`_ of the spec.
+
+ .. _`RFC4634`: https://tools.ietf.org/html/rfc4634
+ .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
+ """
+
+ # The HMAC-SHA256 function variables are used in following way:
+
+ # text is set to the value of the signature base string from
+ # `Section 3.4.1.1`_.
+ #
+ # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ text = sig_base_str
+
+ # key is set to the concatenated values of:
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ key = utils.escape(client_secret or '')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included
+ # even when either secret is empty.
+ key += '&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ key += utils.escape(resource_owner_secret or '')
+
+ # Get the hashing algorithm to use
+
+ m = {
+ 'SHA-1': hashlib.sha1,
+ 'SHA-256': hashlib.sha256,
+ 'SHA-512': hashlib.sha512,
+ }
+ hash_alg = m[hash_algorithm_name]
+
+ # Calculate the signature
+
+ # FIXME: HMAC does not support unicode!
+ key_utf8 = key.encode('utf-8')
+ text_utf8 = text.encode('utf-8')
+ signature = hmac.new(key_utf8, text_utf8, hash_alg)
+
+ # digest is used to set the value of the "oauth_signature" protocol
+ # parameter, after the result octet string is base64-encoded
+ # per `RFC2045, Section 6.8`.
+ #
+ # .. _`RFC2045, Sec 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
+ return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+
+def _verify_hmac(hash_algorithm_name: str,
+ request,
+ client_secret=None,
+ resource_owner_secret=None):
+ """Verify a HMAC-SHA1 signature.
+
+ Per `section 3.4`_ of the spec.
+
+ .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+
+ To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+ attribute MUST be an absolute URI whose netloc part identifies the
+ origin server or gateway on which the resource resides. Any Host
+ item of the request argument's headers dict attribute will be
+ ignored.
+
+ .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
+
+ """
+ norm_params = normalize_parameters(request.params)
+ bs_uri = base_string_uri(request.uri)
+ sig_base_str = signature_base_string(request.http_method, bs_uri,
+ norm_params)
+ signature = _sign_hmac(hash_algorithm_name, sig_base_str,
+ client_secret, resource_owner_secret)
+ match = safe_string_equals(signature, request.signature)
+ if not match:
+ log.debug('Verify HMAC failed: signature base string: %s', sig_base_str)
+ return match
+
+
+# ==== HMAC-SHA1 =================================================
+
+def sign_hmac_sha1_with_client(sig_base_str, client):
+ return _sign_hmac('SHA-1', sig_base_str,
+ client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha1(request, client_secret=None, resource_owner_secret=None):
+ return _verify_hmac('SHA-1', request, client_secret, resource_owner_secret)
+
+
+def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
+ """
+ Deprecated function for calculating a HMAC-SHA1 signature.
+
+ This function has been replaced by invoking ``sign_hmac`` with "SHA-1"
+ as the hash algorithm name.
+
+ This function was invoked by sign_hmac_sha1_with_client and
+ test_signatures.py, but does any application invoke it directly? If not,
+ it can be removed.
+ """
+ warnings.warn('use sign_hmac_sha1_with_client instead of sign_hmac_sha1',
+ DeprecationWarning)
+
+ # For some unknown reason, the original implementation assumed base_string
+ # could either be bytes or str. The signature base string calculating
+ # function always returned a str, so the new ``sign_rsa`` only expects that.
+
+ base_string = base_string.decode('ascii') \
+ if isinstance(base_string, bytes) else base_string
+
+ return _sign_hmac('SHA-1', base_string,
+ client_secret, resource_owner_secret)
+
+
+# ==== HMAC-SHA256 ===============================================
+
+def sign_hmac_sha256_with_client(sig_base_str, client):
+ return _sign_hmac('SHA-256', sig_base_str,
+ client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha256(request, client_secret=None, resource_owner_secret=None):
+ return _verify_hmac('SHA-256', request,
+ client_secret, resource_owner_secret)
+
+
+def sign_hmac_sha256(base_string, client_secret, resource_owner_secret):
+ """
+ Deprecated function for calculating a HMAC-SHA256 signature.
+
+ This function has been replaced by invoking ``sign_hmac`` with "SHA-256"
+ as the hash algorithm name.
+
+ This function was invoked by sign_hmac_sha256_with_client and
+ test_signatures.py, but does any application invoke it directly? If not,
+ it can be removed.
+ """
+ warnings.warn(
+ 'use sign_hmac_sha256_with_client instead of sign_hmac_sha256',
+ DeprecationWarning)
+
+ # For some unknown reason, the original implementation assumed base_string
+ # could either be bytes or str. The signature base string calculating
+ # function always returned a str, so the new ``sign_rsa`` only expects that.
+
+ base_string = base_string.decode('ascii') \
+ if isinstance(base_string, bytes) else base_string
+
+ return _sign_hmac('SHA-256', base_string,
+ client_secret, resource_owner_secret)
+
+
+# ==== HMAC-SHA512 ===============================================
+
+def sign_hmac_sha512_with_client(sig_base_str: str,
+ client):
+ return _sign_hmac('SHA-512', sig_base_str,
+ client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha512(request,
+ client_secret: str = None,
+ resource_owner_secret: str = None):
+ return _verify_hmac('SHA-512', request,
+ client_secret, resource_owner_secret)
+
+
+# ==== Common functions for RSA-based signature methods ==========
+
+_jwt_rsa = {} # cache of RSA-hash implementations from PyJWT jwt.algorithms
+
+
+def _get_jwt_rsa_algorithm(hash_algorithm_name: str):
+ """
+ Obtains an RSAAlgorithm object that implements RSA with the hash algorithm.
+
+ This method maintains the ``_jwt_rsa`` cache.
+
+ Returns a jwt.algorithm.RSAAlgorithm.
+ """
+ if hash_algorithm_name in _jwt_rsa:
+ # Found in cache: return it
+ return _jwt_rsa[hash_algorithm_name]
+ else:
+ # Not in cache: instantiate a new RSAAlgorithm
+
+ # PyJWT has some nice pycrypto/cryptography abstractions
+ import jwt.algorithms as jwt_algorithms
+ m = {
+ 'SHA-1': jwt_algorithms.hashes.SHA1,
+ 'SHA-256': jwt_algorithms.hashes.SHA256,
+ 'SHA-512': jwt_algorithms.hashes.SHA512,
+ }
+ v = jwt_algorithms.RSAAlgorithm(m[hash_algorithm_name])
+
+ _jwt_rsa[hash_algorithm_name] = v # populate cache
+
+ return v
+
+
+def _prepare_key_plus(alg, keystr):
+ """
+ Prepare a PEM encoded key (public or private), by invoking the `prepare_key`
+ method on alg with the keystr.
+
+ The keystr should be a string or bytes. If the keystr is bytes, it is
+ decoded as UTF-8 before being passed to prepare_key. Otherwise, it
+ is passed directly.
+ """
+ if isinstance(keystr, bytes):
+ keystr = keystr.decode('utf-8')
+ return alg.prepare_key(keystr)
+
+
+def _sign_rsa(hash_algorithm_name: str,
+ sig_base_str: str,
+ rsa_private_key: str):
+ """
+ Calculate the signature for an RSA-based signature method.
+
+ The ``alg`` is used to calculate the digest over the signature base string.
+ For the "RSA_SHA1" signature method, the alg must be SHA-1. While OAuth 1.0a
+ only defines the RSA-SHA1 signature method, this function can be used for
+ other non-standard signature methods that only differ from RSA-SHA1 by the
+ digest algorithm.
+
+ Signing for the RSA-SHA1 signature method is defined in
+ `section 3.4.3`_ of RFC 5849.
+
+ The RSASSA-PKCS1-v1_5 signature algorithm used defined by
+ `RFC3447, Section 8.2`_ (also known as PKCS#1), with the `alg` as the
+ hash function for EMSA-PKCS1-v1_5. To
+ use this method, the client MUST have established client credentials
+ with the server that included its RSA public key (in a manner that is
+ beyond the scope of this specification).
+
+ .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
+ .. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2
+ """
+
+ # Get the implementation of RSA-hash
+
+ alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
+
+ # Check private key
+
+ if not rsa_private_key:
+ raise ValueError('rsa_private_key required for RSA with ' +
+ alg.hash_alg.name + ' signature method')
+
+ # Convert the "signature base string" into a sequence of bytes (M)
+ #
+ # The signature base string, by definition, only contain printable US-ASCII
+ # characters. So encoding it as 'ascii' will always work. It will raise a
+ # ``UnicodeError`` if it can't encode the value, which will never happen
+ # if the signature base string was created correctly. Therefore, using
+ # 'ascii' encoding provides an extra level of error checking.
+
+ m = sig_base_str.encode('ascii')
+
+ # Perform signing: S = RSASSA-PKCS1-V1_5-SIGN (K, M)
+
+ key = _prepare_key_plus(alg, rsa_private_key)
+ s = alg.sign(m, key)
+
+ # base64-encoded per RFC2045 section 6.8.
+ #
+ # 1. While b2a_base64 implements base64 defined by RFC 3548. As used here,
+ # it is the same as base64 defined by RFC 2045.
+ # 2. b2a_base64 includes a "\n" at the end of its result ([:-1] removes it)
+ # 3. b2a_base64 produces a binary string. Use decode to produce a str.
+ # It should only contain only printable US-ASCII characters.
+
+ return binascii.b2a_base64(s)[:-1].decode('ascii')
+
+
+def _verify_rsa(hash_algorithm_name: str,
+ request,
+ rsa_public_key: str):
+ """
+ Verify a base64 encoded signature for a RSA-based signature method.
+
+ The ``alg`` is used to calculate the digest over the signature base string.
+ For the "RSA_SHA1" signature method, the alg must be SHA-1. While OAuth 1.0a
+ only defines the RSA-SHA1 signature method, this function can be used for
+ other non-standard signature methods that only differ from RSA-SHA1 by the
+ digest algorithm.
+
+ Verification for the RSA-SHA1 signature method is defined in
+ `section 3.4.3`_ of RFC 5849.
+
+ .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
+
+ To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+ attribute MUST be an absolute URI whose netloc part identifies the
+ origin server or gateway on which the resource resides. Any Host
+ item of the request argument's headers dict attribute will be
+ ignored.
+
+ .. _`RFC2616 Sec 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
+ """
+
+ try:
+ # Calculate the *signature base string* of the actual received request
+
+ norm_params = normalize_parameters(request.params)
+ bs_uri = base_string_uri(request.uri)
+ sig_base_str = signature_base_string(
+ request.http_method, bs_uri, norm_params)
+
+ # Obtain the signature that was received in the request
+
+ sig = binascii.a2b_base64(request.signature.encode('ascii'))
+
+ # Get the implementation of RSA-with-hash algorithm to use
+
+ alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
+
+ # Verify the received signature was produced by the private key
+ # corresponding to the `rsa_public_key`, signing exact same
+ # *signature base string*.
+ #
+ # RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S)
+
+ key = _prepare_key_plus(alg, rsa_public_key)
+
+ # The signature base string only contain printable US-ASCII characters.
+ # The ``encode`` method with the default "strict" error handling will
+ # raise a ``UnicodeError`` if it can't encode the value. So using
+ # "ascii" will always work.
+
+ verify_ok = alg.verify(sig_base_str.encode('ascii'), key, sig)
+
+ if not verify_ok:
+ log.debug('Verify failed: RSA with ' + alg.hash_alg.name +
+ ': signature base string=%s' + sig_base_str)
+ return verify_ok
+
+ except UnicodeError:
+ # A properly encoded signature will only contain printable US-ASCII
+ # characters. The ``encode`` method with the default "strict" error
+ # handling will raise a ``UnicodeError`` if it can't decode the value.
+ # So using "ascii" will work with all valid signatures. But an
+ # incorrectly or maliciously produced signature could contain other
+ # bytes.
+ #
+ # This implementation treats that situation as equivalent to the
+ # signature verification having failed.
+ #
+ # Note: simply changing the encode to use 'utf-8' will not remove this
+ # case, since an incorrect or malicious request can contain bytes which
+ # are invalid as UTF-8.
+ return False
+
+
+# ==== RSA-SHA1 ==================================================
+
+def sign_rsa_sha1_with_client(sig_base_str, client):
+ # For some reason, this function originally accepts both str and bytes.
+ # This behaviour is preserved here. But won't be done for the newer
+ # sign_rsa_sha256_with_client and sign_rsa_sha512_with_client functions,
+ # which will only accept strings. The function to calculate a
+ # "signature base string" always produces a string, so it is not clear
+ # why support for bytes would ever be needed.
+ sig_base_str = sig_base_str.decode('ascii')\
+ if isinstance(sig_base_str, bytes) else sig_base_str
+
+ return _sign_rsa('SHA-1', sig_base_str, client.rsa_key)
+
+
+def verify_rsa_sha1(request, rsa_public_key: str):
+ return _verify_rsa('SHA-1', request, rsa_public_key)
+
+
+def sign_rsa_sha1(base_string, rsa_private_key):
+ """
+ Deprecated function for calculating a RSA-SHA1 signature.
+
+ This function has been replaced by invoking ``sign_rsa`` with "SHA-1"
+ as the hash algorithm name.
+
+ This function was invoked by sign_rsa_sha1_with_client and
+ test_signatures.py, but does any application invoke it directly? If not,
+ it can be removed.
+ """
+ warnings.warn('use _sign_rsa("SHA-1", ...) instead of sign_rsa_sha1',
+ DeprecationWarning)
+
+ if isinstance(base_string, bytes):
+ base_string = base_string.decode('ascii')
+
+ return _sign_rsa('SHA-1', base_string, rsa_private_key)
+
+
+# ==== RSA-SHA256 ================================================
+
+def sign_rsa_sha256_with_client(sig_base_str: str, client):
+ return _sign_rsa('SHA-256', sig_base_str, client.rsa_key)
+
+
+def verify_rsa_sha256(request, rsa_public_key: str):
+ return _verify_rsa('SHA-256', request, rsa_public_key)
+
+
+# ==== RSA-SHA512 ================================================
+
+def sign_rsa_sha512_with_client(sig_base_str: str, client):
+ return _sign_rsa('SHA-512', sig_base_str, client.rsa_key)
+
+
+def verify_rsa_sha512(request, rsa_public_key: str):
+ return _verify_rsa('SHA-512', request, rsa_public_key)
+
+
+# ==== PLAINTEXT =================================================
+
+def sign_plaintext_with_client(_signature_base_string, client):
+ # _signature_base_string is not used because the signature with PLAINTEXT
+ # is just the secret: it isn't a real signature.
+ return sign_plaintext(client.client_secret, client.resource_owner_secret)
+
+
+def sign_plaintext(client_secret, resource_owner_secret):
+ """Sign a request using plaintext.
+
+ Per `section 3.4.4`_ of the spec.
+
+ The "PLAINTEXT" method does not employ a signature algorithm. It
+ MUST be used with a transport-layer mechanism such as TLS or SSL (or
+ sent over a secure channel with equivalent protections). It does not
+ utilize the signature base string or the "oauth_timestamp" and
+ "oauth_nonce" parameters.
+
+ .. _`section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4
+
+ """
+
+ # The "oauth_signature" protocol parameter is set to the concatenated
+ # value of:
+
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ signature = utils.escape(client_secret or '')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included even
+ # when either secret is empty.
+ signature += '&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ signature += utils.escape(resource_owner_secret or '')
+
+ return signature
+
+
+def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
+ """Verify a PLAINTEXT signature.
+
+ Per `section 3.4`_ of the spec.
+
+ .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+ """
+ signature = sign_plaintext(client_secret, resource_owner_secret)
+ match = safe_string_equals(signature, request.signature)
+ if not match:
+ log.debug('Verify PLAINTEXT failed')
+ return match
diff --git a/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/utils.py b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/utils.py
new file mode 100644
index 0000000000..8fb8302e30
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth1/rfc5849/utils.py
@@ -0,0 +1,83 @@
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth
+spec.
+"""
+import urllib.request as urllib2
+
+from oauthlib.common import quote, unquote
+
+UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ '0123456789')
+
+
+def filter_params(target):
+ """Decorator which filters params to remove non-oauth_* parameters
+
+ Assumes the decorated method takes a params dict or list of tuples as its
+ first argument.
+ """
+ def wrapper(params, *args, **kwargs):
+ params = filter_oauth_params(params)
+ return target(params, *args, **kwargs)
+
+ wrapper.__doc__ = target.__doc__
+ return wrapper
+
+
+def filter_oauth_params(params):
+ """Removes all non oauth parameters from a dict or a list of params."""
+ is_oauth = lambda kv: kv[0].startswith("oauth_")
+ if isinstance(params, dict):
+ return list(filter(is_oauth, list(params.items())))
+ else:
+ return list(filter(is_oauth, params))
+
+
+def escape(u):
+ """Escape a unicode string in an OAuth-compatible fashion.
+
+ Per `section 3.6`_ of the spec.
+
+ .. _`section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+
+ """
+ if not isinstance(u, str):
+ raise ValueError('Only unicode objects are escapable. ' +
+ 'Got {!r} of type {}.'.format(u, type(u)))
+ # Letters, digits, and the characters '_.-' are already treated as safe
+ # by urllib.quote(). We need to add '~' to fully support rfc5849.
+ return quote(u, safe=b'~')
+
+
+def unescape(u):
+ if not isinstance(u, str):
+ raise ValueError('Only unicode objects are unescapable.')
+ return unquote(u)
+
+
+def parse_keqv_list(l):
+ """A unicode-safe version of urllib2.parse_keqv_list"""
+ # With Python 2.6, parse_http_list handles unicode fine
+ return urllib2.parse_keqv_list(l)
+
+
+def parse_http_list(u):
+ """A unicode-safe version of urllib2.parse_http_list"""
+ # With Python 2.6, parse_http_list handles unicode fine
+ return urllib2.parse_http_list(u)
+
+
+def parse_authorization_header(authorization_header):
+ """Parse an OAuth authorization header into a list of 2-tuples"""
+ auth_scheme = 'OAuth '.lower()
+ if authorization_header[:len(auth_scheme)].lower().startswith(auth_scheme):
+ items = parse_http_list(authorization_header[len(auth_scheme):])
+ try:
+ return list(parse_keqv_list(items).items())
+ except (IndexError, ValueError):
+ pass
+ raise ValueError('Malformed authorization header')
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/__init__.py
new file mode 100644
index 0000000000..deefb1af78
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/__init__.py
@@ -0,0 +1,36 @@
+"""
+oauthlib.oauth2
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 2.0 Client
+and Server classes.
+"""
+from .rfc6749.clients import (
+ BackendApplicationClient, Client, LegacyApplicationClient,
+ MobileApplicationClient, ServiceApplicationClient, WebApplicationClient,
+)
+from .rfc6749.endpoints import (
+ AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint,
+ LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer,
+ ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint,
+ WebApplicationServer,
+)
+from .rfc6749.errors import (
+ AccessDeniedError, FatalClientError, InsecureTransportError,
+ InvalidClientError, InvalidClientIdError, InvalidGrantError,
+ InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError,
+ InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError,
+ MissingClientIdError, MissingCodeError, MissingRedirectURIError,
+ MissingResponseTypeError, MissingTokenError, MissingTokenTypeError,
+ OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError,
+ UnauthorizedClientError, UnsupportedGrantTypeError,
+ UnsupportedResponseTypeError, UnsupportedTokenTypeError,
+)
+from .rfc6749.grant_types import (
+ AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
+ RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
+)
+from .rfc6749.request_validator import RequestValidator
+from .rfc6749.tokens import BearerToken, OAuth2Token
+from .rfc6749.utils import is_secure_transport
+from .rfc8628.clients import DeviceClient
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/__init__.py
new file mode 100644
index 0000000000..4b75a8a196
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/__init__.py
@@ -0,0 +1,16 @@
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import functools
+import logging
+
+from .endpoints.base import BaseEndpoint, catch_errors_and_unavailability
+from .errors import (
+ FatalClientError, OAuth2Error, ServerError, TemporarilyUnavailableError,
+)
+
+log = logging.getLogger(__name__)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/__init__.py
new file mode 100644
index 0000000000..8fc6c955a2
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 RFC6749.
+"""
+from .backend_application import BackendApplicationClient
+from .base import AUTH_HEADER, BODY, URI_QUERY, Client
+from .legacy_application import LegacyApplicationClient
+from .mobile_application import MobileApplicationClient
+from .service_application import ServiceApplicationClient
+from .web_application import WebApplicationClient
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/backend_application.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/backend_application.py
new file mode 100644
index 0000000000..e11e8fae38
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/backend_application.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from ..parameters import prepare_token_request
+from .base import Client
+
+
+class BackendApplicationClient(Client):
+
+ """A public client utilizing the client credentials grant workflow.
+
+ The client can request an access token using only its client
+ credentials (or other supported means of authentication) when the
+ client is requesting access to the protected resources under its
+ control, or those of another resource owner which has been previously
+ arranged with the authorization server (the method of which is beyond
+ the scope of this specification).
+
+ The client credentials grant type MUST only be used by confidential
+ clients.
+
+ Since the client authentication is used as the authorization grant,
+ no additional authorization request is needed.
+ """
+
+ grant_type = 'client_credentials'
+
+ def prepare_request_body(self, body='', scope=None,
+ include_client_id=False, **kwargs):
+ """Add the client credentials to the request body.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format per `Appendix B`_ in the HTTP request entity-body:
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+
+ :param include_client_id: `True` to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_. False otherwise (default).
+ :type include_client_id: Boolean
+
+ :param kwargs: Extra credentials to include in the token request.
+
+ The client MUST authenticate with the authorization server as
+ described in `Section 3.2.1`_.
+
+ The prepared body will include all provided credentials as well as
+ the ``grant_type`` parameter set to ``client_credentials``::
+
+ >>> from oauthlib.oauth2 import BackendApplicationClient
+ >>> client = BackendApplicationClient('your_id')
+ >>> client.prepare_request_body(scope=['hello', 'world'])
+ 'grant_type=client_credentials&scope=hello+world'
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.grant_type, body=body,
+ scope=scope, **kwargs)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/base.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/base.py
new file mode 100644
index 0000000000..d5eb0cc15f
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/base.py
@@ -0,0 +1,604 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 RFC6749.
+"""
+import base64
+import hashlib
+import re
+import secrets
+import time
+import warnings
+
+from oauthlib.common import generate_token
+from oauthlib.oauth2.rfc6749 import tokens
+from oauthlib.oauth2.rfc6749.errors import (
+ InsecureTransportError, TokenExpiredError,
+)
+from oauthlib.oauth2.rfc6749.parameters import (
+ parse_token_response, prepare_token_request,
+ prepare_token_revocation_request,
+)
+from oauthlib.oauth2.rfc6749.utils import is_secure_transport
+
+AUTH_HEADER = 'auth_header'
+URI_QUERY = 'query'
+BODY = 'body'
+
+FORM_ENC_HEADERS = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+}
+
+
+class Client:
+ """Base OAuth2 client responsible for access token management.
+
+ This class also acts as a generic interface providing methods common to all
+ client types such as ``prepare_authorization_request`` and
+ ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
+ the recommended way of interacting with clients (as opposed to the abstract
+ prepare uri/body/etc methods). They are recommended over the older set
+ because they are easier to use (more consistent) and add a few additional
+ security checks, such as HTTPS and state checking.
+
+ Some of these methods require further implementation only provided by the
+ specific purpose clients such as
+ :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
+ seek to use the client class matching the OAuth workflow you need. For
+ Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
+
+ """
+ refresh_token_key = 'refresh_token'
+
+ def __init__(self, client_id,
+ default_token_placement=AUTH_HEADER,
+ token_type='Bearer',
+ access_token=None,
+ refresh_token=None,
+ mac_key=None,
+ mac_algorithm=None,
+ token=None,
+ scope=None,
+ state=None,
+ redirect_url=None,
+ state_generator=generate_token,
+ code_verifier=None,
+ code_challenge=None,
+ code_challenge_method=None,
+ **kwargs):
+ """Initialize a client with commonly used attributes.
+
+ :param client_id: Client identifier given by the OAuth provider upon
+ registration.
+
+ :param default_token_placement: Tokens can be supplied in the Authorization
+ header (default), the URL query component (``query``) or the request
+ body (``body``).
+
+ :param token_type: OAuth 2 token type. Defaults to Bearer. Change this
+ if you specify the ``access_token`` parameter and know it is of a
+ different token type, such as a MAC, JWT or SAML token. Can
+ also be supplied as ``token_type`` inside the ``token`` dict parameter.
+
+ :param access_token: An access token (string) used to authenticate
+ requests to protected resources. Can also be supplied inside the
+ ``token`` dict parameter.
+
+ :param refresh_token: A refresh token (string) used to refresh expired
+ tokens. Can also be supplied inside the ``token`` dict parameter.
+
+ :param mac_key: Encryption key used with MAC tokens.
+
+ :param mac_algorithm: Hashing algorithm for MAC tokens.
+
+ :param token: A dict of token attributes such as ``access_token``,
+ ``token_type`` and ``expires_at``.
+
+ :param scope: A list of default scopes to request authorization for.
+
+ :param state: A CSRF protection string used during authorization.
+
+ :param redirect_url: The redirection endpoint on the client side to which
+ the user returns after authorization.
+
+ :param state_generator: A no argument state generation callable. Defaults
+ to :py:meth:`oauthlib.common.generate_token`.
+
+ :param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
+ authorization request to the token request.
+
+ :param code_challenge: PKCE parameter. A challenge derived from the code verifier that is sent in the
+ authorization request, to be verified against later.
+
+ :param code_challenge_method: PKCE parameter. A method that was used to derive code challenge.
+ Defaults to "plain" if not present in the request.
+ """
+
+ self.client_id = client_id
+ self.default_token_placement = default_token_placement
+ self.token_type = token_type
+ self.access_token = access_token
+ self.refresh_token = refresh_token
+ self.mac_key = mac_key
+ self.mac_algorithm = mac_algorithm
+ self.token = token or {}
+ self.scope = scope
+ self.state_generator = state_generator
+ self.state = state
+ self.redirect_url = redirect_url
+ self.code_verifier = code_verifier
+ self.code_challenge = code_challenge
+ self.code_challenge_method = code_challenge_method
+ self.code = None
+ self.expires_in = None
+ self._expires_at = None
+ self.populate_token_attributes(self.token)
+
+ @property
+ def token_types(self):
+ """Supported token types and their respective methods
+
+ Additional tokens can be supported by extending this dictionary.
+
+ The Bearer token spec is stable and safe to use.
+
+ The MAC token spec is not yet stable and support for MAC tokens
+ is experimental and currently matching version 00 of the spec.
+ """
+ return {
+ 'Bearer': self._add_bearer_token,
+ 'MAC': self._add_mac_token
+ }
+
+ def prepare_request_uri(self, *args, **kwargs):
+ """Abstract method used to create request URIs."""
+ raise NotImplementedError("Must be implemented by inheriting classes.")
+
+ def prepare_request_body(self, *args, **kwargs):
+ """Abstract method used to create request bodies."""
+ raise NotImplementedError("Must be implemented by inheriting classes.")
+
+ def parse_request_uri_response(self, *args, **kwargs):
+ """Abstract method used to parse redirection responses."""
+ raise NotImplementedError("Must be implemented by inheriting classes.")
+
+ def add_token(self, uri, http_method='GET', body=None, headers=None,
+ token_placement=None, **kwargs):
+ """Add token to the request uri, body or authorization header.
+
+ The access token type provides the client with the information
+ required to successfully utilize the access token to make a protected
+ resource request (along with type-specific attributes). The client
+ MUST NOT use an access token if it does not understand the token
+ type.
+
+ For example, the "bearer" token type defined in
+ [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
+ token string in the request:
+
+ .. code-block:: http
+
+ GET /resource/1 HTTP/1.1
+ Host: example.com
+ Authorization: Bearer mF_9.B5f-4.1JqM
+
+ while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
+ utilized by issuing a MAC key together with the access token which is
+ used to sign certain components of the HTTP requests:
+
+ .. code-block:: http
+
+ GET /resource/1 HTTP/1.1
+ Host: example.com
+ Authorization: MAC id="h480djs93hd8",
+ nonce="274312:dj83hs9s",
+ mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
+
+ .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2
+ .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ token_placement = token_placement or self.default_token_placement
+
+ case_insensitive_token_types = {
+ k.lower(): v for k, v in self.token_types.items()}
+ if not self.token_type.lower() in case_insensitive_token_types:
+ raise ValueError("Unsupported token type: %s" % self.token_type)
+
+ if not (self.access_token or self.token.get('access_token')):
+ raise ValueError("Missing access token.")
+
+ if self._expires_at and self._expires_at < time.time():
+ raise TokenExpiredError()
+
+ return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
+ headers, token_placement, **kwargs)
+
+ def prepare_authorization_request(self, authorization_url, state=None,
+ redirect_url=None, scope=None, **kwargs):
+ """Prepare the authorization request.
+
+ This is the first step in many OAuth flows in which the user is
+ redirected to a certain authorization URL. This method adds
+ required parameters to the authorization URL.
+
+ :param authorization_url: Provider authorization endpoint URL.
+ :param state: CSRF protection string. Will be automatically created if
+ not provided. The generated state is available via the ``state``
+ attribute. Clients should verify that the state is unchanged and
+ present in the authorization response. This verification is done
+ automatically if using the ``authorization_response`` parameter
+ with ``prepare_token_request``.
+ :param redirect_url: Redirect URL to which the user will be returned
+ after authorization. Must be provided unless previously setup with
+ the provider. If provided then it must also be provided in the
+ token request.
+ :param scope: List of scopes to request. Must be equal to
+ or a subset of the scopes granted when obtaining the refresh
+ token. If none is provided, the ones provided in the constructor are
+ used.
+ :param kwargs: Additional parameters to included in the request.
+ :returns: The prepared request tuple with (url, headers, body).
+ """
+ if not is_secure_transport(authorization_url):
+ raise InsecureTransportError()
+
+ self.state = state or self.state_generator()
+ self.redirect_url = redirect_url or self.redirect_url
+ # do not assign scope to self automatically anymore
+ scope = self.scope if scope is None else scope
+ auth_url = self.prepare_request_uri(
+ authorization_url, redirect_uri=self.redirect_url,
+ scope=scope, state=self.state, **kwargs)
+ return auth_url, FORM_ENC_HEADERS, ''
+
+ def prepare_token_request(self, token_url, authorization_response=None,
+ redirect_url=None, state=None, body='', **kwargs):
+ """Prepare a token creation request.
+
+ Note that these requests usually require client authentication, either
+ by including client_id or a set of provider specific authentication
+ credentials.
+
+ :param token_url: Provider token creation endpoint URL.
+ :param authorization_response: The full redirection URL string, i.e.
+ the location to which the user was redirected after successful
+ authorization. Used to mine credentials needed to obtain a token
+ in this step, such as authorization code.
+ :param redirect_url: The redirect_url supplied with the authorization
+ request (if there was one).
+ :param state:
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+ :param kwargs: Additional parameters to included in the request.
+ :returns: The prepared request tuple with (url, headers, body).
+ """
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ state = state or self.state
+ if authorization_response:
+ self.parse_request_uri_response(
+ authorization_response, state=state)
+ self.redirect_url = redirect_url or self.redirect_url
+ body = self.prepare_request_body(body=body,
+ redirect_uri=self.redirect_url, **kwargs)
+
+ return token_url, FORM_ENC_HEADERS, body
+
+ def prepare_refresh_token_request(self, token_url, refresh_token=None,
+ body='', scope=None, **kwargs):
+ """Prepare an access token refresh request.
+
+ Expired access tokens can be replaced by new access tokens without
+ going through the OAuth dance if the client obtained a refresh token.
+ This refresh token and authentication credentials can be used to
+ obtain a new access token, and possibly a new refresh token.
+
+ :param token_url: Provider token refresh endpoint URL.
+ :param refresh_token: Refresh token string.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+ :param scope: List of scopes to request. Must be equal to
+ or a subset of the scopes granted when obtaining the refresh
+ token. If none is provided, the ones provided in the constructor are
+ used.
+ :param kwargs: Additional parameters to included in the request.
+ :returns: The prepared request tuple with (url, headers, body).
+ """
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ # do not assign scope to self automatically anymore
+ scope = self.scope if scope is None else scope
+ body = self.prepare_refresh_body(body=body,
+ refresh_token=refresh_token, scope=scope, **kwargs)
+ return token_url, FORM_ENC_HEADERS, body
+
+ def prepare_token_revocation_request(self, revocation_url, token,
+ token_type_hint="access_token", body='', callback=None, **kwargs):
+ """Prepare a token revocation request.
+
+ :param revocation_url: Provider token revocation endpoint URL.
+ :param token: The access or refresh token to be revoked (string).
+ :param token_type_hint: ``"access_token"`` (default) or
+ ``"refresh_token"``. This is optional and if you wish to not pass it you
+ must provide ``token_type_hint=None``.
+ :param body:
+ :param callback: A jsonp callback such as ``package.callback`` to be invoked
+ upon receiving the response. Not that it should not include a () suffix.
+ :param kwargs: Additional parameters to included in the request.
+ :returns: The prepared request tuple with (url, headers, body).
+
+ Note that JSONP request may use GET requests as the parameters will
+ be added to the request URL query as opposed to the request body.
+
+ An example of a revocation request
+
+ .. code-block:: http
+
+ POST /revoke HTTP/1.1
+ Host: server.example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
+
+ token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
+
+ An example of a jsonp revocation request
+
+ .. code-block:: http
+
+ GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
+ Host: server.example.com
+ Content-Type: application/x-www-form-urlencoded
+ Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
+
+ and an error response
+
+ .. code-block:: javascript
+
+ package.myCallback({"error":"unsupported_token_type"});
+
+ Note that these requests usually require client credentials, client_id in
+ the case for public clients and provider specific authentication
+ credentials for confidential clients.
+ """
+ if not is_secure_transport(revocation_url):
+ raise InsecureTransportError()
+
+ return prepare_token_revocation_request(revocation_url, token,
+ token_type_hint=token_type_hint, body=body, callback=callback,
+ **kwargs)
+
+ def parse_request_body_response(self, body, scope=None, **kwargs):
+ """Parse the JSON response body.
+
+ If the access token request is valid and authorized, the
+ authorization server issues an access token as described in
+ `Section 5.1`_. A refresh token SHOULD NOT be included. If the request
+ failed client authentication or is invalid, the authorization server
+ returns an error response as described in `Section 5.2`_.
+
+ :param body: The response body from the token request.
+ :param scope: Scopes originally requested. If none is provided, the ones
+ provided in the constructor are used.
+ :return: Dictionary of token parameters.
+ :raises: Warning if scope has changed. :py:class:`oauthlib.oauth2.errors.OAuth2Error`
+ if response is invalid.
+
+ These response are json encoded and could easily be parsed without
+ the assistance of OAuthLib. However, there are a few subtle issues
+ to be aware of regarding the response which are helpfully addressed
+ through the raising of various errors.
+
+ A successful response should always contain
+
+ **access_token**
+ The access token issued by the authorization server. Often
+ a random string.
+
+ **token_type**
+ The type of the token issued as described in `Section 7.1`_.
+ Commonly ``Bearer``.
+
+ While it is not mandated it is recommended that the provider include
+
+ **expires_in**
+ The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ **scope**
+ Providers may supply this in all responses but are required to only
+ if it has changed since the authorization request.
+
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
+ """
+ scope = self.scope if scope is None else scope
+ self.token = parse_token_response(body, scope=scope)
+ self.populate_token_attributes(self.token)
+ return self.token
+
+ def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
+ """Prepare an access token request, using a refresh token.
+
+ If the authorization server issued a refresh token to the client, the
+ client makes a refresh request to the token endpoint by adding the
+ following parameters using the `application/x-www-form-urlencoded`
+ format in the HTTP request entity-body:
+
+ :param refresh_token: REQUIRED. The refresh token issued to the client.
+ :param scope: OPTIONAL. The scope of the access request as described by
+ Section 3.3. The requested scope MUST NOT include any scope
+ not originally granted by the resource owner, and if omitted is
+ treated as equal to the scope originally granted by the
+ resource owner. Note that if none is provided, the ones provided
+ in the constructor are used if any.
+ """
+ refresh_token = refresh_token or self.refresh_token
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.refresh_token_key, body=body, scope=scope,
+ refresh_token=refresh_token, **kwargs)
+
+ def _add_bearer_token(self, uri, http_method='GET', body=None,
+ headers=None, token_placement=None):
+ """Add a bearer token to the request uri, body or authorization header."""
+ if token_placement == AUTH_HEADER:
+ headers = tokens.prepare_bearer_headers(self.access_token, headers)
+
+ elif token_placement == URI_QUERY:
+ uri = tokens.prepare_bearer_uri(self.access_token, uri)
+
+ elif token_placement == BODY:
+ body = tokens.prepare_bearer_body(self.access_token, body)
+
+ else:
+ raise ValueError("Invalid token placement.")
+ return uri, headers, body
+
+ def create_code_verifier(self, length):
+ """Create PKCE **code_verifier** used in computing **code_challenge**.
+ See `RFC7636 Section 4.1`_
+
+ :param length: REQUIRED. The length of the code_verifier.
+
+ The client first creates a code verifier, "code_verifier", for each
+ OAuth 2.0 [RFC6749] Authorization Request, in the following manner:
+
+ .. code-block:: text
+
+ code_verifier = high-entropy cryptographic random STRING using the
+ unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
+ from Section 2.3 of [RFC3986], with a minimum length of 43 characters
+ and a maximum length of 128 characters.
+
+ .. _`RFC7636 Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1
+ """
+ code_verifier = None
+
+ if not length >= 43:
+ raise ValueError("Length must be greater than or equal to 43")
+
+ if not length <= 128:
+ raise ValueError("Length must be less than or equal to 128")
+
+ allowed_characters = re.compile('^[A-Zaa-z0-9-._~]')
+ code_verifier = secrets.token_urlsafe(length)
+
+ if not re.search(allowed_characters, code_verifier):
+ raise ValueError("code_verifier contains invalid characters")
+
+ self.code_verifier = code_verifier
+
+ return code_verifier
+
+ def create_code_challenge(self, code_verifier, code_challenge_method=None):
+ """Create PKCE **code_challenge** derived from the **code_verifier**.
+ See `RFC7636 Section 4.2`_
+
+ :param code_verifier: REQUIRED. The **code_verifier** generated from `create_code_verifier()`.
+ :param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable values include `S256`. DEFAULT is `plain`.
+
+ The client then creates a code challenge derived from the code
+ verifier by using one of the following transformations on the code
+ verifier::
+
+ plain
+ code_challenge = code_verifier
+ S256
+ code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
+
+ If the client is capable of using `S256`, it MUST use `S256`, as
+ `S256` is Mandatory To Implement (MTI) on the server. Clients are
+ permitted to use `plain` only if they cannot support `S256` for some
+ technical reason and know via out-of-band configuration that the
+ server supports `plain`.
+
+ The plain transformation is for compatibility with existing
+ deployments and for constrained environments that can't use the S256 transformation.
+
+ .. _`RFC7636 Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2
+ """
+ code_challenge = None
+
+ if code_verifier == None:
+ raise ValueError("Invalid code_verifier")
+
+ if code_challenge_method == None:
+ code_challenge_method = "plain"
+ self.code_challenge_method = code_challenge_method
+ code_challenge = code_verifier
+ self.code_challenge = code_challenge
+
+ if code_challenge_method == "S256":
+ h = hashlib.sha256()
+ h.update(code_verifier.encode(encoding='ascii'))
+ sha256_val = h.digest()
+ code_challenge = bytes.decode(base64.urlsafe_b64encode(sha256_val))
+ # replace '+' with '-', '/' with '_', and remove trailing '='
+ code_challenge = code_challenge.replace("+", "-").replace("/", "_").replace("=", "")
+ self.code_challenge = code_challenge
+
+ return code_challenge
+
+ def _add_mac_token(self, uri, http_method='GET', body=None,
+ headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
+ """Add a MAC token to the request authorization header.
+
+ Warning: MAC token support is experimental as the spec is not yet stable.
+ """
+ if token_placement != AUTH_HEADER:
+ raise ValueError("Invalid token placement.")
+
+ headers = tokens.prepare_mac_header(self.access_token, uri,
+ self.mac_key, http_method, headers=headers, body=body, ext=ext,
+ hash_algorithm=self.mac_algorithm, **kwargs)
+ return uri, headers, body
+
+ def _populate_attributes(self, response):
+ warnings.warn("Please switch to the public method "
+ "populate_token_attributes.", DeprecationWarning)
+ return self.populate_token_attributes(response)
+
+ def populate_code_attributes(self, response):
+ """Add attributes from an auth code response to self."""
+
+ if 'code' in response:
+ self.code = response.get('code')
+
+ def populate_token_attributes(self, response):
+ """Add attributes from a token exchange response to self."""
+
+ if 'access_token' in response:
+ self.access_token = response.get('access_token')
+
+ if 'refresh_token' in response:
+ self.refresh_token = response.get('refresh_token')
+
+ if 'token_type' in response:
+ self.token_type = response.get('token_type')
+
+ if 'expires_in' in response:
+ self.expires_in = response.get('expires_in')
+ self._expires_at = time.time() + int(self.expires_in)
+
+ if 'expires_at' in response:
+ try:
+ self._expires_at = int(response.get('expires_at'))
+ except:
+ self._expires_at = None
+
+ if 'mac_key' in response:
+ self.mac_key = response.get('mac_key')
+
+ if 'mac_algorithm' in response:
+ self.mac_algorithm = response.get('mac_algorithm')
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/legacy_application.py
new file mode 100644
index 0000000000..9920981d2c
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/legacy_application.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from ..parameters import prepare_token_request
+from .base import Client
+
+
+class LegacyApplicationClient(Client):
+
+ """A public client using the resource owner password and username directly.
+
+ The resource owner password credentials grant type is suitable in
+ cases where the resource owner has a trust relationship with the
+ client, such as the device operating system or a highly privileged
+ application. The authorization server should take special care when
+ enabling this grant type, and only allow it when other flows are not
+ viable.
+
+ The grant type is suitable for clients capable of obtaining the
+ resource owner's credentials (username and password, typically using
+ an interactive form). It is also used to migrate existing clients
+ using direct authentication schemes such as HTTP Basic or Digest
+ authentication to OAuth by converting the stored credentials to an
+ access token.
+
+ The method through which the client obtains the resource owner
+ credentials is beyond the scope of this specification. The client
+ MUST discard the credentials once an access token has been obtained.
+ """
+
+ grant_type = 'password'
+
+ def __init__(self, client_id, **kwargs):
+ super().__init__(client_id, **kwargs)
+
+ def prepare_request_body(self, username, password, body='', scope=None,
+ include_client_id=False, **kwargs):
+ """Add the resource owner password and username to the request body.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format per `Appendix B`_ in the HTTP request entity-body:
+
+ :param username: The resource owner username.
+ :param password: The resource owner password.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+ :param include_client_id: `True` to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_. False otherwise (default).
+ :type include_client_id: Boolean
+ :param kwargs: Extra credentials to include in the token request.
+
+ If the client type is confidential or the client was issued client
+ credentials (or assigned other authentication requirements), the
+ client MUST authenticate with the authorization server as described
+ in `Section 3.2.1`_.
+
+ The prepared body will include all provided credentials as well as
+ the ``grant_type`` parameter set to ``password``::
+
+ >>> from oauthlib.oauth2 import LegacyApplicationClient
+ >>> client = LegacyApplicationClient('your_id')
+ >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world'])
+ 'grant_type=password&username=foo&scope=hello+world&password=bar'
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.grant_type, body=body, username=username,
+ password=password, scope=scope, **kwargs)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/mobile_application.py
new file mode 100644
index 0000000000..b10b41ced3
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/mobile_application.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from ..parameters import parse_implicit_response, prepare_grant_uri
+from .base import Client
+
+
+class MobileApplicationClient(Client):
+
+ """A public client utilizing the implicit code grant workflow.
+
+ A user-agent-based application is a public client in which the
+ client code is downloaded from a web server and executes within a
+ user-agent (e.g. web browser) on the device used by the resource
+ owner. Protocol data and credentials are easily accessible (and
+ often visible) to the resource owner. Since such applications
+ reside within the user-agent, they can make seamless use of the
+ user-agent capabilities when requesting authorization.
+
+ The implicit grant type is used to obtain access tokens (it does not
+ support the issuance of refresh tokens) and is optimized for public
+ clients known to operate a particular redirection URI. These clients
+ are typically implemented in a browser using a scripting language
+ such as JavaScript.
+
+ As a redirection-based flow, the client must be capable of
+ interacting with the resource owner's user-agent (typically a web
+ browser) and capable of receiving incoming requests (via redirection)
+ from the authorization server.
+
+ Unlike the authorization code grant type in which the client makes
+ separate requests for authorization and access token, the client
+ receives the access token as the result of the authorization request.
+
+ The implicit grant type does not include client authentication, and
+ relies on the presence of the resource owner and the registration of
+ the redirection URI. Because the access token is encoded into the
+ redirection URI, it may be exposed to the resource owner and other
+ applications residing on the same device.
+ """
+
+ response_type = 'token'
+
+ def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
+ state=None, **kwargs):
+ """Prepare the implicit grant request URI.
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
+ and it should have been registered with the OAuth
+ provider prior to use. As described in `Section 3.1.2`_.
+
+ :param scope: OPTIONAL. The scope of the access request as described by
+ Section 3.3`_. These may be any string but are commonly
+ URIs or various categories such as ``videos`` or ``documents``.
+
+ :param state: RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ :param kwargs: Extra arguments to include in the request URI.
+
+ In addition to supplied parameters, OAuthLib will append the ``client_id``
+ that was provided in the constructor as well as the mandatory ``response_type``
+ argument, set to ``token``::
+
+ >>> from oauthlib.oauth2 import MobileApplicationClient
+ >>> client = MobileApplicationClient('your_id')
+ >>> client.prepare_request_uri('https://example.com')
+ 'https://example.com?client_id=your_id&response_type=token'
+ >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
+ 'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
+ >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
+ 'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures'
+ >>> client.prepare_request_uri('https://example.com', foo='bar')
+ 'https://example.com?client_id=your_id&response_type=token&foo=bar'
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ scope = self.scope if scope is None else scope
+ return prepare_grant_uri(uri, self.client_id, self.response_type,
+ redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
+
+ def parse_request_uri_response(self, uri, state=None, scope=None):
+ """Parse the response URI fragment.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ the following parameters to the fragment component of the redirection
+ URI using the "application/x-www-form-urlencoded" format:
+
+ :param uri: The callback URI that resulted from the user being redirected
+ back from the provider to you, the client.
+ :param state: The state provided in the authorization request.
+ :param scope: The scopes provided in the authorization request.
+ :return: Dictionary of token parameters.
+ :raises: OAuth2Error if response is invalid.
+
+ A successful response should always contain
+
+ **access_token**
+ The access token issued by the authorization server. Often
+ a random string.
+
+ **token_type**
+ The type of the token issued as described in `Section 7.1`_.
+ Commonly ``Bearer``.
+
+ **state**
+ If you provided the state parameter in the authorization phase, then
+ the provider is required to include that exact state value in the
+ response.
+
+ While it is not mandated it is recommended that the provider include
+
+ **expires_in**
+ The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ **scope**
+ Providers may supply this in all responses but are required to only
+ if it has changed since the authorization request.
+
+ A few example responses can be seen below::
+
+ >>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world'
+ >>> from oauthlib.oauth2 import MobileApplicationClient
+ >>> client = MobileApplicationClient('your_id')
+ >>> client.parse_request_uri_response(response_uri)
+ {
+ 'access_token': 'sdlfkj452',
+ 'token_type': 'Bearer',
+ 'state': 'ss345asyht',
+ 'scope': [u'hello', u'world']
+ }
+ >>> client.parse_request_uri_response(response_uri, state='other')
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response
+ **scope**
+ File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response
+ raise ValueError("Mismatching or missing state in params.")
+ ValueError: Mismatching or missing state in params.
+ >>> def alert_scope_changed(message, old, new):
+ ... print(message, old, new)
+ ...
+ >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
+ >>> client.parse_request_body_response(response_body, scope=['other'])
+ ('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
+
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ """
+ scope = self.scope if scope is None else scope
+ self.token = parse_implicit_response(uri, state=state, scope=scope)
+ self.populate_token_attributes(self.token)
+ return self.token
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/service_application.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/service_application.py
new file mode 100644
index 0000000000..8fb173776d
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/service_application.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import time
+
+from oauthlib.common import to_unicode
+
+from ..parameters import prepare_token_request
+from .base import Client
+
+
+class ServiceApplicationClient(Client):
+ """A public client utilizing the JWT bearer grant.
+
+ JWT bearer tokes can be used to request an access token when a client
+ wishes to utilize an existing trust relationship, expressed through the
+ semantics of (and digital signature or keyed message digest calculated
+ over) the JWT, without a direct user approval step at the authorization
+ server.
+
+ This grant type does not involve an authorization step. It may be
+ used by both public and confidential clients.
+ """
+
+ grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
+
+ def __init__(self, client_id, private_key=None, subject=None, issuer=None,
+ audience=None, **kwargs):
+ """Initialize a JWT client with defaults for implicit use later.
+
+ :param client_id: Client identifier given by the OAuth provider upon
+ registration.
+
+ :param private_key: Private key used for signing and encrypting.
+ Must be given as a string.
+
+ :param subject: The principal that is the subject of the JWT, i.e.
+ which user is the token requested on behalf of.
+ For example, ``foo@example.com.
+
+ :param issuer: The JWT MUST contain an "iss" (issuer) claim that
+ contains a unique identifier for the entity that issued
+ the JWT. For example, ``your-client@provider.com``.
+
+ :param audience: A value identifying the authorization server as an
+ intended audience, e.g.
+ ``https://provider.com/oauth2/token``.
+
+ :param kwargs: Additional arguments to pass to base client, such as
+ state and token. See ``Client.__init__.__doc__`` for
+ details.
+ """
+ super().__init__(client_id, **kwargs)
+ self.private_key = private_key
+ self.subject = subject
+ self.issuer = issuer
+ self.audience = audience
+
+ def prepare_request_body(self,
+ private_key=None,
+ subject=None,
+ issuer=None,
+ audience=None,
+ expires_at=None,
+ issued_at=None,
+ extra_claims=None,
+ body='',
+ scope=None,
+ include_client_id=False,
+ **kwargs):
+ """Create and add a JWT assertion to the request body.
+
+ :param private_key: Private key used for signing and encrypting.
+ Must be given as a string.
+
+ :param subject: (sub) The principal that is the subject of the JWT,
+ i.e. which user is the token requested on behalf of.
+ For example, ``foo@example.com.
+
+ :param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that
+ contains a unique identifier for the entity that issued
+ the JWT. For example, ``your-client@provider.com``.
+
+ :param audience: (aud) A value identifying the authorization server as an
+ intended audience, e.g.
+ ``https://provider.com/oauth2/token``.
+
+ :param expires_at: A unix expiration timestamp for the JWT. Defaults
+ to an hour from now, i.e. ``time.time() + 3600``.
+
+ :param issued_at: A unix timestamp of when the JWT was created.
+ Defaults to now, i.e. ``time.time()``.
+
+ :param extra_claims: A dict of additional claims to include in the JWT.
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+
+ :param scope: The scope of the access request.
+
+ :param include_client_id: `True` to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_. False otherwise (default).
+ :type include_client_id: Boolean
+
+ :param not_before: A unix timestamp after which the JWT may be used.
+ Not included unless provided. *
+
+ :param jwt_id: A unique JWT token identifier. Not included unless
+ provided. *
+
+ :param kwargs: Extra credentials to include in the token request.
+
+ Parameters marked with a `*` above are not explicit arguments in the
+ function signature, but are specially documented arguments for items
+ appearing in the generic `**kwargs` keyworded input.
+
+ The "scope" parameter may be used, as defined in the Assertion
+ Framework for OAuth 2.0 Client Authentication and Authorization Grants
+ [I-D.ietf-oauth-assertions] specification, to indicate the requested
+ scope.
+
+ Authentication of the client is optional, as described in
+ `Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the
+ "client_id" is only needed when a form of client authentication that
+ relies on the parameter is used.
+
+ The following non-normative example demonstrates an Access Token
+ Request with a JWT as an authorization grant (with extra line breaks
+ for display purposes only):
+
+ .. code-block: http
+
+ POST /token.oauth2 HTTP/1.1
+ Host: as.example.com
+ Content-Type: application/x-www-form-urlencoded
+
+ grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
+ &assertion=eyJhbGciOiJFUzI1NiJ9.
+ eyJpc3Mi[...omitted for brevity...].
+ J9l-ZhwP[...omitted for brevity...]
+
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ import jwt
+
+ key = private_key or self.private_key
+ if not key:
+ raise ValueError('An encryption key must be supplied to make JWT'
+ ' token requests.')
+ claim = {
+ 'iss': issuer or self.issuer,
+ 'aud': audience or self.audience,
+ 'sub': subject or self.subject,
+ 'exp': int(expires_at or time.time() + 3600),
+ 'iat': int(issued_at or time.time()),
+ }
+
+ for attr in ('iss', 'aud', 'sub'):
+ if claim[attr] is None:
+ raise ValueError(
+ 'Claim must include %s but none was given.' % attr)
+
+ if 'not_before' in kwargs:
+ claim['nbf'] = kwargs.pop('not_before')
+
+ if 'jwt_id' in kwargs:
+ claim['jti'] = kwargs.pop('jwt_id')
+
+ claim.update(extra_claims or {})
+
+ assertion = jwt.encode(claim, key, 'RS256')
+ assertion = to_unicode(assertion)
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.grant_type,
+ body=body,
+ assertion=assertion,
+ scope=scope,
+ **kwargs)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/web_application.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/web_application.py
new file mode 100644
index 0000000000..50890fbf8a
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/clients/web_application.py
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import warnings
+
+from ..parameters import (
+ parse_authorization_code_response, prepare_grant_uri,
+ prepare_token_request,
+)
+from .base import Client
+
+
+class WebApplicationClient(Client):
+
+ """A client utilizing the authorization code grant workflow.
+
+ A web application is a confidential client running on a web
+ server. Resource owners access the client via an HTML user
+ interface rendered in a user-agent on the device used by the
+ resource owner. The client credentials as well as any access
+ token issued to the client are stored on the web server and are
+ not exposed to or accessible by the resource owner.
+
+ The authorization code grant type is used to obtain both access
+ tokens and refresh tokens and is optimized for confidential clients.
+ As a redirection-based flow, the client must be capable of
+ interacting with the resource owner's user-agent (typically a web
+ browser) and capable of receiving incoming requests (via redirection)
+ from the authorization server.
+ """
+
+ grant_type = 'authorization_code'
+
+ def __init__(self, client_id, code=None, **kwargs):
+ super().__init__(client_id, **kwargs)
+ self.code = code
+
+ def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
+ state=None, code_challenge=None, code_challenge_method='plain', **kwargs):
+ """Prepare the authorization code request URI
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI
+ and it should have been registered with the OAuth
+ provider prior to use. As described in `Section 3.1.2`_.
+
+ :param scope: OPTIONAL. The scope of the access request as described by
+ Section 3.3`_. These may be any string but are commonly
+ URIs or various categories such as ``videos`` or ``documents``.
+
+ :param state: RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ :param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced.
+ A challenge derived from the code_verifier that is sent in the
+ authorization request, to be verified against later.
+
+ :param code_challenge_method: OPTIONAL. PKCE parameter. A method that was used to derive code challenge.
+ Defaults to "plain" if not present in the request.
+
+ :param kwargs: Extra arguments to include in the request URI.
+
+ In addition to supplied parameters, OAuthLib will append the ``client_id``
+ that was provided in the constructor as well as the mandatory ``response_type``
+ argument, set to ``code``::
+
+ >>> from oauthlib.oauth2 import WebApplicationClient
+ >>> client = WebApplicationClient('your_id')
+ >>> client.prepare_request_uri('https://example.com')
+ 'https://example.com?client_id=your_id&response_type=code'
+ >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
+ 'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
+ >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
+ 'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures'
+ >>> client.prepare_request_uri('https://example.com', code_challenge='kjasBS523KdkAILD2k78NdcJSk2k3KHG6')
+ 'https://example.com?client_id=your_id&response_type=code&code_challenge=kjasBS523KdkAILD2k78NdcJSk2k3KHG6'
+ >>> client.prepare_request_uri('https://example.com', code_challenge_method='S256')
+ 'https://example.com?client_id=your_id&response_type=code&code_challenge_method=S256'
+ >>> client.prepare_request_uri('https://example.com', foo='bar')
+ 'https://example.com?client_id=your_id&response_type=code&foo=bar'
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ scope = self.scope if scope is None else scope
+ return prepare_grant_uri(uri, self.client_id, 'code',
+ redirect_uri=redirect_uri, scope=scope, state=state, code_challenge=code_challenge,
+ code_challenge_method=code_challenge_method, **kwargs)
+
+ def prepare_request_body(self, code=None, redirect_uri=None, body='',
+ include_client_id=True, code_verifier=None, **kwargs):
+ """Prepare the access token request body.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format in the HTTP request entity-body:
+
+ :param code: REQUIRED. The authorization code received from the
+ authorization server.
+
+ :param redirect_uri: REQUIRED, if the "redirect_uri" parameter was included in the
+ authorization request as described in `Section 4.1.1`_, and their
+ values MUST be identical.
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+
+ :param include_client_id: `True` (default) to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
+ :param code_verifier: OPTIONAL. A cryptographically random string that is used to correlate the
+ authorization request to the token request.
+
+ :param kwargs: Extra parameters to include in the token request.
+
+ In addition OAuthLib will add the ``grant_type`` parameter set to
+ ``authorization_code``.
+
+ If the client type is confidential or the client was issued client
+ credentials (or assigned other authentication requirements), the
+ client MUST authenticate with the authorization server as described
+ in `Section 3.2.1`_::
+
+ >>> from oauthlib.oauth2 import WebApplicationClient
+ >>> client = WebApplicationClient('your_id')
+ >>> client.prepare_request_body(code='sh35ksdf09sf')
+ 'grant_type=authorization_code&code=sh35ksdf09sf'
+ >>> client.prepare_request_body(code_verifier='KB46DCKJ873NCGXK5GD682NHDKK34GR')
+ 'grant_type=authorization_code&code_verifier=KB46DCKJ873NCGXK5GD682NHDKK34GR'
+ >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
+ 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
+
+ `Section 3.2.1` also states:
+ In the "authorization_code" "grant_type" request to the token
+ endpoint, an unauthenticated client MUST send its "client_id" to
+ prevent itself from inadvertently accepting a code intended for a
+ client with a different "client_id". This protects the client from
+ substitution of the authentication code. (It provides no additional
+ security for the protected resource.)
+
+ .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ code = code or self.code
+ if 'client_id' in kwargs:
+ warnings.warn("`client_id` has been deprecated in favor of "
+ "`include_client_id`, a boolean value which will "
+ "include the already configured `self.client_id`.",
+ DeprecationWarning)
+ if kwargs['client_id'] != self.client_id:
+ raise ValueError("`client_id` was supplied as an argument, but "
+ "it does not match `self.client_id`")
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ return prepare_token_request(self.grant_type, code=code, body=body,
+ redirect_uri=redirect_uri, code_verifier=code_verifier, **kwargs)
+
+ def parse_request_uri_response(self, uri, state=None):
+ """Parse the URI query for code and state.
+
+ If the resource owner grants the access request, the authorization
+ server issues an authorization code and delivers it to the client by
+ adding the following parameters to the query component of the
+ redirection URI using the "application/x-www-form-urlencoded" format:
+
+ :param uri: The callback URI that resulted from the user being redirected
+ back from the provider to you, the client.
+ :param state: The state provided in the authorization request.
+
+ **code**
+ The authorization code generated by the authorization server.
+ The authorization code MUST expire shortly after it is issued
+ to mitigate the risk of leaks. A maximum authorization code
+ lifetime of 10 minutes is RECOMMENDED. The client MUST NOT
+ use the authorization code more than once. If an authorization
+ code is used more than once, the authorization server MUST deny
+ the request and SHOULD revoke (when possible) all tokens
+ previously issued based on that authorization code.
+ The authorization code is bound to the client identifier and
+ redirection URI.
+
+ **state**
+ If the "state" parameter was present in the authorization request.
+
+ This method is mainly intended to enforce strict state checking with
+ the added benefit of easily extracting parameters from the URI::
+
+ >>> from oauthlib.oauth2 import WebApplicationClient
+ >>> client = WebApplicationClient('your_id')
+ >>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45'
+ >>> client.parse_request_uri_response(uri, state='sfetw45')
+ {'state': 'sfetw45', 'code': 'sdfkjh345'}
+ >>> client.parse_request_uri_response(uri, state='other')
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response
+ back from the provider to you, the client.
+ File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response
+ raise MismatchingStateError()
+ oauthlib.oauth2.rfc6749.errors.MismatchingStateError
+ """
+ response = parse_authorization_code_response(uri, state=state)
+ self.populate_code_attributes(response)
+ return response
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/__init__.py
new file mode 100644
index 0000000000..1695b41b66
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/__init__.py
@@ -0,0 +1,17 @@
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from .authorization import AuthorizationEndpoint
+from .introspect import IntrospectEndpoint
+from .metadata import MetadataEndpoint
+from .pre_configured import (
+ BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
+ Server, WebApplicationServer,
+)
+from .resource import ResourceEndpoint
+from .revocation import RevocationEndpoint
+from .token import TokenEndpoint
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/authorization.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/authorization.py
new file mode 100644
index 0000000000..71967865dc
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/authorization.py
@@ -0,0 +1,114 @@
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import logging
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import utils
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationEndpoint(BaseEndpoint):
+
+ """Authorization endpoint - used by the client to obtain authorization
+ from the resource owner via user-agent redirection.
+
+ The authorization endpoint is used to interact with the resource
+ owner and obtain an authorization grant. The authorization server
+ MUST first verify the identity of the resource owner. The way in
+ which the authorization server authenticates the resource owner (e.g.
+ username and password login, session cookies) is beyond the scope of
+ this specification.
+
+ The endpoint URI MAY include an "application/x-www-form-urlencoded"
+ formatted (per `Appendix B`_) query component,
+ which MUST be retained when adding additional query parameters. The
+ endpoint URI MUST NOT include a fragment component::
+
+ https://example.com/path?query=component # OK
+ https://example.com/path?query=component#fragment # Not OK
+
+ Since requests to the authorization endpoint result in user
+ authentication and the transmission of clear-text credentials (in the
+ HTTP response), the authorization server MUST require the use of TLS
+ as described in Section 1.6 when sending requests to the
+ authorization endpoint::
+
+ # We will deny any request which URI schema is not with https
+
+ The authorization server MUST support the use of the HTTP "GET"
+ method [RFC2616] for the authorization endpoint, and MAY support the
+ use of the "POST" method as well::
+
+ # HTTP method is currently not enforced
+
+ Parameters sent without a value MUST be treated as if they were
+ omitted from the request. The authorization server MUST ignore
+ unrecognized request parameters. Request and response parameters
+ MUST NOT be included more than once::
+
+ # Enforced through the design of oauthlib.common.Request
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ """
+
+ def __init__(self, default_response_type, default_token_type,
+ response_types):
+ BaseEndpoint.__init__(self)
+ self._response_types = response_types
+ self._default_response_type = default_response_type
+ self._default_token_type = default_token_type
+
+ @property
+ def response_types(self):
+ return self._response_types
+
+ @property
+ def default_response_type(self):
+ return self._default_response_type
+
+ @property
+ def default_response_type_handler(self):
+ return self.response_types.get(self.default_response_type)
+
+ @property
+ def default_token_type(self):
+ return self._default_token_type
+
+ @catch_errors_and_unavailability
+ def create_authorization_response(self, uri, http_method='GET', body=None,
+ headers=None, scopes=None, credentials=None):
+ """Extract response_type and route to the designated handler."""
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ request.scopes = scopes
+ # TODO: decide whether this should be a required argument
+ request.user = None # TODO: explain this in docs
+ for k, v in (credentials or {}).items():
+ setattr(request, k, v)
+ response_type_handler = self.response_types.get(
+ request.response_type, self.default_response_type_handler)
+ log.debug('Dispatching response_type %s request to %r.',
+ request.response_type, response_type_handler)
+ return response_type_handler.create_authorization_response(
+ request, self.default_token_type)
+
+ @catch_errors_and_unavailability
+ def validate_authorization_request(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Extract response_type and route to the designated handler."""
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+
+ request.scopes = utils.scope_to_list(request.scope)
+
+ response_type_handler = self.response_types.get(
+ request.response_type, self.default_response_type_handler)
+ return response_type_handler.validate_authorization_request(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/base.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/base.py
new file mode 100644
index 0000000000..3f239917cb
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/base.py
@@ -0,0 +1,113 @@
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import functools
+import logging
+
+from ..errors import (
+ FatalClientError, InvalidClientError, InvalidRequestError, OAuth2Error,
+ ServerError, TemporarilyUnavailableError, UnsupportedTokenTypeError,
+)
+
+log = logging.getLogger(__name__)
+
+
+class BaseEndpoint:
+
+ def __init__(self):
+ self._available = True
+ self._catch_errors = False
+ self._valid_request_methods = None
+
+ @property
+ def valid_request_methods(self):
+ return self._valid_request_methods
+
+ @valid_request_methods.setter
+ def valid_request_methods(self, valid_request_methods):
+ if valid_request_methods is not None:
+ valid_request_methods = [x.upper() for x in valid_request_methods]
+ self._valid_request_methods = valid_request_methods
+
+
+ @property
+ def available(self):
+ return self._available
+
+ @available.setter
+ def available(self, available):
+ self._available = available
+
+ @property
+ def catch_errors(self):
+ return self._catch_errors
+
+ @catch_errors.setter
+ def catch_errors(self, catch_errors):
+ self._catch_errors = catch_errors
+
+ def _raise_on_missing_token(self, request):
+ """Raise error on missing token."""
+ if not request.token:
+ raise InvalidRequestError(request=request,
+ description='Missing token parameter.')
+ def _raise_on_invalid_client(self, request):
+ """Raise on failed client authentication."""
+ if self.request_validator.client_authentication_required(request):
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ log.debug('Client authentication failed, %r.', request)
+ raise InvalidClientError(request=request)
+
+ def _raise_on_unsupported_token(self, request):
+ """Raise on unsupported tokens."""
+ if (request.token_type_hint and
+ request.token_type_hint in self.valid_token_types and
+ request.token_type_hint not in self.supported_token_types):
+ raise UnsupportedTokenTypeError(request=request)
+
+ def _raise_on_bad_method(self, request):
+ if self.valid_request_methods is None:
+ raise ValueError('Configure "valid_request_methods" property first')
+ if request.http_method.upper() not in self.valid_request_methods:
+ raise InvalidRequestError(request=request,
+ description=('Unsupported request method %s' % request.http_method.upper()))
+
+ def _raise_on_bad_post_request(self, request):
+ """Raise if invalid POST request received
+ """
+ if request.http_method.upper() == 'POST':
+ query_params = request.uri_query or ""
+ if query_params:
+ raise InvalidRequestError(request=request,
+ description=('URL query parameters are not allowed'))
+
+def catch_errors_and_unavailability(f):
+ @functools.wraps(f)
+ def wrapper(endpoint, uri, *args, **kwargs):
+ if not endpoint.available:
+ e = TemporarilyUnavailableError()
+ log.info('Endpoint unavailable, ignoring request %s.' % uri)
+ return {}, e.json, 503
+
+ if endpoint.catch_errors:
+ try:
+ return f(endpoint, uri, *args, **kwargs)
+ except OAuth2Error:
+ raise
+ except FatalClientError:
+ raise
+ except Exception as e:
+ error = ServerError()
+ log.warning(
+ 'Exception caught while processing request, %s.' % e)
+ return {}, error.json, 500
+ else:
+ return f(endpoint, uri, *args, **kwargs)
+ return wrapper
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/introspect.py
new file mode 100644
index 0000000000..3cc61e6627
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/introspect.py
@@ -0,0 +1,120 @@
+"""
+oauthlib.oauth2.rfc6749.endpoint.introspect
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the OAuth 2.0 `Token Introspection`.
+
+.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662
+"""
+import json
+import logging
+
+from oauthlib.common import Request
+
+from ..errors import OAuth2Error
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class IntrospectEndpoint(BaseEndpoint):
+
+ """Introspect token endpoint.
+
+ This endpoint defines a method to query an OAuth 2.0 authorization
+ server to determine the active state of an OAuth 2.0 token and to
+ determine meta-information about this token. OAuth 2.0 deployments
+ can use this method to convey information about the authorization
+ context of the token from the authorization server to the protected
+ resource.
+
+ To prevent the values of access tokens from leaking into
+ server-side logs via query parameters, an authorization server
+ offering token introspection MAY disallow the use of HTTP GET on
+ the introspection endpoint and instead require the HTTP POST method
+ to be used at the introspection endpoint.
+ """
+
+ valid_token_types = ('access_token', 'refresh_token')
+ valid_request_methods = ('POST',)
+
+ def __init__(self, request_validator, supported_token_types=None):
+ BaseEndpoint.__init__(self)
+ self.request_validator = request_validator
+ self.supported_token_types = (
+ supported_token_types or self.valid_token_types)
+
+ @catch_errors_and_unavailability
+ def create_introspect_response(self, uri, http_method='POST', body=None,
+ headers=None):
+ """Create introspect valid or invalid response
+
+ If the authorization server is unable to determine the state
+ of the token without additional information, it SHOULD return
+ an introspection response indicating the token is not active
+ as described in Section 2.2.
+ """
+ resp_headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ request = Request(uri, http_method, body, headers)
+ try:
+ self.validate_introspect_request(request)
+ log.debug('Token introspect valid for %r.', request)
+ except OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ resp_headers.update(e.headers)
+ return resp_headers, e.json, e.status_code
+
+ claims = self.request_validator.introspect_token(
+ request.token,
+ request.token_type_hint,
+ request
+ )
+ if claims is None:
+ return resp_headers, json.dumps(dict(active=False)), 200
+ if "active" in claims:
+ claims.pop("active")
+ return resp_headers, json.dumps(dict(active=True, **claims)), 200
+
+ def validate_introspect_request(self, request):
+ """Ensure the request is valid.
+
+ The protected resource calls the introspection endpoint using
+ an HTTP POST request with parameters sent as
+ "application/x-www-form-urlencoded".
+
+ * token REQUIRED. The string value of the token.
+ * token_type_hint OPTIONAL.
+
+ A hint about the type of the token submitted for
+ introspection. The protected resource MAY pass this parameter to
+ help the authorization server optimize the token lookup. If the
+ server is unable to locate the token using the given hint, it MUST
+ extend its search across all of its supported token types. An
+ authorization server MAY ignore this parameter, particularly if it
+ is able to detect the token type automatically.
+
+ * access_token: An Access Token as defined in [`RFC6749`], `section 1.4`_
+ * refresh_token: A Refresh Token as defined in [`RFC6749`], `section 1.5`_
+
+ The introspection endpoint MAY accept other OPTIONAL
+ parameters to provide further context to the query. For
+ instance, an authorization server may desire to know the IP
+ address of the client accessing the protected resource to
+ determine if the correct client is likely to be presenting the
+ token. The definition of this or any other parameters are
+ outside the scope of this specification, to be defined by
+ service documentation or extensions to this specification.
+
+ .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`RFC6749`: http://tools.ietf.org/html/rfc6749
+ """
+ self._raise_on_bad_method(request)
+ self._raise_on_bad_post_request(request)
+ self._raise_on_missing_token(request)
+ self._raise_on_invalid_client(request)
+ self._raise_on_unsupported_token(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/metadata.py
new file mode 100644
index 0000000000..a2820f28a5
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/metadata.py
@@ -0,0 +1,238 @@
+"""
+oauthlib.oauth2.rfc6749.endpoint.metadata
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the `OAuth 2.0 Authorization Server Metadata`.
+
+.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414
+"""
+import copy
+import json
+import logging
+
+from .. import grant_types, utils
+from .authorization import AuthorizationEndpoint
+from .base import BaseEndpoint, catch_errors_and_unavailability
+from .introspect import IntrospectEndpoint
+from .revocation import RevocationEndpoint
+from .token import TokenEndpoint
+
+log = logging.getLogger(__name__)
+
+
+class MetadataEndpoint(BaseEndpoint):
+
+ """OAuth2.0 Authorization Server Metadata endpoint.
+
+ This specification generalizes the metadata format defined by
+ `OpenID Connect Discovery 1.0` in a way that is compatible
+ with OpenID Connect Discovery while being applicable to a wider set
+ of OAuth 2.0 use cases. This is intentionally parallel to the way
+ that OAuth 2.0 Dynamic Client Registration Protocol [`RFC7591`_]
+ generalized the dynamic client registration mechanisms defined by
+ OpenID Connect Dynamic Client Registration 1.0
+ in a way that is compatible with it.
+
+ .. _`OpenID Connect Discovery 1.0`: https://openid.net/specs/openid-connect-discovery-1_0.html
+ .. _`RFC7591`: https://tools.ietf.org/html/rfc7591
+ """
+
+ def __init__(self, endpoints, claims={}, raise_errors=True):
+ assert isinstance(claims, dict)
+ for endpoint in endpoints:
+ assert isinstance(endpoint, BaseEndpoint)
+
+ BaseEndpoint.__init__(self)
+ self.raise_errors = raise_errors
+ self.endpoints = endpoints
+ self.initial_claims = claims
+ self.claims = self.validate_metadata_server()
+
+ @catch_errors_and_unavailability
+ def create_metadata_response(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Create metadata response
+ """
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ }
+ return headers, json.dumps(self.claims), 200
+
+ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False):
+ if not self.raise_errors:
+ return
+
+ if key not in array:
+ if is_required:
+ raise ValueError("key {} is a mandatory metadata.".format(key))
+
+ elif is_issuer:
+ if not utils.is_secure_transport(array[key]):
+ raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key]))
+ if "?" in array[key] or "&" in array[key] or "#" in array[key]:
+ raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key]))
+
+ elif is_url:
+ if not array[key].startswith("http"):
+ raise ValueError("key {}: {} must be an URL".format(key, array[key]))
+
+ elif is_list:
+ if not isinstance(array[key], list):
+ raise ValueError("key {}: {} must be an Array".format(key, array[key]))
+ for elem in array[key]:
+ if not isinstance(elem, str):
+ raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem))
+
+ def validate_metadata_token(self, claims, endpoint):
+ """
+ If the token endpoint is used in the grant type, the value of this
+ parameter MUST be the same as the value of the "grant_type"
+ parameter passed to the token endpoint defined in the grant type
+ definition.
+ """
+ self._grant_types.extend(endpoint._grant_types.keys())
+ claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_authorization(self, claims, endpoint):
+ claims.setdefault("response_types_supported",
+ list(filter(lambda x: x != "none", endpoint._response_types.keys())))
+ claims.setdefault("response_modes_supported", ["query", "fragment"])
+
+ # The OAuth2.0 Implicit flow is defined as a "grant type" but it is not
+ # using the "token" endpoint, as such, we have to add it explicitly to
+ # the list of "grant_types_supported" when enabled.
+ if "token" in claims["response_types_supported"]:
+ self._grant_types.append("implicit")
+
+ self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True)
+ self.validate_metadata(claims, "response_modes_supported", is_list=True)
+ if "code" in claims["response_types_supported"]:
+ code_grant = endpoint._response_types["code"]
+ if not isinstance(code_grant, grant_types.AuthorizationCodeGrant) and hasattr(code_grant, "default_grant"):
+ code_grant = code_grant.default_grant
+
+ claims.setdefault("code_challenge_methods_supported",
+ list(code_grant._code_challenge_methods.keys()))
+ self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True)
+ self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_revocation(self, claims, endpoint):
+ claims.setdefault("revocation_endpoint_auth_methods_supported",
+ ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_introspection(self, claims, endpoint):
+ claims.setdefault("introspection_endpoint_auth_methods_supported",
+ ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_server(self):
+ """
+ Authorization servers can have metadata describing their
+ configuration. The following authorization server metadata values
+ are used by this specification. More details can be found in
+ `RFC8414 section 2`_ :
+
+ issuer
+ REQUIRED
+
+ authorization_endpoint
+ URL of the authorization server's authorization endpoint
+ [`RFC6749#Authorization`_]. This is REQUIRED unless no grant types are supported
+ that use the authorization endpoint.
+
+ token_endpoint
+ URL of the authorization server's token endpoint [`RFC6749#Token`_]. This
+ is REQUIRED unless only the implicit grant type is supported.
+
+ scopes_supported
+ RECOMMENDED.
+
+ response_types_supported
+ REQUIRED.
+
+ Other OPTIONAL fields:
+ jwks_uri,
+ registration_endpoint,
+ response_modes_supported
+
+ grant_types_supported
+ OPTIONAL. JSON array containing a list of the OAuth 2.0 grant
+ type values that this authorization server supports. The array
+ values used are the same as those used with the "grant_types"
+ parameter defined by "OAuth 2.0 Dynamic Client Registration
+ Protocol" [`RFC7591`_]. If omitted, the default value is
+ "["authorization_code", "implicit"]".
+
+ token_endpoint_auth_methods_supported
+
+ token_endpoint_auth_signing_alg_values_supported
+
+ service_documentation
+
+ ui_locales_supported
+
+ op_policy_uri
+
+ op_tos_uri
+
+ revocation_endpoint
+
+ revocation_endpoint_auth_methods_supported
+
+ revocation_endpoint_auth_signing_alg_values_supported
+
+ introspection_endpoint
+
+ introspection_endpoint_auth_methods_supported
+
+ introspection_endpoint_auth_signing_alg_values_supported
+
+ code_challenge_methods_supported
+
+ Additional authorization server metadata parameters MAY also be used.
+ Some are defined by other specifications, such as OpenID Connect
+ Discovery 1.0 [`OpenID.Discovery`_].
+
+ .. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2
+ .. _`RFC6749#Authorization`: https://tools.ietf.org/html/rfc6749#section-3.1
+ .. _`RFC6749#Token`: https://tools.ietf.org/html/rfc6749#section-3.2
+ .. _`RFC7591`: https://tools.ietf.org/html/rfc7591
+ .. _`OpenID.Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html
+ """
+ claims = copy.deepcopy(self.initial_claims)
+ self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True)
+ self.validate_metadata(claims, "jwks_uri", is_url=True)
+ self.validate_metadata(claims, "scopes_supported", is_list=True)
+ self.validate_metadata(claims, "service_documentation", is_url=True)
+ self.validate_metadata(claims, "ui_locales_supported", is_list=True)
+ self.validate_metadata(claims, "op_policy_uri", is_url=True)
+ self.validate_metadata(claims, "op_tos_uri", is_url=True)
+
+ self._grant_types = []
+ for endpoint in self.endpoints:
+ if isinstance(endpoint, TokenEndpoint):
+ self.validate_metadata_token(claims, endpoint)
+ if isinstance(endpoint, AuthorizationEndpoint):
+ self.validate_metadata_authorization(claims, endpoint)
+ if isinstance(endpoint, RevocationEndpoint):
+ self.validate_metadata_revocation(claims, endpoint)
+ if isinstance(endpoint, IntrospectEndpoint):
+ self.validate_metadata_introspection(claims, endpoint)
+
+ # "grant_types_supported" is a combination of all OAuth2 grant types
+ # allowed in the current provider implementation.
+ claims.setdefault("grant_types_supported", self._grant_types)
+ self.validate_metadata(claims, "grant_types_supported", is_list=True)
+ return claims
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
new file mode 100644
index 0000000000..d64a166391
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
@@ -0,0 +1,216 @@
+"""
+oauthlib.oauth2.rfc6749.endpoints.pre_configured
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various endpoints needed
+for providing OAuth 2.0 RFC6749 servers.
+"""
+from ..grant_types import (
+ AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
+ RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
+)
+from ..tokens import BearerToken
+from .authorization import AuthorizationEndpoint
+from .introspect import IntrospectEndpoint
+from .resource import ResourceEndpoint
+from .revocation import RevocationEndpoint
+from .token import TokenEndpoint
+
+
+class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
+
+ """An all-in-one endpoint featuring all four major grant types."""
+
+ def __init__(self, request_validator, token_expires_in=None,
+ token_generator=None, refresh_token_generator=None,
+ *args, **kwargs):
+ """Construct a new all-grants-in-one server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ self.auth_grant = AuthorizationCodeGrant(request_validator)
+ self.implicit_grant = ImplicitGrant(request_validator)
+ self.password_grant = ResourceOwnerPasswordCredentialsGrant(
+ request_validator)
+ self.credentials_grant = ClientCredentialsGrant(request_validator)
+ self.refresh_grant = RefreshTokenGrant(request_validator)
+
+ self.bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+
+ AuthorizationEndpoint.__init__(self, default_response_type='code',
+ response_types={
+ 'code': self.auth_grant,
+ 'token': self.implicit_grant,
+ 'none': self.auth_grant
+ },
+ default_token_type=self.bearer)
+
+ TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+ grant_types={
+ 'authorization_code': self.auth_grant,
+ 'password': self.password_grant,
+ 'client_credentials': self.credentials_grant,
+ 'refresh_token': self.refresh_grant,
+ },
+ default_token_type=self.bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': self.bearer})
+ RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
+
+
+class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a new web application server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ self.auth_grant = AuthorizationCodeGrant(request_validator)
+ self.refresh_grant = RefreshTokenGrant(request_validator)
+ self.bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ AuthorizationEndpoint.__init__(self, default_response_type='code',
+ response_types={'code': self.auth_grant},
+ default_token_type=self.bearer)
+ TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+ grant_types={
+ 'authorization_code': self.auth_grant,
+ 'refresh_token': self.refresh_grant,
+ },
+ default_token_type=self.bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': self.bearer})
+ RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
+
+
+class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a new implicit grant server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ self.implicit_grant = ImplicitGrant(request_validator)
+ self.bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ AuthorizationEndpoint.__init__(self, default_response_type='token',
+ response_types={
+ 'token': self.implicit_grant},
+ default_token_type=self.bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': self.bearer})
+ RevocationEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
+ IntrospectEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
+
+
+class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a resource owner password credentials grant server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ self.password_grant = ResourceOwnerPasswordCredentialsGrant(
+ request_validator)
+ self.refresh_grant = RefreshTokenGrant(request_validator)
+ self.bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ TokenEndpoint.__init__(self, default_grant_type='password',
+ grant_types={
+ 'password': self.password_grant,
+ 'refresh_token': self.refresh_grant,
+ },
+ default_token_type=self.bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': self.bearer})
+ RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
+
+
+class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
+
+ """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
+
+ def __init__(self, request_validator, token_generator=None,
+ token_expires_in=None, refresh_token_generator=None, **kwargs):
+ """Construct a client credentials grant server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ self.credentials_grant = ClientCredentialsGrant(request_validator)
+ self.bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+ TokenEndpoint.__init__(self, default_grant_type='client_credentials',
+ grant_types={
+ 'client_credentials': self.credentials_grant},
+ default_token_type=self.bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': self.bearer})
+ RevocationEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
+ IntrospectEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/resource.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/resource.py
new file mode 100644
index 0000000000..f7562255df
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/resource.py
@@ -0,0 +1,84 @@
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class ResourceEndpoint(BaseEndpoint):
+
+ """Authorizes access to protected resources.
+
+ The client accesses protected resources by presenting the access
+ token to the resource server. The resource server MUST validate the
+ access token and ensure that it has not expired and that its scope
+ covers the requested resource. The methods used by the resource
+ server to validate the access token (as well as any error responses)
+ are beyond the scope of this specification but generally involve an
+ interaction or coordination between the resource server and the
+ authorization server::
+
+ # For most cases, returning a 403 should suffice.
+
+ The method in which the client utilizes the access token to
+ authenticate with the resource server depends on the type of access
+ token issued by the authorization server. Typically, it involves
+ using the HTTP "Authorization" request header field [RFC2617] with an
+ authentication scheme defined by the specification of the access
+ token type used, such as [RFC6750]::
+
+ # Access tokens may also be provided in query and body
+ https://example.com/protected?access_token=kjfch2345sdf # Query
+ access_token=sdf23409df # Body
+ """
+
+ def __init__(self, default_token, token_types):
+ BaseEndpoint.__init__(self)
+ self._tokens = token_types
+ self._default_token = default_token
+
+ @property
+ def default_token(self):
+ return self._default_token
+
+ @property
+ def default_token_type_handler(self):
+ return self.tokens.get(self.default_token)
+
+ @property
+ def tokens(self):
+ return self._tokens
+
+ @catch_errors_and_unavailability
+ def verify_request(self, uri, http_method='GET', body=None, headers=None,
+ scopes=None):
+ """Validate client, code etc, return body + headers"""
+ request = Request(uri, http_method, body, headers)
+ request.token_type = self.find_token_type(request)
+ request.scopes = scopes
+ token_type_handler = self.tokens.get(request.token_type,
+ self.default_token_type_handler)
+ log.debug('Dispatching token_type %s request to %r.',
+ request.token_type, token_type_handler)
+ return token_type_handler.validate_request(request), request
+
+ def find_token_type(self, request):
+ """Token type identification.
+
+ RFC 6749 does not provide a method for easily differentiating between
+ different token types during protected resource access. We estimate
+ the most likely token type (if any) by asking each known token type
+ to give an estimation based on the request.
+ """
+ estimates = sorted(((t.estimate_type(request), n)
+ for n, t in self.tokens.items()), reverse=True)
+ return estimates[0][1] if len(estimates) else None
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/revocation.py
new file mode 100644
index 0000000000..596d0860fa
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/revocation.py
@@ -0,0 +1,126 @@
+"""
+oauthlib.oauth2.rfc6749.endpoint.revocation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11).
+
+.. _`Token Revocation`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11
+"""
+import logging
+
+from oauthlib.common import Request
+
+from ..errors import OAuth2Error
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class RevocationEndpoint(BaseEndpoint):
+
+ """Token revocation endpoint.
+
+ Endpoint used by authenticated clients to revoke access and refresh tokens.
+ Commonly this will be part of the Authorization Endpoint.
+ """
+
+ valid_token_types = ('access_token', 'refresh_token')
+ valid_request_methods = ('POST',)
+
+ def __init__(self, request_validator, supported_token_types=None,
+ enable_jsonp=False):
+ BaseEndpoint.__init__(self)
+ self.request_validator = request_validator
+ self.supported_token_types = (
+ supported_token_types or self.valid_token_types)
+ self.enable_jsonp = enable_jsonp
+
+ @catch_errors_and_unavailability
+ def create_revocation_response(self, uri, http_method='POST', body=None,
+ headers=None):
+ """Revoke supplied access or refresh token.
+
+
+ The authorization server responds with HTTP status code 200 if the
+ token has been revoked successfully or if the client submitted an
+ invalid token.
+
+ Note: invalid tokens do not cause an error response since the client
+ cannot handle such an error in a reasonable way. Moreover, the purpose
+ of the revocation request, invalidating the particular token, is
+ already achieved.
+
+ The content of the response body is ignored by the client as all
+ necessary information is conveyed in the response code.
+
+ An invalid token type hint value is ignored by the authorization server
+ and does not influence the revocation response.
+ """
+ resp_headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ try:
+ self.validate_revocation_request(request)
+ log.debug('Token revocation valid for %r.', request)
+ except OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ response_body = e.json
+ if self.enable_jsonp and request.callback:
+ response_body = '{}({});'.format(request.callback, response_body)
+ resp_headers.update(e.headers)
+ return resp_headers, response_body, e.status_code
+
+ self.request_validator.revoke_token(request.token,
+ request.token_type_hint, request)
+
+ response_body = ''
+ if self.enable_jsonp and request.callback:
+ response_body = request.callback + '();'
+ return {}, response_body, 200
+
+ def validate_revocation_request(self, request):
+ """Ensure the request is valid.
+
+ The client constructs the request by including the following parameters
+ using the "application/x-www-form-urlencoded" format in the HTTP
+ request entity-body:
+
+ token (REQUIRED). The token that the client wants to get revoked.
+
+ token_type_hint (OPTIONAL). A hint about the type of the token
+ submitted for revocation. Clients MAY pass this parameter in order to
+ help the authorization server to optimize the token lookup. If the
+ server is unable to locate the token using the given hint, it MUST
+ extend its search across all of its supported token types. An
+ authorization server MAY ignore this parameter, particularly if it is
+ able to detect the token type automatically. This specification
+ defines two such values:
+
+ * access_token: An Access Token as defined in [RFC6749],
+ `section 1.4`_
+
+ * refresh_token: A Refresh Token as defined in [RFC6749],
+ `section 1.5`_
+
+ Specific implementations, profiles, and extensions of this
+ specification MAY define other values for this parameter using
+ the registry defined in `Section 4.1.2`_.
+
+ The client also includes its authentication credentials as described in
+ `Section 2.3`_. of [`RFC6749`_].
+
+ .. _`section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`section 2.3`: https://tools.ietf.org/html/rfc6749#section-2.3
+ .. _`Section 4.1.2`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
+ .. _`RFC6749`: https://tools.ietf.org/html/rfc6749
+ """
+ self._raise_on_bad_method(request)
+ self._raise_on_bad_post_request(request)
+ self._raise_on_missing_token(request)
+ self._raise_on_invalid_client(request)
+ self._raise_on_unsupported_token(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/token.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/token.py
new file mode 100644
index 0000000000..ab9e0918b3
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/endpoints/token.py
@@ -0,0 +1,119 @@
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+import logging
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import utils
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class TokenEndpoint(BaseEndpoint):
+
+ """Token issuing endpoint.
+
+ The token endpoint is used by the client to obtain an access token by
+ presenting its authorization grant or refresh token. The token
+ endpoint is used with every authorization grant except for the
+ implicit grant type (since an access token is issued directly).
+
+ The means through which the client obtains the location of the token
+ endpoint are beyond the scope of this specification, but the location
+ is typically provided in the service documentation.
+
+ The endpoint URI MAY include an "application/x-www-form-urlencoded"
+ formatted (per `Appendix B`_) query component,
+ which MUST be retained when adding additional query parameters. The
+ endpoint URI MUST NOT include a fragment component::
+
+ https://example.com/path?query=component # OK
+ https://example.com/path?query=component#fragment # Not OK
+
+ Since requests to the token endpoint result in the transmission of
+ clear-text credentials (in the HTTP request and response), the
+ authorization server MUST require the use of TLS as described in
+ Section 1.6 when sending requests to the token endpoint::
+
+ # We will deny any request which URI schema is not with https
+
+ The client MUST use the HTTP "POST" method when making access token
+ requests::
+
+ # HTTP method is currently not enforced
+
+ Parameters sent without a value MUST be treated as if they were
+ omitted from the request. The authorization server MUST ignore
+ unrecognized request parameters. Request and response parameters
+ MUST NOT be included more than once::
+
+ # Delegated to each grant type.
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ """
+
+ valid_request_methods = ('POST',)
+
+ def __init__(self, default_grant_type, default_token_type, grant_types):
+ BaseEndpoint.__init__(self)
+ self._grant_types = grant_types
+ self._default_token_type = default_token_type
+ self._default_grant_type = default_grant_type
+
+ @property
+ def grant_types(self):
+ return self._grant_types
+
+ @property
+ def default_grant_type(self):
+ return self._default_grant_type
+
+ @property
+ def default_grant_type_handler(self):
+ return self.grant_types.get(self.default_grant_type)
+
+ @property
+ def default_token_type(self):
+ return self._default_token_type
+
+ @catch_errors_and_unavailability
+ def create_token_response(self, uri, http_method='POST', body=None,
+ headers=None, credentials=None, grant_type_for_scope=None,
+ claims=None):
+ """Extract grant_type and route to the designated handler."""
+ request = Request(
+ uri, http_method=http_method, body=body, headers=headers)
+ self.validate_token_request(request)
+ # 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant"
+ # and "Client Credentials Grant" flows
+ # https://tools.ietf.org/html/rfc6749#section-4.3.2
+ # https://tools.ietf.org/html/rfc6749#section-4.4.2
+ request.scopes = utils.scope_to_list(request.scope)
+
+ request.extra_credentials = credentials
+ if grant_type_for_scope:
+ request.grant_type = grant_type_for_scope
+
+ # OpenID Connect claims, if provided. The server using oauthlib might choose
+ # to implement the claims parameter of the Authorization Request. In this case
+ # it should retrieve those claims and pass them via the claims argument here,
+ # as a dict.
+ if claims:
+ request.claims = claims
+
+ grant_type_handler = self.grant_types.get(request.grant_type,
+ self.default_grant_type_handler)
+ log.debug('Dispatching grant_type %s request to %r.',
+ request.grant_type, grant_type_handler)
+ return grant_type_handler.create_token_response(
+ request, self.default_token_type)
+
+ def validate_token_request(self, request):
+ self._raise_on_bad_method(request)
+ self._raise_on_bad_post_request(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/errors.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/errors.py
new file mode 100644
index 0000000000..da24feab75
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/errors.py
@@ -0,0 +1,400 @@
+"""
+oauthlib.oauth2.rfc6749.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 2 clients and providers to represent the spec
+defined error responses for all four core grant types.
+"""
+import json
+
+from oauthlib.common import add_params_to_uri, urlencode
+
+
+class OAuth2Error(Exception):
+ error = None
+ status_code = 400
+ description = ''
+
+ def __init__(self, description=None, uri=None, state=None,
+ status_code=None, request=None):
+ """
+ :param description: A human-readable ASCII [USASCII] text providing
+ additional information, used to assist the client
+ developer in understanding the error that occurred.
+ Values for the "error_description" parameter
+ MUST NOT include characters outside the set
+ x20-21 / x23-5B / x5D-7E.
+
+ :param uri: A URI identifying a human-readable web page with information
+ about the error, used to provide the client developer with
+ additional information about the error. Values for the
+ "error_uri" parameter MUST conform to the URI- Reference
+ syntax, and thus MUST NOT include characters outside the set
+ x21 / x23-5B / x5D-7E.
+
+ :param state: A CSRF protection value received from the client.
+
+ :param status_code:
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ if description is not None:
+ self.description = description
+
+ message = '({}) {}'.format(self.error, self.description)
+ if request:
+ message += ' ' + repr(request)
+ super().__init__(message)
+
+ self.uri = uri
+ self.state = state
+
+ if status_code:
+ self.status_code = status_code
+
+ if request:
+ self.redirect_uri = request.redirect_uri
+ self.client_id = request.client_id
+ self.scopes = request.scopes
+ self.response_type = request.response_type
+ self.response_mode = request.response_mode
+ self.grant_type = request.grant_type
+ if not state:
+ self.state = request.state
+ else:
+ self.redirect_uri = None
+ self.client_id = None
+ self.scopes = None
+ self.response_type = None
+ self.response_mode = None
+ self.grant_type = None
+
+ def in_uri(self, uri):
+ fragment = self.response_mode == "fragment"
+ return add_params_to_uri(uri, self.twotuples, fragment)
+
+ @property
+ def twotuples(self):
+ error = [('error', self.error)]
+ if self.description:
+ error.append(('error_description', self.description))
+ if self.uri:
+ error.append(('error_uri', self.uri))
+ if self.state:
+ error.append(('state', self.state))
+ return error
+
+ @property
+ def urlencoded(self):
+ return urlencode(self.twotuples)
+
+ @property
+ def json(self):
+ return json.dumps(dict(self.twotuples))
+
+ @property
+ def headers(self):
+ if self.status_code == 401:
+ """
+ https://tools.ietf.org/html/rfc6750#section-3
+
+ All challenges defined by this specification MUST use the auth-scheme
+ value "Bearer". This scheme MUST be followed by one or more
+ auth-param values.
+ """
+ authvalues = ['error="{}"'.format(self.error)]
+ if self.description:
+ authvalues.append('error_description="{}"'.format(self.description))
+ if self.uri:
+ authvalues.append('error_uri="{}"'.format(self.uri))
+ return {"WWW-Authenticate": "Bearer " + ", ".join(authvalues)}
+ return {}
+
+
+class TokenExpiredError(OAuth2Error):
+ error = 'token_expired'
+
+
+class InsecureTransportError(OAuth2Error):
+ error = 'insecure_transport'
+ description = 'OAuth 2 MUST utilize https.'
+
+
+class MismatchingStateError(OAuth2Error):
+ error = 'mismatching_state'
+ description = 'CSRF Warning! State not equal in request and response.'
+
+
+class MissingCodeError(OAuth2Error):
+ error = 'missing_code'
+
+
+class MissingTokenError(OAuth2Error):
+ error = 'missing_token'
+
+
+class MissingTokenTypeError(OAuth2Error):
+ error = 'missing_token_type'
+
+
+class FatalClientError(OAuth2Error):
+ """
+ Errors during authorization where user should not be redirected back.
+
+ If the request fails due to a missing, invalid, or mismatching
+ redirection URI, or if the client identifier is missing or invalid,
+ the authorization server SHOULD inform the resource owner of the
+ error and MUST NOT automatically redirect the user-agent to the
+ invalid redirection URI.
+
+ Instead the user should be informed of the error by the provider itself.
+ """
+ pass
+
+
+class InvalidRequestFatalError(FatalClientError):
+ """
+ For fatal errors, the request is missing a required parameter, includes
+ an invalid parameter value, includes a parameter more than once, or is
+ otherwise malformed.
+ """
+ error = 'invalid_request'
+
+
+class InvalidRedirectURIError(InvalidRequestFatalError):
+ description = 'Invalid redirect URI.'
+
+
+class MissingRedirectURIError(InvalidRequestFatalError):
+ description = 'Missing redirect URI.'
+
+
+class MismatchingRedirectURIError(InvalidRequestFatalError):
+ description = 'Mismatching redirect URI.'
+
+
+class InvalidClientIdError(InvalidRequestFatalError):
+ description = 'Invalid client_id parameter value.'
+
+
+class MissingClientIdError(InvalidRequestFatalError):
+ description = 'Missing client_id parameter.'
+
+
+class InvalidRequestError(OAuth2Error):
+ """
+ The request is missing a required parameter, includes an invalid
+ parameter value, includes a parameter more than once, or is
+ otherwise malformed.
+ """
+ error = 'invalid_request'
+
+
+class MissingResponseTypeError(InvalidRequestError):
+ description = 'Missing response_type parameter.'
+
+
+class MissingCodeChallengeError(InvalidRequestError):
+ """
+ If the server requires Proof Key for Code Exchange (PKCE) by OAuth
+ public clients and the client does not send the "code_challenge" in
+ the request, the authorization endpoint MUST return the authorization
+ error response with the "error" value set to "invalid_request". The
+ "error_description" or the response of "error_uri" SHOULD explain the
+ nature of error, e.g., code challenge required.
+ """
+ description = 'Code challenge required.'
+
+
+class MissingCodeVerifierError(InvalidRequestError):
+ """
+ The request to the token endpoint, when PKCE is enabled, has
+ the parameter `code_verifier` REQUIRED.
+ """
+ description = 'Code verifier required.'
+
+
+class AccessDeniedError(OAuth2Error):
+ """
+ The resource owner or authorization server denied the request.
+ """
+ error = 'access_denied'
+
+
+class UnsupportedResponseTypeError(OAuth2Error):
+ """
+ The authorization server does not support obtaining an authorization
+ code using this method.
+ """
+ error = 'unsupported_response_type'
+
+
+class UnsupportedCodeChallengeMethodError(InvalidRequestError):
+ """
+ If the server supporting PKCE does not support the requested
+ transformation, the authorization endpoint MUST return the
+ authorization error response with "error" value set to
+ "invalid_request". The "error_description" or the response of
+ "error_uri" SHOULD explain the nature of error, e.g., transform
+ algorithm not supported.
+ """
+ description = 'Transform algorithm not supported.'
+
+
+class InvalidScopeError(OAuth2Error):
+ """
+ The requested scope is invalid, unknown, or malformed, or
+ exceeds the scope granted by the resource owner.
+
+ https://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ error = 'invalid_scope'
+
+
+class ServerError(OAuth2Error):
+ """
+ The authorization server encountered an unexpected condition that
+ prevented it from fulfilling the request. (This error code is needed
+ because a 500 Internal Server Error HTTP status code cannot be returned
+ to the client via a HTTP redirect.)
+ """
+ error = 'server_error'
+
+
+class TemporarilyUnavailableError(OAuth2Error):
+ """
+ The authorization server is currently unable to handle the request
+ due to a temporary overloading or maintenance of the server.
+ (This error code is needed because a 503 Service Unavailable HTTP
+ status code cannot be returned to the client via a HTTP redirect.)
+ """
+ error = 'temporarily_unavailable'
+
+
+class InvalidClientError(FatalClientError):
+ """
+ Client authentication failed (e.g. unknown client, no client
+ authentication included, or unsupported authentication method).
+ The authorization server MAY return an HTTP 401 (Unauthorized) status
+ code to indicate which HTTP authentication schemes are supported.
+ If the client attempted to authenticate via the "Authorization" request
+ header field, the authorization server MUST respond with an
+ HTTP 401 (Unauthorized) status code, and include the "WWW-Authenticate"
+ response header field matching the authentication scheme used by the
+ client.
+ """
+ error = 'invalid_client'
+ status_code = 401
+
+
+class InvalidGrantError(OAuth2Error):
+ """
+ The provided authorization grant (e.g. authorization code, resource
+ owner credentials) or refresh token is invalid, expired, revoked, does
+ not match the redirection URI used in the authorization request, or was
+ issued to another client.
+
+ https://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ error = 'invalid_grant'
+ status_code = 400
+
+
+class UnauthorizedClientError(OAuth2Error):
+ """
+ The authenticated client is not authorized to use this authorization
+ grant type.
+ """
+ error = 'unauthorized_client'
+
+
+class UnsupportedGrantTypeError(OAuth2Error):
+ """
+ The authorization grant type is not supported by the authorization
+ server.
+ """
+ error = 'unsupported_grant_type'
+
+
+class UnsupportedTokenTypeError(OAuth2Error):
+ """
+ The authorization server does not support the hint of the
+ presented token type. I.e. the client tried to revoke an access token
+ on a server not supporting this feature.
+ """
+ error = 'unsupported_token_type'
+
+
+class InvalidTokenError(OAuth2Error):
+ """
+ The access token provided is expired, revoked, malformed, or
+ invalid for other reasons. The resource SHOULD respond with
+ the HTTP 401 (Unauthorized) status code. The client MAY
+ request a new access token and retry the protected resource
+ request.
+ """
+ error = 'invalid_token'
+ status_code = 401
+ description = ("The access token provided is expired, revoked, malformed, "
+ "or invalid for other reasons.")
+
+
+class InsufficientScopeError(OAuth2Error):
+ """
+ The request requires higher privileges than provided by the
+ access token. The resource server SHOULD respond with the HTTP
+ 403 (Forbidden) status code and MAY include the "scope"
+ attribute with the scope necessary to access the protected
+ resource.
+ """
+ error = 'insufficient_scope'
+ status_code = 403
+ description = ("The request requires higher privileges than provided by "
+ "the access token.")
+
+
+class ConsentRequired(OAuth2Error):
+ """
+ The Authorization Server requires End-User consent.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User consent.
+ """
+ error = 'consent_required'
+
+
+class LoginRequired(OAuth2Error):
+ """
+ The Authorization Server requires End-User authentication.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User authentication.
+ """
+ error = 'login_required'
+
+
+class CustomOAuth2Error(OAuth2Error):
+ """
+ This error is a placeholder for all custom errors not described by the RFC.
+ Some of the popular OAuth2 providers are using custom errors.
+ """
+ def __init__(self, error, *args, **kwargs):
+ self.error = error
+ super().__init__(*args, **kwargs)
+
+
+def raise_from_error(error, params=None):
+ import inspect
+ import sys
+ kwargs = {
+ 'description': params.get('error_description'),
+ 'uri': params.get('error_uri'),
+ 'state': params.get('state')
+ }
+ for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
+ if cls.error == error:
+ raise cls(**kwargs)
+ raise CustomOAuth2Error(error=error, **kwargs)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/__init__.py
new file mode 100644
index 0000000000..eb88cfc2e9
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/__init__.py
@@ -0,0 +1,11 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from .authorization_code import AuthorizationCodeGrant
+from .client_credentials import ClientCredentialsGrant
+from .implicit import ImplicitGrant
+from .refresh_token import RefreshTokenGrant
+from .resource_owner_password_credentials import (
+ ResourceOwnerPasswordCredentialsGrant,
+)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
new file mode 100644
index 0000000000..858855a174
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
@@ -0,0 +1,548 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import base64
+import hashlib
+import json
+import logging
+
+from oauthlib import common
+
+from .. import errors
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+def code_challenge_method_s256(verifier, challenge):
+ """
+ If the "code_challenge_method" from `Section 4.3`_ was "S256", the
+ received "code_verifier" is hashed by SHA-256, base64url-encoded, and
+ then compared to the "code_challenge", i.e.:
+
+ BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
+
+ How to implement a base64url-encoding
+ function without padding, based upon the standard base64-encoding
+ function that uses padding.
+
+ To be concrete, example C# code implementing these functions is shown
+ below. Similar code could be used in other languages.
+
+ static string base64urlencode(byte [] arg)
+ {
+ string s = Convert.ToBase64String(arg); // Regular base64 encoder
+ s = s.Split('=')[0]; // Remove any trailing '='s
+ s = s.Replace('+', '-'); // 62nd char of encoding
+ s = s.Replace('/', '_'); // 63rd char of encoding
+ return s;
+ }
+
+ In python urlsafe_b64encode is already replacing '+' and '/', but preserve
+ the trailing '='. So we have to remove it.
+
+ .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
+ """
+ return base64.urlsafe_b64encode(
+ hashlib.sha256(verifier.encode()).digest()
+ ).decode().rstrip('=') == challenge
+
+
+def code_challenge_method_plain(verifier, challenge):
+ """
+ If the "code_challenge_method" from `Section 4.3`_ was "plain", they are
+ compared directly, i.e.:
+
+ code_verifier == code_challenge.
+
+ .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
+ """
+ return verifier == challenge
+
+
+class AuthorizationCodeGrant(GrantTypeBase):
+
+ """`Authorization Code Grant`_
+
+ The authorization code grant type is used to obtain both access
+ tokens and refresh tokens and is optimized for confidential clients.
+ Since this is a redirection-based flow, the client must be capable of
+ interacting with the resource owner's user-agent (typically a web
+ browser) and capable of receiving incoming requests (via redirection)
+ from the authorization server::
+
+ +----------+
+ | Resource |
+ | Owner |
+ | |
+ +----------+
+ ^
+ |
+ (B)
+ +----|-----+ Client Identifier +---------------+
+ | -+----(A)-- & Redirection URI ---->| |
+ | User- | | Authorization |
+ | Agent -+----(B)-- User authenticates --->| Server |
+ | | | |
+ | -+----(C)-- Authorization Code ---<| |
+ +-|----|---+ +---------------+
+ | | ^ v
+ (A) (C) | |
+ | | | |
+ ^ v | |
+ +---------+ | |
+ | |>---(D)-- Authorization Code ---------' |
+ | Client | & Redirection URI |
+ | | |
+ | |<---(E)----- Access Token -------------------'
+ +---------+ (w/ Optional Refresh Token)
+
+ Note: The lines illustrating steps (A), (B), and (C) are broken into
+ two parts as they pass through the user-agent.
+
+ Figure 3: Authorization Code Flow
+
+ The flow illustrated in Figure 3 includes the following steps:
+
+ (A) The client initiates the flow by directing the resource owner's
+ user-agent to the authorization endpoint. The client includes
+ its client identifier, requested scope, local state, and a
+ redirection URI to which the authorization server will send the
+ user-agent back once access is granted (or denied).
+
+ (B) The authorization server authenticates the resource owner (via
+ the user-agent) and establishes whether the resource owner
+ grants or denies the client's access request.
+
+ (C) Assuming the resource owner grants access, the authorization
+ server redirects the user-agent back to the client using the
+ redirection URI provided earlier (in the request or during
+ client registration). The redirection URI includes an
+ authorization code and any local state provided by the client
+ earlier.
+
+ (D) The client requests an access token from the authorization
+ server's token endpoint by including the authorization code
+ received in the previous step. When making the request, the
+ client authenticates with the authorization server. The client
+ includes the redirection URI used to obtain the authorization
+ code for verification.
+
+ (E) The authorization server authenticates the client, validates the
+ authorization code, and ensures that the redirection URI
+ received matches the URI used to redirect the client in
+ step (C). If valid, the authorization server responds back with
+ an access token and, optionally, a refresh token.
+
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are
+ susceptible to the authorization code interception attack.
+
+ A technique to mitigate against the threat through the use of Proof Key for Code
+ Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib
+ implementation.
+
+ .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
+ .. _`PKCE`: https://tools.ietf.org/html/rfc7636
+ """
+
+ default_response_mode = 'query'
+ response_types = ['code']
+
+ # This dict below is private because as RFC mention it:
+ # "S256" is Mandatory To Implement (MTI) on the server.
+ #
+ _code_challenge_methods = {
+ 'plain': code_challenge_method_plain,
+ 'S256': code_challenge_method_s256
+ }
+
+ def create_authorization_code(self, request):
+ """
+ Generates an authorization grant represented as a dictionary.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ grant = {'code': common.generate_token()}
+ if hasattr(request, 'state') and request.state:
+ grant['state'] = request.state
+ log.debug('Created authorization code grant %r for request %r.',
+ grant, request)
+ return grant
+
+ def create_authorization_response(self, request, token_handler):
+ """
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ response_type
+ REQUIRED. Value MUST be set to "code" for standard OAuth2
+ authorization flow. For OpenID Connect it must be one of
+ "code token", "code id_token", or "code token id_token" - we
+ essentially test that "code" appears in the response_type.
+ client_id
+ REQUIRED. The client identifier as described in `Section 2.2`_.
+ redirect_uri
+ OPTIONAL. As described in `Section 3.1.2`_.
+ scope
+ OPTIONAL. The scope of the access request as described by
+ `Section 3.3`_.
+ state
+ RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ The client directs the resource owner to the constructed URI using an
+ HTTP redirection response, or by other means available to it via the
+ user-agent.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ :returns: headers, body, status
+ :raises: FatalClientError on invalid redirect URI or client id.
+
+ A few examples::
+
+ >>> from your_validator import your_validator
+ >>> request = Request('https://example.com/authorize?client_id=valid'
+ ... '&redirect_uri=http%3A%2F%2Fclient.com%2F')
+ >>> from oauthlib.common import Request
+ >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
+ >>> token = BearerToken(your_validator)
+ >>> grant = AuthorizationCodeGrant(your_validator)
+ >>> request.scopes = ['authorized', 'in', 'some', 'form']
+ >>> grant.create_authorization_response(request, token)
+ (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
+ >>> request = Request('https://example.com/authorize?client_id=valid'
+ ... '&redirect_uri=http%3A%2F%2Fclient.com%2F'
+ ... '&response_type=code')
+ >>> request.scopes = ['authorized', 'in', 'some', 'form']
+ >>> grant.create_authorization_response(request, token)
+ (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
+ >>> # If the client id or redirect uri fails validation
+ >>> grant.create_authorization_response(request, token)
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
+ >>> grant.create_authorization_response(request, token)
+ File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
+ oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ try:
+ self.validate_authorization_request(request)
+ log.debug('Pre resource owner authorization validation ok for %r.',
+ request)
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+ except errors.FatalClientError as e:
+ log.debug('Fatal client error during validation of %r. %r.',
+ request, e)
+ raise
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the query component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B:
+ # https://tools.ietf.org/html/rfc6749#appendix-B
+ except errors.OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ request.redirect_uri = request.redirect_uri or self.error_uri
+ redirect_uri = common.add_params_to_uri(
+ request.redirect_uri, e.twotuples,
+ fragment=request.response_mode == "fragment")
+ return {'Location': redirect_uri}, None, 302
+
+ grant = self.create_authorization_code(request)
+ for modifier in self._code_modifiers:
+ grant = modifier(grant, token_handler, request)
+ if 'access_token' in grant:
+ self.request_validator.save_token(grant, request)
+ log.debug('Saving grant %r for %r.', grant, request)
+ self.request_validator.save_authorization_code(
+ request.client_id, grant, request)
+ return self.prepare_authorization_response(
+ request, grant, {}, None, 302)
+
+ def create_token_response(self, request, token_handler):
+ """Validate the authorization code.
+
+ The client MUST NOT use the authorization code more than once. If an
+ authorization code is used more than once, the authorization server
+ MUST deny the request and SHOULD revoke (when possible) all tokens
+ previously issued based on that authorization code. The authorization
+ code is bound to the client identifier and redirection URI.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
+ """
+ headers = self._get_default_headers()
+ try:
+ self.validate_token_request(request)
+ log.debug('Token request validation ok for %r.', request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ headers.update(e.headers)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request, refresh_token=self.refresh_token)
+
+ for modifier in self._token_modifiers:
+ token = modifier(token, token_handler, request)
+
+ self.request_validator.save_token(token, request)
+ self.request_validator.invalidate_authorization_code(
+ request.client_id, request.code, request)
+ headers.update(self._create_cors_headers(request))
+ return headers, json.dumps(token), 200
+
+ def validate_authorization_request(self, request):
+ """Check the authorization request for normal and fatal errors.
+
+ A normal error could be a missing response_type parameter or the client
+ attempting to access scope it is not allowed to ask authorization for.
+ Normal errors can safely be included in the redirection URI and
+ sent back to the client.
+
+ Fatal errors occur when the client_id or redirect_uri is invalid or
+ missing. These must be caught by the provider and handled, how this
+ is done is outside of the scope of OAuthLib but showing an error
+ page describing the issue is a good idea.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+
+ # First check for fatal errors
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+
+ # First check duplicate parameters
+ for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+ try:
+ duplicate_params = request.duplicate_params
+ except ValueError:
+ raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
+ if param in duplicate_params:
+ raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
+
+ # REQUIRED. The client identifier as described in Section 2.2.
+ # https://tools.ietf.org/html/rfc6749#section-2.2
+ if not request.client_id:
+ raise errors.MissingClientIdError(request=request)
+
+ if not self.request_validator.validate_client_id(request.client_id, request):
+ raise errors.InvalidClientIdError(request=request)
+
+ # OPTIONAL. As described in Section 3.1.2.
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ log.debug('Validating redirection uri %s for client %s.',
+ request.redirect_uri, request.client_id)
+
+ # OPTIONAL. As described in Section 3.1.2.
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ self._handle_redirects(request)
+
+ # Then check for normal errors.
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the query component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B.
+ # https://tools.ietf.org/html/rfc6749#appendix-B
+
+ # Note that the correct parameters to be added are automatically
+ # populated through the use of specific exceptions.
+
+ request_info = {}
+ for validator in self.custom_validators.pre_auth:
+ request_info.update(validator(request))
+
+ # REQUIRED.
+ if request.response_type is None:
+ raise errors.MissingResponseTypeError(request=request)
+ # Value MUST be set to "code" or one of the OpenID authorization code including
+ # response_types "code token", "code id_token", "code token id_token"
+ elif not 'code' in request.response_type and request.response_type != 'none':
+ raise errors.UnsupportedResponseTypeError(request=request)
+
+ if not self.request_validator.validate_response_type(request.client_id,
+ request.response_type,
+ request.client, request):
+
+ log.debug('Client %s is not authorized to use response_type %s.',
+ request.client_id, request.response_type)
+ raise errors.UnauthorizedClientError(request=request)
+
+ # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
+ # https://tools.ietf.org/html/rfc6749#section-4.4.1
+ if self.request_validator.is_pkce_required(request.client_id, request) is True:
+ if request.code_challenge is None:
+ raise errors.MissingCodeChallengeError(request=request)
+
+ if request.code_challenge is not None:
+ request_info["code_challenge"] = request.code_challenge
+
+ # OPTIONAL, defaults to "plain" if not present in the request.
+ if request.code_challenge_method is None:
+ request.code_challenge_method = "plain"
+
+ if request.code_challenge_method not in self._code_challenge_methods:
+ raise errors.UnsupportedCodeChallengeMethodError(request=request)
+ request_info["code_challenge_method"] = request.code_challenge_method
+
+ # OPTIONAL. The scope of the access request as described by Section 3.3
+ # https://tools.ietf.org/html/rfc6749#section-3.3
+ self.validate_scopes(request)
+
+ request_info.update({
+ 'client_id': request.client_id,
+ 'redirect_uri': request.redirect_uri,
+ 'response_type': request.response_type,
+ 'state': request.state,
+ 'request': request
+ })
+
+ for validator in self.custom_validators.post_auth:
+ request_info.update(validator(request))
+
+ return request.scopes, request_info
+
+ def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ # REQUIRED. Value MUST be set to "authorization_code".
+ if request.grant_type not in ('authorization_code', 'openid'):
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ for validator in self.custom_validators.pre_token:
+ validator(request)
+
+ if request.code is None:
+ raise errors.InvalidRequestError(
+ description='Missing code parameter.', request=request)
+
+ for param in ('client_id', 'grant_type', 'redirect_uri'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
+ request=request)
+
+ if self.request_validator.client_authentication_required(request):
+ # If the client type is confidential or the client was issued client
+ # credentials (or assigned other authentication requirements), the
+ # client MUST authenticate with the authorization server as described
+ # in Section 3.2.1.
+ # https://tools.ietf.org/html/rfc6749#section-3.2.1
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ # REQUIRED, if the client is not authenticating with the
+ # authorization server as described in Section 3.2.1.
+ # https://tools.ietf.org/html/rfc6749#section-3.2.1
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+
+ if not hasattr(request.client, 'client_id'):
+ raise NotImplementedError('Authenticate client must set the '
+ 'request.client.client_id attribute '
+ 'in authenticate_client.')
+
+ request.client_id = request.client_id or request.client.client_id
+
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ # REQUIRED. The authorization code received from the
+ # authorization server.
+ if not self.request_validator.validate_code(request.client_id,
+ request.code, request.client, request):
+ log.debug('Client, %r (%r), is not allowed access to scopes %r.',
+ request.client_id, request.client, request.scopes)
+ raise errors.InvalidGrantError(request=request)
+
+ # OPTIONAL. Validate PKCE code_verifier
+ challenge = self.request_validator.get_code_challenge(request.code, request)
+
+ if challenge is not None:
+ if request.code_verifier is None:
+ raise errors.MissingCodeVerifierError(request=request)
+
+ challenge_method = self.request_validator.get_code_challenge_method(request.code, request)
+ if challenge_method is None:
+ raise errors.InvalidGrantError(request=request, description="Challenge method not found")
+
+ if challenge_method not in self._code_challenge_methods:
+ raise errors.ServerError(
+ description="code_challenge_method {} is not supported.".format(challenge_method),
+ request=request
+ )
+
+ if not self.validate_code_challenge(challenge,
+ challenge_method,
+ request.code_verifier):
+ log.debug('request provided a invalid code_verifier.')
+ raise errors.InvalidGrantError(request=request)
+ elif self.request_validator.is_pkce_required(request.client_id, request) is True:
+ if request.code_verifier is None:
+ raise errors.MissingCodeVerifierError(request=request)
+ raise errors.InvalidGrantError(request=request, description="Challenge not found")
+
+ for attr in ('user', 'scopes'):
+ if getattr(request, attr, None) is None:
+ log.debug('request.%s was not set on code validation.', attr)
+
+ # REQUIRED, if the "redirect_uri" parameter was included in the
+ # authorization request as described in Section 4.1.1, and their
+ # values MUST be identical.
+ if request.redirect_uri is None:
+ request.using_default_redirect_uri = True
+ request.redirect_uri = self.request_validator.get_default_redirect_uri(
+ request.client_id, request)
+ log.debug('Using default redirect_uri %s.', request.redirect_uri)
+ if not request.redirect_uri:
+ raise errors.MissingRedirectURIError(request=request)
+ else:
+ request.using_default_redirect_uri = False
+ log.debug('Using provided redirect_uri %s', request.redirect_uri)
+
+ if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
+ request.redirect_uri, request.client,
+ request):
+ log.debug('Redirect_uri (%r) invalid for client %r (%r).',
+ request.redirect_uri, request.client_id, request.client)
+ raise errors.MismatchingRedirectURIError(request=request)
+
+ for validator in self.custom_validators.post_token:
+ validator(request)
+
+ def validate_code_challenge(self, challenge, challenge_method, verifier):
+ if challenge_method in self._code_challenge_methods:
+ return self._code_challenge_methods[challenge_method](verifier, challenge)
+ raise NotImplementedError('Unknown challenge_method %s' % challenge_method)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/base.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/base.py
new file mode 100644
index 0000000000..ca343a1193
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/base.py
@@ -0,0 +1,268 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+from itertools import chain
+
+from oauthlib.common import add_params_to_uri
+from oauthlib.oauth2.rfc6749 import errors, utils
+from oauthlib.uri_validate import is_absolute_uri
+
+from ..request_validator import RequestValidator
+from ..utils import is_secure_transport
+
+log = logging.getLogger(__name__)
+
+
+class ValidatorsContainer:
+ """
+ Container object for holding custom validator callables to be invoked
+ as part of the grant type `validate_authorization_request()` or
+ `validate_authorization_request()` methods on the various grant types.
+
+ Authorization validators must be callables that take a request object and
+ return a dict, which may contain items to be added to the `request_info`
+ returned from the grant_type after validation.
+
+ Token validators must be callables that take a request object and
+ return None.
+
+ Both authorization validators and token validators may raise OAuth2
+ exceptions if validation conditions fail.
+
+ Authorization validators added to `pre_auth` will be run BEFORE
+ the standard validations (but after the critical ones that raise
+ fatal errors) as part of `validate_authorization_request()`
+
+ Authorization validators added to `post_auth` will be run AFTER
+ the standard validations as part of `validate_authorization_request()`
+
+ Token validators added to `pre_token` will be run BEFORE
+ the standard validations as part of `validate_token_request()`
+
+ Token validators added to `post_token` will be run AFTER
+ the standard validations as part of `validate_token_request()`
+
+ For example:
+
+ >>> def my_auth_validator(request):
+ ... return {'myval': True}
+ >>> auth_code_grant = AuthorizationCodeGrant(request_validator)
+ >>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator)
+ >>> def my_token_validator(request):
+ ... if not request.everything_okay:
+ ... raise errors.OAuth2Error("uh-oh")
+ >>> auth_code_grant.custom_validators.post_token.append(my_token_validator)
+ """
+
+ def __init__(self, post_auth, post_token,
+ pre_auth, pre_token):
+ self.pre_auth = pre_auth
+ self.post_auth = post_auth
+ self.pre_token = pre_token
+ self.post_token = post_token
+
+ @property
+ def all_pre(self):
+ return chain(self.pre_auth, self.pre_token)
+
+ @property
+ def all_post(self):
+ return chain(self.post_auth, self.post_token)
+
+
+class GrantTypeBase:
+ error_uri = None
+ request_validator = None
+ default_response_mode = 'fragment'
+ refresh_token = True
+ response_types = ['code']
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.request_validator = request_validator or RequestValidator()
+
+ # Transforms class variables into instance variables:
+ self.response_types = self.response_types
+ self.refresh_token = self.refresh_token
+ self._setup_custom_validators(kwargs)
+ self._code_modifiers = []
+ self._token_modifiers = []
+
+ for kw, val in kwargs.items():
+ setattr(self, kw, val)
+
+ def _setup_custom_validators(self, kwargs):
+ post_auth = kwargs.get('post_auth', [])
+ post_token = kwargs.get('post_token', [])
+ pre_auth = kwargs.get('pre_auth', [])
+ pre_token = kwargs.get('pre_token', [])
+ if not hasattr(self, 'validate_authorization_request'):
+ if post_auth or pre_auth:
+ msg = ("{} does not support authorization validators. Use "
+ "token validators instead.").format(self.__class__.__name__)
+ raise ValueError(msg)
+ # Using tuples here because they can't be appended to:
+ post_auth, pre_auth = (), ()
+ self.custom_validators = ValidatorsContainer(post_auth, post_token,
+ pre_auth, pre_token)
+
+ def register_response_type(self, response_type):
+ self.response_types.append(response_type)
+
+ def register_code_modifier(self, modifier):
+ self._code_modifiers.append(modifier)
+
+ def register_token_modifier(self, modifier):
+ self._token_modifiers.append(modifier)
+
+ def create_authorization_response(self, request, token_handler):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def create_token_response(self, request, token_handler):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def add_token(self, token, token_handler, request):
+ """
+ :param token:
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ # Only add a hybrid access token on auth step if asked for
+ if not request.response_type in ["token", "code token", "id_token token", "code id_token token"]:
+ return token
+
+ token.update(token_handler.create_token(request, refresh_token=False))
+ return token
+
+ def validate_grant_type(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ client_id = getattr(request, 'client_id', None)
+ if not self.request_validator.validate_grant_type(client_id,
+ request.grant_type, request.client, request):
+ log.debug('Unauthorized from %r (%r) access to grant type %s.',
+ request.client_id, request.client, request.grant_type)
+ raise errors.UnauthorizedClientError(request=request)
+
+ def validate_scopes(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ if not request.scopes:
+ request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list(
+ self.request_validator.get_default_scopes(request.client_id, request))
+ log.debug('Validating access to scopes %r for client %r (%r).',
+ request.scopes, request.client_id, request.client)
+ if not self.request_validator.validate_scopes(request.client_id,
+ request.scopes, request.client, request):
+ raise errors.InvalidScopeError(request=request)
+
+ def prepare_authorization_response(self, request, token, headers, body, status):
+ """Place token according to response mode.
+
+ Base classes can define a default response mode for their authorization
+ response by overriding the static `default_response_mode` member.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token:
+ :param headers:
+ :param body:
+ :param status:
+ """
+ request.response_mode = request.response_mode or self.default_response_mode
+
+ if request.response_mode not in ('query', 'fragment'):
+ log.debug('Overriding invalid response mode %s with %s',
+ request.response_mode, self.default_response_mode)
+ request.response_mode = self.default_response_mode
+
+ token_items = token.items()
+
+ if request.response_type == 'none':
+ state = token.get('state', None)
+ if state:
+ token_items = [('state', state)]
+ else:
+ token_items = []
+
+ if request.response_mode == 'query':
+ headers['Location'] = add_params_to_uri(
+ request.redirect_uri, token_items, fragment=False)
+ return headers, body, status
+
+ if request.response_mode == 'fragment':
+ headers['Location'] = add_params_to_uri(
+ request.redirect_uri, token_items, fragment=True)
+ return headers, body, status
+
+ raise NotImplementedError(
+ 'Subclasses must set a valid default_response_mode')
+
+ def _get_default_headers(self):
+ """Create default headers for grant responses."""
+ return {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+
+ def _handle_redirects(self, request):
+ if request.redirect_uri is not None:
+ request.using_default_redirect_uri = False
+ log.debug('Using provided redirect_uri %s', request.redirect_uri)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
+
+ # The authorization server MUST verify that the redirection URI
+ # to which it will redirect the access token matches a
+ # redirection URI registered by the client as described in
+ # Section 3.1.2.
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ if not self.request_validator.validate_redirect_uri(
+ request.client_id, request.redirect_uri, request):
+ raise errors.MismatchingRedirectURIError(request=request)
+ else:
+ request.redirect_uri = self.request_validator.get_default_redirect_uri(
+ request.client_id, request)
+ request.using_default_redirect_uri = True
+ log.debug('Using default redirect_uri %s.', request.redirect_uri)
+ if not request.redirect_uri:
+ raise errors.MissingRedirectURIError(request=request)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
+
+ def _create_cors_headers(self, request):
+ """If CORS is allowed, create the appropriate headers."""
+ if 'origin' not in request.headers:
+ return {}
+
+ origin = request.headers['origin']
+ if not is_secure_transport(origin):
+ log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin)
+ return {}
+ elif not self.request_validator.is_origin_allowed(
+ request.client_id, origin, request):
+ log.debug('Invalid origin "%s", CORS not allowed.', origin)
+ return {}
+ else:
+ log.debug('Valid origin "%s", injecting CORS headers.', origin)
+ return {'Access-Control-Allow-Origin': origin}
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
new file mode 100644
index 0000000000..e7b4618977
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
@@ -0,0 +1,123 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import json
+import logging
+
+from .. import errors
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class ClientCredentialsGrant(GrantTypeBase):
+
+ """`Client Credentials Grant`_
+
+ The client can request an access token using only its client
+ credentials (or other supported means of authentication) when the
+ client is requesting access to the protected resources under its
+ control, or those of another resource owner that have been previously
+ arranged with the authorization server (the method of which is beyond
+ the scope of this specification).
+
+ The client credentials grant type MUST only be used by confidential
+ clients::
+
+ +---------+ +---------------+
+ : : : :
+ : :>-- A - Client Authentication --->: Authorization :
+ : Client : : Server :
+ : :<-- B ---- Access Token ---------<: :
+ : : : :
+ +---------+ +---------------+
+
+ Figure 6: Client Credentials Flow
+
+ The flow illustrated in Figure 6 includes the following steps:
+
+ (A) The client authenticates with the authorization server and
+ requests an access token from the token endpoint.
+
+ (B) The authorization server authenticates the client, and if valid,
+ issues an access token.
+
+ .. _`Client Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.4
+ """
+
+ def create_token_response(self, request, token_handler):
+ """Return token or error in JSON format.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
+ If the access token request is valid and authorized, the
+ authorization server issues an access token as described in
+ `Section 5.1`_. A refresh token SHOULD NOT be included. If the request
+ failed client authentication or is invalid, the authorization server
+ returns an error response as described in `Section 5.2`_.
+
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ headers = self._get_default_headers()
+ try:
+ log.debug('Validating access token request, %r.', request)
+ self.validate_token_request(request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error in token request. %s.', e)
+ headers.update(e.headers)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request, refresh_token=False)
+
+ for modifier in self._token_modifiers:
+ token = modifier(token)
+
+ self.request_validator.save_token(token, request)
+
+ log.debug('Issuing token to client id %r (%r), %r.',
+ request.client_id, request.client, token)
+ return headers, json.dumps(token), 200
+
+ def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ for validator in self.custom_validators.pre_token:
+ validator(request)
+
+ if not getattr(request, 'grant_type', None):
+ raise errors.InvalidRequestError('Request is missing grant type.',
+ request=request)
+
+ if not request.grant_type == 'client_credentials':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ for param in ('grant_type', 'scope'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
+ request=request)
+
+ log.debug('Authenticating client, %r.', request)
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ else:
+ if not hasattr(request.client, 'client_id'):
+ raise NotImplementedError('Authenticate client must set the '
+ 'request.client.client_id attribute '
+ 'in authenticate_client.')
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ request.client_id = request.client_id or request.client.client_id
+ log.debug('Authorizing access to client %r.', request.client_id)
+ self.validate_scopes(request)
+
+ for validator in self.custom_validators.post_token:
+ validator(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/implicit.py
new file mode 100644
index 0000000000..6110b6f337
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/implicit.py
@@ -0,0 +1,376 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+from oauthlib import common
+
+from .. import errors
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class ImplicitGrant(GrantTypeBase):
+
+ """`Implicit Grant`_
+
+ The implicit grant type is used to obtain access tokens (it does not
+ support the issuance of refresh tokens) and is optimized for public
+ clients known to operate a particular redirection URI. These clients
+ are typically implemented in a browser using a scripting language
+ such as JavaScript.
+
+ Unlike the authorization code grant type, in which the client makes
+ separate requests for authorization and for an access token, the
+ client receives the access token as the result of the authorization
+ request.
+
+ The implicit grant type does not include client authentication, and
+ relies on the presence of the resource owner and the registration of
+ the redirection URI. Because the access token is encoded into the
+ redirection URI, it may be exposed to the resource owner and other
+ applications residing on the same device::
+
+ +----------+
+ | Resource |
+ | Owner |
+ | |
+ +----------+
+ ^
+ |
+ (B)
+ +----|-----+ Client Identifier +---------------+
+ | -+----(A)-- & Redirection URI --->| |
+ | User- | | Authorization |
+ | Agent -|----(B)-- User authenticates -->| Server |
+ | | | |
+ | |<---(C)--- Redirection URI ----<| |
+ | | with Access Token +---------------+
+ | | in Fragment
+ | | +---------------+
+ | |----(D)--- Redirection URI ---->| Web-Hosted |
+ | | without Fragment | Client |
+ | | | Resource |
+ | (F) |<---(E)------- Script ---------<| |
+ | | +---------------+
+ +-|--------+
+ | |
+ (A) (G) Access Token
+ | |
+ ^ v
+ +---------+
+ | |
+ | Client |
+ | |
+ +---------+
+
+ Note: The lines illustrating steps (A) and (B) are broken into two
+ parts as they pass through the user-agent.
+
+ Figure 4: Implicit Grant Flow
+
+ The flow illustrated in Figure 4 includes the following steps:
+
+ (A) The client initiates the flow by directing the resource owner's
+ user-agent to the authorization endpoint. The client includes
+ its client identifier, requested scope, local state, and a
+ redirection URI to which the authorization server will send the
+ user-agent back once access is granted (or denied).
+
+ (B) The authorization server authenticates the resource owner (via
+ the user-agent) and establishes whether the resource owner
+ grants or denies the client's access request.
+
+ (C) Assuming the resource owner grants access, the authorization
+ server redirects the user-agent back to the client using the
+ redirection URI provided earlier. The redirection URI includes
+ the access token in the URI fragment.
+
+ (D) The user-agent follows the redirection instructions by making a
+ request to the web-hosted client resource (which does not
+ include the fragment per [RFC2616]). The user-agent retains the
+ fragment information locally.
+
+ (E) The web-hosted client resource returns a web page (typically an
+ HTML document with an embedded script) capable of accessing the
+ full redirection URI including the fragment retained by the
+ user-agent, and extracting the access token (and other
+ parameters) contained in the fragment.
+
+ (F) The user-agent executes the script provided by the web-hosted
+ client resource locally, which extracts the access token.
+
+ (G) The user-agent passes the access token to the client.
+
+ See `Section 10.3`_ and `Section 10.16`_ for important security considerations
+ when using the implicit grant.
+
+ .. _`Implicit Grant`: https://tools.ietf.org/html/rfc6749#section-4.2
+ .. _`Section 10.3`: https://tools.ietf.org/html/rfc6749#section-10.3
+ .. _`Section 10.16`: https://tools.ietf.org/html/rfc6749#section-10.16
+ """
+
+ response_types = ['token']
+ grant_allows_refresh_token = False
+
+ def create_authorization_response(self, request, token_handler):
+ """Create an authorization response.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+ response_type
+ REQUIRED. Value MUST be set to "token" for standard OAuth2 implicit flow
+ or "id_token token" or just "id_token" for OIDC implicit flow
+
+ client_id
+ REQUIRED. The client identifier as described in `Section 2.2`_.
+
+ redirect_uri
+ OPTIONAL. As described in `Section 3.1.2`_.
+
+ scope
+ OPTIONAL. The scope of the access request as described by
+ `Section 3.3`_.
+
+ state
+ RECOMMENDED. An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent back
+ to the client. The parameter SHOULD be used for preventing
+ cross-site request forgery as described in `Section 10.12`_.
+
+ The authorization server validates the request to ensure that all
+ required parameters are present and valid. The authorization server
+ MUST verify that the redirection URI to which it will redirect the
+ access token matches a redirection URI registered by the client as
+ described in `Section 3.1.2`_.
+
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ """
+ return self.create_token_response(request, token_handler)
+
+ def create_token_response(self, request, token_handler):
+ """Return token or error embedded in the URI fragment.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ the following parameters to the fragment component of the redirection
+ URI using the "application/x-www-form-urlencoded" format, per
+ `Appendix B`_:
+
+ access_token
+ REQUIRED. The access token issued by the authorization server.
+
+ token_type
+ REQUIRED. The type of the token issued as described in
+ `Section 7.1`_. Value is case insensitive.
+
+ expires_in
+ RECOMMENDED. The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ scope
+ OPTIONAL, if identical to the scope requested by the client;
+ otherwise, REQUIRED. The scope of the access token as
+ described by `Section 3.3`_.
+
+ state
+ REQUIRED if the "state" parameter was present in the client
+ authorization request. The exact value received from the
+ client.
+
+ The authorization server MUST NOT issue a refresh token.
+
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
+ """
+ try:
+ self.validate_token_request(request)
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+ except errors.FatalClientError as e:
+ log.debug('Fatal client error during validation of %r. %r.',
+ request, e)
+ raise
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the fragment component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B:
+ # https://tools.ietf.org/html/rfc6749#appendix-B
+ except errors.OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
+ fragment=True)}, None, 302
+
+ # In OIDC implicit flow it is possible to have a request_type that does not include the access_token!
+ # "id_token token" - return the access token and the id token
+ # "id_token" - don't return the access token
+ if "token" in request.response_type.split():
+ token = token_handler.create_token(request, refresh_token=False)
+ else:
+ token = {}
+
+ if request.state is not None:
+ token['state'] = request.state
+
+ for modifier in self._token_modifiers:
+ token = modifier(token, token_handler, request)
+
+ # In OIDC implicit flow it is possible to have a request_type that does
+ # not include the access_token! In this case there is no need to save a token.
+ if "token" in request.response_type.split():
+ self.request_validator.save_token(token, request)
+
+ return self.prepare_authorization_response(
+ request, token, {}, None, 302)
+
+ def validate_authorization_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ return self.validate_token_request(request)
+
+ def validate_token_request(self, request):
+ """Check the token request for normal and fatal errors.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ This method is very similar to validate_authorization_request in
+ the AuthorizationCodeGrant but differ in a few subtle areas.
+
+ A normal error could be a missing response_type parameter or the client
+ attempting to access scope it is not allowed to ask authorization for.
+ Normal errors can safely be included in the redirection URI and
+ sent back to the client.
+
+ Fatal errors occur when the client_id or redirect_uri is invalid or
+ missing. These must be caught by the provider and handled, how this
+ is done is outside of the scope of OAuthLib but showing an error
+ page describing the issue is a good idea.
+ """
+
+ # First check for fatal errors
+
+ # If the request fails due to a missing, invalid, or mismatching
+ # redirection URI, or if the client identifier is missing or invalid,
+ # the authorization server SHOULD inform the resource owner of the
+ # error and MUST NOT automatically redirect the user-agent to the
+ # invalid redirection URI.
+
+ # First check duplicate parameters
+ for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+ try:
+ duplicate_params = request.duplicate_params
+ except ValueError:
+ raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
+ if param in duplicate_params:
+ raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
+
+ # REQUIRED. The client identifier as described in Section 2.2.
+ # https://tools.ietf.org/html/rfc6749#section-2.2
+ if not request.client_id:
+ raise errors.MissingClientIdError(request=request)
+
+ if not self.request_validator.validate_client_id(request.client_id, request):
+ raise errors.InvalidClientIdError(request=request)
+
+ # OPTIONAL. As described in Section 3.1.2.
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ self._handle_redirects(request)
+
+ # Then check for normal errors.
+
+ request_info = self._run_custom_validators(request,
+ self.custom_validators.all_pre)
+
+ # If the resource owner denies the access request or if the request
+ # fails for reasons other than a missing or invalid redirection URI,
+ # the authorization server informs the client by adding the following
+ # parameters to the fragment component of the redirection URI using the
+ # "application/x-www-form-urlencoded" format, per Appendix B.
+ # https://tools.ietf.org/html/rfc6749#appendix-B
+
+ # Note that the correct parameters to be added are automatically
+ # populated through the use of specific exceptions
+
+ # REQUIRED.
+ if request.response_type is None:
+ raise errors.MissingResponseTypeError(request=request)
+ # Value MUST be one of our registered types: "token" by default or if using OIDC "id_token" or "id_token token"
+ elif not set(request.response_type.split()).issubset(self.response_types):
+ raise errors.UnsupportedResponseTypeError(request=request)
+
+ log.debug('Validating use of response_type token for client %r (%r).',
+ request.client_id, request.client)
+ if not self.request_validator.validate_response_type(request.client_id,
+ request.response_type,
+ request.client, request):
+
+ log.debug('Client %s is not authorized to use response_type %s.',
+ request.client_id, request.response_type)
+ raise errors.UnauthorizedClientError(request=request)
+
+ # OPTIONAL. The scope of the access request as described by Section 3.3
+ # https://tools.ietf.org/html/rfc6749#section-3.3
+ self.validate_scopes(request)
+
+ request_info.update({
+ 'client_id': request.client_id,
+ 'redirect_uri': request.redirect_uri,
+ 'response_type': request.response_type,
+ 'state': request.state,
+ 'request': request,
+ })
+
+ request_info = self._run_custom_validators(
+ request,
+ self.custom_validators.all_post,
+ request_info
+ )
+
+ return request.scopes, request_info
+
+ def _run_custom_validators(self,
+ request,
+ validations,
+ request_info=None):
+ # Make a copy so we don't modify the existing request_info dict
+ request_info = {} if request_info is None else request_info.copy()
+ # For implicit grant, auth_validators and token_validators are
+ # basically equivalent since the token is returned from the
+ # authorization endpoint.
+ for validator in validations:
+ result = validator(request)
+ if result is not None:
+ request_info.update(result)
+ return request_info
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
new file mode 100644
index 0000000000..ce33df0e7d
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
@@ -0,0 +1,136 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import json
+import logging
+
+from .. import errors, utils
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class RefreshTokenGrant(GrantTypeBase):
+
+ """`Refresh token grant`_
+
+ .. _`Refresh token grant`: https://tools.ietf.org/html/rfc6749#section-6
+ """
+
+ def __init__(self, request_validator=None,
+ issue_new_refresh_tokens=True,
+ **kwargs):
+ super().__init__(
+ request_validator,
+ issue_new_refresh_tokens=issue_new_refresh_tokens,
+ **kwargs)
+
+ def create_token_response(self, request, token_handler):
+ """Create a new access token from a refresh_token.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
+ If valid and authorized, the authorization server issues an access
+ token as described in `Section 5.1`_. If the request failed
+ verification or is invalid, the authorization server returns an error
+ response as described in `Section 5.2`_.
+
+ The authorization server MAY issue a new refresh token, in which case
+ the client MUST discard the old refresh token and replace it with the
+ new refresh token. The authorization server MAY revoke the old
+ refresh token after issuing a new refresh token to the client. If a
+ new refresh token is issued, the refresh token scope MUST be
+ identical to that of the refresh token included by the client in the
+ request.
+
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ headers = self._get_default_headers()
+ try:
+ log.debug('Validating refresh token request, %r.', request)
+ self.validate_token_request(request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error in token request, %s.', e)
+ headers.update(e.headers)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request,
+ refresh_token=self.issue_new_refresh_tokens)
+
+ for modifier in self._token_modifiers:
+ token = modifier(token, token_handler, request)
+
+ self.request_validator.save_token(token, request)
+
+ log.debug('Issuing new token to client id %r (%r), %r.',
+ request.client_id, request.client, token)
+ headers.update(self._create_cors_headers(request))
+ return headers, json.dumps(token), 200
+
+ def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ # REQUIRED. Value MUST be set to "refresh_token".
+ if request.grant_type != 'refresh_token':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ for validator in self.custom_validators.pre_token:
+ validator(request)
+
+ if request.refresh_token is None:
+ raise errors.InvalidRequestError(
+ description='Missing refresh token parameter.',
+ request=request)
+
+ # Because refresh tokens are typically long-lasting credentials used to
+ # request additional access tokens, the refresh token is bound to the
+ # client to which it was issued. If the client type is confidential or
+ # the client was issued client credentials (or assigned other
+ # authentication requirements), the client MUST authenticate with the
+ # authorization server as described in Section 3.2.1.
+ # https://tools.ietf.org/html/rfc6749#section-3.2.1
+ if self.request_validator.client_authentication_required(request):
+ log.debug('Authenticating client, %r.', request)
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Invalid client (%r), denying access.', request)
+ raise errors.InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ # REQUIRED. The refresh token issued to the client.
+ log.debug('Validating refresh token %s for client %r.',
+ request.refresh_token, request.client)
+ if not self.request_validator.validate_refresh_token(
+ request.refresh_token, request.client, request):
+ log.debug('Invalid refresh token, %s, for client %r.',
+ request.refresh_token, request.client)
+ raise errors.InvalidGrantError(request=request)
+
+ original_scopes = utils.scope_to_list(
+ self.request_validator.get_original_scopes(
+ request.refresh_token, request))
+
+ if request.scope:
+ request.scopes = utils.scope_to_list(request.scope)
+ if (not all(s in original_scopes for s in request.scopes)
+ and not self.request_validator.is_within_original_scope(
+ request.scopes, request.refresh_token, request)):
+ log.debug('Refresh token %s lack requested scopes, %r.',
+ request.refresh_token, request.scopes)
+ raise errors.InvalidScopeError(request=request)
+ else:
+ request.scopes = original_scopes
+
+ for validator in self.custom_validators.post_token:
+ validator(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
new file mode 100644
index 0000000000..4b0de5bf6f
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
@@ -0,0 +1,199 @@
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import json
+import logging
+
+from .. import errors
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
+
+ """`Resource Owner Password Credentials Grant`_
+
+ The resource owner password credentials grant type is suitable in
+ cases where the resource owner has a trust relationship with the
+ client, such as the device operating system or a highly privileged
+ application. The authorization server should take special care when
+ enabling this grant type and only allow it when other flows are not
+ viable.
+
+ This grant type is suitable for clients capable of obtaining the
+ resource owner's credentials (username and password, typically using
+ an interactive form). It is also used to migrate existing clients
+ using direct authentication schemes such as HTTP Basic or Digest
+ authentication to OAuth by converting the stored credentials to an
+ access token::
+
+ +----------+
+ | Resource |
+ | Owner |
+ | |
+ +----------+
+ v
+ | Resource Owner
+ (A) Password Credentials
+ |
+ v
+ +---------+ +---------------+
+ | |>--(B)---- Resource Owner ------->| |
+ | | Password Credentials | Authorization |
+ | Client | | Server |
+ | |<--(C)---- Access Token ---------<| |
+ | | (w/ Optional Refresh Token) | |
+ +---------+ +---------------+
+
+ Figure 5: Resource Owner Password Credentials Flow
+
+ The flow illustrated in Figure 5 includes the following steps:
+
+ (A) The resource owner provides the client with its username and
+ password.
+
+ (B) The client requests an access token from the authorization
+ server's token endpoint by including the credentials received
+ from the resource owner. When making the request, the client
+ authenticates with the authorization server.
+
+ (C) The authorization server authenticates the client and validates
+ the resource owner credentials, and if valid, issues an access
+ token.
+
+ .. _`Resource Owner Password Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.3
+ """
+
+ def create_token_response(self, request, token_handler):
+ """Return token or error in json format.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
+ If the access token request is valid and authorized, the
+ authorization server issues an access token and optional refresh
+ token as described in `Section 5.1`_. If the request failed client
+ authentication or is invalid, the authorization server returns an
+ error response as described in `Section 5.2`_.
+
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
+ """
+ headers = self._get_default_headers()
+ try:
+ if self.request_validator.client_authentication_required(request):
+ log.debug('Authenticating client, %r.', request)
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ log.debug('Client authentication failed, %r.', request)
+ raise errors.InvalidClientError(request=request)
+ log.debug('Validating access token request, %r.', request)
+ self.validate_token_request(request)
+ except errors.OAuth2Error as e:
+ log.debug('Client error in token request, %s.', e)
+ headers.update(e.headers)
+ return headers, e.json, e.status_code
+
+ token = token_handler.create_token(request, self.refresh_token)
+
+ for modifier in self._token_modifiers:
+ token = modifier(token)
+
+ self.request_validator.save_token(token, request)
+
+ log.debug('Issuing token %r to client id %r (%r) and username %s.',
+ token, request.client_id, request.client, request.username)
+ return headers, json.dumps(token), 200
+
+ def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the "application/x-www-form-urlencoded"
+ format per Appendix B with a character encoding of UTF-8 in the HTTP
+ request entity-body:
+
+ grant_type
+ REQUIRED. Value MUST be set to "password".
+
+ username
+ REQUIRED. The resource owner username.
+
+ password
+ REQUIRED. The resource owner password.
+
+ scope
+ OPTIONAL. The scope of the access request as described by
+ `Section 3.3`_.
+
+ If the client type is confidential or the client was issued client
+ credentials (or assigned other authentication requirements), the
+ client MUST authenticate with the authorization server as described
+ in `Section 3.2.1`_.
+
+ The authorization server MUST:
+
+ o require client authentication for confidential clients or for any
+ client that was issued client credentials (or with other
+ authentication requirements),
+
+ o authenticate the client if client authentication is included, and
+
+ o validate the resource owner password credentials using its
+ existing password validation algorithm.
+
+ Since this access token request utilizes the resource owner's
+ password, the authorization server MUST protect the endpoint against
+ brute force attacks (e.g., using rate-limitation or generating
+ alerts).
+
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
+ """
+ for validator in self.custom_validators.pre_token:
+ validator(request)
+
+ for param in ('grant_type', 'username', 'password'):
+ if not getattr(request, param, None):
+ raise errors.InvalidRequestError(
+ 'Request is missing %s parameter.' % param, request=request)
+
+ for param in ('grant_type', 'username', 'password', 'scope'):
+ if param in request.duplicate_params:
+ raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+ # This error should rarely (if ever) occur if requests are routed to
+ # grant type handlers based on the grant_type parameter.
+ if not request.grant_type == 'password':
+ raise errors.UnsupportedGrantTypeError(request=request)
+
+ log.debug('Validating username %s.', request.username)
+ if not self.request_validator.validate_user(request.username,
+ request.password, request.client, request):
+ raise errors.InvalidGrantError(
+ 'Invalid credentials given.', request=request)
+ else:
+ if not hasattr(request.client, 'client_id'):
+ raise NotImplementedError(
+ 'Validate user must set the '
+ 'request.client.client_id attribute '
+ 'in authenticate_client.')
+ log.debug('Authorizing access to user %r.', request.user)
+
+ # Ensure client is authorized use of this grant type
+ self.validate_grant_type(request)
+
+ if request.client:
+ request.client_id = request.client_id or request.client.client_id
+ self.validate_scopes(request)
+
+ for validator in self.custom_validators.post_token:
+ validator(request)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/parameters.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/parameters.py
new file mode 100644
index 0000000000..8f6ce2c7fc
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/parameters.py
@@ -0,0 +1,471 @@
+"""
+oauthlib.oauth2.rfc6749.parameters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
+
+.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4
+"""
+import json
+import os
+import time
+import urllib.parse as urlparse
+
+from oauthlib.common import add_params_to_qs, add_params_to_uri
+from oauthlib.signals import scope_changed
+
+from .errors import (
+ InsecureTransportError, MismatchingStateError, MissingCodeError,
+ MissingTokenError, MissingTokenTypeError, raise_from_error,
+)
+from .tokens import OAuth2Token
+from .utils import is_secure_transport, list_to_scope, scope_to_list
+
+
+def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
+ scope=None, state=None, code_challenge=None, code_challenge_method='plain', **kwargs):
+ """Prepare the authorization grant request URI.
+
+ The client constructs the request URI by adding the following
+ parameters to the query component of the authorization endpoint URI
+ using the ``application/x-www-form-urlencoded`` format as defined by
+ [`W3C.REC-html401-19991224`_]:
+
+ :param uri:
+ :param client_id: The client identifier as described in `Section 2.2`_.
+ :param response_type: To indicate which OAuth 2 grant/flow is required,
+ "code" and "token".
+ :param redirect_uri: The client provided URI to redirect back to after
+ authorization as described in `Section 3.1.2`_.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+ :param state: An opaque value used by the client to maintain
+ state between the request and callback. The authorization
+ server includes this value when redirecting the user-agent
+ back to the client. The parameter SHOULD be used for
+ preventing cross-site request forgery as described in
+ `Section 10.12`_.
+ :param code_challenge: PKCE parameter. A challenge derived from the
+ code_verifier that is sent in the authorization
+ request, to be verified against later.
+ :param code_challenge_method: PKCE parameter. A method that was used to derive the
+ code_challenge. Defaults to "plain" if not present in the request.
+ :param kwargs: Extra arguments to embed in the grant/authorization URL.
+
+ An example of an authorization code grant authorization URL:
+
+ .. code-block:: http
+
+ GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
+ &code_challenge=kjasBS523KdkAILD2k78NdcJSk2k3KHG6&code_challenge_method=S256
+ &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
+ Host: server.example.com
+
+ .. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ params = [(('response_type', response_type)),
+ (('client_id', client_id))]
+
+ if redirect_uri:
+ params.append(('redirect_uri', redirect_uri))
+ if scope:
+ params.append(('scope', list_to_scope(scope)))
+ if state:
+ params.append(('state', state))
+ if code_challenge is not None:
+ params.append(('code_challenge', code_challenge))
+ params.append(('code_challenge_method', code_challenge_method))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((str(k), kwargs[k]))
+
+ return add_params_to_uri(uri, params)
+
+
+def prepare_token_request(grant_type, body='', include_client_id=True, code_verifier=None, **kwargs):
+ """Prepare the access token request.
+
+ The client makes a request to the token endpoint by adding the
+ following parameters using the ``application/x-www-form-urlencoded``
+ format in the HTTP request entity-body:
+
+ :param grant_type: To indicate grant type being used, i.e. "password",
+ "authorization_code" or "client_credentials".
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+
+ :param include_client_id: `True` (default) to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
+ :param client_id: Unicode client identifier. Will only appear if
+ `include_client_id` is True. *
+
+ :param client_secret: Unicode client secret. Will only appear if set to a
+ value that is not `None`. Invoking this function with
+ an empty string will send an empty `client_secret`
+ value to the server. *
+
+ :param code: If using authorization_code grant, pass the previously
+ obtained authorization code as the ``code`` argument. *
+
+ :param redirect_uri: If the "redirect_uri" parameter was included in the
+ authorization request as described in
+ `Section 4.1.1`_, and their values MUST be identical. *
+
+ :param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
+ authorization request to the token request.
+
+ :param kwargs: Extra arguments to embed in the request body.
+
+ Parameters marked with a `*` above are not explicit arguments in the
+ function signature, but are specially documented arguments for items
+ appearing in the generic `**kwargs` keyworded input.
+
+ An example of an authorization code token request body:
+
+ .. code-block:: http
+
+ grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
+ &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
+
+ .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
+ """
+ params = [('grant_type', grant_type)]
+
+ if 'scope' in kwargs:
+ kwargs['scope'] = list_to_scope(kwargs['scope'])
+
+ # pull the `client_id` out of the kwargs.
+ client_id = kwargs.pop('client_id', None)
+ if include_client_id:
+ if client_id is not None:
+ params.append(('client_id', client_id))
+
+ # use code_verifier if code_challenge was passed in the authorization request
+ if code_verifier is not None:
+ params.append(('code_verifier', code_verifier))
+
+ # the kwargs iteration below only supports including boolean truth (truthy)
+ # values, but some servers may require an empty string for `client_secret`
+ client_secret = kwargs.pop('client_secret', None)
+ if client_secret is not None:
+ params.append(('client_secret', client_secret))
+
+ # this handles: `code`, `redirect_uri`, and other undocumented params
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((str(k), kwargs[k]))
+
+ return add_params_to_qs(body, params)
+
+
+def prepare_token_revocation_request(url, token, token_type_hint="access_token",
+ callback=None, body='', **kwargs):
+ """Prepare a token revocation request.
+
+ The client constructs the request by including the following parameters
+ using the ``application/x-www-form-urlencoded`` format in the HTTP request
+ entity-body:
+
+ :param token: REQUIRED. The token that the client wants to get revoked.
+
+ :param token_type_hint: OPTIONAL. A hint about the type of the token
+ submitted for revocation. Clients MAY pass this
+ parameter in order to help the authorization server
+ to optimize the token lookup. If the server is
+ unable to locate the token using the given hint, it
+ MUST extend its search across all of its supported
+ token types. An authorization server MAY ignore
+ this parameter, particularly if it is able to detect
+ the token type automatically.
+
+ This specification defines two values for `token_type_hint`:
+
+ * access_token: An access token as defined in [RFC6749],
+ `Section 1.4`_
+
+ * refresh_token: A refresh token as defined in [RFC6749],
+ `Section 1.5`_
+
+ Specific implementations, profiles, and extensions of this
+ specification MAY define other values for this parameter using the
+ registry defined in `Section 4.1.2`_.
+
+ .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2
+
+ """
+ if not is_secure_transport(url):
+ raise InsecureTransportError()
+
+ params = [('token', token)]
+
+ if token_type_hint:
+ params.append(('token_type_hint', token_type_hint))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((str(k), kwargs[k]))
+
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+
+ if callback:
+ params.append(('callback', callback))
+ return add_params_to_uri(url, params), headers, body
+ else:
+ return url, headers, add_params_to_qs(body, params)
+
+
+def parse_authorization_code_response(uri, state=None):
+ """Parse authorization grant response URI into a dict.
+
+ If the resource owner grants the access request, the authorization
+ server issues an authorization code and delivers it to the client by
+ adding the following parameters to the query component of the
+ redirection URI using the ``application/x-www-form-urlencoded`` format:
+
+ **code**
+ REQUIRED. The authorization code generated by the
+ authorization server. The authorization code MUST expire
+ shortly after it is issued to mitigate the risk of leaks. A
+ maximum authorization code lifetime of 10 minutes is
+ RECOMMENDED. The client MUST NOT use the authorization code
+ more than once. If an authorization code is used more than
+ once, the authorization server MUST deny the request and SHOULD
+ revoke (when possible) all tokens previously issued based on
+ that authorization code. The authorization code is bound to
+ the client identifier and redirection URI.
+
+ **state**
+ REQUIRED if the "state" parameter was present in the client
+ authorization request. The exact value received from the
+ client.
+
+ :param uri: The full redirect URL back to the client.
+ :param state: The state parameter from the authorization request.
+
+ For example, the authorization server redirects the user-agent by
+ sending the following HTTP response:
+
+ .. code-block:: http
+
+ HTTP/1.1 302 Found
+ Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
+ &state=xyz
+
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ query = urlparse.urlparse(uri).query
+ params = dict(urlparse.parse_qsl(query))
+
+ if state and params.get('state', None) != state:
+ raise MismatchingStateError()
+
+ if 'error' in params:
+ raise_from_error(params.get('error'), params)
+
+ if not 'code' in params:
+ raise MissingCodeError("Missing code parameter in response.")
+
+ return params
+
+
+def parse_implicit_response(uri, state=None, scope=None):
+ """Parse the implicit token response URI into a dict.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ the following parameters to the fragment component of the redirection
+ URI using the ``application/x-www-form-urlencoded`` format:
+
+ **access_token**
+ REQUIRED. The access token issued by the authorization server.
+
+ **token_type**
+ REQUIRED. The type of the token issued as described in
+ Section 7.1. Value is case insensitive.
+
+ **expires_in**
+ RECOMMENDED. The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+
+ **scope**
+ OPTIONAL, if identical to the scope requested by the client,
+ otherwise REQUIRED. The scope of the access token as described
+ by Section 3.3.
+
+ **state**
+ REQUIRED if the "state" parameter was present in the client
+ authorization request. The exact value received from the
+ client.
+
+ :param uri:
+ :param state:
+ :param scope:
+
+ Similar to the authorization code response, but with a full token provided
+ in the URL fragment:
+
+ .. code-block:: http
+
+ HTTP/1.1 302 Found
+ Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
+ &state=xyz&token_type=example&expires_in=3600
+ """
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ fragment = urlparse.urlparse(uri).fragment
+ params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
+
+ for key in ('expires_in',):
+ if key in params: # cast things to int
+ params[key] = int(params[key])
+
+ if 'scope' in params:
+ params['scope'] = scope_to_list(params['scope'])
+
+ if 'expires_in' in params:
+ params['expires_at'] = time.time() + int(params['expires_in'])
+
+ if state and params.get('state', None) != state:
+ raise ValueError("Mismatching or missing state in params.")
+
+ params = OAuth2Token(params, old_scope=scope)
+ validate_token_parameters(params)
+ return params
+
+
+def parse_token_response(body, scope=None):
+ """Parse the JSON token response body into a dict.
+
+ The authorization server issues an access token and optional refresh
+ token, and constructs the response by adding the following parameters
+ to the entity body of the HTTP response with a 200 (OK) status code:
+
+ access_token
+ REQUIRED. The access token issued by the authorization server.
+ token_type
+ REQUIRED. The type of the token issued as described in
+ `Section 7.1`_. Value is case insensitive.
+ expires_in
+ RECOMMENDED. The lifetime in seconds of the access token. For
+ example, the value "3600" denotes that the access token will
+ expire in one hour from the time the response was generated.
+ If omitted, the authorization server SHOULD provide the
+ expiration time via other means or document the default value.
+ refresh_token
+ OPTIONAL. The refresh token which can be used to obtain new
+ access tokens using the same authorization grant as described
+ in `Section 6`_.
+ scope
+ OPTIONAL, if identical to the scope requested by the client,
+ otherwise REQUIRED. The scope of the access token as described
+ by `Section 3.3`_.
+
+ The parameters are included in the entity body of the HTTP response
+ using the "application/json" media type as defined by [`RFC4627`_]. The
+ parameters are serialized into a JSON structure by adding each
+ parameter at the highest structure level. Parameter names and string
+ values are included as JSON strings. Numerical values are included
+ as JSON numbers. The order of parameters does not matter and can
+ vary.
+
+ :param body: The full json encoded response body.
+ :param scope: The scope requested during authorization.
+
+ For example:
+
+ .. code-block:: http
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json
+ Cache-Control: no-store
+ Pragma: no-cache
+
+ {
+ "access_token":"2YotnFZFEjr1zCsicMWpAA",
+ "token_type":"example",
+ "expires_in":3600,
+ "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
+ "example_parameter":"example_value"
+ }
+
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`RFC4627`: https://tools.ietf.org/html/rfc4627
+ """
+ try:
+ params = json.loads(body)
+ except ValueError:
+
+ # Fall back to URL-encoded string, to support old implementations,
+ # including (at time of writing) Facebook. See:
+ # https://github.com/oauthlib/oauthlib/issues/267
+
+ params = dict(urlparse.parse_qsl(body))
+ for key in ('expires_in',):
+ if key in params: # cast things to int
+ params[key] = int(params[key])
+
+ if 'scope' in params:
+ params['scope'] = scope_to_list(params['scope'])
+
+ if 'expires_in' in params:
+ if params['expires_in'] is None:
+ params.pop('expires_in')
+ else:
+ params['expires_at'] = time.time() + int(params['expires_in'])
+
+ params = OAuth2Token(params, old_scope=scope)
+ validate_token_parameters(params)
+ return params
+
+
+def validate_token_parameters(params):
+ """Ensures token presence, token type, expiration and scope in params."""
+ if 'error' in params:
+ raise_from_error(params.get('error'), params)
+
+ if not 'access_token' in params:
+ raise MissingTokenError(description="Missing access token parameter.")
+
+ if not 'token_type' in params:
+ if os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
+ raise MissingTokenTypeError()
+
+ # If the issued access token scope is different from the one requested by
+ # the client, the authorization server MUST include the "scope" response
+ # parameter to inform the client of the actual scope granted.
+ # https://tools.ietf.org/html/rfc6749#section-3.3
+ if params.scope_changed:
+ message = 'Scope has changed from "{old}" to "{new}".'.format(
+ old=params.old_scope, new=params.scope,
+ )
+ scope_changed.send(message=message, old=params.old_scopes, new=params.scopes)
+ if not os.environ.get('OAUTHLIB_RELAX_TOKEN_SCOPE', None):
+ w = Warning(message)
+ w.token = params
+ w.old_scope = params.old_scopes
+ w.new_scope = params.scopes
+ raise w
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/request_validator.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/request_validator.py
new file mode 100644
index 0000000000..3910c0b918
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/request_validator.py
@@ -0,0 +1,680 @@
+"""
+oauthlib.oauth2.rfc6749.request_validator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class RequestValidator:
+
+ def client_authentication_required(self, request, *args, **kwargs):
+ """Determine if client authentication is required for current request.
+
+ According to the rfc6749, client authentication is required in the following cases:
+ - Resource Owner Password Credentials Grant, when Client type is Confidential or when
+ Client was issued client credentials or whenever Client provided client
+ authentication, see `Section 4.3.2`_.
+ - Authorization Code Grant, when Client type is Confidential or when Client was issued
+ client credentials or whenever Client provided client authentication,
+ see `Section 4.1.3`_.
+ - Refresh Token Grant, when Client type is Confidential or when Client was issued
+ client credentials or whenever Client provided client authentication, see
+ `Section 6`_
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Resource Owner Password Credentials Grant
+ - Refresh Token Grant
+
+ .. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2
+ .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
+ .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
+ """
+ return True
+
+ def authenticate_client(self, request, *args, **kwargs):
+ """Authenticate client through means outside the OAuth 2 spec.
+
+ Means of authentication is negotiated beforehand and may for example
+ be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization
+ header.
+
+ Headers may be accesses through request.headers and parameters found in
+ both body and query can be obtained by direct attribute access, i.e.
+ request.client_id for client_id in the URL query.
+
+ The authentication process is required to contain the identification of
+ the client (i.e. search the database based on the client_id). In case the
+ client doesn't exist based on the received client_id, this method has to
+ return False and the HTTP response created by the library will contain
+ 'invalid_client' message.
+
+ After the client identification succeeds, this method needs to set the
+ client on the request, i.e. request.client = client. A client object's
+ class must contain the 'client_id' attribute and the 'client_id' must have
+ a value.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Resource Owner Password Credentials Grant (may be disabled)
+ - Client Credentials Grant
+ - Refresh Token Grant
+
+ .. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def authenticate_client_id(self, client_id, request, *args, **kwargs):
+ """Ensure client_id belong to a non-confidential client.
+
+ A non-confidential client is one that is not required to authenticate
+ through other means, such as using HTTP Basic.
+
+ Note, while not strictly necessary it can often be very convenient
+ to set request.client to the client object associated with the
+ given client_id.
+
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request,
+ *args, **kwargs):
+ """Ensure that the authorization process represented by this authorization
+ code began with this 'redirect_uri'.
+
+ If the client specifies a redirect_uri when obtaining code then that
+ redirect URI must be bound to the code and verified equal in this
+ method, according to RFC 6749 section 4.1.3. Do not compare against
+ the client's allowed redirect URIs, but against the URI used when the
+ code was saved.
+
+ :param client_id: Unicode client identifier.
+ :param code: Unicode authorization_code.
+ :param redirect_uri: Unicode absolute URI.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant (during token request)
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
+ """Get the default redirect URI for the client.
+
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: The default redirect URI for the client
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_default_scopes(self, client_id, request, *args, **kwargs):
+ """Get the default scopes for the client.
+
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: List of default scopes
+
+ Method is used by all core grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_original_scopes(self, refresh_token, request, *args, **kwargs):
+ """Get the list of scopes associated with the refresh token.
+
+ :param refresh_token: Unicode refresh token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: List of scopes.
+
+ Method is used by:
+ - Refresh token grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def is_within_original_scope(self, request_scopes, refresh_token, request, *args, **kwargs):
+ """Check if requested scopes are within a scope of the refresh token.
+
+ When access tokens are refreshed the scope of the new token
+ needs to be within the scope of the original token. This is
+ ensured by checking that all requested scopes strings are on
+ the list returned by the get_original_scopes. If this check
+ fails, is_within_original_scope is called. The method can be
+ used in situations where returning all valid scopes from the
+ get_original_scopes is not practical.
+
+ :param request_scopes: A list of scopes that were requested by client.
+ :param refresh_token: Unicode refresh_token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Refresh token grant
+ """
+ return False
+
+ def introspect_token(self, token, token_type_hint, request, *args, **kwargs):
+ """Introspect an access or refresh token.
+
+ Called once the introspect request is validated. This method should
+ verify the *token* and either return a dictionary with the list of
+ claims associated, or `None` in case the token is unknown.
+
+ Below the list of registered claims you should be interested in:
+
+ - scope : space-separated list of scopes
+ - client_id : client identifier
+ - username : human-readable identifier for the resource owner
+ - token_type : type of the token
+ - exp : integer timestamp indicating when this token will expire
+ - iat : integer timestamp indicating when this token was issued
+ - nbf : integer timestamp indicating when it can be "not-before" used
+ - sub : subject of the token - identifier of the resource owner
+ - aud : list of string identifiers representing the intended audience
+ - iss : string representing issuer of this token
+ - jti : string identifier for the token
+
+ Note that most of them are coming directly from JWT RFC. More details
+ can be found in `Introspect Claims`_ or `JWT Claims`_.
+
+ The implementation can use *token_type_hint* to improve lookup
+ efficiency, but must fallback to other types to be compliant with RFC.
+
+ The dict of claims is added to request.token after this method.
+
+ :param token: The token string.
+ :param token_type_hint: access_token or refresh_token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ Method is used by:
+ - Introspect Endpoint (all grants are compatible)
+
+ .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2
+ .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
+ """Invalidate an authorization code after use.
+
+ :param client_id: Unicode client identifier.
+ :param code: The authorization code grant (request.code).
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
+ """Revoke an access or refresh token.
+
+ :param token: The token string.
+ :param token_type_hint: access_token or refresh_token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ Method is used by:
+ - Revocation Endpoint
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def rotate_refresh_token(self, request):
+ """Determine whether to rotate the refresh token. Default, yes.
+
+ When access tokens are refreshed the old refresh token can be kept
+ or replaced with a new one (rotated). Return True to rotate and
+ and False for keeping original.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Refresh Token Grant
+ """
+ return True
+
+ def save_authorization_code(self, client_id, code, request, *args, **kwargs):
+ """Persist the authorization_code.
+
+ The code should at minimum be stored with:
+ - the client_id (``client_id``)
+ - the redirect URI used (``request.redirect_uri``)
+ - a resource owner / user (``request.user``)
+ - the authorized scopes (``request.scopes``)
+
+ To support PKCE, you MUST associate the code with:
+ - Code Challenge (``request.code_challenge``) and
+ - Code Challenge Method (``request.code_challenge_method``)
+
+ To support OIDC, you MUST associate the code with:
+ - nonce, if present (``code["nonce"]``)
+
+ The ``code`` argument is actually a dictionary, containing at least a
+ ``code`` key with the actual authorization code:
+
+ ``{'code': 'sdf345jsdf0934f'}``
+
+ It may also have a ``claims`` parameter which, when present, will be a dict
+ deserialized from JSON as described at
+ http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
+ This value should be saved in this method and used again in ``.validate_code``.
+
+ :param client_id: Unicode client identifier.
+ :param code: A dict of the authorization code grant and, optionally, state.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def save_token(self, token, request, *args, **kwargs):
+ """Persist the token with a token type specific method.
+
+ Currently, only save_bearer_token is supported.
+
+ :param token: A (Bearer) token dict.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ return self.save_bearer_token(token, request, *args, **kwargs)
+
+ def save_bearer_token(self, token, request, *args, **kwargs):
+ """Persist the Bearer token.
+
+ The Bearer token should at minimum be associated with:
+ - a client and it's client_id, if available
+ - a resource owner / user (request.user)
+ - authorized scopes (request.scopes)
+ - an expiration time
+ - a refresh token, if issued
+ - a claims document, if present in request.claims
+
+ The Bearer token dict may hold a number of items::
+
+ {
+ 'token_type': 'Bearer',
+ 'access_token': 'askfjh234as9sd8',
+ 'expires_in': 3600,
+ 'scope': 'string of space separated authorized scopes',
+ 'refresh_token': '23sdf876234', # if issued
+ 'state': 'given_by_client', # if supplied by client (implicit ONLY)
+ }
+
+ Note that while "scope" is a string-separated list of authorized scopes,
+ the original list is still available in request.scopes.
+
+ The token dict is passed as a reference so any changes made to the dictionary
+ will go back to the user. If additional information must return to the client
+ user, and it is only possible to get this information after writing the token
+ to storage, it should be added to the token dictionary. If the token
+ dictionary must be modified but the changes should not go back to the user,
+ a copy of the dictionary must be made before making the changes.
+
+ Also note that if an Authorization Code grant request included a valid claims
+ parameter (for OpenID Connect) then the request.claims property will contain
+ the claims dict, which should be saved for later use when generating the
+ id_token and/or UserInfo response content.
+
+ :param token: A Bearer token dict.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: The default redirect URI for the client
+
+ Method is used by all core grant types issuing Bearer tokens:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant (might not associate a client)
+ - Client Credentials grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_bearer_token(self, token, scopes, request):
+ """Ensure the Bearer token is valid and authorized access to scopes.
+
+ :param token: A string of random characters.
+ :param scopes: A list of scopes associated with the protected resource.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ A key to OAuth 2 security and restricting impact of leaked tokens is
+ the short expiration time of tokens, *always ensure the token has not
+ expired!*.
+
+ Two different approaches to scope validation:
+
+ 1) all(scopes). The token must be authorized access to all scopes
+ associated with the resource. For example, the
+ token has access to ``read-only`` and ``images``,
+ thus the client can view images but not upload new.
+ Allows for fine grained access control through
+ combining various scopes.
+
+ 2) any(scopes). The token must be authorized access to one of the
+ scopes associated with the resource. For example,
+ token has access to ``read-only-images``.
+ Allows for fine grained, although arguably less
+ convenient, access control.
+
+ A powerful way to use scopes would mimic UNIX ACLs and see a scope
+ as a group with certain privileges. For a restful API these might
+ map to HTTP verbs instead of read, write and execute.
+
+ Note, the request.user attribute can be set to the resource owner
+ associated with this token. Similarly the request.client and
+ request.scopes attribute can be set to associated client object
+ and authorized scopes. If you then use a decorator such as the
+ one provided for django these attributes will be made available
+ in all protected views as keyword arguments.
+
+ :param token: Unicode Bearer token
+ :param scopes: List of scopes (defined by you)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core Bearer token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_client_id(self, client_id, request, *args, **kwargs):
+ """Ensure client_id belong to a valid and active client.
+
+ Note, while not strictly necessary it can often be very convenient
+ to set request.client to the client object associated with the
+ given client_id.
+
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_code(self, client_id, code, client, request, *args, **kwargs):
+ """Verify that the authorization_code is valid and assigned to the given
+ client.
+
+ Before returning true, set the following based on the information stored
+ with the code in 'save_authorization_code':
+
+ - request.user
+ - request.scopes
+ - request.claims (if given)
+
+ OBS! The request.user attribute should be set to the resource owner
+ associated with this authorization code. Similarly request.scopes
+ must also be set.
+
+ The request.claims property, if it was given, should assigned a dict.
+
+ If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code')
+ you MUST set the following based on the information stored:
+
+ - request.code_challenge
+ - request.code_challenge_method
+
+ :param client_id: Unicode client identifier.
+ :param code: Unicode authorization code.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
+ """Ensure client is authorized to use the grant_type requested.
+
+ :param client_id: Unicode client identifier.
+ :param grant_type: Unicode grant type, i.e. authorization_code, password.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials Grant
+ - Refresh Token Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
+ """Ensure client is authorized to redirect to the redirect_uri requested.
+
+ All clients should register the absolute URIs of all URIs they intend
+ to redirect to. The registration is outside of the scope of oauthlib.
+
+ :param client_id: Unicode client identifier.
+ :param redirect_uri: Unicode absolute URI.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
+ """Ensure the Bearer token is valid and authorized access to scopes.
+
+ OBS! The request.user attribute should be set to the resource owner
+ associated with this refresh token.
+
+ :param refresh_token: Unicode refresh token.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant (indirectly by issuing refresh tokens)
+ - Resource Owner Password Credentials Grant (also indirectly)
+ - Refresh Token Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
+ """Ensure client is authorized to use the response_type requested.
+
+ :param client_id: Unicode client identifier.
+ :param response_type: Unicode response type, i.e. code, token.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+ - Implicit Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
+ """Ensure the client is authorized access to requested scopes.
+
+ :param client_id: Unicode client identifier.
+ :param scopes: List of scopes (defined by you).
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by all core grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Resource Owner Password Credentials Grant
+ - Client Credentials Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_user(self, username, password, client, request, *args, **kwargs):
+ """Ensure the username and password is valid.
+
+ OBS! The validation should also set the user attribute of the request
+ to a valid resource owner, i.e. request.user = username or similar. If
+ not set you will be unable to associate a token with a user in the
+ persistence method used (commonly, save_bearer_token).
+
+ :param username: Unicode username.
+ :param password: Unicode password.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Resource Owner Password Credentials Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def is_pkce_required(self, client_id, request):
+ """Determine if current request requires PKCE. Default, False.
+ This is called for both "authorization" and "token" requests.
+
+ Override this method by ``return True`` to enable PKCE for everyone.
+ You might want to enable it only for public clients.
+ Note that PKCE can also be used in addition of a client authentication.
+
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are
+ susceptible to the authorization code interception attack. This
+ specification describes the attack as well as a technique to mitigate
+ against the threat through the use of Proof Key for Code Exchange
+ (PKCE, pronounced "pixy"). See `RFC7636`_.
+
+ :param client_id: Client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+
+ .. _`RFC7636`: https://tools.ietf.org/html/rfc7636
+ """
+ return False
+
+ def get_code_challenge(self, code, request):
+ """Is called for every "token" requests.
+
+ When the server issues the authorization code in the authorization
+ response, it MUST associate the ``code_challenge`` and
+ ``code_challenge_method`` values with the authorization code so it can
+ be verified later.
+
+ Typically, the ``code_challenge`` and ``code_challenge_method`` values
+ are stored in encrypted form in the ``code`` itself but could
+ alternatively be stored on the server associated with the code. The
+ server MUST NOT include the ``code_challenge`` value in client requests
+ in a form that other entities can extract.
+
+ Return the ``code_challenge`` associated to the code.
+ If ``None`` is returned, code is considered to not be associated to any
+ challenges.
+
+ :param code: Authorization code.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: code_challenge string
+
+ Method is used by:
+ - Authorization Code Grant - when PKCE is active
+
+ """
+ return None
+
+ def get_code_challenge_method(self, code, request):
+ """Is called during the "token" request processing, when a
+ ``code_verifier`` and a ``code_challenge`` has been provided.
+
+ See ``.get_code_challenge``.
+
+ Must return ``plain`` or ``S256``. You can return a custom value if you have
+ implemented your own ``AuthorizationCodeGrant`` class.
+
+ :param code: Authorization code.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: code_challenge_method string
+
+ Method is used by:
+ - Authorization Code Grant - when PKCE is active
+
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def is_origin_allowed(self, client_id, origin, request, *args, **kwargs):
+ """Indicate if the given origin is allowed to access the token endpoint
+ via Cross-Origin Resource Sharing (CORS). CORS is used by browser-based
+ clients, such as Single-Page Applications, to perform the Authorization
+ Code Grant.
+
+ (Note: If performing Authorization Code Grant via a public client such
+ as a browser, you should use PKCE as well.)
+
+ If this method returns true, the appropriate CORS headers will be added
+ to the response. By default this method always returns False, meaning
+ CORS is disabled.
+
+ :param client_id: Unicode client identifier.
+ :param redirect_uri: Unicode origin.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: bool
+
+ Method is used by:
+ - Authorization Code Grant
+ - Refresh Token Grant
+
+ """
+ return False
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/tokens.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/tokens.py
new file mode 100644
index 0000000000..0757d07ea5
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/tokens.py
@@ -0,0 +1,356 @@
+"""
+oauthlib.oauth2.rfc6749.tokens
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods for adding two types of access tokens to requests.
+
+- Bearer https://tools.ietf.org/html/rfc6750
+- MAC https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+"""
+import hashlib
+import hmac
+import warnings
+from binascii import b2a_base64
+from urllib.parse import urlparse
+
+from oauthlib import common
+from oauthlib.common import add_params_to_qs, add_params_to_uri
+
+from . import utils
+
+
+class OAuth2Token(dict):
+
+ def __init__(self, params, old_scope=None):
+ super().__init__(params)
+ self._new_scope = None
+ if 'scope' in params and params['scope']:
+ self._new_scope = set(utils.scope_to_list(params['scope']))
+ if old_scope is not None:
+ self._old_scope = set(utils.scope_to_list(old_scope))
+ if self._new_scope is None:
+ # the rfc says that if the scope hasn't changed, it's optional
+ # in params so set the new scope to the old scope
+ self._new_scope = self._old_scope
+ else:
+ self._old_scope = self._new_scope
+
+ @property
+ def scope_changed(self):
+ return self._new_scope != self._old_scope
+
+ @property
+ def old_scope(self):
+ return utils.list_to_scope(self._old_scope)
+
+ @property
+ def old_scopes(self):
+ return list(self._old_scope)
+
+ @property
+ def scope(self):
+ return utils.list_to_scope(self._new_scope)
+
+ @property
+ def scopes(self):
+ return list(self._new_scope)
+
+ @property
+ def missing_scopes(self):
+ return list(self._old_scope - self._new_scope)
+
+ @property
+ def additional_scopes(self):
+ return list(self._new_scope - self._old_scope)
+
+
+def prepare_mac_header(token, uri, key, http_method,
+ nonce=None,
+ headers=None,
+ body=None,
+ ext='',
+ hash_algorithm='hmac-sha-1',
+ issue_time=None,
+ draft=0):
+ """Add an `MAC Access Authentication`_ signature to headers.
+
+ Unlike OAuth 1, this HMAC signature does not require inclusion of the
+ request payload/body, neither does it use a combination of client_secret
+ and token_secret but rather a mac_key provided together with the access
+ token.
+
+ Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
+ `extension algorithms`_ are not supported.
+
+ Example MAC Authorization header, linebreaks added for clarity
+
+ Authorization: MAC id="h480djs93hd8",
+ nonce="1336363200:dj83hs9s",
+ mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
+
+ .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+ .. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
+
+ :param token:
+ :param uri: Request URI.
+ :param key: MAC given provided by token endpoint.
+ :param http_method: HTTP Request method.
+ :param nonce:
+ :param headers: Request headers as a dictionary.
+ :param body:
+ :param ext:
+ :param hash_algorithm: HMAC algorithm provided by token endpoint.
+ :param issue_time: Time when the MAC credentials were issued (datetime).
+ :param draft: MAC authentication specification version.
+ :return: headers dictionary with the authorization field added.
+ """
+ http_method = http_method.upper()
+ host, port = utils.host_from_uri(uri)
+
+ if hash_algorithm.lower() == 'hmac-sha-1':
+ h = hashlib.sha1
+ elif hash_algorithm.lower() == 'hmac-sha-256':
+ h = hashlib.sha256
+ else:
+ raise ValueError('unknown hash algorithm')
+
+ if draft == 0:
+ nonce = nonce or '{}:{}'.format(utils.generate_age(issue_time),
+ common.generate_nonce())
+ else:
+ ts = common.generate_timestamp()
+ nonce = common.generate_nonce()
+
+ sch, net, path, par, query, fra = urlparse(uri)
+
+ if query:
+ request_uri = path + '?' + query
+ else:
+ request_uri = path
+
+ # Hash the body/payload
+ if body is not None and draft == 0:
+ body = body.encode('utf-8')
+ bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
+ else:
+ bodyhash = ''
+
+ # Create the normalized base string
+ base = []
+ if draft == 0:
+ base.append(nonce)
+ else:
+ base.append(ts)
+ base.append(nonce)
+ base.append(http_method.upper())
+ base.append(request_uri)
+ base.append(host)
+ base.append(port)
+ if draft == 0:
+ base.append(bodyhash)
+ base.append(ext or '')
+ base_string = '\n'.join(base) + '\n'
+
+ # hmac struggles with unicode strings - http://bugs.python.org/issue5285
+ if isinstance(key, str):
+ key = key.encode('utf-8')
+ sign = hmac.new(key, base_string.encode('utf-8'), h)
+ sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
+
+ header = []
+ header.append('MAC id="%s"' % token)
+ if draft != 0:
+ header.append('ts="%s"' % ts)
+ header.append('nonce="%s"' % nonce)
+ if bodyhash:
+ header.append('bodyhash="%s"' % bodyhash)
+ if ext:
+ header.append('ext="%s"' % ext)
+ header.append('mac="%s"' % sign)
+
+ headers = headers or {}
+ headers['Authorization'] = ', '.join(header)
+ return headers
+
+
+def prepare_bearer_uri(token, uri):
+ """Add a `Bearer Token`_ to the request URI.
+ Not recommended, use only if client can't use authorization header or body.
+
+ http://www.example.com/path?access_token=h480djs93hd8
+
+ .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
+
+ :param token:
+ :param uri:
+ """
+ return add_params_to_uri(uri, [(('access_token', token))])
+
+
+def prepare_bearer_headers(token, headers=None):
+ """Add a `Bearer Token`_ to the request URI.
+ Recommended method of passing bearer tokens.
+
+ Authorization: Bearer h480djs93hd8
+
+ .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
+
+ :param token:
+ :param headers:
+ """
+ headers = headers or {}
+ headers['Authorization'] = 'Bearer %s' % token
+ return headers
+
+
+def prepare_bearer_body(token, body=''):
+ """Add a `Bearer Token`_ to the request body.
+
+ access_token=h480djs93hd8
+
+ .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
+
+ :param token:
+ :param body:
+ """
+ return add_params_to_qs(body, [(('access_token', token))])
+
+
+def random_token_generator(request, refresh_token=False):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param refresh_token:
+ """
+ return common.generate_token()
+
+
+def signed_token_generator(private_pem, **kwargs):
+ """
+ :param private_pem:
+ """
+ def signed_token_generator(request):
+ request.claims = kwargs
+ return common.generate_signed_token(private_pem, request)
+
+ return signed_token_generator
+
+
+def get_token_from_header(request):
+ """
+ Helper function to extract a token from the request header.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: Return the token or None if the Authorization header is malformed.
+ """
+ token = None
+
+ if 'Authorization' in request.headers:
+ split_header = request.headers.get('Authorization').split()
+ if len(split_header) == 2 and split_header[0].lower() == 'bearer':
+ token = split_header[1]
+ else:
+ token = request.access_token
+
+ return token
+
+
+class TokenBase:
+ __slots__ = ()
+
+ def __call__(self, request, refresh_token=False):
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def estimate_type(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+
+class BearerToken(TokenBase):
+ __slots__ = (
+ 'request_validator', 'token_generator',
+ 'refresh_token_generator', 'expires_in'
+ )
+
+ def __init__(self, request_validator=None, token_generator=None,
+ expires_in=None, refresh_token_generator=None):
+ self.request_validator = request_validator
+ self.token_generator = token_generator or random_token_generator
+ self.refresh_token_generator = (
+ refresh_token_generator or self.token_generator
+ )
+ self.expires_in = expires_in or 3600
+
+ def create_token(self, request, refresh_token=False, **kwargs):
+ """
+ Create a BearerToken, by default without refresh token.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param refresh_token:
+ """
+ if "save_token" in kwargs:
+ warnings.warn("`save_token` has been deprecated, it was not called internally."
+ "If you do, call `request_validator.save_token()` instead.",
+ DeprecationWarning)
+
+ if callable(self.expires_in):
+ expires_in = self.expires_in(request)
+ else:
+ expires_in = self.expires_in
+
+ request.expires_in = expires_in
+
+ token = {
+ 'access_token': self.token_generator(request),
+ 'expires_in': expires_in,
+ 'token_type': 'Bearer',
+ }
+
+ # If provided, include - this is optional in some cases https://tools.ietf.org/html/rfc6749#section-3.3 but
+ # there is currently no mechanism to coordinate issuing a token for only a subset of the requested scopes so
+ # all tokens issued are for the entire set of requested scopes.
+ if request.scopes is not None:
+ token['scope'] = ' '.join(request.scopes)
+
+ if refresh_token:
+ if (request.refresh_token and
+ not self.request_validator.rotate_refresh_token(request)):
+ token['refresh_token'] = request.refresh_token
+ else:
+ token['refresh_token'] = self.refresh_token_generator(request)
+
+ token.update(request.extra_credentials or {})
+ return OAuth2Token(token)
+
+ def validate_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ token = get_token_from_header(request)
+ return self.request_validator.validate_bearer_token(
+ token, request.scopes, request)
+
+ def estimate_type(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ if request.headers.get('Authorization', '').split(' ')[0].lower() == 'bearer':
+ return 9
+ elif request.access_token is not None:
+ return 5
+ else:
+ return 0
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/utils.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/utils.py
new file mode 100644
index 0000000000..7dc27b3dff
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc6749/utils.py
@@ -0,0 +1,83 @@
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth 2 spec.
+"""
+import datetime
+import os
+from urllib.parse import quote, urlparse
+
+from oauthlib.common import urldecode
+
+
+def list_to_scope(scope):
+ """Convert a list of scopes to a space separated string."""
+ if isinstance(scope, str) or scope is None:
+ return scope
+ elif isinstance(scope, (set, tuple, list)):
+ return " ".join([str(s) for s in scope])
+ else:
+ raise ValueError("Invalid scope (%s), must be string, tuple, set, or list." % scope)
+
+
+def scope_to_list(scope):
+ """Convert a space separated string to a list of scopes."""
+ if isinstance(scope, (tuple, list, set)):
+ return [str(s) for s in scope]
+ elif scope is None:
+ return None
+ else:
+ return scope.strip().split(" ")
+
+
+def params_from_uri(uri):
+ params = dict(urldecode(urlparse(uri).query))
+ if 'scope' in params:
+ params['scope'] = scope_to_list(params['scope'])
+ return params
+
+
+def host_from_uri(uri):
+ """Extract hostname and port from URI.
+
+ Will use default port for HTTP and HTTPS if none is present in the URI.
+ """
+ default_ports = {
+ 'HTTP': '80',
+ 'HTTPS': '443',
+ }
+
+ sch, netloc, path, par, query, fra = urlparse(uri)
+ if ':' in netloc:
+ netloc, port = netloc.split(':', 1)
+ else:
+ port = default_ports.get(sch.upper())
+
+ return netloc, port
+
+
+def escape(u):
+ """Escape a string in an OAuth-compatible fashion.
+
+ TODO: verify whether this can in fact be used for OAuth 2
+
+ """
+ if not isinstance(u, str):
+ raise ValueError('Only unicode objects are escapable.')
+ return quote(u.encode('utf-8'), safe=b'~')
+
+
+def generate_age(issue_time):
+ """Generate a age parameter for MAC authentication draft 00."""
+ td = datetime.datetime.now() - issue_time
+ age = (td.microseconds + (td.seconds + td.days * 24 * 3600)
+ * 10 ** 6) / 10 ** 6
+ return str(age)
+
+
+def is_secure_transport(uri):
+ """Check if the uri is over ssl."""
+ if os.environ.get('OAUTHLIB_INSECURE_TRANSPORT'):
+ return True
+ return uri.lower().startswith('https://')
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/__init__.py
new file mode 100644
index 0000000000..531929dcc7
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/__init__.py
@@ -0,0 +1,10 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 Device Authorization RFC8628.
+"""
+import logging
+
+log = logging.getLogger(__name__)
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/__init__.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/__init__.py
new file mode 100644
index 0000000000..130b52e381
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/__init__.py
@@ -0,0 +1,8 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 Device Authorization RFC8628.
+"""
+from .device import DeviceClient
diff --git a/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/device.py b/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/device.py
new file mode 100644
index 0000000000..b9ba2150a2
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/oauth2/rfc8628/clients/device.py
@@ -0,0 +1,95 @@
+"""
+oauthlib.oauth2.rfc8628
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 Device Authorization RFC8628.
+"""
+from oauthlib.common import add_params_to_uri
+from oauthlib.oauth2 import BackendApplicationClient, Client
+from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
+from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope
+
+
+class DeviceClient(Client):
+
+ """A public client utilizing the device authorization workflow.
+
+ The client can request an access token using a device code and
+ a public client id associated with the device code as defined
+ in RFC8628.
+
+ The device authorization grant type can be used to obtain both
+ access tokens and refresh tokens and is intended to be used in
+ a scenario where the device being authorized does not have a
+ user interface that is suitable for performing authentication.
+ """
+
+ grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
+
+ def __init__(self, client_id, **kwargs):
+ super().__init__(client_id, **kwargs)
+ self.client_secret = kwargs.get('client_secret')
+
+ def prepare_request_uri(self, uri, scope=None, **kwargs):
+ if not is_secure_transport(uri):
+ raise InsecureTransportError()
+
+ scope = self.scope if scope is None else scope
+ params = [(('client_id', self.client_id)), (('grant_type', self.grant_type))]
+
+ if self.client_secret is not None:
+ params.append(('client_secret', self.client_secret))
+
+ if scope:
+ params.append(('scope', list_to_scope(scope)))
+
+ for k in kwargs:
+ if kwargs[k]:
+ params.append((str(k), kwargs[k]))
+
+ return add_params_to_uri(uri, params)
+
+ def prepare_request_body(self, device_code, body='', scope=None,
+ include_client_id=False, **kwargs):
+ """Add device_code to request body
+
+ The client makes a request to the token endpoint by adding the
+ device_code as a parameter using the
+ "application/x-www-form-urlencoded" format to the HTTP request
+ body.
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra parameters. Default ''.
+ :param scope: The scope of the access request as described by
+ `Section 3.3`_.
+
+ :param include_client_id: `True` to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_. False otherwise (default).
+ :type include_client_id: Boolean
+
+ :param kwargs: Extra credentials to include in the token request.
+
+ The prepared body will include all provided device_code as well as
+ the ``grant_type`` parameter set to
+ ``urn:ietf:params:oauth:grant-type:device_code``::
+
+ >>> from oauthlib.oauth2 import DeviceClient
+ >>> client = DeviceClient('your_id', 'your_code')
+ >>> client.prepare_request_body(scope=['hello', 'world'])
+ 'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world'
+
+ .. _`Section 3.2.1`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1
+ .. _`Section 3.3`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
+ .. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4
+ """
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ scope = self.scope if scope is None else scope
+ return prepare_token_request(self.grant_type, body=body, device_code=device_code,
+ scope=scope, **kwargs)
diff --git a/contrib/python/oauthlib/oauthlib/openid/__init__.py b/contrib/python/oauthlib/oauthlib/openid/__init__.py
new file mode 100644
index 0000000000..e317437479
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/__init__.py
@@ -0,0 +1,7 @@
+"""
+oauthlib.openid
+~~~~~~~~~~~~~~
+
+"""
+from .connect.core.endpoints import Server, UserInfoEndpoint
+from .connect.core.request_validator import RequestValidator
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/__init__.py b/contrib/python/oauthlib/oauthlib/openid/connect/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/__init__.py
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/__init__.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/__init__.py
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/__init__.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/__init__.py
new file mode 100644
index 0000000000..7017ff4f32
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/__init__.py
@@ -0,0 +1,9 @@
+"""
+oauthlib.oopenid.core
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OpenID Connect
+"""
+from .pre_configured import Server
+from .userinfo import UserInfoEndpoint
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/pre_configured.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/pre_configured.py
new file mode 100644
index 0000000000..8ce8bee67b
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/pre_configured.py
@@ -0,0 +1,97 @@
+"""
+oauthlib.openid.connect.core.endpoints.pre_configured
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various endpoints needed
+for providing OpenID Connect servers.
+"""
+from oauthlib.oauth2.rfc6749.endpoints import (
+ AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint,
+ RevocationEndpoint, TokenEndpoint,
+)
+from oauthlib.oauth2.rfc6749.grant_types import (
+ AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
+ ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant,
+ RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant,
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from ..grant_types import AuthorizationCodeGrant, HybridGrant, ImplicitGrant
+from ..grant_types.dispatchers import (
+ AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher,
+ ImplicitTokenGrantDispatcher,
+)
+from ..tokens import JWTToken
+from .userinfo import UserInfoEndpoint
+
+
+class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
+ ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint):
+
+ """An all-in-one endpoint featuring all four major grant types."""
+
+ def __init__(self, request_validator, token_expires_in=None,
+ token_generator=None, refresh_token_generator=None,
+ *args, **kwargs):
+ """Construct a new all-grants-in-one server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator)
+ self.implicit_grant = OAuth2ImplicitGrant(request_validator)
+ self.password_grant = ResourceOwnerPasswordCredentialsGrant(
+ request_validator)
+ self.credentials_grant = ClientCredentialsGrant(request_validator)
+ self.refresh_grant = RefreshTokenGrant(request_validator)
+ self.openid_connect_auth = AuthorizationCodeGrant(request_validator)
+ self.openid_connect_implicit = ImplicitGrant(request_validator)
+ self.openid_connect_hybrid = HybridGrant(request_validator)
+
+ self.bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+
+ self.jwt = JWTToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+
+ self.auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth)
+ self.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit)
+
+ # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations
+ # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination
+ AuthorizationEndpoint.__init__(self, default_response_type='code',
+ response_types={
+ 'code': self.auth_grant_choice,
+ 'token': self.implicit_grant_choice,
+ 'id_token': self.openid_connect_implicit,
+ 'id_token token': self.openid_connect_implicit,
+ 'code token': self.openid_connect_hybrid,
+ 'code id_token': self.openid_connect_hybrid,
+ 'code id_token token': self.openid_connect_hybrid,
+ 'none': self.auth_grant
+ },
+ default_token_type=self.bearer)
+
+ self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth)
+
+ TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+ grant_types={
+ 'authorization_code': self.token_grant_choice,
+ 'password': self.password_grant,
+ 'client_credentials': self.credentials_grant,
+ 'refresh_token': self.refresh_grant,
+ },
+ default_token_type=self.bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': self.bearer, 'JWT': self.jwt})
+ RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
+ UserInfoEndpoint.__init__(self, request_validator)
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/userinfo.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/userinfo.py
new file mode 100644
index 0000000000..7aa2bbe97d
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/endpoints/userinfo.py
@@ -0,0 +1,106 @@
+"""
+oauthlib.openid.connect.core.endpoints.userinfo
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of userinfo endpoint.
+"""
+import json
+import logging
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.endpoints.base import (
+ BaseEndpoint, catch_errors_and_unavailability,
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+log = logging.getLogger(__name__)
+
+
+class UserInfoEndpoint(BaseEndpoint):
+ """Authorizes access to userinfo resource.
+ """
+ def __init__(self, request_validator):
+ self.bearer = BearerToken(request_validator, None, None, None)
+ self.request_validator = request_validator
+ BaseEndpoint.__init__(self)
+
+ @catch_errors_and_unavailability
+ def create_userinfo_response(self, uri, http_method='GET', body=None, headers=None):
+ """Validate BearerToken and return userinfo from RequestValidator
+
+ The UserInfo Endpoint MUST return a
+ content-type header to indicate which format is being returned. The
+ content-type of the HTTP response MUST be application/json if the
+ response body is a text JSON object; the response body SHOULD be encoded
+ using UTF-8.
+ """
+ request = Request(uri, http_method, body, headers)
+ request.scopes = ["openid"]
+ self.validate_userinfo_request(request)
+
+ claims = self.request_validator.get_userinfo_claims(request)
+ if claims is None:
+ log.error('Userinfo MUST have claims for %r.', request)
+ raise errors.ServerError(status_code=500)
+
+ if isinstance(claims, dict):
+ resp_headers = {
+ 'Content-Type': 'application/json'
+ }
+ if "sub" not in claims:
+ log.error('Userinfo MUST have "sub" for %r.', request)
+ raise errors.ServerError(status_code=500)
+ body = json.dumps(claims)
+ elif isinstance(claims, str):
+ resp_headers = {
+ 'Content-Type': 'application/jwt'
+ }
+ body = claims
+ else:
+ log.error('Userinfo return unknown response for %r.', request)
+ raise errors.ServerError(status_code=500)
+ log.debug('Userinfo access valid for %r.', request)
+ return resp_headers, body, 200
+
+ def validate_userinfo_request(self, request):
+ """Ensure the request is valid.
+
+ 5.3.1. UserInfo Request
+ The Client sends the UserInfo Request using either HTTP GET or HTTP
+ POST. The Access Token obtained from an OpenID Connect Authentication
+ Request MUST be sent as a Bearer Token, per `Section 2`_ of OAuth 2.0
+ Bearer Token Usage [RFC6750].
+
+ It is RECOMMENDED that the request use the HTTP GET method and the
+ Access Token be sent using the Authorization header field.
+
+ The following is a non-normative example of a UserInfo Request:
+
+ .. code-block:: http
+
+ GET /userinfo HTTP/1.1
+ Host: server.example.com
+ Authorization: Bearer SlAV32hkKG
+
+ 5.3.3. UserInfo Error Response
+ When an error condition occurs, the UserInfo Endpoint returns an Error
+ Response as defined in `Section 3`_ of OAuth 2.0 Bearer Token Usage
+ [RFC6750]. (HTTP errors unrelated to RFC 6750 are returned to the User
+ Agent using the appropriate HTTP status code.)
+
+ The following is a non-normative example of a UserInfo Error Response:
+
+ .. code-block:: http
+
+ HTTP/1.1 401 Unauthorized
+ WWW-Authenticate: Bearer error="invalid_token",
+ error_description="The Access Token expired"
+
+ .. _`Section 2`: https://datatracker.ietf.org/doc/html/rfc6750#section-2
+ .. _`Section 3`: https://datatracker.ietf.org/doc/html/rfc6750#section-3
+ """
+ if not self.bearer.validate_request(request):
+ raise errors.InvalidTokenError()
+ if "openid" not in request.scopes:
+ raise errors.InsufficientScopeError()
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/exceptions.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/exceptions.py
new file mode 100644
index 0000000000..099b84e2da
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/exceptions.py
@@ -0,0 +1,149 @@
+"""
+oauthlib.oauth2.rfc6749.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 2 clients and providers to represent the spec
+defined error responses for all four core grant types.
+"""
+from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error
+
+
+class FatalOpenIDClientError(FatalClientError):
+ pass
+
+
+class OpenIDClientError(OAuth2Error):
+ pass
+
+
+class InteractionRequired(OpenIDClientError):
+ """
+ The Authorization Server requires End-User interaction to proceed.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User interaction.
+ """
+ error = 'interaction_required'
+ status_code = 401
+
+
+class LoginRequired(OpenIDClientError):
+ """
+ The Authorization Server requires End-User authentication.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User authentication.
+ """
+ error = 'login_required'
+ status_code = 401
+
+
+class AccountSelectionRequired(OpenIDClientError):
+ """
+ The End-User is REQUIRED to select a session at the Authorization Server.
+
+ The End-User MAY be authenticated at the Authorization Server with
+ different associated accounts, but the End-User did not select a session.
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface to prompt for a session to
+ use.
+ """
+ error = 'account_selection_required'
+
+
+class ConsentRequired(OpenIDClientError):
+ """
+ The Authorization Server requires End-User consent.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User consent.
+ """
+ error = 'consent_required'
+ status_code = 401
+
+
+class InvalidRequestURI(OpenIDClientError):
+ """
+ The request_uri in the Authorization Request returns an error or
+ contains invalid data.
+ """
+ error = 'invalid_request_uri'
+ description = 'The request_uri in the Authorization Request returns an ' \
+ 'error or contains invalid data.'
+
+
+class InvalidRequestObject(OpenIDClientError):
+ """
+ The request parameter contains an invalid Request Object.
+ """
+ error = 'invalid_request_object'
+ description = 'The request parameter contains an invalid Request Object.'
+
+
+class RequestNotSupported(OpenIDClientError):
+ """
+ The OP does not support use of the request parameter.
+ """
+ error = 'request_not_supported'
+ description = 'The request parameter is not supported.'
+
+
+class RequestURINotSupported(OpenIDClientError):
+ """
+ The OP does not support use of the request_uri parameter.
+ """
+ error = 'request_uri_not_supported'
+ description = 'The request_uri parameter is not supported.'
+
+
+class RegistrationNotSupported(OpenIDClientError):
+ """
+ The OP does not support use of the registration parameter.
+ """
+ error = 'registration_not_supported'
+ description = 'The registration parameter is not supported.'
+
+
+class InvalidTokenError(OAuth2Error):
+ """
+ The access token provided is expired, revoked, malformed, or
+ invalid for other reasons. The resource SHOULD respond with
+ the HTTP 401 (Unauthorized) status code. The client MAY
+ request a new access token and retry the protected resource
+ request.
+ """
+ error = 'invalid_token'
+ status_code = 401
+ description = ("The access token provided is expired, revoked, malformed, "
+ "or invalid for other reasons.")
+
+
+class InsufficientScopeError(OAuth2Error):
+ """
+ The request requires higher privileges than provided by the
+ access token. The resource server SHOULD respond with the HTTP
+ 403 (Forbidden) status code and MAY include the "scope"
+ attribute with the scope necessary to access the protected
+ resource.
+ """
+ error = 'insufficient_scope'
+ status_code = 403
+ description = ("The request requires higher privileges than provided by "
+ "the access token.")
+
+
+def raise_from_error(error, params=None):
+ import inspect
+ import sys
+ kwargs = {
+ 'description': params.get('error_description'),
+ 'uri': params.get('error_uri'),
+ 'state': params.get('state')
+ }
+ for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
+ if cls.error == error:
+ raise cls(**kwargs)
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/__init__.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/__init__.py
new file mode 100644
index 0000000000..8dad5f607b
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/__init__.py
@@ -0,0 +1,13 @@
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from .authorization_code import AuthorizationCodeGrant
+from .base import GrantTypeBase
+from .dispatchers import (
+ AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher,
+ ImplicitTokenGrantDispatcher,
+)
+from .hybrid import HybridGrant
+from .implicit import ImplicitGrant
+from .refresh_token import RefreshTokenGrant
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/authorization_code.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/authorization_code.py
new file mode 100644
index 0000000000..6b2dcc3bdd
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/authorization_code.py
@@ -0,0 +1,43 @@
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+from oauthlib.oauth2.rfc6749.grant_types.authorization_code import (
+ AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
+)
+
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationCodeGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.proxy_target = OAuth2AuthorizationCodeGrant(
+ request_validator=request_validator, **kwargs)
+ self.custom_validators.post_auth.append(
+ self.openid_authorization_validator)
+ self.register_token_modifier(self.add_id_token)
+
+ def add_id_token(self, token, token_handler, request):
+ """
+ Construct an initial version of id_token, and let the
+ request_validator sign or encrypt it.
+
+ The authorization_code version of this method is used to
+ retrieve the nonce accordingly to the code storage.
+ """
+ # Treat it as normal OAuth 2 auth code request if openid is not present
+ if not request.scopes or 'openid' not in request.scopes:
+ return token
+
+ nonce = self.request_validator.get_authorization_code_nonce(
+ request.client_id,
+ request.code,
+ request.redirect_uri,
+ request
+ )
+ return super().add_id_token(token, token_handler, request, nonce=nonce)
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/base.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/base.py
new file mode 100644
index 0000000000..33411dad75
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/base.py
@@ -0,0 +1,326 @@
+import base64
+import hashlib
+import logging
+import time
+from json import loads
+
+from oauthlib.oauth2.rfc6749.errors import (
+ ConsentRequired, InvalidRequestError, LoginRequired,
+)
+
+log = logging.getLogger(__name__)
+
+
+class GrantTypeBase:
+
+ # Just proxy the majority of method calls through to the
+ # proxy_target grant type handler, which will usually be either
+ # the standard OAuth2 AuthCode or Implicit grant types.
+ def __getattr__(self, attr):
+ return getattr(self.proxy_target, attr)
+
+ def __setattr__(self, attr, value):
+ proxied_attrs = {'refresh_token', 'response_types'}
+ if attr in proxied_attrs:
+ setattr(self.proxy_target, attr, value)
+ else:
+ super(OpenIDConnectBase, self).__setattr__(attr, value)
+
+ def validate_authorization_request(self, request):
+ """Validates the OpenID Connect authorization request parameters.
+
+ :returns: (list of scopes, dict of request info)
+ """
+ return self.proxy_target.validate_authorization_request(request)
+
+ def _inflate_claims(self, request):
+ # this may be called multiple times in a single request so make sure we only de-serialize the claims once
+ if request.claims and not isinstance(request.claims, dict):
+ # specific claims are requested during the Authorization Request and may be requested for inclusion
+ # in either the id_token or the UserInfo endpoint response
+ # see http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
+ try:
+ request.claims = loads(request.claims)
+ except Exception as ex:
+ raise InvalidRequestError(description="Malformed claims parameter",
+ uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter")
+
+ def id_token_hash(self, value, hashfunc=hashlib.sha256):
+ """
+ Its value is the base64url encoding of the left-most half of the
+ hash of the octets of the ASCII representation of the access_token
+ value, where the hash algorithm used is the hash algorithm used in
+ the alg Header Parameter of the ID Token's JOSE Header.
+
+ For instance, if the alg is RS256, hash the access_token value
+ with SHA-256, then take the left-most 128 bits and
+ base64url-encode them.
+ For instance, if the alg is HS512, hash the code value with
+ SHA-512, then take the left-most 256 bits and base64url-encode
+ them. The c_hash value is a case-sensitive string.
+
+ Example of hash from OIDC specification (bound to a JWS using RS256):
+
+ code:
+ Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk
+
+ c_hash:
+ LDktKdoQak3Pk0cnXxCltA
+ """
+ digest = hashfunc(value.encode()).digest()
+ left_most = len(digest) // 2
+ return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=")
+
+ def add_id_token(self, token, token_handler, request, nonce=None):
+ """
+ Construct an initial version of id_token, and let the
+ request_validator sign or encrypt it.
+
+ The initial version can contain the fields below, accordingly
+ to the spec:
+ - aud
+ - iat
+ - nonce
+ - at_hash
+ - c_hash
+ """
+ # Treat it as normal OAuth 2 auth code request if openid is not present
+ if not request.scopes or 'openid' not in request.scopes:
+ return token
+
+ # Only add an id token on auth/token step if asked for.
+ if request.response_type and 'id_token' not in request.response_type:
+ return token
+
+ # Implementation mint its own id_token without help.
+ id_token = self.request_validator.get_id_token(token, token_handler, request)
+ if id_token:
+ token['id_token'] = id_token
+ return token
+
+ # Fallback for asking some help from oauthlib framework.
+ # Start with technicals fields bound to the specification.
+ id_token = {}
+ id_token['aud'] = request.client_id
+ id_token['iat'] = int(time.time())
+
+ # nonce is REQUIRED when response_type value is:
+ # - id_token token (Implicit)
+ # - id_token (Implicit)
+ # - code id_token (Hybrid)
+ # - code id_token token (Hybrid)
+ #
+ # nonce is OPTIONAL when response_type value is:
+ # - code (Authorization Code)
+ # - code token (Hybrid)
+ if nonce is not None:
+ id_token["nonce"] = nonce
+
+ # at_hash is REQUIRED when response_type value is:
+ # - id_token token (Implicit)
+ # - code id_token token (Hybrid)
+ #
+ # at_hash is OPTIONAL when:
+ # - code (Authorization code)
+ # - code id_token (Hybrid)
+ # - code token (Hybrid)
+ #
+ # at_hash MAY NOT be used when:
+ # - id_token (Implicit)
+ if "access_token" in token:
+ id_token["at_hash"] = self.id_token_hash(token["access_token"])
+
+ # c_hash is REQUIRED when response_type value is:
+ # - code id_token (Hybrid)
+ # - code id_token token (Hybrid)
+ #
+ # c_hash is OPTIONAL for others.
+ if "code" in token:
+ id_token["c_hash"] = self.id_token_hash(token["code"])
+
+ # Call request_validator to complete/sign/encrypt id_token
+ token['id_token'] = self.request_validator.finalize_id_token(id_token, token, token_handler, request)
+
+ return token
+
+ def openid_authorization_validator(self, request):
+ """Perform OpenID Connect specific authorization request validation.
+
+ nonce
+ OPTIONAL. String value used to associate a Client session with
+ an ID Token, and to mitigate replay attacks. The value is
+ passed through unmodified from the Authentication Request to
+ the ID Token. Sufficient entropy MUST be present in the nonce
+ values used to prevent attackers from guessing values
+
+ display
+ OPTIONAL. ASCII string value that specifies how the
+ Authorization Server displays the authentication and consent
+ user interface pages to the End-User. The defined values are:
+
+ page - The Authorization Server SHOULD display the
+ authentication and consent UI consistent with a full User
+ Agent page view. If the display parameter is not specified,
+ this is the default display mode.
+
+ popup - The Authorization Server SHOULD display the
+ authentication and consent UI consistent with a popup User
+ Agent window. The popup User Agent window should be of an
+ appropriate size for a login-focused dialog and should not
+ obscure the entire window that it is popping up over.
+
+ touch - The Authorization Server SHOULD display the
+ authentication and consent UI consistent with a device that
+ leverages a touch interface.
+
+ wap - The Authorization Server SHOULD display the
+ authentication and consent UI consistent with a "feature
+ phone" type display.
+
+ The Authorization Server MAY also attempt to detect the
+ capabilities of the User Agent and present an appropriate
+ display.
+
+ prompt
+ OPTIONAL. Space delimited, case sensitive list of ASCII string
+ values that specifies whether the Authorization Server prompts
+ the End-User for reauthentication and consent. The defined
+ values are:
+
+ none - The Authorization Server MUST NOT display any
+ authentication or consent user interface pages. An error is
+ returned if an End-User is not already authenticated or the
+ Client does not have pre-configured consent for the
+ requested Claims or does not fulfill other conditions for
+ processing the request. The error code will typically be
+ login_required, interaction_required, or another code
+ defined in Section 3.1.2.6. This can be used as a method to
+ check for existing authentication and/or consent.
+
+ login - The Authorization Server SHOULD prompt the End-User
+ for reauthentication. If it cannot reauthenticate the
+ End-User, it MUST return an error, typically
+ login_required.
+
+ consent - The Authorization Server SHOULD prompt the
+ End-User for consent before returning information to the
+ Client. If it cannot obtain consent, it MUST return an
+ error, typically consent_required.
+
+ select_account - The Authorization Server SHOULD prompt the
+ End-User to select a user account. This enables an End-User
+ who has multiple accounts at the Authorization Server to
+ select amongst the multiple accounts that they might have
+ current sessions for. If it cannot obtain an account
+ selection choice made by the End-User, it MUST return an
+ error, typically account_selection_required.
+
+ The prompt parameter can be used by the Client to make sure
+ that the End-User is still present for the current session or
+ to bring attention to the request. If this parameter contains
+ none with any other value, an error is returned.
+
+ max_age
+ OPTIONAL. Maximum Authentication Age. Specifies the allowable
+ elapsed time in seconds since the last time the End-User was
+ actively authenticated by the OP. If the elapsed time is
+ greater than this value, the OP MUST attempt to actively
+ re-authenticate the End-User. (The max_age request parameter
+ corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] max_auth_age
+ request parameter.) When max_age is used, the ID Token returned
+ MUST include an auth_time Claim Value.
+
+ ui_locales
+ OPTIONAL. End-User's preferred languages and scripts for the
+ user interface, represented as a space-separated list of BCP47
+ [RFC5646] language tag values, ordered by preference. For
+ instance, the value "fr-CA fr en" represents a preference for
+ French as spoken in Canada, then French (without a region
+ designation), followed by English (without a region
+ designation). An error SHOULD NOT result if some or all of the
+ requested locales are not supported by the OpenID Provider.
+
+ id_token_hint
+ OPTIONAL. ID Token previously issued by the Authorization
+ Server being passed as a hint about the End-User's current or
+ past authenticated session with the Client. If the End-User
+ identified by the ID Token is logged in or is logged in by the
+ request, then the Authorization Server returns a positive
+ response; otherwise, it SHOULD return an error, such as
+ login_required. When possible, an id_token_hint SHOULD be
+ present when prompt=none is used and an invalid_request error
+ MAY be returned if it is not; however, the server SHOULD
+ respond successfully when possible, even if it is not present.
+ The Authorization Server need not be listed as an audience of
+ the ID Token when it is used as an id_token_hint value. If the
+ ID Token received by the RP from the OP is encrypted, to use it
+ as an id_token_hint, the Client MUST decrypt the signed ID
+ Token contained within the encrypted ID Token. The Client MAY
+ re-encrypt the signed ID token to the Authentication Server
+ using a key that enables the server to decrypt the ID Token,
+ and use the re-encrypted ID token as the id_token_hint value.
+
+ login_hint
+ OPTIONAL. Hint to the Authorization Server about the login
+ identifier the End-User might use to log in (if necessary).
+ This hint can be used by an RP if it first asks the End-User
+ for their e-mail address (or other identifier) and then wants
+ to pass that value as a hint to the discovered authorization
+ service. It is RECOMMENDED that the hint value match the value
+ used for discovery. This value MAY also be a phone number in
+ the format specified for the phone_number Claim. The use of
+ this parameter is left to the OP's discretion.
+
+ acr_values
+ OPTIONAL. Requested Authentication Context Class Reference
+ values. Space-separated string that specifies the acr values
+ that the Authorization Server is being requested to use for
+ processing this Authentication Request, with the values
+ appearing in order of preference. The Authentication Context
+ Class satisfied by the authentication performed is returned as
+ the acr Claim Value, as specified in Section 2. The acr Claim
+ is requested as a Voluntary Claim by this parameter.
+ """
+
+ # Treat it as normal OAuth 2 auth code request if openid is not present
+ if not request.scopes or 'openid' not in request.scopes:
+ return {}
+
+ prompt = request.prompt if request.prompt else []
+ if hasattr(prompt, 'split'):
+ prompt = prompt.strip().split()
+ prompt = set(prompt)
+
+ if 'none' in prompt:
+
+ if len(prompt) > 1:
+ msg = "Prompt none is mutually exclusive with other values."
+ raise InvalidRequestError(request=request, description=msg)
+
+ if not self.request_validator.validate_silent_login(request):
+ raise LoginRequired(request=request)
+
+ if not self.request_validator.validate_silent_authorization(request):
+ raise ConsentRequired(request=request)
+
+ self._inflate_claims(request)
+
+ if not self.request_validator.validate_user_match(
+ request.id_token_hint, request.scopes, request.claims, request):
+ msg = "Session user does not match client supplied user."
+ raise LoginRequired(request=request, description=msg)
+
+ request_info = {
+ 'display': request.display,
+ 'nonce': request.nonce,
+ 'prompt': prompt,
+ 'ui_locales': request.ui_locales.split() if request.ui_locales else [],
+ 'id_token_hint': request.id_token_hint,
+ 'login_hint': request.login_hint,
+ 'claims': request.claims
+ }
+
+ return request_info
+
+
+OpenIDConnectBase = GrantTypeBase
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/dispatchers.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/dispatchers.py
new file mode 100644
index 0000000000..5aa7d4698b
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/dispatchers.py
@@ -0,0 +1,101 @@
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class Dispatcher:
+ default_grant = None
+ oidc_grant = None
+
+
+class AuthorizationCodeGrantDispatcher(Dispatcher):
+ """
+ This is an adapter class that will route simple Authorization Code
+ requests, those that have `response_type=code` and a scope including
+ `openid` to either the `default_grant` or the `oidc_grant` based on
+ the scopes requested.
+ """
+ def __init__(self, default_grant=None, oidc_grant=None):
+ self.default_grant = default_grant
+ self.oidc_grant = oidc_grant
+
+ def _handler_for_request(self, request):
+ handler = self.default_grant
+
+ if request.scopes and "openid" in request.scopes:
+ handler = self.oidc_grant
+
+ log.debug('Selecting handler for request %r.', handler)
+ return handler
+
+ def create_authorization_response(self, request, token_handler):
+ """Read scope and route to the designated handler."""
+ return self._handler_for_request(request).create_authorization_response(request, token_handler)
+
+ def validate_authorization_request(self, request):
+ """Read scope and route to the designated handler."""
+ return self._handler_for_request(request).validate_authorization_request(request)
+
+
+class ImplicitTokenGrantDispatcher(Dispatcher):
+ """
+ This is an adapter class that will route simple Authorization
+ requests, those that have `id_token` in `response_type` and a scope
+ including `openid` to either the `default_grant` or the `oidc_grant`
+ based on the scopes requested.
+ """
+ def __init__(self, default_grant=None, oidc_grant=None):
+ self.default_grant = default_grant
+ self.oidc_grant = oidc_grant
+
+ def _handler_for_request(self, request):
+ handler = self.default_grant
+
+ if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type:
+ handler = self.oidc_grant
+
+ log.debug('Selecting handler for request %r.', handler)
+ return handler
+
+ def create_authorization_response(self, request, token_handler):
+ """Read scope and route to the designated handler."""
+ return self._handler_for_request(request).create_authorization_response(request, token_handler)
+
+ def validate_authorization_request(self, request):
+ """Read scope and route to the designated handler."""
+ return self._handler_for_request(request).validate_authorization_request(request)
+
+
+class AuthorizationTokenGrantDispatcher(Dispatcher):
+ """
+ This is an adapter class that will route simple Token requests, those that authorization_code have a scope
+ including 'openid' to either the default_grant or the oidc_grant based on the scopes requested.
+ """
+ def __init__(self, request_validator, default_grant=None, oidc_grant=None):
+ self.default_grant = default_grant
+ self.oidc_grant = oidc_grant
+ self.request_validator = request_validator
+
+ def _handler_for_request(self, request):
+ handler = self.default_grant
+ scopes = ()
+ parameters = dict(request.decoded_body)
+ client_id = parameters.get('client_id', None)
+ code = parameters.get('code', None)
+ redirect_uri = parameters.get('redirect_uri', None)
+
+ # If code is not present fallback to `default_grant` which will
+ # raise an error for the missing `code` in `create_token_response` step.
+ if code:
+ scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request)
+
+ if 'openid' in scopes:
+ handler = self.oidc_grant
+
+ log.debug('Selecting handler for request %r.', handler)
+ return handler
+
+ def create_token_response(self, request, token_handler):
+ """Read scope and route to the designated handler."""
+ handler = self._handler_for_request(request)
+ return handler.create_token_response(request, token_handler)
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/hybrid.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/hybrid.py
new file mode 100644
index 0000000000..7cb0758b81
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/hybrid.py
@@ -0,0 +1,63 @@
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+from oauthlib.oauth2.rfc6749.errors import InvalidRequestError
+from oauthlib.oauth2.rfc6749.grant_types.authorization_code import (
+ AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
+)
+
+from ..request_validator import RequestValidator
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class HybridGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.request_validator = request_validator or RequestValidator()
+
+ self.proxy_target = OAuth2AuthorizationCodeGrant(
+ request_validator=request_validator, **kwargs)
+ # All hybrid response types should be fragment-encoded.
+ self.proxy_target.default_response_mode = "fragment"
+ self.register_response_type('code id_token')
+ self.register_response_type('code token')
+ self.register_response_type('code id_token token')
+ self.custom_validators.post_auth.append(
+ self.openid_authorization_validator)
+ # Hybrid flows can return the id_token from the authorization
+ # endpoint as part of the 'code' response
+ self.register_code_modifier(self.add_token)
+ self.register_code_modifier(self.add_id_token)
+ self.register_token_modifier(self.add_id_token)
+
+ def add_id_token(self, token, token_handler, request):
+ return super().add_id_token(token, token_handler, request, nonce=request.nonce)
+
+ def openid_authorization_validator(self, request):
+ """Additional validation when following the Authorization Code flow.
+ """
+ request_info = super().openid_authorization_validator(request)
+ if not request_info: # returns immediately if OAuth2.0
+ return request_info
+
+ # REQUIRED if the Response Type of the request is `code
+ # id_token` or `code id_token token` and OPTIONAL when the
+ # Response Type of the request is `code token`. It is a string
+ # value used to associate a Client session with an ID Token,
+ # and to mitigate replay attacks. The value is passed through
+ # unmodified from the Authentication Request to the ID
+ # Token. Sufficient entropy MUST be present in the `nonce`
+ # values used to prevent attackers from guessing values. For
+ # implementation notes, see Section 15.5.2.
+ if request.response_type in ["code id_token", "code id_token token"]:
+ if not request.nonce:
+ raise InvalidRequestError(
+ request=request,
+ description='Request is missing mandatory nonce parameter.'
+ )
+ return request_info
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/implicit.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/implicit.py
new file mode 100644
index 0000000000..a4fe6049bc
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/implicit.py
@@ -0,0 +1,51 @@
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+from oauthlib.oauth2.rfc6749.errors import InvalidRequestError
+from oauthlib.oauth2.rfc6749.grant_types.implicit import (
+ ImplicitGrant as OAuth2ImplicitGrant,
+)
+
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class ImplicitGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.proxy_target = OAuth2ImplicitGrant(
+ request_validator=request_validator, **kwargs)
+ self.register_response_type('id_token')
+ self.register_response_type('id_token token')
+ self.custom_validators.post_auth.append(
+ self.openid_authorization_validator)
+ self.register_token_modifier(self.add_id_token)
+
+ def add_id_token(self, token, token_handler, request):
+ if 'state' not in token and request.state:
+ token['state'] = request.state
+ return super().add_id_token(token, token_handler, request, nonce=request.nonce)
+
+ def openid_authorization_validator(self, request):
+ """Additional validation when following the implicit flow.
+ """
+ request_info = super().openid_authorization_validator(request)
+ if not request_info: # returns immediately if OAuth2.0
+ return request_info
+
+ # REQUIRED. String value used to associate a Client session with an ID
+ # Token, and to mitigate replay attacks. The value is passed through
+ # unmodified from the Authentication Request to the ID Token.
+ # Sufficient entropy MUST be present in the nonce values used to
+ # prevent attackers from guessing values. For implementation notes, see
+ # Section 15.5.2.
+ if not request.nonce:
+ raise InvalidRequestError(
+ request=request,
+ description='Request is missing mandatory nonce parameter.'
+ )
+ return request_info
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/refresh_token.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/refresh_token.py
new file mode 100644
index 0000000000..43e4499c53
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/grant_types/refresh_token.py
@@ -0,0 +1,34 @@
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+from oauthlib.oauth2.rfc6749.grant_types.refresh_token import (
+ RefreshTokenGrant as OAuth2RefreshTokenGrant,
+)
+
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class RefreshTokenGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.proxy_target = OAuth2RefreshTokenGrant(
+ request_validator=request_validator, **kwargs)
+ self.register_token_modifier(self.add_id_token)
+
+ def add_id_token(self, token, token_handler, request):
+ """
+ Construct an initial version of id_token, and let the
+ request_validator sign or encrypt it.
+
+ The authorization_code version of this method is used to
+ retrieve the nonce accordingly to the code storage.
+ """
+ if not self.request_validator.refresh_id_token(request):
+ return token
+
+ return super().add_id_token(token, token_handler, request)
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/request_validator.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/request_validator.py
new file mode 100644
index 0000000000..47c4cd9406
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/request_validator.py
@@ -0,0 +1,320 @@
+"""
+oauthlib.openid.connect.core.request_validator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+import logging
+
+from oauthlib.oauth2.rfc6749.request_validator import (
+ RequestValidator as OAuth2RequestValidator,
+)
+
+log = logging.getLogger(__name__)
+
+
+class RequestValidator(OAuth2RequestValidator):
+
+ def get_authorization_code_scopes(self, client_id, code, redirect_uri, request):
+ """ Extracts scopes from saved authorization code.
+
+ The scopes returned by this method is used to route token requests
+ based on scopes passed to Authorization Code requests.
+
+ With that the token endpoint knows when to include OpenIDConnect
+ id_token in token response only based on authorization code scopes.
+
+ Only code param should be sufficient to retrieve grant code from
+ any storage you are using, `client_id` and `redirect_uri` can have a
+ blank value `""` don't forget to check it before using those values
+ in a select query if a database is used.
+
+ :param client_id: Unicode client identifier
+ :param code: Unicode authorization code grant
+ :param redirect_uri: Unicode absolute URI
+ :return: A list of scope
+
+ Method is used by:
+ - Authorization Token Grant Dispatcher
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_authorization_code_nonce(self, client_id, code, redirect_uri, request):
+ """ Extracts nonce from saved authorization code.
+
+ If present in the Authentication Request, Authorization
+ Servers MUST include a nonce Claim in the ID Token with the
+ Claim Value being the nonce value sent in the Authentication
+ Request. Authorization Servers SHOULD perform no other
+ processing on nonce values used. The nonce value is a
+ case-sensitive string.
+
+ Only code param should be sufficient to retrieve grant code from
+ any storage you are using. However, `client_id` and `redirect_uri`
+ have been validated and can be used also.
+
+ :param client_id: Unicode client identifier
+ :param code: Unicode authorization code grant
+ :param redirect_uri: Unicode absolute URI
+ :return: Unicode nonce
+
+ Method is used by:
+ - Authorization Token Grant Dispatcher
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_jwt_bearer_token(self, token, token_handler, request):
+ """Get JWT Bearer token or OpenID Connect ID token
+
+ If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
+
+ :param token: A Bearer token dict
+ :param token_handler: the token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT)
+
+ Method is used by JWT Bearer and OpenID Connect tokens:
+ - JWTToken.create_token
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_id_token(self, token, token_handler, request):
+ """Get OpenID Connect ID token
+
+ This method is OPTIONAL and is NOT RECOMMENDED.
+ `finalize_id_token` SHOULD be implemented instead. However, if you
+ want a full control over the minting of the `id_token`, you
+ MAY want to override `get_id_token` instead of using
+ `finalize_id_token`.
+
+ In the OpenID Connect workflows when an ID Token is requested this method is called.
+ Subclasses should implement the construction, signing and optional encryption of the
+ ID Token as described in the OpenID Connect spec.
+
+ In addition to the standard OAuth2 request properties, the request may also contain
+ these OIDC specific properties which are useful to this method:
+
+ - nonce, if workflow is implicit or hybrid and it was provided
+ - claims, if provided to the original Authorization Code request
+
+ The token parameter is a dict which may contain an ``access_token`` entry, in which
+ case the resulting ID Token *should* include a calculated ``at_hash`` claim.
+
+ Similarly, when the request parameter has a ``code`` property defined, the ID Token
+ *should* include a calculated ``c_hash`` claim.
+
+ http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_)
+
+ .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
+ .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
+ .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
+
+ :param token: A Bearer token dict
+ :param token_handler: the token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The ID Token (a JWS signed JWT)
+ """
+ return None
+
+ def finalize_id_token(self, id_token, token, token_handler, request):
+ """Finalize OpenID Connect ID token & Sign or Encrypt.
+
+ In the OpenID Connect workflows when an ID Token is requested
+ this method is called. Subclasses should implement the
+ construction, signing and optional encryption of the ID Token
+ as described in the OpenID Connect spec.
+
+ The `id_token` parameter is a dict containing a couple of OIDC
+ technical fields related to the specification. Prepopulated
+ attributes are:
+
+ - `aud`, equals to `request.client_id`.
+ - `iat`, equals to current time.
+ - `nonce`, if present, is equals to the `nonce` from the
+ authorization request.
+ - `at_hash`, hash of `access_token`, if relevant.
+ - `c_hash`, hash of `code`, if relevant.
+
+ This method MUST provide required fields as below:
+
+ - `iss`, REQUIRED. Issuer Identifier for the Issuer of the response.
+ - `sub`, REQUIRED. Subject Identifier
+ - `exp`, REQUIRED. Expiration time on or after which the ID
+ Token MUST NOT be accepted by the RP when performing
+ authentication with the OP.
+
+ Additionals claims must be added, note that `request.scope`
+ should be used to determine the list of claims.
+
+ More information can be found at `OpenID Connect Core#Claims`_
+
+ .. _`OpenID Connect Core#Claims`: https://openid.net/specs/openid-connect-core-1_0.html#Claims
+
+ :param id_token: A dict containing technical fields of id_token
+ :param token: A Bearer token dict
+ :param token_handler: the token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The ID Token (a JWS signed JWT or JWE encrypted JWT)
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_jwt_bearer_token(self, token, scopes, request):
+ """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes.
+
+ If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
+
+ If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response.
+
+ OpenID connect core 1.0 describe how to validate an id_token:
+ - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
+
+ :param token: Unicode Bearer token
+ :param scopes: List of scopes (defined by you)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core OpenID connect JWT token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Hybrid Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_id_token(self, token, scopes, request):
+ """Ensure the id token is valid and authorized access to scopes.
+
+ OpenID connect core 1.0 describe how to validate an id_token:
+ - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
+
+ :param token: Unicode Bearer token
+ :param scopes: List of scopes (defined by you)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core OpenID connect JWT token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Hybrid Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_silent_authorization(self, request):
+ """Ensure the logged in user has authorized silent OpenID authorization.
+
+ Silent OpenID authorization allows access tokens and id tokens to be
+ granted to clients without any user prompt or interaction.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - OpenIDConnectAuthCode
+ - OpenIDConnectImplicit
+ - OpenIDConnectHybrid
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_silent_login(self, request):
+ """Ensure session user has authorized silent OpenID login.
+
+ If no user is logged in or has not authorized silent login, this
+ method should return False.
+
+ If the user is logged in but associated with multiple accounts and
+ not selected which one to link to the token then this method should
+ raise an oauthlib.oauth2.AccountSelectionRequired error.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - OpenIDConnectAuthCode
+ - OpenIDConnectImplicit
+ - OpenIDConnectHybrid
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_user_match(self, id_token_hint, scopes, claims, request):
+ """Ensure client supplied user id hint matches session user.
+
+ If the sub claim or id_token_hint is supplied then the session
+ user must match the given ID.
+
+ :param id_token_hint: User identifier string.
+ :param scopes: List of OAuth 2 scopes and OpenID claims (strings).
+ :param claims: OpenID Connect claims dict.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - OpenIDConnectAuthCode
+ - OpenIDConnectImplicit
+ - OpenIDConnectHybrid
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_userinfo_claims(self, request):
+ """Return the UserInfo claims in JSON or Signed or Encrypted.
+
+ The UserInfo Claims MUST be returned as the members of a JSON object
+ unless a signed or encrypted response was requested during Client
+ Registration. The Claims defined in Section 5.1 can be returned, as can
+ additional Claims not specified there.
+
+ For privacy reasons, OpenID Providers MAY elect to not return values for
+ some requested Claims.
+
+ If a Claim is not returned, that Claim Name SHOULD be omitted from the
+ JSON object representing the Claims; it SHOULD NOT be present with a
+ null or empty string value.
+
+ The sub (subject) Claim MUST always be returned in the UserInfo
+ Response.
+
+ Upon receipt of the UserInfo Request, the UserInfo Endpoint MUST return
+ the JSON Serialization of the UserInfo Response as in Section 13.3 in
+ the HTTP response body unless a different format was specified during
+ Registration [OpenID.Registration].
+
+ If the UserInfo Response is signed and/or encrypted, then the Claims are
+ returned in a JWT and the content-type MUST be application/jwt. The
+ response MAY be encrypted without also being signed. If both signing and
+ encryption are requested, the response MUST be signed then encrypted,
+ with the result being a Nested JWT, as defined in [JWT].
+
+ If signed, the UserInfo Response SHOULD contain the Claims iss (issuer)
+ and aud (audience) as members. The iss value SHOULD be the OP's Issuer
+ Identifier URL. The aud value SHOULD be or include the RP's Client ID
+ value.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: Claims as a dict OR JWT/JWS/JWE as a string
+
+ Method is used by:
+ UserInfoEndpoint
+ """
+
+ def refresh_id_token(self, request):
+ """Whether the id token should be refreshed. Default, True
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ RefreshTokenGrant
+ """
+ return True
diff --git a/contrib/python/oauthlib/oauthlib/openid/connect/core/tokens.py b/contrib/python/oauthlib/oauthlib/openid/connect/core/tokens.py
new file mode 100644
index 0000000000..936ab52e38
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/openid/connect/core/tokens.py
@@ -0,0 +1,48 @@
+"""
+authlib.openid.connect.core.tokens
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods for adding JWT tokens to requests.
+"""
+from oauthlib.oauth2.rfc6749.tokens import (
+ TokenBase, get_token_from_header, random_token_generator,
+)
+
+
+class JWTToken(TokenBase):
+ __slots__ = (
+ 'request_validator', 'token_generator',
+ 'refresh_token_generator', 'expires_in'
+ )
+
+ def __init__(self, request_validator=None, token_generator=None,
+ expires_in=None, refresh_token_generator=None):
+ self.request_validator = request_validator
+ self.token_generator = token_generator or random_token_generator
+ self.refresh_token_generator = (
+ refresh_token_generator or self.token_generator
+ )
+ self.expires_in = expires_in or 3600
+
+ def create_token(self, request, refresh_token=False):
+ """Create a JWT Token, using requestvalidator method."""
+
+ if callable(self.expires_in):
+ expires_in = self.expires_in(request)
+ else:
+ expires_in = self.expires_in
+
+ request.expires_in = expires_in
+
+ return self.request_validator.get_jwt_bearer_token(None, None, request)
+
+ def validate_request(self, request):
+ token = get_token_from_header(request)
+ return self.request_validator.validate_jwt_bearer_token(
+ token, request.scopes, request)
+
+ def estimate_type(self, request):
+ token = get_token_from_header(request)
+ if token and token.startswith('ey') and token.count('.') in (2, 4):
+ return 10
+ return 0
diff --git a/contrib/python/oauthlib/oauthlib/signals.py b/contrib/python/oauthlib/oauthlib/signals.py
new file mode 100644
index 0000000000..8fd347a5c8
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/signals.py
@@ -0,0 +1,40 @@
+"""
+ Implements signals based on blinker if available, otherwise
+ falls silently back to a noop. Shamelessly stolen from flask.signals:
+ https://github.com/mitsuhiko/flask/blob/master/flask/signals.py
+"""
+signals_available = False
+try:
+ from blinker import Namespace
+ signals_available = True
+except ImportError: # noqa
+ class Namespace:
+ def signal(self, name, doc=None):
+ return _FakeSignal(name, doc)
+
+ class _FakeSignal:
+ """If blinker is unavailable, create a fake class with the same
+ interface that allows sending of signals but will fail with an
+ error on anything else. Instead of doing anything on send, it
+ will just ignore the arguments and do nothing instead.
+ """
+
+ def __init__(self, name, doc=None):
+ self.name = name
+ self.__doc__ = doc
+ def _fail(self, *args, **kwargs):
+ raise RuntimeError('signalling support is unavailable '
+ 'because the blinker library is '
+ 'not installed.')
+ send = lambda *a, **kw: None
+ connect = disconnect = has_receivers_for = receivers_for = \
+ temporarily_connected_to = connected_to = _fail
+ del _fail
+
+# The namespace for code signals. If you are not oauthlib code, do
+# not put signals in here. Create your own namespace instead.
+_signals = Namespace()
+
+
+# Core signals.
+scope_changed = _signals.signal('scope-changed')
diff --git a/contrib/python/oauthlib/oauthlib/uri_validate.py b/contrib/python/oauthlib/oauthlib/uri_validate.py
new file mode 100644
index 0000000000..a6fe0fb23e
--- /dev/null
+++ b/contrib/python/oauthlib/oauthlib/uri_validate.py
@@ -0,0 +1,190 @@
+"""
+Regex for URIs
+
+These regex are directly derived from the collected ABNF in RFC3986
+(except for DIGIT, ALPHA and HEXDIG, defined by RFC2234).
+
+They should be processed with re.VERBOSE.
+
+Thanks Mark Nottingham for this code - https://gist.github.com/138549
+"""
+import re
+
+# basics
+
+DIGIT = r"[\x30-\x39]"
+
+ALPHA = r"[\x41-\x5A\x61-\x7A]"
+
+HEXDIG = r"[\x30-\x39A-Fa-f]"
+
+# pct-encoded = "%" HEXDIG HEXDIG
+pct_encoded = r" %% %(HEXDIG)s %(HEXDIG)s" % locals()
+
+# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+unreserved = r"(?: %(ALPHA)s | %(DIGIT)s | \- | \. | _ | ~ )" % locals()
+
+# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+gen_delims = r"(?: : | / | \? | \# | \[ | \] | @ )"
+
+# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+# / "*" / "+" / "," / ";" / "="
+sub_delims = r"""(?: ! | \$ | & | ' | \( | \) |
+ \* | \+ | , | ; | = )"""
+
+# pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+pchar = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : | @ )" % locals(
+)
+
+# reserved = gen-delims / sub-delims
+reserved = r"(?: %(gen_delims)s | %(sub_delims)s )" % locals()
+
+
+# scheme
+
+# scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+scheme = r"%(ALPHA)s (?: %(ALPHA)s | %(DIGIT)s | \+ | \- | \. )*" % locals()
+
+
+# authority
+
+# dec-octet = DIGIT ; 0-9
+# / %x31-39 DIGIT ; 10-99
+# / "1" 2DIGIT ; 100-199
+# / "2" %x30-34 DIGIT ; 200-249
+# / "25" %x30-35 ; 250-255
+dec_octet = r"""(?: %(DIGIT)s |
+ [\x31-\x39] %(DIGIT)s |
+ 1 %(DIGIT)s{2} |
+ 2 [\x30-\x34] %(DIGIT)s |
+ 25 [\x30-\x35]
+ )
+""" % locals()
+
+# IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
+IPv4address = r"%(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s" % locals(
+)
+
+# IPv6address
+IPv6address = r"([A-Fa-f0-9:]+[:$])[A-Fa-f0-9]{1,4}"
+
+# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals()
+
+# IP-literal = "[" ( IPv6address / IPvFuture ) "]"
+IP_literal = r"\[ (?: %(IPv6address)s | %(IPvFuture)s ) \]" % locals()
+
+# reg-name = *( unreserved / pct-encoded / sub-delims )
+reg_name = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s )*" % locals()
+
+# userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+userinfo = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : )" % locals(
+)
+
+# host = IP-literal / IPv4address / reg-name
+host = r"(?: %(IP_literal)s | %(IPv4address)s | %(reg_name)s )" % locals()
+
+# port = *DIGIT
+port = r"(?: %(DIGIT)s )*" % locals()
+
+# authority = [ userinfo "@" ] host [ ":" port ]
+authority = r"(?: %(userinfo)s @)? %(host)s (?: : %(port)s)?" % locals()
+
+# Path
+
+# segment = *pchar
+segment = r"%(pchar)s*" % locals()
+
+# segment-nz = 1*pchar
+segment_nz = r"%(pchar)s+" % locals()
+
+# segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+# ; non-zero-length segment without any colon ":"
+segment_nz_nc = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | @ )+" % locals()
+
+# path-abempty = *( "/" segment )
+path_abempty = r"(?: / %(segment)s )*" % locals()
+
+# path-absolute = "/" [ segment-nz *( "/" segment ) ]
+path_absolute = r"/ (?: %(segment_nz)s (?: / %(segment)s )* )?" % locals()
+
+# path-noscheme = segment-nz-nc *( "/" segment )
+path_noscheme = r"%(segment_nz_nc)s (?: / %(segment)s )*" % locals()
+
+# path-rootless = segment-nz *( "/" segment )
+path_rootless = r"%(segment_nz)s (?: / %(segment)s )*" % locals()
+
+# path-empty = 0<pchar>
+path_empty = r"" # FIXME
+
+# path = path-abempty ; begins with "/" or is empty
+# / path-absolute ; begins with "/" but not "//"
+# / path-noscheme ; begins with a non-colon segment
+# / path-rootless ; begins with a segment
+# / path-empty ; zero characters
+path = r"""(?: %(path_abempty)s |
+ %(path_absolute)s |
+ %(path_noscheme)s |
+ %(path_rootless)s |
+ %(path_empty)s
+ )
+""" % locals()
+
+### Query and Fragment
+
+# query = *( pchar / "/" / "?" )
+query = r"(?: %(pchar)s | / | \? )*" % locals()
+
+# fragment = *( pchar / "/" / "?" )
+fragment = r"(?: %(pchar)s | / | \? )*" % locals()
+
+# URIs
+
+# hier-part = "//" authority path-abempty
+# / path-absolute
+# / path-rootless
+# / path-empty
+hier_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
+ %(path_absolute)s |
+ %(path_rootless)s |
+ %(path_empty)s
+ )
+""" % locals()
+
+# relative-part = "//" authority path-abempty
+# / path-absolute
+# / path-noscheme
+# / path-empty
+relative_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
+ %(path_absolute)s |
+ %(path_noscheme)s |
+ %(path_empty)s
+ )
+""" % locals()
+
+# relative-ref = relative-part [ "?" query ] [ "#" fragment ]
+relative_ref = r"%(relative_part)s (?: \? %(query)s)? (?: \# %(fragment)s)?" % locals(
+)
+
+# URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s )? )$" % locals(
+)
+
+# URI-reference = URI / relative-ref
+URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
+
+# absolute-URI = scheme ":" hier-part [ "?" query ]
+absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals(
+)
+
+
+def is_uri(uri):
+ return re.match(URI, uri, re.VERBOSE)
+
+
+def is_uri_reference(uri):
+ return re.match(URI_reference, uri, re.VERBOSE)
+
+
+def is_absolute_uri(uri):
+ return re.match(absolute_URI, uri, re.VERBOSE)
diff --git a/contrib/python/oauthlib/tests/__init__.py b/contrib/python/oauthlib/tests/__init__.py
new file mode 100644
index 0000000000..f33236b5ee
--- /dev/null
+++ b/contrib/python/oauthlib/tests/__init__.py
@@ -0,0 +1,3 @@
+import oauthlib
+
+oauthlib.set_debug(True)
diff --git a/contrib/python/oauthlib/tests/oauth1/__init__.py b/contrib/python/oauthlib/tests/oauth1/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/__init__.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/__init__.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_access_token.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_access_token.py
new file mode 100644
index 0000000000..57d8117531
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_access_token.py
@@ -0,0 +1,91 @@
+from unittest.mock import ANY, MagicMock
+
+from oauthlib.oauth1 import RequestValidator
+from oauthlib.oauth1.rfc5849 import Client
+from oauthlib.oauth1.rfc5849.endpoints import AccessTokenEndpoint
+
+from tests.unittest import TestCase
+
+
+class AccessTokenEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.check_client_key.return_value = True
+ self.validator.check_request_token.return_value = True
+ self.validator.check_verifier.return_value = True
+ self.validator.allowed_signature_methods = ['HMAC-SHA1']
+ self.validator.get_client_secret.return_value = 'bar'
+ self.validator.get_request_token_secret.return_value = 'secret'
+ self.validator.get_realms.return_value = ['foo']
+ self.validator.timestamp_lifetime = 600
+ self.validator.validate_client_key.return_value = True
+ self.validator.validate_request_token.return_value = True
+ self.validator.validate_verifier.return_value = True
+ self.validator.validate_timestamp_and_nonce.return_value = True
+ self.validator.invalidate_request_token.return_value = True
+ self.validator.dummy_client = 'dummy'
+ self.validator.dummy_secret = 'dummy'
+ self.validator.dummy_request_token = 'dummy'
+ self.validator.save_access_token = MagicMock()
+ self.endpoint = AccessTokenEndpoint(self.validator)
+ self.client = Client('foo',
+ client_secret='bar',
+ resource_owner_key='token',
+ resource_owner_secret='secret',
+ verifier='verfier')
+ self.uri, self.headers, self.body = self.client.sign(
+ 'https://i.b/access_token')
+
+ def test_check_request_token(self):
+ self.validator.check_request_token.return_value = False
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_check_verifier(self):
+ self.validator.check_verifier.return_value = False
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_validate_client_key(self):
+ self.validator.validate_client_key.return_value = False
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 401)
+
+ def test_validate_request_token(self):
+ self.validator.validate_request_token.return_value = False
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 401)
+
+ def test_validate_verifier(self):
+ self.validator.validate_verifier.return_value = False
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 401)
+
+ def test_validate_signature(self):
+ client = Client('foo',
+ resource_owner_key='token',
+ resource_owner_secret='secret',
+ verifier='verfier')
+ _, headers, _ = client.sign(self.uri + '/extra')
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=headers)
+ self.assertEqual(s, 401)
+
+ def test_valid_request(self):
+ h, b, s = self.endpoint.create_access_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 200)
+ self.assertIn('oauth_token', b)
+ self.validator.validate_timestamp_and_nonce.assert_called_once_with(
+ self.client.client_key, ANY, ANY, ANY,
+ request_token=self.client.resource_owner_key)
+ self.validator.invalidate_request_token.assert_called_once_with(
+ self.client.client_key, self.client.resource_owner_key, ANY)
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_authorization.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_authorization.py
new file mode 100644
index 0000000000..a9b2fc0c9f
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_authorization.py
@@ -0,0 +1,54 @@
+from unittest.mock import MagicMock
+
+from oauthlib.oauth1 import RequestValidator
+from oauthlib.oauth1.rfc5849 import errors
+from oauthlib.oauth1.rfc5849.endpoints import AuthorizationEndpoint
+
+from tests.unittest import TestCase
+
+
+class AuthorizationEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.verify_request_token.return_value = True
+ self.validator.verify_realms.return_value = True
+ self.validator.get_realms.return_value = ['test']
+ self.validator.save_verifier = MagicMock()
+ self.endpoint = AuthorizationEndpoint(self.validator)
+ self.uri = 'https://i.b/authorize?oauth_token=foo'
+
+ def test_get_realms_and_credentials(self):
+ realms, credentials = self.endpoint.get_realms_and_credentials(self.uri)
+ self.assertEqual(realms, ['test'])
+
+ def test_verify_token(self):
+ self.validator.verify_request_token.return_value = False
+ self.assertRaises(errors.InvalidClientError,
+ self.endpoint.get_realms_and_credentials, self.uri)
+ self.assertRaises(errors.InvalidClientError,
+ self.endpoint.create_authorization_response, self.uri)
+
+ def test_verify_realms(self):
+ self.validator.verify_realms.return_value = False
+ self.assertRaises(errors.InvalidRequestError,
+ self.endpoint.create_authorization_response,
+ self.uri,
+ realms=['bar'])
+
+ def test_create_authorization_response(self):
+ self.validator.get_redirect_uri.return_value = 'https://c.b/cb'
+ h, b, s = self.endpoint.create_authorization_response(self.uri)
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ location = h['Location']
+ self.assertTrue(location.startswith('https://c.b/cb'))
+ self.assertIn('oauth_verifier', location)
+
+ def test_create_authorization_response_oob(self):
+ self.validator.get_redirect_uri.return_value = 'oob'
+ h, b, s = self.endpoint.create_authorization_response(self.uri)
+ self.assertEqual(s, 200)
+ self.assertNotIn('Location', h)
+ self.assertIn('oauth_verifier', b)
+ self.assertIn('oauth_token', b)
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_base.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_base.py
new file mode 100644
index 0000000000..e87f359baa
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_base.py
@@ -0,0 +1,406 @@
+from re import sub
+from unittest.mock import MagicMock
+
+from oauthlib.common import CaseInsensitiveDict, safe_string_equals
+from oauthlib.oauth1 import Client, RequestValidator
+from oauthlib.oauth1.rfc5849 import (
+ SIGNATURE_HMAC, SIGNATURE_PLAINTEXT, SIGNATURE_RSA, errors,
+)
+from oauthlib.oauth1.rfc5849.endpoints import (
+ BaseEndpoint, RequestTokenEndpoint,
+)
+
+from tests.unittest import TestCase
+
+URLENCODED = {"Content-Type": "application/x-www-form-urlencoded"}
+
+
+class BaseEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(spec=RequestValidator)
+ self.validator.allowed_signature_methods = ['HMAC-SHA1']
+ self.validator.timestamp_lifetime = 600
+ self.endpoint = RequestTokenEndpoint(self.validator)
+ self.client = Client('foo', callback_uri='https://c.b/cb')
+ self.uri, self.headers, self.body = self.client.sign(
+ 'https://i.b/request_token')
+
+ def test_ssl_enforcement(self):
+ uri, headers, _ = self.client.sign('http://i.b/request_token')
+ h, b, s = self.endpoint.create_request_token_response(
+ uri, headers=headers)
+ self.assertEqual(s, 400)
+ self.assertIn('insecure_transport_protocol', b)
+
+ def test_missing_parameters(self):
+ h, b, s = self.endpoint.create_request_token_response(self.uri)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_signature_methods(self):
+ headers = {}
+ headers['Authorization'] = self.headers['Authorization'].replace(
+ 'HMAC', 'RSA')
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_signature_method', b)
+
+ def test_invalid_version(self):
+ headers = {}
+ headers['Authorization'] = self.headers['Authorization'].replace(
+ '1.0', '2.0')
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_expired_timestamp(self):
+ headers = {}
+ for pattern in ('12345678901', '4567890123', '123456789K'):
+ headers['Authorization'] = sub(r'timestamp="\d*k?"',
+ 'timestamp="%s"' % pattern,
+ self.headers['Authorization'])
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_client_key_check(self):
+ self.validator.check_client_key.return_value = False
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_noncecheck(self):
+ self.validator.check_nonce.return_value = False
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_enforce_ssl(self):
+ """Ensure SSL is enforced by default."""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+ c = Client('foo')
+ u, h, b = c.sign('http://example.com')
+ r = e._create_request(u, 'GET', b, h)
+ self.assertRaises(errors.InsecureTransportError,
+ e._check_transport_security, r)
+
+ def test_multiple_source_params(self):
+ """Check for duplicate params"""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+ self.assertRaises(errors.InvalidRequestError, e._create_request,
+ 'https://a.b/?oauth_signature_method=HMAC-SHA1',
+ 'GET', 'oauth_version=foo', URLENCODED)
+ headers = {'Authorization': 'OAuth oauth_signature="foo"'}
+ headers.update(URLENCODED)
+ self.assertRaises(errors.InvalidRequestError, e._create_request,
+ 'https://a.b/?oauth_signature_method=HMAC-SHA1',
+ 'GET',
+ 'oauth_version=foo',
+ headers)
+ headers = {'Authorization': 'OAuth oauth_signature_method="foo"'}
+ headers.update(URLENCODED)
+ self.assertRaises(errors.InvalidRequestError, e._create_request,
+ 'https://a.b/',
+ 'GET',
+ 'oauth_signature=foo',
+ headers)
+
+ def test_duplicate_params(self):
+ """Ensure params are only supplied once"""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+ self.assertRaises(errors.InvalidRequestError, e._create_request,
+ 'https://a.b/?oauth_version=a&oauth_version=b',
+ 'GET', None, URLENCODED)
+ self.assertRaises(errors.InvalidRequestError, e._create_request,
+ 'https://a.b/', 'GET', 'oauth_version=a&oauth_version=b',
+ URLENCODED)
+
+ def test_mandated_params(self):
+ """Ensure all mandatory params are present."""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+ r = e._create_request('https://a.b/', 'GET',
+ 'oauth_signature=a&oauth_consumer_key=b&oauth_nonce',
+ URLENCODED)
+ self.assertRaises(errors.InvalidRequestError,
+ e._check_mandatory_parameters, r)
+
+ def test_oauth_version(self):
+ """OAuth version must be 1.0 if present."""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+ r = e._create_request('https://a.b/', 'GET',
+ ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&'
+ 'oauth_timestamp=a&oauth_signature_method=RSA-SHA1&'
+ 'oauth_version=2.0'),
+ URLENCODED)
+ self.assertRaises(errors.InvalidRequestError,
+ e._check_mandatory_parameters, r)
+
+ def test_oauth_timestamp(self):
+ """Check for a valid UNIX timestamp."""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+
+ # Invalid timestamp length, must be 10
+ r = e._create_request('https://a.b/', 'GET',
+ ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&'
+ 'oauth_version=1.0&oauth_signature_method=RSA-SHA1&'
+ 'oauth_timestamp=123456789'),
+ URLENCODED)
+ self.assertRaises(errors.InvalidRequestError,
+ e._check_mandatory_parameters, r)
+
+ # Invalid timestamp age, must be younger than 10 minutes
+ r = e._create_request('https://a.b/', 'GET',
+ ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&'
+ 'oauth_version=1.0&oauth_signature_method=RSA-SHA1&'
+ 'oauth_timestamp=1234567890'),
+ URLENCODED)
+ self.assertRaises(errors.InvalidRequestError,
+ e._check_mandatory_parameters, r)
+
+ # Timestamp must be an integer
+ r = e._create_request('https://a.b/', 'GET',
+ ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&'
+ 'oauth_version=1.0&oauth_signature_method=RSA-SHA1&'
+ 'oauth_timestamp=123456789a'),
+ URLENCODED)
+ self.assertRaises(errors.InvalidRequestError,
+ e._check_mandatory_parameters, r)
+
+ def test_case_insensitive_headers(self):
+ """Ensure headers are case-insensitive"""
+ v = RequestValidator()
+ e = BaseEndpoint(v)
+ r = e._create_request('https://a.b', 'POST',
+ ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&'
+ 'oauth_version=1.0&oauth_signature_method=RSA-SHA1&'
+ 'oauth_timestamp=123456789a'),
+ URLENCODED)
+ self.assertIsInstance(r.headers, CaseInsensitiveDict)
+
+ def test_signature_method_validation(self):
+ """Ensure valid signature method is used."""
+
+ body = ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&'
+ 'oauth_version=1.0&oauth_signature_method=%s&'
+ 'oauth_timestamp=1234567890')
+
+ uri = 'https://example.com/'
+
+ class HMACValidator(RequestValidator):
+
+ @property
+ def allowed_signature_methods(self):
+ return (SIGNATURE_HMAC,)
+
+ v = HMACValidator()
+ e = BaseEndpoint(v)
+ r = e._create_request(uri, 'GET', body % 'RSA-SHA1', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+ r = e._create_request(uri, 'GET', body % 'PLAINTEXT', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+ r = e._create_request(uri, 'GET', body % 'shibboleth', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+
+ class RSAValidator(RequestValidator):
+
+ @property
+ def allowed_signature_methods(self):
+ return (SIGNATURE_RSA,)
+
+ v = RSAValidator()
+ e = BaseEndpoint(v)
+ r = e._create_request(uri, 'GET', body % 'HMAC-SHA1', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+ r = e._create_request(uri, 'GET', body % 'PLAINTEXT', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+ r = e._create_request(uri, 'GET', body % 'shibboleth', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+
+ class PlainValidator(RequestValidator):
+
+ @property
+ def allowed_signature_methods(self):
+ return (SIGNATURE_PLAINTEXT,)
+
+ v = PlainValidator()
+ e = BaseEndpoint(v)
+ r = e._create_request(uri, 'GET', body % 'HMAC-SHA1', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+ r = e._create_request(uri, 'GET', body % 'RSA-SHA1', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+ r = e._create_request(uri, 'GET', body % 'shibboleth', URLENCODED)
+ self.assertRaises(errors.InvalidSignatureMethodError,
+ e._check_mandatory_parameters, r)
+
+
+class ClientValidator(RequestValidator):
+ clients = ['foo']
+ nonces = [('foo', 'once', '1234567891', 'fez')]
+ owners = {'foo': ['abcdefghijklmnopqrstuvxyz', 'fez']}
+ assigned_realms = {('foo', 'abcdefghijklmnopqrstuvxyz'): 'photos'}
+ verifiers = {('foo', 'fez'): 'shibboleth'}
+
+ @property
+ def client_key_length(self):
+ return 1, 30
+
+ @property
+ def request_token_length(self):
+ return 1, 30
+
+ @property
+ def access_token_length(self):
+ return 1, 30
+
+ @property
+ def nonce_length(self):
+ return 2, 30
+
+ @property
+ def verifier_length(self):
+ return 2, 30
+
+ @property
+ def realms(self):
+ return ['photos']
+
+ @property
+ def timestamp_lifetime(self):
+ # Disabled check to allow hardcoded verification signatures
+ return 1000000000
+
+ @property
+ def dummy_client(self):
+ return 'dummy'
+
+ @property
+ def dummy_request_token(self):
+ return 'dumbo'
+
+ @property
+ def dummy_access_token(self):
+ return 'dumbo'
+
+ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+ request, request_token=None, access_token=None):
+ resource_owner_key = request_token if request_token else access_token
+ return not (client_key, nonce, timestamp, resource_owner_key) in self.nonces
+
+ def validate_client_key(self, client_key):
+ return client_key in self.clients
+
+ def validate_access_token(self, client_key, access_token, request):
+ return (self.owners.get(client_key) and
+ access_token in self.owners.get(client_key))
+
+ def validate_request_token(self, client_key, request_token, request):
+ return (self.owners.get(client_key) and
+ request_token in self.owners.get(client_key))
+
+ def validate_requested_realm(self, client_key, realm, request):
+ return True
+
+ def validate_realm(self, client_key, access_token, request, uri=None,
+ required_realm=None):
+ return (client_key, access_token) in self.assigned_realms
+
+ def validate_verifier(self, client_key, request_token, verifier,
+ request):
+ return ((client_key, request_token) in self.verifiers and
+ safe_string_equals(verifier, self.verifiers.get(
+ (client_key, request_token))))
+
+ def validate_redirect_uri(self, client_key, redirect_uri, request):
+ return redirect_uri.startswith('http://client.example.com/')
+
+ def get_client_secret(self, client_key, request):
+ return 'super secret'
+
+ def get_access_token_secret(self, client_key, access_token, request):
+ return 'even more secret'
+
+ def get_request_token_secret(self, client_key, request_token, request):
+ return 'even more secret'
+
+ def get_rsa_key(self, client_key, request):
+ return ("-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNA"
+ "DCBiQKBgQDVLQCATX8iK+aZuGVdkGb6uiar\nLi/jqFwL1dYj0JLIsdQc"
+ "KaMWtPC06K0+vI+RRZcjKc6sNB9/7kJcKN9Ekc9BUxyT\n/D09Cz47cmC"
+ "YsUoiW7G8NSqbE4wPiVpGkJRzFAxaCWwOSSQ+lpC9vwxnvVQfOoZ1\nnp"
+ "mWbCdA0iTxsMahwQIDAQAB\n-----END PUBLIC KEY-----")
+
+
+class SignatureVerificationTest(TestCase):
+
+ def setUp(self):
+ v = ClientValidator()
+ self.e = BaseEndpoint(v)
+
+ self.uri = 'https://example.com/'
+ self.sig = ('oauth_signature=%s&'
+ 'oauth_timestamp=1234567890&'
+ 'oauth_nonce=abcdefghijklmnopqrstuvwxyz&'
+ 'oauth_version=1.0&'
+ 'oauth_signature_method=%s&'
+ 'oauth_token=abcdefghijklmnopqrstuvxyz&'
+ 'oauth_consumer_key=foo')
+
+ def test_signature_too_short(self):
+ short_sig = ('oauth_signature=fmrXnTF4lO4o%2BD0%2FlZaJHP%2FXqEY&'
+ 'oauth_timestamp=1234567890&'
+ 'oauth_nonce=abcdefghijklmnopqrstuvwxyz&'
+ 'oauth_version=1.0&oauth_signature_method=HMAC-SHA1&'
+ 'oauth_token=abcdefghijklmnopqrstuvxyz&'
+ 'oauth_consumer_key=foo')
+ r = self.e._create_request(self.uri, 'GET', short_sig, URLENCODED)
+ self.assertFalse(self.e._check_signature(r))
+
+ plain = ('oauth_signature=correctlengthbutthewrongcontent1111&'
+ 'oauth_timestamp=1234567890&'
+ 'oauth_nonce=abcdefghijklmnopqrstuvwxyz&'
+ 'oauth_version=1.0&oauth_signature_method=PLAINTEXT&'
+ 'oauth_token=abcdefghijklmnopqrstuvxyz&'
+ 'oauth_consumer_key=foo')
+ r = self.e._create_request(self.uri, 'GET', plain, URLENCODED)
+ self.assertFalse(self.e._check_signature(r))
+
+ def test_hmac_signature(self):
+ hmac_sig = "fmrXnTF4lO4o%2BD0%2FlZaJHP%2FXqEY%3D"
+ sig = self.sig % (hmac_sig, "HMAC-SHA1")
+ r = self.e._create_request(self.uri, 'GET', sig, URLENCODED)
+ self.assertTrue(self.e._check_signature(r))
+
+ def test_rsa_signature(self):
+ rsa_sig = ("fxFvCx33oKlR9wDquJ%2FPsndFzJphyBa3RFPPIKi3flqK%2BJ7yIrMVbH"
+ "YTM%2FLHPc7NChWz4F4%2FzRA%2BDN1k08xgYGSBoWJUOW6VvOQ6fbYhMA"
+ "FkOGYbuGDbje487XMzsAcv6ZjqZHCROSCk5vofgLk2SN7RZ3OrgrFzf4in"
+ "xetClqA%3D")
+ sig = self.sig % (rsa_sig, "RSA-SHA1")
+ r = self.e._create_request(self.uri, 'GET', sig, URLENCODED)
+ self.assertTrue(self.e._check_signature(r))
+
+ def test_plaintext_signature(self):
+ plain_sig = "super%252520secret%26even%252520more%252520secret"
+ sig = self.sig % (plain_sig, "PLAINTEXT")
+ r = self.e._create_request(self.uri, 'GET', sig, URLENCODED)
+ self.assertTrue(self.e._check_signature(r))
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_request_token.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_request_token.py
new file mode 100644
index 0000000000..879cad2f48
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_request_token.py
@@ -0,0 +1,90 @@
+from unittest.mock import ANY, MagicMock
+
+from oauthlib.oauth1 import RequestValidator
+from oauthlib.oauth1.rfc5849 import Client
+from oauthlib.oauth1.rfc5849.endpoints import RequestTokenEndpoint
+
+from tests.unittest import TestCase
+
+
+class RequestTokenEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.check_client_key.return_value = True
+ self.validator.allowed_signature_methods = ['HMAC-SHA1']
+ self.validator.get_client_secret.return_value = 'bar'
+ self.validator.get_default_realms.return_value = ['foo']
+ self.validator.timestamp_lifetime = 600
+ self.validator.check_realms.return_value = True
+ self.validator.validate_client_key.return_value = True
+ self.validator.validate_requested_realms.return_value = True
+ self.validator.validate_redirect_uri.return_value = True
+ self.validator.validate_timestamp_and_nonce.return_value = True
+ self.validator.dummy_client = 'dummy'
+ self.validator.dummy_secret = 'dummy'
+ self.validator.save_request_token = MagicMock()
+ self.endpoint = RequestTokenEndpoint(self.validator)
+ self.client = Client('foo', client_secret='bar', realm='foo',
+ callback_uri='https://c.b/cb')
+ self.uri, self.headers, self.body = self.client.sign(
+ 'https://i.b/request_token')
+
+ def test_check_redirect_uri(self):
+ client = Client('foo')
+ uri, headers, _ = client.sign(self.uri)
+ h, b, s = self.endpoint.create_request_token_response(
+ uri, headers=headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_check_realms(self):
+ self.validator.check_realms.return_value = False
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 400)
+ self.assertIn('invalid_request', b)
+
+ def test_validate_client_key(self):
+ self.validator.validate_client_key.return_value = False
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 401)
+
+ def test_validate_realms(self):
+ self.validator.validate_requested_realms.return_value = False
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 401)
+
+ def test_validate_redirect_uri(self):
+ self.validator.validate_redirect_uri.return_value = False
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 401)
+
+ def test_validate_signature(self):
+ client = Client('foo', callback_uri='https://c.b/cb')
+ _, headers, _ = client.sign(self.uri + '/extra')
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=headers)
+ self.assertEqual(s, 401)
+
+ def test_valid_request(self):
+ h, b, s = self.endpoint.create_request_token_response(
+ self.uri, headers=self.headers)
+ self.assertEqual(s, 200)
+ self.assertIn('oauth_token', b)
+ self.validator.validate_timestamp_and_nonce.assert_called_once_with(
+ self.client.client_key, ANY, ANY, ANY,
+ request_token=self.client.resource_owner_key)
+
+ def test_uri_provided_realm(self):
+ client = Client('foo', callback_uri='https://c.b/cb',
+ client_secret='bar')
+ uri = self.uri + '?realm=foo'
+ _, headers, _ = client.sign(uri)
+ h, b, s = self.endpoint.create_request_token_response(
+ uri, headers=headers)
+ self.assertEqual(s, 200)
+ self.assertIn('oauth_token', b)
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_resource.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_resource.py
new file mode 100644
index 0000000000..416216f737
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_resource.py
@@ -0,0 +1,102 @@
+from unittest.mock import ANY, MagicMock
+
+from oauthlib.oauth1 import RequestValidator
+from oauthlib.oauth1.rfc5849 import Client
+from oauthlib.oauth1.rfc5849.endpoints import ResourceEndpoint
+
+from tests.unittest import TestCase
+
+
+class ResourceEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.check_client_key.return_value = True
+ self.validator.check_access_token.return_value = True
+ self.validator.allowed_signature_methods = ['HMAC-SHA1']
+ self.validator.get_client_secret.return_value = 'bar'
+ self.validator.get_access_token_secret.return_value = 'secret'
+ self.validator.timestamp_lifetime = 600
+ self.validator.validate_client_key.return_value = True
+ self.validator.validate_access_token.return_value = True
+ self.validator.validate_timestamp_and_nonce.return_value = True
+ self.validator.validate_realms.return_value = True
+ self.validator.dummy_client = 'dummy'
+ self.validator.dummy_secret = 'dummy'
+ self.validator.dummy_access_token = 'dummy'
+ self.endpoint = ResourceEndpoint(self.validator)
+ self.client = Client('foo',
+ client_secret='bar',
+ resource_owner_key='token',
+ resource_owner_secret='secret')
+ self.uri, self.headers, self.body = self.client.sign(
+ 'https://i.b/protected_resource')
+
+ def test_missing_parameters(self):
+ self.validator.check_access_token.return_value = False
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri)
+ self.assertFalse(v)
+
+ def test_check_access_token(self):
+ self.validator.check_access_token.return_value = False
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri, headers=self.headers)
+ self.assertFalse(v)
+
+ def test_validate_client_key(self):
+ self.validator.validate_client_key.return_value = False
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri, headers=self.headers)
+ self.assertFalse(v)
+ # the validator log should have `False` values
+ self.assertFalse(r.validator_log['client'])
+ self.assertTrue(r.validator_log['realm'])
+ self.assertTrue(r.validator_log['resource_owner'])
+ self.assertTrue(r.validator_log['signature'])
+
+ def test_validate_access_token(self):
+ self.validator.validate_access_token.return_value = False
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri, headers=self.headers)
+ self.assertFalse(v)
+ # the validator log should have `False` values
+ self.assertTrue(r.validator_log['client'])
+ self.assertTrue(r.validator_log['realm'])
+ self.assertFalse(r.validator_log['resource_owner'])
+ self.assertTrue(r.validator_log['signature'])
+
+ def test_validate_realms(self):
+ self.validator.validate_realms.return_value = False
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri, headers=self.headers)
+ self.assertFalse(v)
+ # the validator log should have `False` values
+ self.assertTrue(r.validator_log['client'])
+ self.assertFalse(r.validator_log['realm'])
+ self.assertTrue(r.validator_log['resource_owner'])
+ self.assertTrue(r.validator_log['signature'])
+
+ def test_validate_signature(self):
+ client = Client('foo',
+ resource_owner_key='token',
+ resource_owner_secret='secret')
+ _, headers, _ = client.sign(self.uri + '/extra')
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri, headers=headers)
+ self.assertFalse(v)
+ # the validator log should have `False` values
+ self.assertTrue(r.validator_log['client'])
+ self.assertTrue(r.validator_log['realm'])
+ self.assertTrue(r.validator_log['resource_owner'])
+ self.assertFalse(r.validator_log['signature'])
+
+ def test_valid_request(self):
+ v, r = self.endpoint.validate_protected_resource_request(
+ self.uri, headers=self.headers)
+ self.assertTrue(v)
+ self.validator.validate_timestamp_and_nonce.assert_called_once_with(
+ self.client.client_key, ANY, ANY, ANY,
+ access_token=self.client.resource_owner_key)
+ # everything in the validator_log should be `True`
+ self.assertTrue(all(r.validator_log.items()))
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_signature_only.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_signature_only.py
new file mode 100644
index 0000000000..16585bd580
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/endpoints/test_signature_only.py
@@ -0,0 +1,50 @@
+from unittest.mock import ANY, MagicMock
+
+from oauthlib.oauth1 import RequestValidator
+from oauthlib.oauth1.rfc5849 import Client
+from oauthlib.oauth1.rfc5849.endpoints import SignatureOnlyEndpoint
+
+from tests.unittest import TestCase
+
+
+class SignatureOnlyEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.check_client_key.return_value = True
+ self.validator.allowed_signature_methods = ['HMAC-SHA1']
+ self.validator.get_client_secret.return_value = 'bar'
+ self.validator.timestamp_lifetime = 600
+ self.validator.validate_client_key.return_value = True
+ self.validator.validate_timestamp_and_nonce.return_value = True
+ self.validator.dummy_client = 'dummy'
+ self.validator.dummy_secret = 'dummy'
+ self.endpoint = SignatureOnlyEndpoint(self.validator)
+ self.client = Client('foo', client_secret='bar')
+ self.uri, self.headers, self.body = self.client.sign(
+ 'https://i.b/protected_resource')
+
+ def test_missing_parameters(self):
+ v, r = self.endpoint.validate_request(
+ self.uri)
+ self.assertFalse(v)
+
+ def test_validate_client_key(self):
+ self.validator.validate_client_key.return_value = False
+ v, r = self.endpoint.validate_request(
+ self.uri, headers=self.headers)
+ self.assertFalse(v)
+
+ def test_validate_signature(self):
+ client = Client('foo')
+ _, headers, _ = client.sign(self.uri + '/extra')
+ v, r = self.endpoint.validate_request(
+ self.uri, headers=headers)
+ self.assertFalse(v)
+
+ def test_valid_request(self):
+ v, r = self.endpoint.validate_request(
+ self.uri, headers=self.headers)
+ self.assertTrue(v)
+ self.validator.validate_timestamp_and_nonce.assert_called_once_with(
+ self.client.client_key, ANY, ANY, ANY)
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/test_client.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_client.py
new file mode 100644
index 0000000000..f7c997f509
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_client.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+from oauthlib.common import Request
+from oauthlib.oauth1 import (
+ SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_PLAINTEXT,
+ SIGNATURE_RSA, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY,
+)
+from oauthlib.oauth1.rfc5849 import Client
+
+from tests.unittest import TestCase
+
+
+class ClientRealmTests(TestCase):
+
+ def test_client_no_realm(self):
+ client = Client("client-key")
+ uri, header, body = client.sign("http://example-uri")
+ self.assertTrue(
+ header["Authorization"].startswith('OAuth oauth_nonce='))
+
+ def test_client_realm_sign_with_default_realm(self):
+ client = Client("client-key", realm="moo-realm")
+ self.assertEqual(client.realm, "moo-realm")
+ uri, header, body = client.sign("http://example-uri")
+ self.assertTrue(
+ header["Authorization"].startswith('OAuth realm="moo-realm",'))
+
+ def test_client_realm_sign_with_additional_realm(self):
+ client = Client("client-key", realm="moo-realm")
+ uri, header, body = client.sign("http://example-uri", realm="baa-realm")
+ self.assertTrue(
+ header["Authorization"].startswith('OAuth realm="baa-realm",'))
+ # make sure sign() does not override the default realm
+ self.assertEqual(client.realm, "moo-realm")
+
+
+class ClientConstructorTests(TestCase):
+
+ def test_convert_to_unicode_resource_owner(self):
+ client = Client('client-key',
+ resource_owner_key=b'owner key')
+ self.assertNotIsInstance(client.resource_owner_key, bytes)
+ self.assertEqual(client.resource_owner_key, 'owner key')
+
+ def test_give_explicit_timestamp(self):
+ client = Client('client-key', timestamp='1')
+ params = dict(client.get_oauth_params(Request('http://example.com')))
+ self.assertEqual(params['oauth_timestamp'], '1')
+
+ def test_give_explicit_nonce(self):
+ client = Client('client-key', nonce='1')
+ params = dict(client.get_oauth_params(Request('http://example.com')))
+ self.assertEqual(params['oauth_nonce'], '1')
+
+ def test_decoding(self):
+ client = Client('client_key', decoding='utf-8')
+ uri, headers, body = client.sign('http://a.b/path?query',
+ http_method='POST', body='a=b',
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertIsInstance(uri, bytes)
+ self.assertIsInstance(body, bytes)
+ for k, v in headers.items():
+ self.assertIsInstance(k, bytes)
+ self.assertIsInstance(v, bytes)
+
+ def test_hmac_sha1(self):
+ client = Client('client_key')
+ # instance is using the correct signer method
+ self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA1],
+ client.SIGNATURE_METHODS[client.signature_method])
+
+ def test_hmac_sha256(self):
+ client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256)
+ # instance is using the correct signer method
+ self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA256],
+ client.SIGNATURE_METHODS[client.signature_method])
+
+ def test_rsa(self):
+ client = Client('client_key', signature_method=SIGNATURE_RSA)
+ # instance is using the correct signer method
+ self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_RSA],
+ client.SIGNATURE_METHODS[client.signature_method])
+ # don't need an RSA key to instantiate
+ self.assertIsNone(client.rsa_key)
+
+
+class SignatureMethodTest(TestCase):
+
+ def test_hmac_sha1_method(self):
+ client = Client('client_key', timestamp='1234567890', nonce='abc')
+ u, h, b = client.sign('http://example.com')
+ correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="HMAC-SHA1", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="hH5BWYVqo7QI4EmPBUUe9owRUUQ%3D"')
+ self.assertEqual(h['Authorization'], correct)
+
+ def test_hmac_sha256_method(self):
+ client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256,
+ timestamp='1234567890', nonce='abc')
+ u, h, b = client.sign('http://example.com')
+ correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="HMAC-SHA256", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="JzgJWBxX664OiMW3WE4MEjtYwOjI%2FpaUWHqtdHe68Es%3D"')
+ self.assertEqual(h['Authorization'], correct)
+
+ def test_rsa_method(self):
+ private_key = (
+ "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDk1/bxy"
+ "S8Q8jiheHeYYp/4rEKJopeQRRKKpZI4s5i+UPwVpupG\nAlwXWfzXw"
+ "SMaKPAoKJNdu7tqKRniqst5uoHXw98gj0x7zamu0Ck1LtQ4c7pFMVa"
+ "h\n5IYGhBi2E9ycNS329W27nJPWNCbESTu7snVlG8V8mfvGGg3xNjT"
+ "MO7IdrwIDAQAB\nAoGBAOQ2KuH8S5+OrsL4K+wfjoCi6MfxCUyqVU9"
+ "GxocdM1m30WyWRFMEz2nKJ8fR\np3vTD4w8yplTOhcoXdQZl0kRoaD"
+ "zrcYkm2VvJtQRrX7dKFT8dR8D/Tr7dNQLOXfC\nDY6xveQczE7qt7V"
+ "k7lp4FqmxBsaaEuokt78pOOjywZoInjZhAkEA9wz3zoZNT0/i\nrf6"
+ "qv2qTIeieUB035N3dyw6f1BGSWYaXSuerDCD/J1qZbAPKKhyHZbVaw"
+ "Ft3UMhe\n542UftBaxQJBAO0iJy1I8GQjGnS7B3yvyH3CcLYGy296+"
+ "XO/2xKp/d/ty1OIeovx\nC60pLNwuFNF3z9d2GVQAdoQ89hUkOtjZL"
+ "eMCQQD0JO6oPHUeUjYT+T7ImAv7UKVT\nSuy30sKjLzqoGw1kR+wv7"
+ "C5PeDRvscs4wa4CW9s6mjSrMDkDrmCLuJDtmf55AkEA\nkmaMg2PNr"
+ "jUR51F0zOEFycaaqXbGcFwe1/xx9zLmHzMDXd4bsnwt9kk+fe0hQzV"
+ "S\nJzatanQit3+feev1PN3QewJAWv4RZeavEUhKv+kLe95Yd0su7lT"
+ "LVduVgh4v5yLT\nGa6FHdjGPcfajt+nrpB1n8UQBEH9ZxniokR/IPv"
+ "dMlxqXA==\n-----END RSA PRIVATE KEY-----"
+ )
+ client = Client('client_key', signature_method=SIGNATURE_RSA,
+ rsa_key=private_key, timestamp='1234567890', nonce='abc')
+ u, h, b = client.sign('http://example.com')
+ correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="RSA-SHA1", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="ktvzkUhtrIawBcq21DRJrAyysTc3E1Zq5GdGu8EzH'
+ 'OtbeaCmOBDLGHAcqlm92mj7xp5E1Z6i2vbExPimYAJL7FzkLnkRE5YEJR4'
+ 'rNtIgAf1OZbYsIUmmBO%2BCLuStuu5Lg3tAluwC7XkkgoXCBaRKT1mUXzP'
+ 'HJILzZ8iFOvS6w5E%3D"')
+ self.assertEqual(h['Authorization'], correct)
+
+ def test_plaintext_method(self):
+ client = Client('client_key',
+ signature_method=SIGNATURE_PLAINTEXT,
+ timestamp='1234567890',
+ nonce='abc',
+ client_secret='foo',
+ resource_owner_secret='bar')
+ u, h, b = client.sign('http://example.com')
+ correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="PLAINTEXT", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="foo%26bar"')
+ self.assertEqual(h['Authorization'], correct)
+
+ def test_invalid_method(self):
+ client = Client('client_key', signature_method='invalid')
+ self.assertRaises(ValueError, client.sign, 'http://example.com')
+
+ def test_rsa_no_key(self):
+ client = Client('client_key', signature_method=SIGNATURE_RSA)
+ self.assertRaises(ValueError, client.sign, 'http://example.com')
+
+ def test_register_method(self):
+ Client.register_signature_method('PIZZA',
+ lambda base_string, client: 'PIZZA')
+
+ self.assertIn('PIZZA', Client.SIGNATURE_METHODS)
+
+ client = Client('client_key', signature_method='PIZZA',
+ timestamp='1234567890', nonce='abc')
+
+ u, h, b = client.sign('http://example.com')
+
+ self.assertEqual(h['Authorization'], (
+ 'OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="PIZZA", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="PIZZA"'
+ ))
+
+
+class SignatureTypeTest(TestCase):
+
+ def test_params_in_body(self):
+ client = Client('client_key', signature_type=SIGNATURE_TYPE_BODY,
+ timestamp='1378988215', nonce='14205877133089081931378988215')
+ _, h, b = client.sign('http://i.b/path', http_method='POST', body='a=b',
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(h['Content-Type'], 'application/x-www-form-urlencoded')
+ correct = ('a=b&oauth_nonce=14205877133089081931378988215&'
+ 'oauth_timestamp=1378988215&'
+ 'oauth_version=1.0&'
+ 'oauth_signature_method=HMAC-SHA1&'
+ 'oauth_consumer_key=client_key&'
+ 'oauth_signature=2JAQomgbShqoscqKWBiYQZwWq94%3D')
+ self.assertEqual(b, correct)
+
+ def test_params_in_query(self):
+ client = Client('client_key', signature_type=SIGNATURE_TYPE_QUERY,
+ timestamp='1378988215', nonce='14205877133089081931378988215')
+ u, _, _ = client.sign('http://i.b/path', http_method='POST')
+ correct = ('http://i.b/path?oauth_nonce=14205877133089081931378988215&'
+ 'oauth_timestamp=1378988215&'
+ 'oauth_version=1.0&'
+ 'oauth_signature_method=HMAC-SHA1&'
+ 'oauth_consumer_key=client_key&'
+ 'oauth_signature=08G5Snvw%2BgDAzBF%2BCmT5KqlrPKo%3D')
+ self.assertEqual(u, correct)
+
+ def test_invalid_signature_type(self):
+ client = Client('client_key', signature_type='invalid')
+ self.assertRaises(ValueError, client.sign, 'http://i.b/path')
+
+
+class SigningTest(TestCase):
+
+ def test_case_insensitive_headers(self):
+ client = Client('client_key')
+ # Uppercase
+ _, h, _ = client.sign('http://i.b/path', http_method='POST', body='',
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(h['Content-Type'], 'application/x-www-form-urlencoded')
+
+ # Lowercase
+ _, h, _ = client.sign('http://i.b/path', http_method='POST', body='',
+ headers={'content-type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(h['content-type'], 'application/x-www-form-urlencoded')
+
+ # Capitalized
+ _, h, _ = client.sign('http://i.b/path', http_method='POST', body='',
+ headers={'Content-type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(h['Content-type'], 'application/x-www-form-urlencoded')
+
+ # Random
+ _, h, _ = client.sign('http://i.b/path', http_method='POST', body='',
+ headers={'conTent-tYpe': 'application/x-www-form-urlencoded'})
+ self.assertEqual(h['conTent-tYpe'], 'application/x-www-form-urlencoded')
+
+ def test_sign_no_body(self):
+ client = Client('client_key', decoding='utf-8')
+ self.assertRaises(ValueError, client.sign, 'http://i.b/path',
+ http_method='POST', body=None,
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+
+ def test_sign_body(self):
+ client = Client('client_key')
+ _, h, b = client.sign('http://i.b/path', http_method='POST', body='',
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(h['Content-Type'], 'application/x-www-form-urlencoded')
+
+ def test_sign_get_with_body(self):
+ client = Client('client_key')
+ for method in ('GET', 'HEAD'):
+ self.assertRaises(ValueError, client.sign, 'http://a.b/path?query',
+ http_method=method, body='a=b',
+ headers={
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ })
+
+ def test_sign_unicode(self):
+ client = Client('client_key', nonce='abc', timestamp='abc')
+ _, h, b = client.sign('http://i.b/path', http_method='POST',
+ body='status=%E5%95%A6%E5%95%A6',
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(b, 'status=%E5%95%A6%E5%95%A6')
+ self.assertIn('oauth_signature="yrtSqp88m%2Fc5UDaucI8BXK4oEtk%3D"', h['Authorization'])
+ _, h, b = client.sign('http://i.b/path', http_method='POST',
+ body='status=%C3%A6%C3%A5%C3%B8',
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(b, 'status=%C3%A6%C3%A5%C3%B8')
+ self.assertIn('oauth_signature="oG5t3Eg%2FXO5FfQgUUlTtUeeZzvk%3D"', h['Authorization'])
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/test_parameters.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_parameters.py
new file mode 100644
index 0000000000..92b95c1167
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_parameters.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+from oauthlib.common import urlencode
+from oauthlib.oauth1.rfc5849.parameters import (
+ _append_params, prepare_form_encoded_body, prepare_headers,
+ prepare_request_uri_query,
+)
+
+from tests.unittest import TestCase
+
+
+class ParameterTests(TestCase):
+ auth_only_params = [
+ ('oauth_consumer_key', "9djdj82h48djs9d2"),
+ ('oauth_token', "kkk9d7dh3k39sjv7"),
+ ('oauth_signature_method', "HMAC-SHA1"),
+ ('oauth_timestamp', "137131201"),
+ ('oauth_nonce', "7d8f3e4a"),
+ ('oauth_signature', "bYT5CMsGcbgUdFHObYMEfcx6bsw=")
+ ]
+ auth_and_data = list(auth_only_params)
+ auth_and_data.append(('data_param_foo', 'foo'))
+ auth_and_data.append(('data_param_1', '1'))
+ realm = 'testrealm'
+ norealm_authorization_header = ' '.join((
+ 'OAuth',
+ 'oauth_consumer_key="9djdj82h48djs9d2",',
+ 'oauth_token="kkk9d7dh3k39sjv7",',
+ 'oauth_signature_method="HMAC-SHA1",',
+ 'oauth_timestamp="137131201",',
+ 'oauth_nonce="7d8f3e4a",',
+ 'oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"',
+ ))
+ withrealm_authorization_header = ' '.join((
+ 'OAuth',
+ 'realm="testrealm",',
+ 'oauth_consumer_key="9djdj82h48djs9d2",',
+ 'oauth_token="kkk9d7dh3k39sjv7",',
+ 'oauth_signature_method="HMAC-SHA1",',
+ 'oauth_timestamp="137131201",',
+ 'oauth_nonce="7d8f3e4a",',
+ 'oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"',
+ ))
+
+ def test_append_params(self):
+ unordered_1 = [
+ ('oauth_foo', 'foo'),
+ ('lala', 123),
+ ('oauth_baz', 'baz'),
+ ('oauth_bar', 'bar'), ]
+ unordered_2 = [
+ ('teehee', 456),
+ ('oauth_quux', 'quux'), ]
+ expected = [
+ ('teehee', 456),
+ ('lala', 123),
+ ('oauth_quux', 'quux'),
+ ('oauth_foo', 'foo'),
+ ('oauth_baz', 'baz'),
+ ('oauth_bar', 'bar'), ]
+ self.assertEqual(_append_params(unordered_1, unordered_2), expected)
+
+ def test_prepare_headers(self):
+ self.assertEqual(
+ prepare_headers(self.auth_only_params, {}),
+ {'Authorization': self.norealm_authorization_header})
+ self.assertEqual(
+ prepare_headers(self.auth_only_params, {}, realm=self.realm),
+ {'Authorization': self.withrealm_authorization_header})
+
+ def test_prepare_headers_ignore_data(self):
+ self.assertEqual(
+ prepare_headers(self.auth_and_data, {}),
+ {'Authorization': self.norealm_authorization_header})
+ self.assertEqual(
+ prepare_headers(self.auth_and_data, {}, realm=self.realm),
+ {'Authorization': self.withrealm_authorization_header})
+
+ def test_prepare_form_encoded_body(self):
+ existing_body = ''
+ form_encoded_body = 'data_param_foo=foo&data_param_1=1&oauth_consumer_key=9djdj82h48djs9d2&oauth_token=kkk9d7dh3k39sjv7&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131201&oauth_nonce=7d8f3e4a&oauth_signature=bYT5CMsGcbgUdFHObYMEfcx6bsw%3D'
+ self.assertEqual(
+ urlencode(prepare_form_encoded_body(self.auth_and_data, existing_body)),
+ form_encoded_body)
+
+ def test_prepare_request_uri_query(self):
+ url = 'http://notarealdomain.com/foo/bar/baz?some=args&go=here'
+ request_uri_query = 'http://notarealdomain.com/foo/bar/baz?some=args&go=here&data_param_foo=foo&data_param_1=1&oauth_consumer_key=9djdj82h48djs9d2&oauth_token=kkk9d7dh3k39sjv7&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131201&oauth_nonce=7d8f3e4a&oauth_signature=bYT5CMsGcbgUdFHObYMEfcx6bsw%3D'
+ self.assertEqual(
+ prepare_request_uri_query(self.auth_and_data, url),
+ request_uri_query)
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/test_request_validator.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_request_validator.py
new file mode 100644
index 0000000000..8d34415040
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_request_validator.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+from oauthlib.oauth1 import RequestValidator
+
+from tests.unittest import TestCase
+
+
+class RequestValidatorTests(TestCase):
+
+ def test_not_implemented(self):
+ v = RequestValidator()
+ self.assertRaises(NotImplementedError, v.get_client_secret, None, None)
+ self.assertRaises(NotImplementedError, v.get_request_token_secret,
+ None, None, None)
+ self.assertRaises(NotImplementedError, v.get_access_token_secret,
+ None, None, None)
+ self.assertRaises(NotImplementedError, lambda: v.dummy_client)
+ self.assertRaises(NotImplementedError, lambda: v.dummy_request_token)
+ self.assertRaises(NotImplementedError, lambda: v.dummy_access_token)
+ self.assertRaises(NotImplementedError, v.get_rsa_key, None, None)
+ self.assertRaises(NotImplementedError, v.get_default_realms, None, None)
+ self.assertRaises(NotImplementedError, v.get_realms, None, None)
+ self.assertRaises(NotImplementedError, v.get_redirect_uri, None, None)
+ self.assertRaises(NotImplementedError, v.validate_client_key, None, None)
+ self.assertRaises(NotImplementedError, v.validate_access_token,
+ None, None, None)
+ self.assertRaises(NotImplementedError, v.validate_request_token,
+ None, None, None)
+ self.assertRaises(NotImplementedError, v.verify_request_token,
+ None, None)
+ self.assertRaises(NotImplementedError, v.verify_realms,
+ None, None, None)
+ self.assertRaises(NotImplementedError, v.validate_timestamp_and_nonce,
+ None, None, None, None)
+ self.assertRaises(NotImplementedError, v.validate_redirect_uri,
+ None, None, None)
+ self.assertRaises(NotImplementedError, v.validate_realms,
+ None, None, None, None, None)
+ self.assertRaises(NotImplementedError, v.validate_requested_realms,
+ None, None, None)
+ self.assertRaises(NotImplementedError, v.validate_verifier,
+ None, None, None, None)
+ self.assertRaises(NotImplementedError, v.save_access_token, None, None)
+ self.assertRaises(NotImplementedError, v.save_request_token, None, None)
+ self.assertRaises(NotImplementedError, v.save_verifier,
+ None, None, None)
+
+ def test_check_length(self):
+ v = RequestValidator()
+
+ for method in (v.check_client_key, v.check_request_token,
+ v.check_access_token, v.check_nonce, v.check_verifier):
+ for not_valid in ('tooshort', 'invalid?characters!',
+ 'thisclientkeyisalittlebittoolong'):
+ self.assertFalse(method(not_valid))
+ for valid in ('itsjustaboutlongenough',):
+ self.assertTrue(method(valid))
+
+ def test_check_realms(self):
+ v = RequestValidator()
+ self.assertFalse(v.check_realms(['foo']))
+
+ class FooRealmValidator(RequestValidator):
+ @property
+ def realms(self):
+ return ['foo']
+
+ v = FooRealmValidator()
+ self.assertTrue(v.check_realms(['foo']))
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/test_signatures.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_signatures.py
new file mode 100644
index 0000000000..2d4735eafd
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_signatures.py
@@ -0,0 +1,896 @@
+# -*- coding: utf-8 -*-
+from oauthlib.oauth1.rfc5849.signature import (
+ base_string_uri, collect_parameters, normalize_parameters,
+ sign_hmac_sha1_with_client, sign_hmac_sha256_with_client,
+ sign_hmac_sha512_with_client, sign_plaintext_with_client,
+ sign_rsa_sha1_with_client, sign_rsa_sha256_with_client,
+ sign_rsa_sha512_with_client, signature_base_string, verify_hmac_sha1,
+ verify_hmac_sha256, verify_hmac_sha512, verify_plaintext, verify_rsa_sha1,
+ verify_rsa_sha256, verify_rsa_sha512,
+)
+
+from tests.unittest import TestCase
+
+# ################################################################
+
+class MockRequest:
+ """
+ Mock of a request used by the verify_* functions.
+ """
+
+ def __init__(self,
+ method: str,
+ uri_str: str,
+ params: list,
+ signature: str):
+ """
+ The params is a list of (name, value) tuples. It is not a dictionary,
+ because there can be multiple parameters with the same name.
+ """
+ self.uri = uri_str
+ self.http_method = method
+ self.params = params
+ self.signature = signature
+
+
+# ################################################################
+
+class MockClient:
+ """
+ Mock of client credentials used by the sign_*_with_client functions.
+
+ For HMAC, set the client_secret and resource_owner_secret.
+
+ For RSA, set the rsa_key to either a PEM formatted PKCS #1 public key or
+ PEM formatted PKCS #1 private key.
+ """
+ def __init__(self,
+ client_secret: str = None,
+ resource_owner_secret: str = None,
+ rsa_key: str = None):
+ self.client_secret = client_secret
+ self.resource_owner_secret = resource_owner_secret
+ self.rsa_key = rsa_key # used for private or public key: a poor design!
+
+
+# ################################################################
+
+class SignatureTests(TestCase):
+ """
+ Unit tests for the oauthlib/oauth1/rfc5849/signature.py module.
+
+ The tests in this class are organised into sections, to test the
+ functions relating to:
+
+ - Signature base string calculation
+ - HMAC-based signature methods
+ - RSA-based signature methods
+ - PLAINTEXT signature method
+
+ Each section is separated by a comment beginning with "====".
+
+ Those comments have been formatted to remain visible when the code is
+ collapsed using PyCharm's code folding feature. That is, those section
+ heading comments do not have any other comment lines around it, so they
+ don't get collapsed when the contents of the class is collapsed. While
+ there is a "Sequential comments" option in the code folding configuration,
+ by default they are folded.
+
+ They all use some/all of the example test vector, defined in the first
+ section below.
+ """
+
+ # ==== Example test vector =======================================
+
+ eg_signature_base_string =\
+ 'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q' \
+ '%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_' \
+ 'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m' \
+ 'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk' \
+ '9d7dh3k39sjv7'
+
+ # The _signature base string_ above is copied from the end of
+ # RFC 5849 section 3.4.1.1.
+ #
+ # It corresponds to the three values below.
+ #
+ # The _normalized parameters_ below is copied from the end of
+ # RFC 5849 section 3.4.1.3.2.
+
+ eg_http_method = 'POST'
+
+ eg_base_string_uri = 'http://example.com/request'
+
+ eg_normalized_parameters =\
+ 'a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj' \
+ 'dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1' \
+ '&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7'
+
+ # The above _normalized parameters_ corresponds to the parameters below.
+ #
+ # The parameters below is copied from the table at the end of
+ # RFC 5849 section 3.4.1.3.1.
+
+ eg_params = [
+ ('b5', '=%3D'),
+ ('a3', 'a'),
+ ('c@', ''),
+ ('a2', 'r b'),
+ ('oauth_consumer_key', '9djdj82h48djs9d2'),
+ ('oauth_token', 'kkk9d7dh3k39sjv7'),
+ ('oauth_signature_method', 'HMAC-SHA1'),
+ ('oauth_timestamp', '137131201'),
+ ('oauth_nonce', '7d8f3e4a'),
+ ('c2', ''),
+ ('a3', '2 q'),
+ ]
+
+ # The above parameters correspond to parameters from the three values below.
+ #
+ # These come from RFC 5849 section 3.4.1.3.1.
+
+ eg_uri_query = 'b5=%3D%253D&a3=a&c%40=&a2=r%20b'
+
+ eg_body = 'c2&a3=2+q'
+
+ eg_authorization_header =\
+ 'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",' \
+ ' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",' \
+ ' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",' \
+ ' oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"'
+
+ # ==== Signature base string calculating function tests ==========
+
+ def test_signature_base_string(self):
+ """
+ Test the ``signature_base_string`` function.
+ """
+
+ # Example from RFC 5849
+
+ self.assertEqual(
+ self.eg_signature_base_string,
+ signature_base_string(
+ self.eg_http_method,
+ self.eg_base_string_uri,
+ self.eg_normalized_parameters))
+
+ # Test method is always uppercase in the signature base string
+
+ for test_method in ['POST', 'Post', 'pOST', 'poST', 'posT', 'post']:
+ self.assertEqual(
+ self.eg_signature_base_string,
+ signature_base_string(
+ test_method,
+ self.eg_base_string_uri,
+ self.eg_normalized_parameters))
+
+ def test_base_string_uri(self):
+ """
+ Test the ``base_string_uri`` function.
+ """
+
+ # ----------------
+ # Examples from the OAuth 1.0a specification: RFC 5849.
+
+ # First example from RFC 5849 section 3.4.1.2.
+ #
+ # GET /r%20v/X?id=123 HTTP/1.1
+ # Host: EXAMPLE.COM:80
+ #
+ # Note: there is a space between "r" and "v"
+
+ self.assertEqual(
+ 'http://example.com/r%20v/X',
+ base_string_uri('http://EXAMPLE.COM:80/r v/X?id=123'))
+
+ # Second example from RFC 5849 section 3.4.1.2.
+ #
+ # GET /?q=1 HTTP/1.1
+ # Host: www.example.net:8080
+
+ self.assertEqual(
+ 'https://www.example.net:8080/',
+ base_string_uri('https://www.example.net:8080/?q=1'))
+
+ # ----------------
+ # Scheme: will always be in lowercase
+
+ for uri in [
+ 'foobar://www.example.com',
+ 'FOOBAR://www.example.com',
+ 'Foobar://www.example.com',
+ 'FooBar://www.example.com',
+ 'fOObAR://www.example.com',
+ ]:
+ self.assertEqual('foobar://www.example.com/', base_string_uri(uri))
+
+ # ----------------
+ # Host: will always be in lowercase
+
+ for uri in [
+ 'http://www.example.com',
+ 'http://WWW.EXAMPLE.COM',
+ 'http://www.EXAMPLE.com',
+ 'http://wWW.eXAMPLE.cOM',
+ ]:
+ self.assertEqual('http://www.example.com/', base_string_uri(uri))
+
+ # base_string_uri has an optional host parameter that can be used to
+ # override the URI's netloc (or used as the host if there is no netloc)
+ # The "netloc" refers to the "hostname[:port]" part of the URI.
+
+ self.assertEqual(
+ 'http://actual.example.com/',
+ base_string_uri('http://IGNORE.example.com', 'ACTUAL.example.com'))
+
+ self.assertEqual(
+ 'http://override.example.com/path',
+ base_string_uri('http:///path', 'OVERRIDE.example.com'))
+
+ # ----------------
+ # Host: valid host allows for IPv4 and IPv6
+
+ self.assertEqual(
+ 'https://192.168.0.1/',
+ base_string_uri('https://192.168.0.1')
+ )
+ self.assertEqual(
+ 'https://192.168.0.1:13000/',
+ base_string_uri('https://192.168.0.1:13000')
+ )
+ self.assertEqual(
+ 'https://[123:db8:fd00:1000::5]:13000/',
+ base_string_uri('https://[123:db8:fd00:1000::5]:13000')
+ )
+ self.assertEqual(
+ 'https://[123:db8:fd00:1000::5]/',
+ base_string_uri('https://[123:db8:fd00:1000::5]')
+ )
+
+ # ----------------
+ # Port: default ports always excluded; non-default ports always included
+
+ self.assertEqual(
+ "http://www.example.com/",
+ base_string_uri("http://www.example.com:80/")) # default port
+
+ self.assertEqual(
+ "https://www.example.com/",
+ base_string_uri("https://www.example.com:443/")) # default port
+
+ self.assertEqual(
+ "https://www.example.com:999/",
+ base_string_uri("https://www.example.com:999/")) # non-default port
+
+ self.assertEqual(
+ "http://www.example.com:443/",
+ base_string_uri("HTTP://www.example.com:443/")) # non-default port
+
+ self.assertEqual(
+ "https://www.example.com:80/",
+ base_string_uri("HTTPS://www.example.com:80/")) # non-default port
+
+ self.assertEqual(
+ "http://www.example.com/",
+ base_string_uri("http://www.example.com:/")) # colon but no number
+
+ # ----------------
+ # Paths
+
+ self.assertEqual(
+ 'http://www.example.com/',
+ base_string_uri('http://www.example.com')) # no slash
+
+ self.assertEqual(
+ 'http://www.example.com/',
+ base_string_uri('http://www.example.com/')) # with slash
+
+ self.assertEqual(
+ 'http://www.example.com:8080/',
+ base_string_uri('http://www.example.com:8080')) # no slash
+
+ self.assertEqual(
+ 'http://www.example.com:8080/',
+ base_string_uri('http://www.example.com:8080/')) # with slash
+
+ self.assertEqual(
+ 'http://www.example.com/foo/bar',
+ base_string_uri('http://www.example.com/foo/bar')) # no slash
+ self.assertEqual(
+ 'http://www.example.com/foo/bar/',
+ base_string_uri('http://www.example.com/foo/bar/')) # with slash
+
+ # ----------------
+ # Query parameters & fragment IDs do not appear in the base string URI
+
+ self.assertEqual(
+ 'https://www.example.com/path',
+ base_string_uri('https://www.example.com/path?foo=bar'))
+
+ self.assertEqual(
+ 'https://www.example.com/path',
+ base_string_uri('https://www.example.com/path#fragment'))
+
+ # ----------------
+ # Percent encoding
+ #
+ # RFC 5849 does not specify what characters are percent encoded, but in
+ # one of its examples it shows spaces being percent encoded.
+ # So it is assumed that spaces must be encoded, but we don't know what
+ # other characters are encoded or not.
+
+ self.assertEqual(
+ 'https://www.example.com/hello%20world',
+ base_string_uri('https://www.example.com/hello world'))
+
+ self.assertEqual(
+ 'https://www.hello%20world.com/',
+ base_string_uri('https://www.hello world.com/'))
+
+ # ----------------
+ # Errors detected
+
+ # base_string_uri expects a string
+ self.assertRaises(ValueError, base_string_uri, None)
+ self.assertRaises(ValueError, base_string_uri, 42)
+ self.assertRaises(ValueError, base_string_uri, b'http://example.com')
+
+ # Missing scheme is an error
+ self.assertRaises(ValueError, base_string_uri, '')
+ self.assertRaises(ValueError, base_string_uri, ' ') # single space
+ self.assertRaises(ValueError, base_string_uri, 'http')
+ self.assertRaises(ValueError, base_string_uri, 'example.com')
+
+ # Missing host is an error
+ self.assertRaises(ValueError, base_string_uri, 'http:')
+ self.assertRaises(ValueError, base_string_uri, 'http://')
+ self.assertRaises(ValueError, base_string_uri, 'http://:8080')
+
+ # Port is not a valid TCP/IP port number
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:0')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:-1')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:65536')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:3.14')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:BAD')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:NaN')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com: ')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:42:42')
+
+ def test_collect_parameters(self):
+ """
+ Test the ``collect_parameters`` function.
+ """
+
+ # ----------------
+ # Examples from the OAuth 1.0a specification: RFC 5849.
+
+ params = collect_parameters(
+ self.eg_uri_query,
+ self.eg_body,
+ {'Authorization': self.eg_authorization_header})
+
+ # Check params contains the same pairs as control_params, ignoring order
+ self.assertEqual(sorted(self.eg_params), sorted(params))
+
+ # ----------------
+ # Examples with no parameters
+
+ self.assertEqual([], collect_parameters('', '', {}))
+
+ self.assertEqual([], collect_parameters(None, None, None))
+
+ self.assertEqual([], collect_parameters())
+
+ self.assertEqual([], collect_parameters(headers={'foo': 'bar'}))
+
+ # ----------------
+ # Test effect of exclude_oauth_signature"
+
+ no_sig = collect_parameters(
+ headers={'authorization': self.eg_authorization_header})
+ with_sig = collect_parameters(
+ headers={'authorization': self.eg_authorization_header},
+ exclude_oauth_signature=False)
+
+ self.assertEqual(sorted(no_sig + [('oauth_signature',
+ 'djosJKDKJSD8743243/jdk33klY=')]),
+ sorted(with_sig))
+
+ # ----------------
+ # Test effect of "with_realm" as well as header name case insensitivity
+
+ no_realm = collect_parameters(
+ headers={'authorization': self.eg_authorization_header},
+ with_realm=False)
+ with_realm = collect_parameters(
+ headers={'AUTHORIZATION': self.eg_authorization_header},
+ with_realm=True)
+
+ self.assertEqual(sorted(no_realm + [('realm', 'Example')]),
+ sorted(with_realm))
+
+ def test_normalize_parameters(self):
+ """
+ Test the ``normalize_parameters`` function.
+ """
+
+ # headers = {'Authorization': self.authorization_header}
+ # parameters = collect_parameters(
+ # uri_query=self.uri_query, body=self.body, headers=headers)
+ # normalized = normalize_parameters(parameters)
+ #
+ # # Unicode everywhere and always
+ # self.assertIsInstance(normalized, str)
+ #
+ # # Lets see if things are in order
+ # # check to see that querystring keys come in alphanumeric order:
+ # querystring_keys = ['a2', 'a3', 'b5', 'oauth_consumer_key',
+ # 'oauth_nonce', 'oauth_signature_method',
+ # 'oauth_timestamp', 'oauth_token']
+ # index = -1 # start at -1 because the 'a2' key starts at index 0
+ # for key in querystring_keys:
+ # self.assertGreater(normalized.index(key), index)
+ # index = normalized.index(key)
+
+ # ----------------
+ # Example from the OAuth 1.0a specification: RFC 5849.
+ # Params from end of section 3.4.1.3.1. and the expected
+ # normalized parameters from the end of section 3.4.1.3.2.
+
+ self.assertEqual(self.eg_normalized_parameters,
+ normalize_parameters(self.eg_params))
+
+ # ==== HMAC-based signature method tests =========================
+
+ hmac_client = MockClient(
+ client_secret='ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ',
+ resource_owner_secret='just-a-string asdasd')
+
+ # The following expected signatures were calculated by putting the value of
+ # the eg_signature_base_string in a file ("base-str.txt") and running:
+ #
+ # echo -n `cat base-str.txt` | openssl dgst -hmac KEY -sha1 -binary| base64
+ #
+ # Where the KEY is the concatenation of the client_secret, an ampersand and
+ # the resource_owner_secret. But those values need to be encoded properly,
+ # so the spaces in the resource_owner_secret must be represented as '%20'.
+ #
+ # Note: the "echo -n" is needed to remove the last newline character, which
+ # most text editors will add.
+
+ expected_signature_hmac_sha1 = \
+ 'wsdNmjGB7lvis0UJuPAmjvX/PXw='
+
+ expected_signature_hmac_sha256 = \
+ 'wdfdHUKXHbOnOGZP8WFAWMSAmWzN3EVBWWgXGlC/Eo4='
+
+ expected_signature_hmac_sha512 = \
+ 'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz' \
+ '1fGONffMpdZxUkUTW3k1kg=='
+
+ def test_sign_hmac_sha1_with_client(self):
+ """
+ Test sign and verify with HMAC-SHA1.
+ """
+ self.assertEqual(
+ self.expected_signature_hmac_sha1,
+ sign_hmac_sha1_with_client(self.eg_signature_base_string,
+ self.hmac_client))
+ self.assertTrue(verify_hmac_sha1(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_hmac_sha1),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ def test_sign_hmac_sha256_with_client(self):
+ """
+ Test sign and verify with HMAC-SHA256.
+ """
+ self.assertEqual(
+ self.expected_signature_hmac_sha256,
+ sign_hmac_sha256_with_client(self.eg_signature_base_string,
+ self.hmac_client))
+ self.assertTrue(verify_hmac_sha256(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_hmac_sha256),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ def test_sign_hmac_sha512_with_client(self):
+ """
+ Test sign and verify with HMAC-SHA512.
+ """
+ self.assertEqual(
+ self.expected_signature_hmac_sha512,
+ sign_hmac_sha512_with_client(self.eg_signature_base_string,
+ self.hmac_client))
+ self.assertTrue(verify_hmac_sha512(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_hmac_sha512),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ def test_hmac_false_positives(self):
+ """
+ Test verify_hmac-* functions will correctly detect invalid signatures.
+ """
+
+ _ros = self.hmac_client.resource_owner_secret
+
+ for functions in [
+ (sign_hmac_sha1_with_client, verify_hmac_sha1),
+ (sign_hmac_sha256_with_client, verify_hmac_sha256),
+ (sign_hmac_sha512_with_client, verify_hmac_sha512),
+ ]:
+ signing_function = functions[0]
+ verify_function = functions[1]
+
+ good_signature = \
+ signing_function(
+ self.eg_signature_base_string,
+ self.hmac_client)
+
+ bad_signature_on_different_value = \
+ signing_function(
+ 'not the signature base string',
+ self.hmac_client)
+
+ bad_signature_produced_by_different_client_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(client_secret='wrong-secret',
+ resource_owner_secret=_ros))
+ bad_signature_produced_by_different_resource_owner_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.hmac_client.client_secret,
+ resource_owner_secret='wrong-secret'))
+
+ bad_signature_produced_with_no_resource_owner_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.hmac_client.client_secret))
+ bad_signature_produced_with_no_client_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(resource_owner_secret=_ros))
+
+ self.assertTrue(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ good_signature),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ for bad_signature in [
+ '',
+ 'ZG9uJ3QgdHJ1c3QgbWUK', # random base64 encoded value
+ 'altérer', # value with a non-ASCII character in it
+ bad_signature_on_different_value,
+ bad_signature_produced_by_different_client_secret,
+ bad_signature_produced_by_different_resource_owner_secret,
+ bad_signature_produced_with_no_resource_owner_secret,
+ bad_signature_produced_with_no_client_secret,
+ ]:
+ self.assertFalse(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ bad_signature),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ # ==== RSA-based signature methods tests =========================
+
+ rsa_private_client = MockClient(rsa_key='''
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDk1/bxyS8Q8jiheHeYYp/4rEKJopeQRRKKpZI4s5i+UPwVpupG
+AlwXWfzXwSMaKPAoKJNdu7tqKRniqst5uoHXw98gj0x7zamu0Ck1LtQ4c7pFMVah
+5IYGhBi2E9ycNS329W27nJPWNCbESTu7snVlG8V8mfvGGg3xNjTMO7IdrwIDAQAB
+AoGBAOQ2KuH8S5+OrsL4K+wfjoCi6MfxCUyqVU9GxocdM1m30WyWRFMEz2nKJ8fR
+p3vTD4w8yplTOhcoXdQZl0kRoaDzrcYkm2VvJtQRrX7dKFT8dR8D/Tr7dNQLOXfC
+DY6xveQczE7qt7Vk7lp4FqmxBsaaEuokt78pOOjywZoInjZhAkEA9wz3zoZNT0/i
+rf6qv2qTIeieUB035N3dyw6f1BGSWYaXSuerDCD/J1qZbAPKKhyHZbVawFt3UMhe
+542UftBaxQJBAO0iJy1I8GQjGnS7B3yvyH3CcLYGy296+XO/2xKp/d/ty1OIeovx
+C60pLNwuFNF3z9d2GVQAdoQ89hUkOtjZLeMCQQD0JO6oPHUeUjYT+T7ImAv7UKVT
+Suy30sKjLzqoGw1kR+wv7C5PeDRvscs4wa4CW9s6mjSrMDkDrmCLuJDtmf55AkEA
+kmaMg2PNrjUR51F0zOEFycaaqXbGcFwe1/xx9zLmHzMDXd4bsnwt9kk+fe0hQzVS
+JzatanQit3+feev1PN3QewJAWv4RZeavEUhKv+kLe95Yd0su7lTLVduVgh4v5yLT
+Ga6FHdjGPcfajt+nrpB1n8UQBEH9ZxniokR/IPvdMlxqXA==
+-----END RSA PRIVATE KEY-----
+''')
+
+ rsa_public_client = MockClient(rsa_key='''
+-----BEGIN RSA PUBLIC KEY-----
+MIGJAoGBAOTX9vHJLxDyOKF4d5hin/isQomil5BFEoqlkjizmL5Q/BWm6kYCXBdZ
+/NfBIxoo8Cgok127u2opGeKqy3m6gdfD3yCPTHvNqa7QKTUu1DhzukUxVqHkhgaE
+GLYT3Jw1Lfb1bbuck9Y0JsRJO7uydWUbxXyZ+8YaDfE2NMw7sh2vAgMBAAE=
+-----END RSA PUBLIC KEY-----
+''')
+
+ # The above private key was generated using:
+ # $ openssl genrsa -out example.pvt 1024
+ # $ chmod 600 example.pvt
+ # Public key was extract from it using:
+ # $ ssh-keygen -e -m pem -f example.pvt
+ # PEM encoding requires the key to be concatenated with linebreaks.
+
+ # The following expected signatures were calculated by putting the private
+ # key in a file (test.pvt) and the value of sig_base_str_rsa in another file
+ # ("base-str.txt") and running:
+ #
+ # echo -n `cat base-str.txt` | openssl dgst -sha1 -sign test.pvt| base64
+ #
+ # Note: the "echo -n" is needed to remove the last newline character, which
+ # most text editors will add.
+
+ expected_signature_rsa_sha1 = \
+ 'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738' \
+ 'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q' \
+ 'yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E='
+
+ expected_signature_rsa_sha256 = \
+ 'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR' \
+ 'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK' \
+ '/N7s9M1WMnfZZ+1dQbbwtTs1TG1+iexUcV7r3M7Heec='
+
+ expected_signature_rsa_sha512 = \
+ 'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL' \
+ 'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm' \
+ 'g4JXdWCGXG8YRvRJTg+QKhXuXwS6TcMNakrgzgFIVhA='
+
+ def test_sign_rsa_sha1_with_client(self):
+ """
+ Test sign and verify with RSA-SHA1.
+ """
+ self.assertEqual(
+ self.expected_signature_rsa_sha1,
+ sign_rsa_sha1_with_client(self.eg_signature_base_string,
+ self.rsa_private_client))
+ self.assertTrue(verify_rsa_sha1(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha1),
+ self.rsa_public_client.rsa_key))
+
+ def test_sign_rsa_sha256_with_client(self):
+ """
+ Test sign and verify with RSA-SHA256.
+ """
+ self.assertEqual(
+ self.expected_signature_rsa_sha256,
+ sign_rsa_sha256_with_client(self.eg_signature_base_string,
+ self.rsa_private_client))
+ self.assertTrue(verify_rsa_sha256(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha256),
+ self.rsa_public_client.rsa_key))
+
+ def test_sign_rsa_sha512_with_client(self):
+ """
+ Test sign and verify with RSA-SHA512.
+ """
+ self.assertEqual(
+ self.expected_signature_rsa_sha512,
+ sign_rsa_sha512_with_client(self.eg_signature_base_string,
+ self.rsa_private_client))
+ self.assertTrue(verify_rsa_sha512(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha512),
+ self.rsa_public_client.rsa_key))
+
+ def test_rsa_false_positives(self):
+ """
+ Test verify_rsa-* functions will correctly detect invalid signatures.
+ """
+
+ another_client = MockClient(rsa_key='''
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDZcD/1OZNJJ6Y3QZM16Z+O7fkD9kTIQuT2BfpAOUvDfxzYhVC9
+TNmSDHCQhr+ClutyolBk5jTE1/FXFUuHoPsTrkI7KQFXPP834D4gnSY9jrAiUJHe
+DVF6wXNuS7H4Ueh16YPjUxgLLRh/nn/JSEj98gsw+7DP01OWMfWS99S7eQIDAQAB
+AoGBALsQZRXVyK7BG7CiC8HwEcNnXDpaXmZjlpNKJTenk1THQMvONd4GBZAuf5D3
+PD9fE4R1u/ByVKecmBaxTV+L0TRQfD8K/nbQe0SKRQIkLI2ymLJKC/eyw5iTKT0E
++BS6wYpVd+mfcqgvpHOYpUmz9X8k/eOa7uslFmvt+sDb5ZcBAkEA+++SRqqUxFEG
+s/ZWAKw9p5YgkeVUOYVUwyAeZ97heySrjVzg1nZ6v6kv7iOPi9KOEpaIGPW7x1K/
+uQuSt4YEqQJBANzyNqZTTPpv7b/R8ABFy0YMwPVNt3b1GOU1Xxl6iuhH2WcHuueo
+UB13JHoZCMZ7hsEqieEz6uteUjdRzRPKclECQFNhVK4iop3emzNQYeJTHwyp+RmQ
+JrHq2MTDioyiDUouNsDQbnFMQQ/RtNVB265Q/0hTnbN1ELLFRkK9+87VghECQQC9
+hacLFPk6+TffCp3sHfI3rEj4Iin1iFhKhHWGzW7JwJfjoOXaQK44GDLZ6Q918g+t
+MmgDHR2tt8KeYTSgfU+BAkBcaVF91EQ7VXhvyABNYjeYP7lU7orOgdWMa/zbLXSU
+4vLsK1WOmwPY9zsXpPkilqszqcru4gzlG462cSbEdAW9
+-----END RSA PRIVATE KEY-----
+''')
+
+ for functions in [
+ (sign_rsa_sha1_with_client, verify_rsa_sha1),
+ (sign_rsa_sha256_with_client, verify_rsa_sha256),
+ (sign_rsa_sha512_with_client, verify_rsa_sha512),
+ ]:
+ signing_function = functions[0]
+ verify_function = functions[1]
+
+ good_signature = \
+ signing_function(self.eg_signature_base_string,
+ self.rsa_private_client)
+
+ bad_signature_on_different_value = \
+ signing_function('wrong value signed', self.rsa_private_client)
+
+ bad_signature_produced_by_different_private_key = \
+ signing_function(self.eg_signature_base_string, another_client)
+
+ self.assertTrue(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ good_signature),
+ self.rsa_public_client.rsa_key))
+
+ for bad_signature in [
+ '',
+ 'ZG9uJ3QgdHJ1c3QgbWUK', # random base64 encoded value
+ 'altérer', # value with a non-ASCII character in it
+ bad_signature_on_different_value,
+ bad_signature_produced_by_different_private_key,
+ ]:
+ self.assertFalse(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ bad_signature),
+ self.rsa_public_client.rsa_key))
+
+ def test_rsa_bad_keys(self):
+ """
+ Testing RSA sign and verify with bad key values produces errors.
+
+ This test is useful for coverage tests, since it runs the code branches
+ that deal with error situations.
+ """
+
+ # Signing needs a private key
+
+ for bad_value in [None, '', 'foobar']:
+ self.assertRaises(ValueError,
+ sign_rsa_sha1_with_client,
+ self.eg_signature_base_string,
+ MockClient(rsa_key=bad_value))
+
+ self.assertRaises(AttributeError,
+ sign_rsa_sha1_with_client,
+ self.eg_signature_base_string,
+ self.rsa_public_client) # public key doesn't sign
+
+ # Verify needs a public key
+
+ for bad_value in [None, '', 'foobar', self.rsa_private_client.rsa_key]:
+ self.assertRaises(TypeError,
+ verify_rsa_sha1,
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha1),
+ MockClient(rsa_key=bad_value))
+
+ # For completeness, this text could repeat the above for RSA-SHA256 and
+ # RSA-SHA512 signing and verification functions.
+
+ def test_rsa_jwt_algorithm_cache(self):
+ # Tests cache of RSAAlgorithm objects is implemented correctly.
+
+ # This is difficult to test, since the cache is internal.
+ #
+ # Running this test with coverage will show the cache-hit branch of code
+ # being executed by two signing operations with the same hash algorithm.
+
+ self.test_sign_rsa_sha1_with_client() # creates cache entry
+ self.test_sign_rsa_sha1_with_client() # reuses cache entry
+
+ # Some possible bugs will be detected if multiple signing operations
+ # with different hash algorithms produce the wrong results (e.g. if the
+ # cache incorrectly returned the previously used algorithm, instead
+ # of the one that is needed).
+
+ self.test_sign_rsa_sha256_with_client()
+ self.test_sign_rsa_sha256_with_client()
+ self.test_sign_rsa_sha1_with_client()
+ self.test_sign_rsa_sha256_with_client()
+ self.test_sign_rsa_sha512_with_client()
+
+ # ==== PLAINTEXT signature method tests ==========================
+
+ plaintext_client = hmac_client # for convenience, use the same HMAC secrets
+
+ expected_signature_plaintext = (
+ 'ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ'
+ '&'
+ 'just-a-string%20%20%20%20asdasd')
+
+ def test_sign_plaintext_with_client(self):
+ # With PLAINTEXT, the "signature" is always the same: regardless of the
+ # contents of the request. It is the concatenation of the encoded
+ # client_secret, an ampersand, and the encoded resource_owner_secret.
+ #
+ # That is why the spaces in the resource owner secret are "%20".
+
+ self.assertEqual(self.expected_signature_plaintext,
+ sign_plaintext_with_client(None, # request is ignored
+ self.plaintext_client))
+ self.assertTrue(verify_plaintext(
+ MockRequest('PUT',
+ 'http://example.com/some-other-path',
+ [('description', 'request is ignored in PLAINTEXT')],
+ self.expected_signature_plaintext),
+ self.plaintext_client.client_secret,
+ self.plaintext_client.resource_owner_secret))
+
+ def test_plaintext_false_positives(self):
+ """
+ Test verify_plaintext function will correctly detect invalid signatures.
+ """
+
+ _ros = self.plaintext_client.resource_owner_secret
+
+ good_signature = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ self.plaintext_client)
+
+ bad_signature_produced_by_different_client_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(client_secret='wrong-secret',
+ resource_owner_secret=_ros))
+ bad_signature_produced_by_different_resource_owner_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.plaintext_client.client_secret,
+ resource_owner_secret='wrong-secret'))
+
+ bad_signature_produced_with_no_resource_owner_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.plaintext_client.client_secret))
+ bad_signature_produced_with_no_client_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(resource_owner_secret=_ros))
+
+ self.assertTrue(verify_plaintext(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ good_signature),
+ self.plaintext_client.client_secret,
+ self.plaintext_client.resource_owner_secret))
+
+ for bad_signature in [
+ '',
+ 'ZG9uJ3QgdHJ1c3QgbWUK', # random base64 encoded value
+ 'altérer', # value with a non-ASCII character in it
+ bad_signature_produced_by_different_client_secret,
+ bad_signature_produced_by_different_resource_owner_secret,
+ bad_signature_produced_with_no_resource_owner_secret,
+ bad_signature_produced_with_no_client_secret,
+ ]:
+ self.assertFalse(verify_plaintext(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ bad_signature),
+ self.plaintext_client.client_secret,
+ self.plaintext_client.resource_owner_secret))
diff --git a/contrib/python/oauthlib/tests/oauth1/rfc5849/test_utils.py b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_utils.py
new file mode 100644
index 0000000000..013c71a910
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth1/rfc5849/test_utils.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+from oauthlib.oauth1.rfc5849.utils import *
+
+from tests.unittest import TestCase
+
+
+class UtilsTests(TestCase):
+
+ sample_params_list = [
+ ("notoauth", "shouldnotbehere"),
+ ("oauth_consumer_key", "9djdj82h48djs9d2"),
+ ("oauth_token", "kkk9d7dh3k39sjv7"),
+ ("notoautheither", "shouldnotbehere")
+ ]
+
+ sample_params_dict = {
+ "notoauth": "shouldnotbehere",
+ "oauth_consumer_key": "9djdj82h48djs9d2",
+ "oauth_token": "kkk9d7dh3k39sjv7",
+ "notoautheither": "shouldnotbehere"
+ }
+
+ sample_params_unicode_list = [
+ ("notoauth", "shouldnotbehere"),
+ ("oauth_consumer_key", "9djdj82h48djs9d2"),
+ ("oauth_token", "kkk9d7dh3k39sjv7"),
+ ("notoautheither", "shouldnotbehere")
+ ]
+
+ sample_params_unicode_dict = {
+ "notoauth": "shouldnotbehere",
+ "oauth_consumer_key": "9djdj82h48djs9d2",
+ "oauth_token": "kkk9d7dh3k39sjv7",
+ "notoautheither": "shouldnotbehere"
+ }
+
+ authorization_header = """OAuth realm="Example",
+ oauth_consumer_key="9djdj82h48djs9d2",
+ oauth_token="kkk9d7dh3k39sjv7",
+ oauth_signature_method="HMAC-SHA1",
+ oauth_timestamp="137131201",
+ oauth_nonce="7d8f3e4a",
+ oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D" """.strip()
+ bad_authorization_headers = (
+ "OAuth",
+ "OAuth oauth_nonce=",
+ "Negotiate b2F1dGhsaWI=",
+ "OA",
+ )
+
+ def test_filter_params(self):
+
+ # The following is an isolated test function used to test the filter_params decorator.
+ @filter_params
+ def special_test_function(params, realm=None):
+ """ I am a special test function """
+ return 'OAuth ' + ','.join(['='.join([k, v]) for k, v in params])
+
+ # check that the docstring got through
+ self.assertEqual(special_test_function.__doc__, " I am a special test function ")
+
+ # Check that the decorator filtering works as per design.
+ # Any param that does not start with 'oauth'
+ # should not be present in the filtered params
+ filtered_params = special_test_function(self.sample_params_list)
+ self.assertNotIn("notoauth", filtered_params)
+ self.assertIn("oauth_consumer_key", filtered_params)
+ self.assertIn("oauth_token", filtered_params)
+ self.assertNotIn("notoautheither", filtered_params)
+
+ def test_filter_oauth_params(self):
+
+ # try with list
+ # try with list
+ # try with list
+ self.assertEqual(len(self.sample_params_list), 4)
+
+ # Any param that does not start with 'oauth'
+ # should not be present in the filtered params
+ filtered_params = filter_oauth_params(self.sample_params_list)
+ self.assertEqual(len(filtered_params), 2)
+
+ self.assertTrue(filtered_params[0][0].startswith('oauth'))
+ self.assertTrue(filtered_params[1][0].startswith('oauth'))
+
+ # try with dict
+ # try with dict
+ # try with dict
+ self.assertEqual(len(self.sample_params_dict), 4)
+
+ # Any param that does not start with 'oauth'
+ # should not be present in the filtered params
+ filtered_params = filter_oauth_params(self.sample_params_dict)
+ self.assertEqual(len(filtered_params), 2)
+
+ self.assertTrue(filtered_params[0][0].startswith('oauth'))
+ self.assertTrue(filtered_params[1][0].startswith('oauth'))
+
+ def test_escape(self):
+ self.assertRaises(ValueError, escape, b"I am a string type. Not a unicode type.")
+ self.assertEqual(escape("I am a unicode type."), "I%20am%20a%20unicode%20type.")
+ self.assertIsInstance(escape("I am a unicode type."), str)
+
+ def test_unescape(self):
+ self.assertRaises(ValueError, unescape, b"I am a string type. Not a unicode type.")
+ self.assertEqual(unescape("I%20am%20a%20unicode%20type."), 'I am a unicode type.')
+ self.assertIsInstance(unescape("I%20am%20a%20unicode%20type."), str)
+
+ def test_parse_authorization_header(self):
+ # make us some headers
+ authorization_headers = parse_authorization_header(self.authorization_header)
+
+ # is it a list?
+ self.assertIsInstance(authorization_headers, list)
+
+ # are the internal items tuples?
+ for header in authorization_headers:
+ self.assertIsInstance(header, tuple)
+
+ # are the internal components of each tuple unicode?
+ for k, v in authorization_headers:
+ self.assertIsInstance(k, str)
+ self.assertIsInstance(v, str)
+
+ # let's check the parsed headers created
+ correct_headers = [
+ ("oauth_nonce", "7d8f3e4a"),
+ ("oauth_timestamp", "137131201"),
+ ("oauth_consumer_key", "9djdj82h48djs9d2"),
+ ('oauth_signature', 'djosJKDKJSD8743243%2Fjdk33klY%3D'),
+ ('oauth_signature_method', 'HMAC-SHA1'),
+ ('oauth_token', 'kkk9d7dh3k39sjv7'),
+ ('realm', 'Example')]
+ self.assertEqual(sorted(authorization_headers), sorted(correct_headers))
+
+ # Check against malformed headers.
+ for header in self.bad_authorization_headers:
+ self.assertRaises(ValueError, parse_authorization_header, header)
diff --git a/contrib/python/oauthlib/tests/oauth2/__init__.py b/contrib/python/oauthlib/tests/oauth2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/__init__.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/__init__.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_backend_application.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_backend_application.py
new file mode 100644
index 0000000000..c1489ac7c6
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_backend_application.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+import os
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2 import BackendApplicationClient
+
+from tests.unittest import TestCase
+
+
+@patch('time.time', new=lambda: 1000)
+class BackendApplicationClientTest(TestCase):
+
+ client_id = "someclientid"
+ client_secret = 'someclientsecret'
+ scope = ["/profile"]
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+
+ body = "not=empty"
+
+ body_up = "not=empty&grant_type=client_credentials"
+ body_kwargs = body_up + "&some=providers&require=extra+arguments"
+
+ token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type":"example",'
+ ' "expires_in":3600,'
+ ' "scope":"/profile",'
+ ' "example_parameter":"example_value"}')
+ token = {
+ "access_token": "2YotnFZFEjr1zCsicMWpAA",
+ "token_type": "example",
+ "expires_in": 3600,
+ "expires_at": 4600,
+ "scope": ["/profile"],
+ "example_parameter": "example_value"
+ }
+
+ def test_request_body(self):
+ client = BackendApplicationClient(self.client_id)
+
+ # Basic, no extra arguments
+ body = client.prepare_request_body(body=self.body)
+ self.assertFormBodyEqual(body, self.body_up)
+
+ rclient = BackendApplicationClient(self.client_id)
+ body = rclient.prepare_request_body(body=self.body)
+ self.assertFormBodyEqual(body, self.body_up)
+
+ # With extra parameters
+ body = client.prepare_request_body(body=self.body, **self.kwargs)
+ self.assertFormBodyEqual(body, self.body_kwargs)
+
+ def test_parse_token_response(self):
+ client = BackendApplicationClient(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_body_response(self.token_json, scope=self.scope)
+ self.assertEqual(response, self.token)
+ self.assertEqual(client.access_token, response.get("access_token"))
+ self.assertEqual(client.refresh_token, response.get("refresh_token"))
+ self.assertEqual(client.token_type, response.get("token_type"))
+
+ # Mismatching state
+ self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '3'
+ token = client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertTrue(token.scope_changed)
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_base.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_base.py
new file mode 100644
index 0000000000..70a22834c3
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_base.py
@@ -0,0 +1,355 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+from oauthlib import common
+from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError
+from oauthlib.oauth2.rfc6749 import utils
+from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY
+
+from tests.unittest import TestCase
+
+
+class ClientTest(TestCase):
+
+ client_id = "someclientid"
+ uri = "https://example.com/path?query=world"
+ body = "not=empty"
+ headers = {}
+ access_token = "token"
+ mac_key = "secret"
+
+ bearer_query = uri + "&access_token=" + access_token
+ bearer_header = {
+ "Authorization": "Bearer " + access_token
+ }
+ bearer_body = body + "&access_token=" + access_token
+
+ mac_00_header = {
+ "Authorization": 'MAC id="' + access_token + '", nonce="0:abc123",' +
+ ' bodyhash="Yqyso8r3hR5Nm1ZFv+6AvNHrxjE=",' +
+ ' mac="0X6aACoBY0G6xgGZVJ1IeE8dF9k="'
+ }
+ mac_01_header = {
+ "Authorization": 'MAC id="' + access_token + '", ts="123456789",' +
+ ' nonce="abc123", mac="Xuk+9oqaaKyhitkgh1CD0xrI6+s="'
+ }
+
+ def test_add_bearer_token(self):
+ """Test a number of bearer token placements"""
+
+ # Invalid token type
+ client = Client(self.client_id, token_type="invalid")
+ self.assertRaises(ValueError, client.add_token, self.uri)
+
+ # Case-insensitive token type
+ client = Client(self.client_id, access_token=self.access_token, token_type="bEAreR")
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers)
+ self.assertURLEqual(uri, self.uri)
+ self.assertFormBodyEqual(body, self.body)
+ self.assertEqual(headers, self.bearer_header)
+
+ # Non-HTTPS
+ insecure_uri = 'http://example.com/path?query=world'
+ client = Client(self.client_id, access_token=self.access_token, token_type="Bearer")
+ self.assertRaises(InsecureTransportError, client.add_token, insecure_uri,
+ body=self.body,
+ headers=self.headers)
+
+ # Missing access token
+ client = Client(self.client_id)
+ self.assertRaises(ValueError, client.add_token, self.uri)
+
+ # Expired token
+ expired = 523549800
+ expired_token = {
+ 'expires_at': expired,
+ }
+ client = Client(self.client_id, token=expired_token, access_token=self.access_token, token_type="Bearer")
+ self.assertRaises(TokenExpiredError, client.add_token, self.uri,
+ body=self.body, headers=self.headers)
+
+ # The default token placement, bearer in auth header
+ client = Client(self.client_id, access_token=self.access_token)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers)
+ self.assertURLEqual(uri, self.uri)
+ self.assertFormBodyEqual(body, self.body)
+ self.assertEqual(headers, self.bearer_header)
+
+ # Setting default placements of tokens
+ client = Client(self.client_id, access_token=self.access_token,
+ default_token_placement=AUTH_HEADER)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers)
+ self.assertURLEqual(uri, self.uri)
+ self.assertFormBodyEqual(body, self.body)
+ self.assertEqual(headers, self.bearer_header)
+
+ client = Client(self.client_id, access_token=self.access_token,
+ default_token_placement=URI_QUERY)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers)
+ self.assertURLEqual(uri, self.bearer_query)
+ self.assertFormBodyEqual(body, self.body)
+ self.assertEqual(headers, self.headers)
+
+ client = Client(self.client_id, access_token=self.access_token,
+ default_token_placement=BODY)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers)
+ self.assertURLEqual(uri, self.uri)
+ self.assertFormBodyEqual(body, self.bearer_body)
+ self.assertEqual(headers, self.headers)
+
+ # Asking for specific placement in the add_token method
+ client = Client(self.client_id, access_token=self.access_token)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers, token_placement=AUTH_HEADER)
+ self.assertURLEqual(uri, self.uri)
+ self.assertFormBodyEqual(body, self.body)
+ self.assertEqual(headers, self.bearer_header)
+
+ client = Client(self.client_id, access_token=self.access_token)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers, token_placement=URI_QUERY)
+ self.assertURLEqual(uri, self.bearer_query)
+ self.assertFormBodyEqual(body, self.body)
+ self.assertEqual(headers, self.headers)
+
+ client = Client(self.client_id, access_token=self.access_token)
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers, token_placement=BODY)
+ self.assertURLEqual(uri, self.uri)
+ self.assertFormBodyEqual(body, self.bearer_body)
+ self.assertEqual(headers, self.headers)
+
+ # Invalid token placement
+ client = Client(self.client_id, access_token=self.access_token)
+ self.assertRaises(ValueError, client.add_token, self.uri, body=self.body,
+ headers=self.headers, token_placement="invalid")
+
+ client = Client(self.client_id, access_token=self.access_token,
+ default_token_placement="invalid")
+ self.assertRaises(ValueError, client.add_token, self.uri, body=self.body,
+ headers=self.headers)
+
+ def test_add_mac_token(self):
+ # Missing access token
+ client = Client(self.client_id, token_type="MAC")
+ self.assertRaises(ValueError, client.add_token, self.uri)
+
+ # Invalid hash algorithm
+ client = Client(self.client_id, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-2")
+ self.assertRaises(ValueError, client.add_token, self.uri)
+
+ orig_generate_timestamp = common.generate_timestamp
+ orig_generate_nonce = common.generate_nonce
+ orig_generate_age = utils.generate_age
+ self.addCleanup(setattr, common, 'generage_timestamp', orig_generate_timestamp)
+ self.addCleanup(setattr, common, 'generage_nonce', orig_generate_nonce)
+ self.addCleanup(setattr, utils, 'generate_age', orig_generate_age)
+ common.generate_timestamp = lambda: '123456789'
+ common.generate_nonce = lambda: 'abc123'
+ utils.generate_age = lambda *args: 0
+
+ # Add the Authorization header (draft 00)
+ client = Client(self.client_id, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-1")
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers, issue_time=datetime.datetime.now())
+ self.assertEqual(uri, self.uri)
+ self.assertEqual(body, self.body)
+ self.assertEqual(headers, self.mac_00_header)
+ # Non-HTTPS
+ insecure_uri = 'http://example.com/path?query=world'
+ self.assertRaises(InsecureTransportError, client.add_token, insecure_uri,
+ body=self.body,
+ headers=self.headers,
+ issue_time=datetime.datetime.now())
+ # Expired Token
+ expired = 523549800
+ expired_token = {
+ 'expires_at': expired,
+ }
+ client = Client(self.client_id, token=expired_token, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-1")
+ self.assertRaises(TokenExpiredError, client.add_token, self.uri,
+ body=self.body,
+ headers=self.headers,
+ issue_time=datetime.datetime.now())
+
+ # Add the Authorization header (draft 01)
+ client = Client(self.client_id, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-1")
+ uri, headers, body = client.add_token(self.uri, body=self.body,
+ headers=self.headers, draft=1)
+ self.assertEqual(uri, self.uri)
+ self.assertEqual(body, self.body)
+ self.assertEqual(headers, self.mac_01_header)
+ # Non-HTTPS
+ insecure_uri = 'http://example.com/path?query=world'
+ self.assertRaises(InsecureTransportError, client.add_token, insecure_uri,
+ body=self.body,
+ headers=self.headers,
+ draft=1)
+ # Expired Token
+ expired = 523549800
+ expired_token = {
+ 'expires_at': expired,
+ }
+ client = Client(self.client_id, token=expired_token, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-1")
+ self.assertRaises(TokenExpiredError, client.add_token, self.uri,
+ body=self.body,
+ headers=self.headers,
+ draft=1)
+
+ def test_revocation_request(self):
+ client = Client(self.client_id)
+
+ url = 'https://example.com/revoke'
+ token = 'foobar'
+
+ # Valid request
+ u, h, b = client.prepare_token_revocation_request(url, token)
+ self.assertEqual(u, url)
+ self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(b, 'token=%s&token_type_hint=access_token' % token)
+
+ # Non-HTTPS revocation endpoint
+ self.assertRaises(InsecureTransportError,
+ client.prepare_token_revocation_request,
+ 'http://example.com/revoke', token)
+
+
+ u, h, b = client.prepare_token_revocation_request(
+ url, token, token_type_hint='refresh_token')
+ self.assertEqual(u, url)
+ self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(b, 'token=%s&token_type_hint=refresh_token' % token)
+
+ # JSONP
+ u, h, b = client.prepare_token_revocation_request(
+ url, token, callback='hello.world')
+ self.assertURLEqual(u, url + '?callback=hello.world&token=%s&token_type_hint=access_token' % token)
+ self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(b, '')
+
+ def test_prepare_authorization_request(self):
+ redirect_url = 'https://example.com/callback/'
+ scopes = 'read'
+ auth_url = 'https://example.com/authorize/'
+ state = 'fake_state'
+
+ client = Client(self.client_id, redirect_url=redirect_url, scope=scopes, state=state)
+
+ # Non-HTTPS
+ self.assertRaises(InsecureTransportError,
+ client.prepare_authorization_request, 'http://example.com/authorize/')
+
+ # NotImplementedError
+ self.assertRaises(NotImplementedError, client.prepare_authorization_request, auth_url)
+
+ def test_prepare_token_request(self):
+ redirect_url = 'https://example.com/callback/'
+ scopes = 'read'
+ token_url = 'https://example.com/token/'
+ state = 'fake_state'
+
+ client = Client(self.client_id, scope=scopes, state=state)
+
+ # Non-HTTPS
+ self.assertRaises(InsecureTransportError,
+ client.prepare_token_request, 'http://example.com/token/')
+
+ # NotImplementedError
+ self.assertRaises(NotImplementedError, client.prepare_token_request, token_url)
+
+ def test_prepare_refresh_token_request(self):
+ client = Client(self.client_id)
+
+ url = 'https://example.com/revoke'
+ token = 'foobar'
+ scope = 'extra_scope'
+
+ u, h, b = client.prepare_refresh_token_request(url, token)
+ self.assertEqual(u, url)
+ self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertFormBodyEqual(b, 'grant_type=refresh_token&refresh_token=%s' % token)
+
+ # Non-HTTPS revocation endpoint
+ self.assertRaises(InsecureTransportError,
+ client.prepare_refresh_token_request,
+ 'http://example.com/revoke', token)
+
+ # provide extra scope
+ u, h, b = client.prepare_refresh_token_request(url, token, scope=scope)
+ self.assertEqual(u, url)
+ self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertFormBodyEqual(b, 'grant_type=refresh_token&scope={}&refresh_token={}'.format(scope, token))
+
+ # provide scope while init
+ client = Client(self.client_id, scope=scope)
+ u, h, b = client.prepare_refresh_token_request(url, token, scope=scope)
+ self.assertEqual(u, url)
+ self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertFormBodyEqual(b, 'grant_type=refresh_token&scope={}&refresh_token={}'.format(scope, token))
+
+ def test_parse_token_response_invalid_expires_at(self):
+ token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type":"example",'
+ ' "expires_at":"2006-01-02T15:04:05Z",'
+ ' "scope":"/profile",'
+ ' "example_parameter":"example_value"}')
+ token = {
+ "access_token": "2YotnFZFEjr1zCsicMWpAA",
+ "token_type": "example",
+ "expires_at": "2006-01-02T15:04:05Z",
+ "scope": ["/profile"],
+ "example_parameter": "example_value"
+ }
+
+ client = Client(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_body_response(token_json, scope=["/profile"])
+ self.assertEqual(response, token)
+ self.assertEqual(None, client._expires_at)
+ self.assertEqual(client.access_token, response.get("access_token"))
+ self.assertEqual(client.refresh_token, response.get("refresh_token"))
+ self.assertEqual(client.token_type, response.get("token_type"))
+
+
+ def test_create_code_verifier_min_length(self):
+ client = Client(self.client_id)
+ length = 43
+ code_verifier = client.create_code_verifier(length=length)
+ self.assertEqual(client.code_verifier, code_verifier)
+
+ def test_create_code_verifier_max_length(self):
+ client = Client(self.client_id)
+ length = 128
+ code_verifier = client.create_code_verifier(length=length)
+ self.assertEqual(client.code_verifier, code_verifier)
+
+ def test_create_code_challenge_plain(self):
+ client = Client(self.client_id)
+ code_verifier = client.create_code_verifier(length=128)
+ code_challenge_plain = client.create_code_challenge(code_verifier=code_verifier)
+
+ # if no code_challenge_method specified, code_challenge = code_verifier
+ self.assertEqual(code_challenge_plain, client.code_verifier)
+ self.assertEqual(client.code_challenge_method, "plain")
+
+ def test_create_code_challenge_s256(self):
+ client = Client(self.client_id)
+ code_verifier = client.create_code_verifier(length=128)
+ code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256')
+ self.assertEqual(code_challenge_s256, client.code_challenge)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_legacy_application.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_legacy_application.py
new file mode 100644
index 0000000000..b5a18194b7
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_legacy_application.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+import os
+import urllib.parse as urlparse
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2 import LegacyApplicationClient
+
+from tests.unittest import TestCase
+
+
+@patch('time.time', new=lambda: 1000)
+class LegacyApplicationClientTest(TestCase):
+
+ client_id = "someclientid"
+ client_secret = 'someclientsecret'
+ scope = ["/profile"]
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+
+ username = "user_username"
+ password = "user_password"
+ body = "not=empty"
+
+ body_up = "not=empty&grant_type=password&username={}&password={}".format(username, password)
+ body_kwargs = body_up + "&some=providers&require=extra+arguments"
+
+ token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type":"example",'
+ ' "expires_in":3600,'
+ ' "scope":"/profile",'
+ ' "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter":"example_value"}')
+ token = {
+ "access_token": "2YotnFZFEjr1zCsicMWpAA",
+ "token_type": "example",
+ "expires_in": 3600,
+ "expires_at": 4600,
+ "scope": scope,
+ "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
+ "example_parameter": "example_value"
+ }
+
+ def test_request_body(self):
+ client = LegacyApplicationClient(self.client_id)
+
+ # Basic, no extra arguments
+ body = client.prepare_request_body(self.username, self.password,
+ body=self.body)
+ self.assertFormBodyEqual(body, self.body_up)
+
+ # With extra parameters
+ body = client.prepare_request_body(self.username, self.password,
+ body=self.body, **self.kwargs)
+ self.assertFormBodyEqual(body, self.body_kwargs)
+
+ def test_parse_token_response(self):
+ client = LegacyApplicationClient(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_body_response(self.token_json, scope=self.scope)
+ self.assertEqual(response, self.token)
+ self.assertEqual(client.access_token, response.get("access_token"))
+ self.assertEqual(client.refresh_token, response.get("refresh_token"))
+ self.assertEqual(client.token_type, response.get("token_type"))
+
+ # Mismatching state
+ self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '5'
+ token = client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertTrue(token.scope_changed)
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
+
+ def test_prepare_request_body(self):
+ """
+ see issue #585
+ https://github.com/oauthlib/oauthlib/issues/585
+ """
+ client = LegacyApplicationClient(self.client_id)
+
+ # scenario 1, default behavior to not include `client_id`
+ r1 = client.prepare_request_body(username=self.username, password=self.password)
+ self.assertIn(r1, ('grant_type=password&username={}&password={}'.format(self.username, self.password),
+ 'grant_type=password&password={}&username={}'.format(self.password, self.username),
+ ))
+
+ # scenario 2, include `client_id` in the body
+ r2 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True)
+ r2_params = dict(urlparse.parse_qsl(r2, keep_blank_values=True))
+ self.assertEqual(len(r2_params.keys()), 4)
+ self.assertEqual(r2_params['grant_type'], 'password')
+ self.assertEqual(r2_params['username'], self.username)
+ self.assertEqual(r2_params['password'], self.password)
+ self.assertEqual(r2_params['client_id'], self.client_id)
+
+ # scenario 3, include `client_id` + `client_secret` in the body
+ r3 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=self.client_secret)
+ r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True))
+ self.assertEqual(len(r3_params.keys()), 5)
+ self.assertEqual(r3_params['grant_type'], 'password')
+ self.assertEqual(r3_params['username'], self.username)
+ self.assertEqual(r3_params['password'], self.password)
+ self.assertEqual(r3_params['client_id'], self.client_id)
+ self.assertEqual(r3_params['client_secret'], self.client_secret)
+
+ # scenario 4, `client_secret` is an empty string
+ r4 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret='')
+ r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True))
+ self.assertEqual(len(r4_params.keys()), 5)
+ self.assertEqual(r4_params['grant_type'], 'password')
+ self.assertEqual(r4_params['username'], self.username)
+ self.assertEqual(r4_params['password'], self.password)
+ self.assertEqual(r4_params['client_id'], self.client_id)
+ self.assertEqual(r4_params['client_secret'], '')
+
+ # scenario 4b`,` client_secret is `None`
+ r4b = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=None)
+ r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True))
+ self.assertEqual(len(r4b_params.keys()), 4)
+ self.assertEqual(r4b_params['grant_type'], 'password')
+ self.assertEqual(r4b_params['username'], self.username)
+ self.assertEqual(r4b_params['password'], self.password)
+ self.assertEqual(r4b_params['client_id'], self.client_id)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_mobile_application.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_mobile_application.py
new file mode 100644
index 0000000000..c40950c978
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_mobile_application.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+import os
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2 import MobileApplicationClient
+
+from tests.unittest import TestCase
+
+
+@patch('time.time', new=lambda: 1000)
+class MobileApplicationClientTest(TestCase):
+
+ client_id = "someclientid"
+ uri = "https://example.com/path?query=world"
+ uri_id = uri + "&response_type=token&client_id=" + client_id
+ uri_redirect = uri_id + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback"
+ redirect_uri = "http://my.page.com/callback"
+ scope = ["/profile"]
+ state = "xyz"
+ uri_scope = uri_id + "&scope=%2Fprofile"
+ uri_state = uri_id + "&state=" + state
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+ uri_kwargs = uri_id + "&some=providers&require=extra+arguments"
+
+ code = "zzzzaaaa"
+
+ response_uri = ('https://client.example.com/cb?#'
+ 'access_token=2YotnFZFEjr1zCsicMWpAA&'
+ 'token_type=example&'
+ 'expires_in=3600&'
+ 'scope=%2Fprofile&'
+ 'example_parameter=example_value')
+ token = {
+ "access_token": "2YotnFZFEjr1zCsicMWpAA",
+ "token_type": "example",
+ "expires_in": 3600,
+ "expires_at": 4600,
+ "scope": scope,
+ "example_parameter": "example_value"
+ }
+
+ def test_implicit_token_uri(self):
+ client = MobileApplicationClient(self.client_id)
+
+ # Basic, no extra arguments
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_id)
+
+ # With redirection uri
+ uri = client.prepare_request_uri(self.uri, redirect_uri=self.redirect_uri)
+ self.assertURLEqual(uri, self.uri_redirect)
+
+ # With scope
+ uri = client.prepare_request_uri(self.uri, scope=self.scope)
+ self.assertURLEqual(uri, self.uri_scope)
+
+ # With state
+ uri = client.prepare_request_uri(self.uri, state=self.state)
+ self.assertURLEqual(uri, self.uri_state)
+
+ # With extra parameters through kwargs
+ uri = client.prepare_request_uri(self.uri, **self.kwargs)
+ self.assertURLEqual(uri, self.uri_kwargs)
+
+ def test_populate_attributes(self):
+
+ client = MobileApplicationClient(self.client_id)
+
+ response_uri = (self.response_uri + "&code=EVIL-CODE")
+
+ client.parse_request_uri_response(response_uri, scope=self.scope)
+
+ # We must not accidentally pick up any further security
+ # credentials at this point.
+ self.assertIsNone(client.code)
+
+ def test_parse_token_response(self):
+ client = MobileApplicationClient(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_uri_response(self.response_uri, scope=self.scope)
+ self.assertEqual(response, self.token)
+ self.assertEqual(client.access_token, response.get("access_token"))
+ self.assertEqual(client.refresh_token, response.get("refresh_token"))
+ self.assertEqual(client.token_type, response.get("token_type"))
+
+ # Mismatching scope
+ self.assertRaises(Warning, client.parse_request_uri_response, self.response_uri, scope="invalid")
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '4'
+ token = client.parse_request_uri_response(self.response_uri, scope='invalid')
+ self.assertTrue(token.scope_changed)
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_uri_response(self.response_uri, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_service_application.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_service_application.py
new file mode 100644
index 0000000000..b97d8554ed
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_service_application.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+import os
+from time import time
+from unittest.mock import patch
+
+import jwt
+
+from oauthlib.common import Request
+from oauthlib.oauth2 import ServiceApplicationClient
+
+from tests.unittest import TestCase
+
+
+class ServiceApplicationClientTest(TestCase):
+
+ gt = ServiceApplicationClient.grant_type
+
+ private_key = """
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDk1/bxyS8Q8jiheHeYYp/4rEKJopeQRRKKpZI4s5i+UPwVpupG
+AlwXWfzXwSMaKPAoKJNdu7tqKRniqst5uoHXw98gj0x7zamu0Ck1LtQ4c7pFMVah
+5IYGhBi2E9ycNS329W27nJPWNCbESTu7snVlG8V8mfvGGg3xNjTMO7IdrwIDAQAB
+AoGBAOQ2KuH8S5+OrsL4K+wfjoCi6MfxCUyqVU9GxocdM1m30WyWRFMEz2nKJ8fR
+p3vTD4w8yplTOhcoXdQZl0kRoaDzrcYkm2VvJtQRrX7dKFT8dR8D/Tr7dNQLOXfC
+DY6xveQczE7qt7Vk7lp4FqmxBsaaEuokt78pOOjywZoInjZhAkEA9wz3zoZNT0/i
+rf6qv2qTIeieUB035N3dyw6f1BGSWYaXSuerDCD/J1qZbAPKKhyHZbVawFt3UMhe
+542UftBaxQJBAO0iJy1I8GQjGnS7B3yvyH3CcLYGy296+XO/2xKp/d/ty1OIeovx
+C60pLNwuFNF3z9d2GVQAdoQ89hUkOtjZLeMCQQD0JO6oPHUeUjYT+T7ImAv7UKVT
+Suy30sKjLzqoGw1kR+wv7C5PeDRvscs4wa4CW9s6mjSrMDkDrmCLuJDtmf55AkEA
+kmaMg2PNrjUR51F0zOEFycaaqXbGcFwe1/xx9zLmHzMDXd4bsnwt9kk+fe0hQzVS
+JzatanQit3+feev1PN3QewJAWv4RZeavEUhKv+kLe95Yd0su7lTLVduVgh4v5yLT
+Ga6FHdjGPcfajt+nrpB1n8UQBEH9ZxniokR/IPvdMlxqXA==
+-----END RSA PRIVATE KEY-----
+"""
+
+ public_key = """
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDk1/bxyS8Q8jiheHeYYp/4rEKJ
+opeQRRKKpZI4s5i+UPwVpupGAlwXWfzXwSMaKPAoKJNdu7tqKRniqst5uoHXw98g
+j0x7zamu0Ck1LtQ4c7pFMVah5IYGhBi2E9ycNS329W27nJPWNCbESTu7snVlG8V8
+mfvGGg3xNjTMO7IdrwIDAQAB
+-----END PUBLIC KEY-----
+"""
+
+ subject = 'resource-owner@provider.com'
+
+ issuer = 'the-client@provider.com'
+
+ audience = 'https://provider.com/token'
+
+ client_id = "someclientid"
+ scope = ["/profile"]
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+
+ body = "isnot=empty"
+
+ body_up = "not=empty&grant_type=%s" % gt
+ body_kwargs = body_up + "&some=providers&require=extra+arguments"
+
+ token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type":"example",'
+ ' "expires_in":3600,'
+ ' "scope":"/profile",'
+ ' "example_parameter":"example_value"}')
+ token = {
+ "access_token": "2YotnFZFEjr1zCsicMWpAA",
+ "token_type": "example",
+ "expires_in": 3600,
+ "scope": ["/profile"],
+ "example_parameter": "example_value"
+ }
+
+ @patch('time.time')
+ def test_request_body(self, t):
+ t.return_value = time()
+ self.token['expires_at'] = self.token['expires_in'] + t.return_value
+
+ client = ServiceApplicationClient(
+ self.client_id, private_key=self.private_key)
+
+ # Basic with min required params
+ body = client.prepare_request_body(issuer=self.issuer,
+ subject=self.subject,
+ audience=self.audience,
+ body=self.body)
+ r = Request('https://a.b', body=body)
+ self.assertEqual(r.isnot, 'empty')
+ self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
+
+ claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256'])
+
+ self.assertEqual(claim['iss'], self.issuer)
+ # audience verification is handled during decode now
+ self.assertEqual(claim['sub'], self.subject)
+ self.assertEqual(claim['iat'], int(t.return_value))
+ self.assertNotIn('nbf', claim)
+ self.assertNotIn('jti', claim)
+
+ # Missing issuer parameter
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=None, subject=self.subject, audience=self.audience, body=self.body)
+
+ # Missing subject parameter
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=self.issuer, subject=None, audience=self.audience, body=self.body)
+
+ # Missing audience parameter
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=self.issuer, subject=self.subject, audience=None, body=self.body)
+
+ # Optional kwargs
+ not_before = time() - 3600
+ jwt_id = '8zd15df4s35f43sd'
+ body = client.prepare_request_body(issuer=self.issuer,
+ subject=self.subject,
+ audience=self.audience,
+ body=self.body,
+ not_before=not_before,
+ jwt_id=jwt_id)
+
+ r = Request('https://a.b', body=body)
+ self.assertEqual(r.isnot, 'empty')
+ self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
+
+ claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256'])
+
+ self.assertEqual(claim['iss'], self.issuer)
+ # audience verification is handled during decode now
+ self.assertEqual(claim['sub'], self.subject)
+ self.assertEqual(claim['iat'], int(t.return_value))
+ self.assertEqual(claim['nbf'], not_before)
+ self.assertEqual(claim['jti'], jwt_id)
+
+ @patch('time.time')
+ def test_request_body_no_initial_private_key(self, t):
+ t.return_value = time()
+ self.token['expires_at'] = self.token['expires_in'] + t.return_value
+
+ client = ServiceApplicationClient(
+ self.client_id, private_key=None)
+
+ # Basic with private key provided
+ body = client.prepare_request_body(issuer=self.issuer,
+ subject=self.subject,
+ audience=self.audience,
+ body=self.body,
+ private_key=self.private_key)
+ r = Request('https://a.b', body=body)
+ self.assertEqual(r.isnot, 'empty')
+ self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
+
+ claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256'])
+
+ self.assertEqual(claim['iss'], self.issuer)
+ # audience verification is handled during decode now
+ self.assertEqual(claim['sub'], self.subject)
+ self.assertEqual(claim['iat'], int(t.return_value))
+
+ # No private key provided
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=self.issuer, subject=self.subject, audience=self.audience, body=self.body)
+
+ @patch('time.time')
+ def test_parse_token_response(self, t):
+ t.return_value = time()
+ self.token['expires_at'] = self.token['expires_in'] + t.return_value
+
+ client = ServiceApplicationClient(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_body_response(self.token_json, scope=self.scope)
+ self.assertEqual(response, self.token)
+ self.assertEqual(client.access_token, response.get("access_token"))
+ self.assertEqual(client.refresh_token, response.get("refresh_token"))
+ self.assertEqual(client.token_type, response.get("token_type"))
+
+ # Mismatching state
+ self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '2'
+ token = client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertTrue(token.scope_changed)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_web_application.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_web_application.py
new file mode 100644
index 0000000000..7a71121512
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/clients/test_web_application.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+import os
+import urllib.parse as urlparse
+import warnings
+from unittest.mock import patch
+
+from oauthlib import common, signals
+from oauthlib.oauth2 import (
+ BackendApplicationClient, Client, LegacyApplicationClient,
+ MobileApplicationClient, WebApplicationClient,
+)
+from oauthlib.oauth2.rfc6749 import errors, utils
+from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY
+
+from tests.unittest import TestCase
+
+
+@patch('time.time', new=lambda: 1000)
+class WebApplicationClientTest(TestCase):
+
+ client_id = "someclientid"
+ client_secret = 'someclientsecret'
+ uri = "https://example.com/path?query=world"
+ uri_id = uri + "&response_type=code&client_id=" + client_id
+ uri_redirect = uri_id + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback"
+ redirect_uri = "http://my.page.com/callback"
+ code_verifier = "code_verifier"
+ scope = ["/profile"]
+ state = "xyz"
+ code_challenge = "code_challenge"
+ code_challenge_method = "S256"
+ uri_scope = uri_id + "&scope=%2Fprofile"
+ uri_state = uri_id + "&state=" + state
+ uri_code_challenge = uri_id + "&code_challenge=" + code_challenge + "&code_challenge_method=" + code_challenge_method
+ uri_code_challenge_method = uri_id + "&code_challenge=" + code_challenge + "&code_challenge_method=plain"
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+ uri_kwargs = uri_id + "&some=providers&require=extra+arguments"
+ uri_authorize_code = uri_redirect + "&scope=%2Fprofile&state=" + state
+
+ code = "zzzzaaaa"
+ body = "not=empty"
+
+ body_code = "not=empty&grant_type=authorization_code&code={}&client_id={}".format(code, client_id)
+ body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback"
+ body_code_verifier = body_code + "&code_verifier=code_verifier"
+ body_kwargs = body_code + "&some=providers&require=extra+arguments"
+
+ response_uri = "https://client.example.com/cb?code=zzzzaaaa&state=xyz"
+ response = {"code": "zzzzaaaa", "state": "xyz"}
+
+ token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type":"example",'
+ ' "expires_in":3600,'
+ ' "scope":"/profile",'
+ ' "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter":"example_value"}')
+ token = {
+ "access_token": "2YotnFZFEjr1zCsicMWpAA",
+ "token_type": "example",
+ "expires_in": 3600,
+ "expires_at": 4600,
+ "scope": scope,
+ "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
+ "example_parameter": "example_value"
+ }
+
+ def test_auth_grant_uri(self):
+ client = WebApplicationClient(self.client_id)
+
+ # Basic, no extra arguments
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_id)
+
+ # With redirection uri
+ uri = client.prepare_request_uri(self.uri, redirect_uri=self.redirect_uri)
+ self.assertURLEqual(uri, self.uri_redirect)
+
+ # With scope
+ uri = client.prepare_request_uri(self.uri, scope=self.scope)
+ self.assertURLEqual(uri, self.uri_scope)
+
+ # With state
+ uri = client.prepare_request_uri(self.uri, state=self.state)
+ self.assertURLEqual(uri, self.uri_state)
+
+ # with code_challenge and code_challenge_method
+ uri = client.prepare_request_uri(self.uri, code_challenge=self.code_challenge, code_challenge_method=self.code_challenge_method)
+ self.assertURLEqual(uri, self.uri_code_challenge)
+
+ # with no code_challenge_method
+ uri = client.prepare_request_uri(self.uri, code_challenge=self.code_challenge)
+ self.assertURLEqual(uri, self.uri_code_challenge_method)
+
+ # With extra parameters through kwargs
+ uri = client.prepare_request_uri(self.uri, **self.kwargs)
+ self.assertURLEqual(uri, self.uri_kwargs)
+
+ def test_request_body(self):
+ client = WebApplicationClient(self.client_id, code=self.code)
+
+ # Basic, no extra arguments
+ body = client.prepare_request_body(body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ rclient = WebApplicationClient(self.client_id)
+ body = rclient.prepare_request_body(code=self.code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ # With redirection uri
+ body = client.prepare_request_body(body=self.body, redirect_uri=self.redirect_uri)
+ self.assertFormBodyEqual(body, self.body_redirect)
+
+ # With code verifier
+ body = client.prepare_request_body(body=self.body, code_verifier=self.code_verifier)
+ self.assertFormBodyEqual(body, self.body_code_verifier)
+
+ # With extra parameters
+ body = client.prepare_request_body(body=self.body, **self.kwargs)
+ self.assertFormBodyEqual(body, self.body_kwargs)
+
+ def test_parse_grant_uri_response(self):
+ client = WebApplicationClient(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_uri_response(self.response_uri, state=self.state)
+ self.assertEqual(response, self.response)
+ self.assertEqual(client.code, self.code)
+
+ # Mismatching state
+ self.assertRaises(errors.MismatchingStateError,
+ client.parse_request_uri_response,
+ self.response_uri,
+ state="invalid")
+
+ def test_populate_attributes(self):
+
+ client = WebApplicationClient(self.client_id)
+
+ response_uri = (self.response_uri +
+ "&access_token=EVIL-TOKEN"
+ "&refresh_token=EVIL-TOKEN"
+ "&mac_key=EVIL-KEY")
+
+ client.parse_request_uri_response(response_uri, self.state)
+
+ self.assertEqual(client.code, self.code)
+
+ # We must not accidentally pick up any further security
+ # credentials at this point.
+ self.assertIsNone(client.access_token)
+ self.assertIsNone(client.refresh_token)
+ self.assertIsNone(client.mac_key)
+
+ def test_parse_token_response(self):
+ client = WebApplicationClient(self.client_id)
+
+ # Parse code and state
+ response = client.parse_request_body_response(self.token_json, scope=self.scope)
+ self.assertEqual(response, self.token)
+ self.assertEqual(client.access_token, response.get("access_token"))
+ self.assertEqual(client.refresh_token, response.get("refresh_token"))
+ self.assertEqual(client.token_type, response.get("token_type"))
+
+ # Mismatching state
+ self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
+ token = client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertTrue(token.scope_changed)
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
+
+ def test_prepare_authorization_requeset(self):
+ client = WebApplicationClient(self.client_id)
+
+ url, header, body = client.prepare_authorization_request(
+ self.uri, redirect_url=self.redirect_uri, state=self.state, scope=self.scope)
+ self.assertURLEqual(url, self.uri_authorize_code)
+ # verify default header and body only
+ self.assertEqual(header, {'Content-Type': 'application/x-www-form-urlencoded'})
+ self.assertEqual(body, '')
+
+ def test_prepare_request_body(self):
+ """
+ see issue #585
+ https://github.com/oauthlib/oauthlib/issues/585
+
+ `prepare_request_body` should support the following scenarios:
+ 1. Include client_id alone in the body (default)
+ 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution)
+ 3. Include client_id and client_secret in the body (RFC alternative solution)
+ 4. Include client_id in the body and an empty string for client_secret.
+ """
+ client = WebApplicationClient(self.client_id)
+
+ # scenario 1, default behavior to include `client_id`
+ r1 = client.prepare_request_body()
+ self.assertEqual(r1, 'grant_type=authorization_code&client_id=%s' % self.client_id)
+
+ r1b = client.prepare_request_body(include_client_id=True)
+ self.assertEqual(r1b, 'grant_type=authorization_code&client_id=%s' % self.client_id)
+
+ # scenario 2, do not include `client_id` in the body, so it can be sent in auth.
+ r2 = client.prepare_request_body(include_client_id=False)
+ self.assertEqual(r2, 'grant_type=authorization_code')
+
+ # scenario 3, Include client_id and client_secret in the body (RFC alternative solution)
+ # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting
+ r3 = client.prepare_request_body(client_secret=self.client_secret)
+ r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True))
+ self.assertEqual(len(r3_params.keys()), 3)
+ self.assertEqual(r3_params['grant_type'], 'authorization_code')
+ self.assertEqual(r3_params['client_id'], self.client_id)
+ self.assertEqual(r3_params['client_secret'], self.client_secret)
+
+ r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret)
+ r3b_params = dict(urlparse.parse_qsl(r3b, keep_blank_values=True))
+ self.assertEqual(len(r3b_params.keys()), 3)
+ self.assertEqual(r3b_params['grant_type'], 'authorization_code')
+ self.assertEqual(r3b_params['client_id'], self.client_id)
+ self.assertEqual(r3b_params['client_secret'], self.client_secret)
+
+ # scenario 4, `client_secret` is an empty string
+ r4 = client.prepare_request_body(include_client_id=True, client_secret='')
+ r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True))
+ self.assertEqual(len(r4_params.keys()), 3)
+ self.assertEqual(r4_params['grant_type'], 'authorization_code')
+ self.assertEqual(r4_params['client_id'], self.client_id)
+ self.assertEqual(r4_params['client_secret'], '')
+
+ # scenario 4b, `client_secret` is `None`
+ r4b = client.prepare_request_body(include_client_id=True, client_secret=None)
+ r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True))
+ self.assertEqual(len(r4b_params.keys()), 2)
+ self.assertEqual(r4b_params['grant_type'], 'authorization_code')
+ self.assertEqual(r4b_params['client_id'], self.client_id)
+
+ # scenario Warnings
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always") # catch all
+
+ # warning1 - raise a DeprecationWarning if a `client_id` is submitted
+ rWarnings1 = client.prepare_request_body(client_id=self.client_id)
+ self.assertEqual(len(w), 1)
+ self.assertIsInstance(w[0].message, DeprecationWarning)
+
+ # testing the exact warning message in Python2&Python3 is a pain
+
+ # scenario Exceptions
+ # exception1 - raise a ValueError if the a different `client_id` is submitted
+ with self.assertRaises(ValueError) as cm:
+ client.prepare_request_body(client_id='different_client_id')
+ # testing the exact exception message in Python2&Python3 is a pain
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/__init__.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py
new file mode 100644
index 0000000000..b1af6c3306
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+from oauthlib.oauth2 import (
+ FatalClientError, OAuth2Error, RequestValidator, Server,
+)
+from oauthlib.oauth2.rfc6749 import (
+ BaseEndpoint, catch_errors_and_unavailability,
+)
+
+from tests.unittest import TestCase
+
+
+class BaseEndpointTest(TestCase):
+
+ def test_default_config(self):
+ endpoint = BaseEndpoint()
+ self.assertFalse(endpoint.catch_errors)
+ self.assertTrue(endpoint.available)
+ endpoint.catch_errors = True
+ self.assertTrue(endpoint.catch_errors)
+ endpoint.available = False
+ self.assertFalse(endpoint.available)
+
+ def test_error_catching(self):
+ validator = RequestValidator()
+ server = Server(validator)
+ server.catch_errors = True
+ h, b, s = server.create_token_response(
+ 'https://example.com', body='grant_type=authorization_code&code=abc'
+ )
+ self.assertIn("server_error", b)
+ self.assertEqual(s, 500)
+
+ def test_unavailability(self):
+ validator = RequestValidator()
+ server = Server(validator)
+ server.available = False
+ h, b, s = server.create_authorization_response('https://example.com')
+ self.assertIn("temporarily_unavailable", b)
+ self.assertEqual(s, 503)
+
+ def test_wrapper(self):
+
+ class TestServer(Server):
+
+ @catch_errors_and_unavailability
+ def throw_error(self, uri):
+ raise ValueError()
+
+ @catch_errors_and_unavailability
+ def throw_oauth_error(self, uri):
+ raise OAuth2Error()
+
+ @catch_errors_and_unavailability
+ def throw_fatal_oauth_error(self, uri):
+ raise FatalClientError()
+
+ validator = RequestValidator()
+ server = TestServer(validator)
+
+ server.catch_errors = True
+ h, b, s = server.throw_error('a')
+ self.assertIn("server_error", b)
+ self.assertEqual(s, 500)
+
+ server.available = False
+ h, b, s = server.throw_error('a')
+ self.assertIn("temporarily_unavailable", b)
+ self.assertEqual(s, 503)
+
+ server.available = True
+ self.assertRaises(OAuth2Error, server.throw_oauth_error, 'a')
+ self.assertRaises(FatalClientError, server.throw_fatal_oauth_error, 'a')
+ server.catch_errors = False
+ self.assertRaises(OAuth2Error, server.throw_oauth_error, 'a')
+ self.assertRaises(FatalClientError, server.throw_fatal_oauth_error, 'a')
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_client_authentication.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_client_authentication.py
new file mode 100644
index 0000000000..0659ee0d25
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_client_authentication.py
@@ -0,0 +1,162 @@
+"""Client authentication tests across all endpoints.
+
+Client authentication in OAuth2 serve two purposes, to authenticate
+confidential clients and to ensure public clients are in fact public. The
+latter is achieved with authenticate_client_id and the former with
+authenticate_client.
+
+We make sure authentication is done by requiring a client object to be set
+on the request object with a client_id parameter. The client_id attribute
+prevents this check from being circumvented with a client form parameter.
+"""
+import json
+from unittest import mock
+
+from oauthlib.oauth2 import (
+ BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
+ RequestValidator, WebApplicationServer,
+)
+
+from tests.unittest import TestCase
+
+from .test_utils import get_fragment_credentials
+
+
+class ClientAuthenticationTest(TestCase):
+
+ def inspect_client(self, request, refresh_token=False):
+ if not request.client or not request.client.client_id:
+ raise ValueError()
+ return 'abc'
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.is_pkce_required.return_value = False
+ self.validator.get_code_challenge.return_value = None
+ self.validator.get_default_redirect_uri.return_value = 'http://i.b./path'
+ self.web = WebApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.mobile = MobileApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.legacy = LegacyApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.backend = BackendApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.token_uri = 'http://example.com/path'
+ self.auth_uri = 'http://example.com/path?client_id=abc&response_type=token'
+ # should be base64 but no added value in this unittest
+ self.basicauth_client_creds = {"Authorization": "john:doe"}
+ self.basicauth_client_id = {"Authorization": "john:"}
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def set_client_id(self, client_id, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def basicauth_authenticate_client(self, request):
+ assert "Authorization" in request.headers
+ assert "john:doe" in request.headers["Authorization"]
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def test_client_id_authentication(self):
+ token_uri = 'http://example.com/path'
+
+ # authorization code grant
+ self.validator.authenticate_client.return_value = False
+ self.validator.authenticate_client_id.return_value = False
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=mock')
+ self.assertEqual(json.loads(body)['error'], 'invalid_client')
+
+ self.validator.authenticate_client_id.return_value = True
+ self.validator.authenticate_client.side_effect = self.set_client
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=mock')
+ self.assertIn('access_token', json.loads(body))
+
+ # implicit grant
+ auth_uri = 'http://example.com/path?client_id=abc&response_type=token'
+ self.assertRaises(ValueError, self.mobile.create_authorization_response,
+ auth_uri, scopes=['random'])
+
+ self.validator.validate_client_id.side_effect = self.set_client_id
+ h, _, s = self.mobile.create_authorization_response(auth_uri, scopes=['random'])
+ self.assertEqual(302, s)
+ self.assertIn('Location', h)
+ self.assertIn('access_token', get_fragment_credentials(h['Location']))
+
+ def test_basicauth_web(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+ _, body, _ = self.web.create_token_response(
+ self.token_uri,
+ body='grant_type=authorization_code&code=mock',
+ headers=self.basicauth_client_creds
+ )
+ self.assertIn('access_token', json.loads(body))
+
+ def test_basicauth_legacy(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+ _, body, _ = self.legacy.create_token_response(
+ self.token_uri,
+ body='grant_type=password&username=abc&password=secret',
+ headers=self.basicauth_client_creds
+ )
+ self.assertIn('access_token', json.loads(body))
+
+ def test_basicauth_backend(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+ _, body, _ = self.backend.create_token_response(
+ self.token_uri,
+ body='grant_type=client_credentials',
+ headers=self.basicauth_client_creds
+ )
+ self.assertIn('access_token', json.loads(body))
+
+ def test_basicauth_revoke(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+
+ # legacy or any other uses the same RevocationEndpoint
+ _, body, status = self.legacy.create_revocation_response(
+ self.token_uri,
+ body='token=foobar',
+ headers=self.basicauth_client_creds
+ )
+ self.assertEqual(status, 200, body)
+
+ def test_basicauth_introspect(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+
+ # legacy or any other uses the same IntrospectEndpoint
+ _, body, status = self.legacy.create_introspect_response(
+ self.token_uri,
+ body='token=foobar',
+ headers=self.basicauth_client_creds
+ )
+ self.assertEqual(status, 200, body)
+
+ def test_custom_authentication(self):
+ token_uri = 'http://example.com/path'
+
+ # authorization code grant
+ self.assertRaises(NotImplementedError,
+ self.web.create_token_response, token_uri,
+ body='grant_type=authorization_code&code=mock')
+
+ # password grant
+ self.validator.authenticate_client.return_value = True
+ self.assertRaises(NotImplementedError,
+ self.legacy.create_token_response, token_uri,
+ body='grant_type=password&username=abc&password=secret')
+
+ # client credentials grant
+ self.validator.authenticate_client.return_value = True
+ self.assertRaises(NotImplementedError,
+ self.backend.create_token_response, token_uri,
+ body='grant_type=client_credentials')
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py
new file mode 100644
index 0000000000..32c770ccb7
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py
@@ -0,0 +1,128 @@
+"""Ensure credentials are preserved through the authorization.
+
+The Authorization Code Grant will need to preserve state as well as redirect
+uri and the Implicit Grant will need to preserve state.
+"""
+import json
+from unittest import mock
+
+from oauthlib.oauth2 import (
+ MobileApplicationServer, RequestValidator, WebApplicationServer,
+)
+from oauthlib.oauth2.rfc6749 import errors
+
+from tests.unittest import TestCase
+
+from .test_utils import get_fragment_credentials, get_query_credentials
+
+
+class PreservationTest(TestCase):
+
+ DEFAULT_REDIRECT_URI = 'http://i.b./path'
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_default_redirect_uri.return_value = self.DEFAULT_REDIRECT_URI
+ self.validator.get_code_challenge.return_value = None
+ self.validator.authenticate_client.side_effect = self.set_client
+ self.web = WebApplicationServer(self.validator)
+ self.mobile = MobileApplicationServer(self.validator)
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def test_state_preservation(self):
+ auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type='
+
+ # authorization grant
+ h, _, s = self.web.create_authorization_response(
+ auth_uri + 'code', scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertEqual(get_query_credentials(h['Location'])['state'][0], 'xyz')
+
+ # implicit grant
+ h, _, s = self.mobile.create_authorization_response(
+ auth_uri + 'token', scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertEqual(get_fragment_credentials(h['Location'])['state'][0], 'xyz')
+
+ def test_redirect_uri_preservation(self):
+ auth_uri = 'http://example.com/path?redirect_uri=http%3A%2F%2Fi.b%2Fpath&client_id=abc'
+ redirect_uri = 'http://i.b/path'
+ token_uri = 'http://example.com/path'
+
+ # authorization grant
+ h, _, s = self.web.create_authorization_response(
+ auth_uri + '&response_type=code', scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertTrue(h['Location'].startswith(redirect_uri))
+
+ # confirm_redirect_uri should return false if the redirect uri
+ # was given in the authorization but not in the token request.
+ self.validator.confirm_redirect_uri.return_value = False
+ code = get_query_credentials(h['Location'])['code'][0]
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=%s' % code)
+ self.assertEqual(json.loads(body)['error'], 'invalid_request')
+
+ # implicit grant
+ h, _, s = self.mobile.create_authorization_response(
+ auth_uri + '&response_type=token', scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertTrue(h['Location'].startswith(redirect_uri))
+
+ def test_invalid_redirect_uri(self):
+ auth_uri = 'http://example.com/path?redirect_uri=http%3A%2F%2Fi.b%2Fpath&client_id=abc'
+ self.validator.validate_redirect_uri.return_value = False
+
+ # authorization grant
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.web.create_authorization_response,
+ auth_uri + '&response_type=code', scopes=['random'])
+
+ # implicit grant
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.mobile.create_authorization_response,
+ auth_uri + '&response_type=token', scopes=['random'])
+
+ def test_default_uri(self):
+ auth_uri = 'http://example.com/path?state=xyz&client_id=abc'
+
+ self.validator.get_default_redirect_uri.return_value = None
+
+ # authorization grant
+ self.assertRaises(errors.MissingRedirectURIError,
+ self.web.create_authorization_response,
+ auth_uri + '&response_type=code', scopes=['random'])
+
+ # implicit grant
+ self.assertRaises(errors.MissingRedirectURIError,
+ self.mobile.create_authorization_response,
+ auth_uri + '&response_type=token', scopes=['random'])
+
+ def test_default_uri_in_token(self):
+ auth_uri = 'http://example.com/path?state=xyz&client_id=abc'
+ token_uri = 'http://example.com/path'
+
+ # authorization grant
+ h, _, s = self.web.create_authorization_response(
+ auth_uri + '&response_type=code', scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertTrue(h['Location'].startswith(self.DEFAULT_REDIRECT_URI))
+
+ # confirm_redirect_uri should return true if the redirect uri
+ # was not given in the authorization AND not in the token request.
+ self.validator.confirm_redirect_uri.return_value = True
+ code = get_query_credentials(h['Location'])['code'][0]
+ self.validator.validate_code.return_value = True
+ _, body, s = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=%s' % code)
+ self.assertEqual(s, 200)
+ self.assertEqual(self.validator.confirm_redirect_uri.call_args[0][2], self.DEFAULT_REDIRECT_URI)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_error_responses.py
new file mode 100644
index 0000000000..f61595e213
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_error_responses.py
@@ -0,0 +1,491 @@
+"""Ensure the correct error responses are returned for all defined error types.
+"""
+import json
+from unittest import mock
+
+from oauthlib.common import urlencode
+from oauthlib.oauth2 import (
+ BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
+ RequestValidator, WebApplicationServer,
+)
+from oauthlib.oauth2.rfc6749 import errors
+
+from tests.unittest import TestCase
+
+
+class ErrorResponseTest(TestCase):
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_default_redirect_uri.return_value = None
+ self.validator.get_code_challenge.return_value = None
+ self.web = WebApplicationServer(self.validator)
+ self.mobile = MobileApplicationServer(self.validator)
+ self.legacy = LegacyApplicationServer(self.validator)
+ self.backend = BackendApplicationServer(self.validator)
+
+ def test_invalid_redirect_uri(self):
+ uri = 'https://example.com/authorize?response_type={0}&client_id=foo&redirect_uri=wrong'
+
+ # Authorization code grant
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
+ def test_invalid_default_redirect_uri(self):
+ uri = 'https://example.com/authorize?response_type={0}&client_id=foo'
+ self.validator.get_default_redirect_uri.return_value = "wrong"
+
+ # Authorization code grant
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
+ def test_missing_redirect_uri(self):
+ uri = 'https://example.com/authorize?response_type={0}&client_id=foo'
+
+ # Authorization code grant
+ self.assertRaises(errors.MissingRedirectURIError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.MissingRedirectURIError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.MissingRedirectURIError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.MissingRedirectURIError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
+ def test_mismatching_redirect_uri(self):
+ uri = 'https://example.com/authorize?response_type={0}&client_id=foo&redirect_uri=https%3A%2F%2Fi.b%2Fback'
+
+ # Authorization code grant
+ self.validator.validate_redirect_uri.return_value = False
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
+ def test_missing_client_id(self):
+ uri = 'https://example.com/authorize?response_type={0}&redirect_uri=https%3A%2F%2Fi.b%2Fback'
+
+ # Authorization code grant
+ self.validator.validate_redirect_uri.return_value = False
+ self.assertRaises(errors.MissingClientIdError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.MissingClientIdError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.MissingClientIdError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.MissingClientIdError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
+ def test_invalid_client_id(self):
+ uri = 'https://example.com/authorize?response_type={0}&client_id=foo&redirect_uri=https%3A%2F%2Fi.b%2Fback'
+
+ # Authorization code grant
+ self.validator.validate_client_id.return_value = False
+ self.assertRaises(errors.InvalidClientIdError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.InvalidClientIdError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.InvalidClientIdError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.InvalidClientIdError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
+ def test_empty_parameter(self):
+ uri = 'https://example.com/authorize?client_id=foo&redirect_uri=https%3A%2F%2Fi.b%2Fback&response_type=code&'
+
+ # Authorization code grant
+ self.assertRaises(errors.InvalidRequestFatalError,
+ self.web.validate_authorization_request, uri)
+
+ # Implicit grant
+ self.assertRaises(errors.InvalidRequestFatalError,
+ self.mobile.validate_authorization_request, uri)
+
+ def test_invalid_request(self):
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+ token_uri = 'https://i.b/token'
+
+ invalid_bodies = [
+ # duplicate params
+ 'grant_type=authorization_code&client_id=nope&client_id=nope&code=foo'
+ ]
+ for body in invalid_bodies:
+ _, body, _ = self.web.create_token_response(token_uri,
+ body=body)
+ self.assertEqual('invalid_request', json.loads(body)['error'])
+
+ # Password credentials grant
+ invalid_bodies = [
+ # duplicate params
+ 'grant_type=password&username=foo&username=bar&password=baz'
+ # missing username
+ 'grant_type=password&password=baz'
+ # missing password
+ 'grant_type=password&username=foo'
+ ]
+ self.validator.authenticate_client.side_effect = self.set_client
+ for body in invalid_bodies:
+ _, body, _ = self.legacy.create_token_response(token_uri,
+ body=body)
+ self.assertEqual('invalid_request', json.loads(body)['error'])
+
+ # Client credentials grant
+ invalid_bodies = [
+ # duplicate params
+ 'grant_type=client_credentials&scope=foo&scope=bar'
+ ]
+ for body in invalid_bodies:
+ _, body, _ = self.backend.create_token_response(token_uri,
+ body=body)
+ self.assertEqual('invalid_request', json.loads(body)['error'])
+
+ def test_invalid_request_duplicate_params(self):
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+ uri = 'https://i.b/auth?client_id=foo&client_id=bar&response_type={0}'
+ description = 'Duplicate client_id parameter.'
+
+ # Authorization code
+ self.assertRaisesRegex(errors.InvalidRequestFatalError,
+ description,
+ self.web.validate_authorization_request,
+ uri.format('code'))
+ self.assertRaisesRegex(errors.InvalidRequestFatalError,
+ description,
+ self.web.create_authorization_response,
+ uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaisesRegex(errors.InvalidRequestFatalError,
+ description,
+ self.mobile.validate_authorization_request,
+ uri.format('token'))
+ self.assertRaisesRegex(errors.InvalidRequestFatalError,
+ description,
+ self.mobile.create_authorization_response,
+ uri.format('token'), scopes=['foo'])
+
+ def test_invalid_request_missing_response_type(self):
+
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+
+ uri = 'https://i.b/auth?client_id=foo'
+
+ # Authorization code
+ self.assertRaises(errors.MissingResponseTypeError,
+ self.web.validate_authorization_request,
+ uri.format('code'))
+ h, _, s = self.web.create_authorization_response(uri, scopes=['foo'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ # Implicit grant
+ self.assertRaises(errors.MissingResponseTypeError,
+ self.mobile.validate_authorization_request,
+ uri.format('token'))
+ h, _, s = self.mobile.create_authorization_response(uri, scopes=['foo'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ def test_unauthorized_client(self):
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+ self.validator.validate_grant_type.return_value = False
+ self.validator.validate_response_type.return_value = False
+ self.validator.authenticate_client.side_effect = self.set_client
+ token_uri = 'https://i.b/token'
+
+ # Authorization code grant
+ self.assertRaises(errors.UnauthorizedClientError,
+ self.web.validate_authorization_request,
+ 'https://i.b/auth?response_type=code&client_id=foo')
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=foo')
+ self.assertEqual('unauthorized_client', json.loads(body)['error'])
+
+ # Implicit grant
+ self.assertRaises(errors.UnauthorizedClientError,
+ self.mobile.validate_authorization_request,
+ 'https://i.b/auth?response_type=token&client_id=foo')
+
+ # Password credentials grant
+ _, body, _ = self.legacy.create_token_response(token_uri,
+ body='grant_type=password&username=foo&password=bar')
+ self.assertEqual('unauthorized_client', json.loads(body)['error'])
+
+ # Client credentials grant
+ _, body, _ = self.backend.create_token_response(token_uri,
+ body='grant_type=client_credentials')
+ self.assertEqual('unauthorized_client', json.loads(body)['error'])
+
+ def test_access_denied(self):
+ self.validator.authenticate_client.side_effect = self.set_client
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+ self.validator.confirm_redirect_uri.return_value = False
+ token_uri = 'https://i.b/token'
+ # Authorization code grant
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=foo')
+ self.assertEqual('invalid_request', json.loads(body)['error'])
+
+ def test_access_denied_no_default_redirecturi(self):
+ self.validator.authenticate_client.side_effect = self.set_client
+ self.validator.get_default_redirect_uri.return_value = None
+ token_uri = 'https://i.b/token'
+ # Authorization code grant
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=foo')
+ self.assertEqual('invalid_request', json.loads(body)['error'])
+
+ def test_unsupported_response_type(self):
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+
+ # Authorization code grant
+ self.assertRaises(errors.UnsupportedResponseTypeError,
+ self.web.validate_authorization_request,
+ 'https://i.b/auth?response_type=foo&client_id=foo')
+
+ # Implicit grant
+ self.assertRaises(errors.UnsupportedResponseTypeError,
+ self.mobile.validate_authorization_request,
+ 'https://i.b/auth?response_type=foo&client_id=foo')
+
+ def test_invalid_scope(self):
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+ self.validator.validate_scopes.return_value = False
+ self.validator.authenticate_client.side_effect = self.set_client
+
+ # Authorization code grant
+ self.assertRaises(errors.InvalidScopeError,
+ self.web.validate_authorization_request,
+ 'https://i.b/auth?response_type=code&client_id=foo')
+
+ # Implicit grant
+ self.assertRaises(errors.InvalidScopeError,
+ self.mobile.validate_authorization_request,
+ 'https://i.b/auth?response_type=token&client_id=foo')
+
+ # Password credentials grant
+ _, body, _ = self.legacy.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=password&username=foo&password=bar')
+ self.assertEqual('invalid_scope', json.loads(body)['error'])
+
+ # Client credentials grant
+ _, body, _ = self.backend.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=client_credentials')
+ self.assertEqual('invalid_scope', json.loads(body)['error'])
+
+ def test_server_error(self):
+ def raise_error(*args, **kwargs):
+ raise ValueError()
+
+ self.validator.validate_client_id.side_effect = raise_error
+ self.validator.authenticate_client.side_effect = raise_error
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+
+ # Authorization code grant
+ self.web.catch_errors = True
+ _, _, s = self.web.create_authorization_response(
+ 'https://i.b/auth?client_id=foo&response_type=code',
+ scopes=['foo'])
+ self.assertEqual(s, 500)
+ _, _, s = self.web.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=authorization_code&code=foo',
+ scopes=['foo'])
+ self.assertEqual(s, 500)
+
+ # Implicit grant
+ self.mobile.catch_errors = True
+ _, _, s = self.mobile.create_authorization_response(
+ 'https://i.b/auth?client_id=foo&response_type=token',
+ scopes=['foo'])
+ self.assertEqual(s, 500)
+
+ # Password credentials grant
+ self.legacy.catch_errors = True
+ _, _, s = self.legacy.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=password&username=foo&password=foo')
+ self.assertEqual(s, 500)
+
+ # Client credentials grant
+ self.backend.catch_errors = True
+ _, _, s = self.backend.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=client_credentials')
+ self.assertEqual(s, 500)
+
+ def test_temporarily_unavailable(self):
+ # Authorization code grant
+ self.web.available = False
+ _, _, s = self.web.create_authorization_response(
+ 'https://i.b/auth?client_id=foo&response_type=code',
+ scopes=['foo'])
+ self.assertEqual(s, 503)
+ _, _, s = self.web.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=authorization_code&code=foo',
+ scopes=['foo'])
+ self.assertEqual(s, 503)
+
+ # Implicit grant
+ self.mobile.available = False
+ _, _, s = self.mobile.create_authorization_response(
+ 'https://i.b/auth?client_id=foo&response_type=token',
+ scopes=['foo'])
+ self.assertEqual(s, 503)
+
+ # Password credentials grant
+ self.legacy.available = False
+ _, _, s = self.legacy.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=password&username=foo&password=foo')
+ self.assertEqual(s, 503)
+
+ # Client credentials grant
+ self.backend.available = False
+ _, _, s = self.backend.create_token_response(
+ 'https://i.b/token',
+ body='grant_type=client_credentials')
+ self.assertEqual(s, 503)
+
+ def test_invalid_client(self):
+ self.validator.authenticate_client.return_value = False
+ self.validator.authenticate_client_id.return_value = False
+
+ # Authorization code grant
+ _, body, _ = self.web.create_token_response('https://i.b/token',
+ body='grant_type=authorization_code&code=foo')
+ self.assertEqual('invalid_client', json.loads(body)['error'])
+
+ # Password credentials grant
+ _, body, _ = self.legacy.create_token_response('https://i.b/token',
+ body='grant_type=password&username=foo&password=bar')
+ self.assertEqual('invalid_client', json.loads(body)['error'])
+
+ # Client credentials grant
+ _, body, _ = self.legacy.create_token_response('https://i.b/token',
+ body='grant_type=client_credentials')
+ self.assertEqual('invalid_client', json.loads(body)['error'])
+
+ def test_invalid_grant(self):
+ self.validator.authenticate_client.side_effect = self.set_client
+
+ # Authorization code grant
+ self.validator.validate_code.return_value = False
+ _, body, _ = self.web.create_token_response('https://i.b/token',
+ body='grant_type=authorization_code&code=foo')
+ self.assertEqual('invalid_grant', json.loads(body)['error'])
+
+ # Password credentials grant
+ self.validator.validate_user.return_value = False
+ _, body, _ = self.legacy.create_token_response('https://i.b/token',
+ body='grant_type=password&username=foo&password=bar')
+ self.assertEqual('invalid_grant', json.loads(body)['error'])
+
+ def test_unsupported_grant_type(self):
+ self.validator.authenticate_client.side_effect = self.set_client
+
+ # Authorization code grant
+ _, body, _ = self.web.create_token_response('https://i.b/token',
+ body='grant_type=bar&code=foo')
+ self.assertEqual('unsupported_grant_type', json.loads(body)['error'])
+
+ # Password credentials grant
+ _, body, _ = self.legacy.create_token_response('https://i.b/token',
+ body='grant_type=bar&username=foo&password=bar')
+ self.assertEqual('unsupported_grant_type', json.loads(body)['error'])
+
+ # Client credentials grant
+ _, body, _ = self.backend.create_token_response('https://i.b/token',
+ body='grant_type=bar')
+ self.assertEqual('unsupported_grant_type', json.loads(body)['error'])
+
+ def test_invalid_request_method(self):
+ test_methods = ['GET', 'pUt', 'dEleTe', 'paTcH']
+ test_methods = test_methods + [x.lower() for x in test_methods] + [x.upper() for x in test_methods]
+ for method in test_methods:
+ self.validator.authenticate_client.side_effect = self.set_client
+
+ uri = "http://i/b/token/"
+ try:
+ _, body, s = self.web.create_token_response(uri,
+ body='grant_type=access_token&code=123', http_method=method)
+ self.fail('This should have failed with InvalidRequestError')
+ except errors.InvalidRequestError as ire:
+ self.assertIn('Unsupported request method', ire.description)
+
+ try:
+ _, body, s = self.legacy.create_token_response(uri,
+ body='grant_type=access_token&code=123', http_method=method)
+ self.fail('This should have failed with InvalidRequestError')
+ except errors.InvalidRequestError as ire:
+ self.assertIn('Unsupported request method', ire.description)
+
+ try:
+ _, body, s = self.backend.create_token_response(uri,
+ body='grant_type=access_token&code=123', http_method=method)
+ self.fail('This should have failed with InvalidRequestError')
+ except errors.InvalidRequestError as ire:
+ self.assertIn('Unsupported request method', ire.description)
+
+ def test_invalid_post_request(self):
+ self.validator.authenticate_client.side_effect = self.set_client
+ for param in ['token', 'secret', 'code', 'foo']:
+ uri = 'https://i/b/token?' + urlencode([(param, 'secret')])
+ try:
+ _, body, s = self.web.create_token_response(uri,
+ body='grant_type=access_token&code=123')
+ self.fail('This should have failed with InvalidRequestError')
+ except errors.InvalidRequestError as ire:
+ self.assertIn('URL query parameters are not allowed', ire.description)
+
+ try:
+ _, body, s = self.legacy.create_token_response(uri,
+ body='grant_type=access_token&code=123')
+ self.fail('This should have failed with InvalidRequestError')
+ except errors.InvalidRequestError as ire:
+ self.assertIn('URL query parameters are not allowed', ire.description)
+
+ try:
+ _, body, s = self.backend.create_token_response(uri,
+ body='grant_type=access_token&code=123')
+ self.fail('This should have failed with InvalidRequestError')
+ except errors.InvalidRequestError as ire:
+ self.assertIn('URL query parameters are not allowed', ire.description)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_extra_credentials.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_extra_credentials.py
new file mode 100644
index 0000000000..97aaf86dff
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_extra_credentials.py
@@ -0,0 +1,69 @@
+"""Ensure extra credentials can be supplied for inclusion in tokens.
+"""
+from unittest import mock
+
+from oauthlib.oauth2 import (
+ BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
+ RequestValidator, WebApplicationServer,
+)
+
+from tests.unittest import TestCase
+
+
+class ExtraCredentialsTest(TestCase):
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
+ self.web = WebApplicationServer(self.validator)
+ self.mobile = MobileApplicationServer(self.validator)
+ self.legacy = LegacyApplicationServer(self.validator)
+ self.backend = BackendApplicationServer(self.validator)
+
+ def test_post_authorization_request(self):
+ def save_code(client_id, token, request):
+ self.assertEqual('creds', request.extra)
+
+ def save_token(token, request):
+ self.assertEqual('creds', request.extra)
+
+ # Authorization code grant
+ self.validator.save_authorization_code.side_effect = save_code
+ self.web.create_authorization_response(
+ 'https://i.b/auth?client_id=foo&response_type=code',
+ scopes=['foo'],
+ credentials={'extra': 'creds'})
+
+ # Implicit grant
+ self.validator.save_bearer_token.side_effect = save_token
+ self.mobile.create_authorization_response(
+ 'https://i.b/auth?client_id=foo&response_type=token',
+ scopes=['foo'],
+ credentials={'extra': 'creds'})
+
+ def test_token_request(self):
+ def save_token(token, request):
+ self.assertIn('extra', token)
+
+ self.validator.save_bearer_token.side_effect = save_token
+ self.validator.authenticate_client.side_effect = self.set_client
+
+ # Authorization code grant
+ self.web.create_token_response('https://i.b/token',
+ body='grant_type=authorization_code&code=foo',
+ credentials={'extra': 'creds'})
+
+ # Password credentials grant
+ self.legacy.create_token_response('https://i.b/token',
+ body='grant_type=password&username=foo&password=bar',
+ credentials={'extra': 'creds'})
+
+ # Client credentials grant
+ self.backend.create_token_response('https://i.b/token',
+ body='grant_type=client_credentials',
+ credentials={'extra': 'creds'})
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py
new file mode 100644
index 0000000000..6d3d119a3b
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+from json import loads
+from unittest.mock import MagicMock
+
+from oauthlib.common import urlencode
+from oauthlib.oauth2 import IntrospectEndpoint, RequestValidator
+
+from tests.unittest import TestCase
+
+
+class IntrospectEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.client_authentication_required.return_value = True
+ self.validator.authenticate_client.return_value = True
+ self.validator.validate_bearer_token.return_value = True
+ self.validator.introspect_token.return_value = {}
+ self.endpoint = IntrospectEndpoint(self.validator)
+
+ self.uri = 'should_not_matter'
+ self.headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+ self.resp_h = {
+ 'Cache-Control': 'no-store',
+ 'Content-Type': 'application/json',
+ 'Pragma': 'no-cache'
+ }
+ self.resp_b = {
+ "active": True
+ }
+
+ def test_introspect_token(self):
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type)])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), self.resp_b)
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_nohint(self):
+ # don't specify token_type_hint
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), self.resp_b)
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_false(self):
+ self.validator.introspect_token.return_value = None
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), {"active": False})
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_claims(self):
+ self.validator.introspect_token.return_value = {"foo": "bar"}
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), {"active": True, "foo": "bar"})
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_claims_spoof_active(self):
+ self.validator.introspect_token.return_value = {"foo": "bar", "active": False}
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), {"active": True, "foo": "bar"})
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_client_authentication_failed(self):
+ self.validator.authenticate_client.return_value = False
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer error="invalid_client"'
+ })
+ self.assertEqual(loads(b)['error'], 'invalid_client')
+ self.assertEqual(s, 401)
+
+ def test_introspect_token_public_client_authentication(self):
+ self.validator.client_authentication_required.return_value = False
+ self.validator.authenticate_client_id.return_value = True
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type)])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), self.resp_b)
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_public_client_authentication_failed(self):
+ self.validator.client_authentication_required.return_value = False
+ self.validator.authenticate_client_id.return_value = False
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer error="invalid_client"'
+ })
+ self.assertEqual(loads(b)['error'], 'invalid_client')
+ self.assertEqual(s, 401)
+
+ def test_introspect_unsupported_token(self):
+ endpoint = IntrospectEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'refresh_token')])
+ h, b, s = endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'unsupported_token_type')
+ self.assertEqual(s, 400)
+
+ h, b, s = endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body='')
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertEqual(s, 400)
+
+ def test_introspect_invalid_request_method(self):
+ endpoint = IntrospectEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ test_methods = ['GET', 'pUt', 'dEleTe', 'paTcH']
+ test_methods = test_methods + [x.lower() for x in test_methods] + [x.upper() for x in test_methods]
+ for method in test_methods:
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'refresh_token')])
+ h, b, s = endpoint.create_introspect_response(self.uri,
+ http_method = method, headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertIn('Unsupported request method', loads(b)['error_description'])
+ self.assertEqual(s, 400)
+
+ def test_introspect_bad_post_request(self):
+ endpoint = IntrospectEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ for param in ['token', 'secret', 'code', 'foo']:
+ uri = 'http://some.endpoint?' + urlencode([(param, 'secret')])
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = endpoint.create_introspect_response(
+ uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertIn('query parameters are not allowed', loads(b)['error_description'])
+ self.assertEqual(s, 400)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_metadata.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_metadata.py
new file mode 100644
index 0000000000..1f5b912100
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_metadata.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+import json
+
+from oauthlib.oauth2 import MetadataEndpoint, Server, TokenEndpoint
+
+from tests.unittest import TestCase
+
+
+class MetadataEndpointTest(TestCase):
+ def setUp(self):
+ self.metadata = {
+ "issuer": 'https://foo.bar'
+ }
+
+ def test_openid_oauth2_preconfigured(self):
+ default_claims = {
+ "issuer": 'https://foo.bar',
+ "authorization_endpoint": "https://foo.bar/authorize",
+ "revocation_endpoint": "https://foo.bar/revoke",
+ "introspection_endpoint": "https://foo.bar/introspect",
+ "token_endpoint": "https://foo.bar/token"
+ }
+ from oauthlib.oauth2 import Server as OAuth2Server
+ from oauthlib.openid import Server as OpenIDServer
+
+ endpoint = OAuth2Server(None)
+ metadata = MetadataEndpoint([endpoint], default_claims)
+ oauth2_claims = metadata.claims
+
+ endpoint = OpenIDServer(None)
+ metadata = MetadataEndpoint([endpoint], default_claims)
+ openid_claims = metadata.claims
+
+ # Pure OAuth2 Authorization Metadata are similar with OpenID but
+ # response_type not! (OIDC contains "id_token" and hybrid flows)
+ del oauth2_claims['response_types_supported']
+ del openid_claims['response_types_supported']
+
+ self.maxDiff = None
+ self.assertEqual(openid_claims, oauth2_claims)
+
+ def test_create_metadata_response(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token"
+ })
+ headers, body, status = metadata.create_metadata_response('/', 'GET')
+ assert headers == {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ }
+ claims = json.loads(body)
+ assert claims['issuer'] == 'https://foo.bar'
+
+ def test_token_endpoint(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token"
+ })
+ self.assertIn("grant_types_supported", metadata.claims)
+ self.assertEqual(metadata.claims["grant_types_supported"], ["password"])
+
+ def test_token_endpoint_overridden(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token",
+ "grant_types_supported": ["pass_word_special_provider"]
+ })
+ self.assertIn("grant_types_supported", metadata.claims)
+ self.assertEqual(metadata.claims["grant_types_supported"], ["pass_word_special_provider"])
+
+ def test_mandatory_fields(self):
+ metadata = MetadataEndpoint([], self.metadata)
+ self.assertIn("issuer", metadata.claims)
+ self.assertEqual(metadata.claims["issuer"], 'https://foo.bar')
+
+ def test_server_metadata(self):
+ endpoint = Server(None)
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "authorization_endpoint": "https://foo.bar/authorize",
+ "introspection_endpoint": "https://foo.bar/introspect",
+ "revocation_endpoint": "https://foo.bar/revoke",
+ "token_endpoint": "https://foo.bar/token",
+ "jwks_uri": "https://foo.bar/certs",
+ "scopes_supported": ["email", "profile"]
+ })
+ expected_claims = {
+ "issuer": "https://foo.bar",
+ "authorization_endpoint": "https://foo.bar/authorize",
+ "introspection_endpoint": "https://foo.bar/introspect",
+ "revocation_endpoint": "https://foo.bar/revoke",
+ "token_endpoint": "https://foo.bar/token",
+ "jwks_uri": "https://foo.bar/certs",
+ "scopes_supported": ["email", "profile"],
+ "grant_types_supported": [
+ "authorization_code",
+ "password",
+ "client_credentials",
+ "refresh_token",
+ "implicit"
+ ],
+ "token_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "response_types_supported": [
+ "code",
+ "token"
+ ],
+ "response_modes_supported": [
+ "query",
+ "fragment"
+ ],
+ "code_challenge_methods_supported": [
+ "plain",
+ "S256"
+ ],
+ "revocation_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "introspection_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ]
+ }
+
+ def sort_list(claims):
+ for k in claims.keys():
+ claims[k] = sorted(claims[k])
+
+ sort_list(metadata.claims)
+ sort_list(expected_claims)
+ self.assertEqual(sorted(metadata.claims.items()), sorted(expected_claims.items()))
+
+ def test_metadata_validate_issuer(self):
+ with self.assertRaises(ValueError):
+ endpoint = TokenEndpoint(
+ None, None, grant_types={"password": None},
+ )
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'http://foo.bar',
+ "token_endpoint": "https://foo.bar/token",
+ })
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py
new file mode 100644
index 0000000000..04533888e9
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py
@@ -0,0 +1,108 @@
+"""Ensure all tokens are associated with a resource owner.
+"""
+import json
+from unittest import mock
+
+from oauthlib.oauth2 import (
+ BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
+ RequestValidator, WebApplicationServer,
+)
+
+from tests.unittest import TestCase
+
+from .test_utils import get_fragment_credentials, get_query_credentials
+
+
+class ResourceOwnerAssociationTest(TestCase):
+
+ auth_uri = 'http://example.com/path?client_id=abc'
+ token_uri = 'http://example.com/path'
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def set_user(self, client_id, code, client, request):
+ request.user = 'test'
+ return True
+
+ def set_user_from_username(self, username, password, client, request):
+ request.user = 'test'
+ return True
+
+ def set_user_from_credentials(self, request):
+ request.user = 'test'
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def inspect_client(self, request, refresh_token=False):
+ if not request.user:
+ raise ValueError()
+ return 'abc'
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_default_redirect_uri.return_value = 'http://i.b./path'
+ self.validator.get_code_challenge.return_value = None
+ self.validator.authenticate_client.side_effect = self.set_client
+ self.web = WebApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.mobile = MobileApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.legacy = LegacyApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+ self.backend = BackendApplicationServer(self.validator,
+ token_generator=self.inspect_client)
+
+ def test_web_application(self):
+ # TODO: code generator + intercept test
+ h, _, s = self.web.create_authorization_response(
+ self.auth_uri + '&response_type=code',
+ credentials={'user': 'test'}, scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ code = get_query_credentials(h['Location'])['code'][0]
+ self.assertRaises(ValueError,
+ self.web.create_token_response, self.token_uri,
+ body='grant_type=authorization_code&code=%s' % code)
+
+ self.validator.validate_code.side_effect = self.set_user
+ _, body, _ = self.web.create_token_response(self.token_uri,
+ body='grant_type=authorization_code&code=%s' % code)
+ self.assertEqual(json.loads(body)['access_token'], 'abc')
+
+ def test_mobile_application(self):
+ self.assertRaises(ValueError,
+ self.mobile.create_authorization_response,
+ self.auth_uri + '&response_type=token')
+
+ h, _, s = self.mobile.create_authorization_response(
+ self.auth_uri + '&response_type=token',
+ credentials={'user': 'test'}, scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertEqual(get_fragment_credentials(h['Location'])['access_token'][0], 'abc')
+
+ def test_legacy_application(self):
+ body = 'grant_type=password&username=abc&password=secret'
+ self.assertRaises(ValueError,
+ self.legacy.create_token_response,
+ self.token_uri, body=body)
+
+ self.validator.validate_user.side_effect = self.set_user_from_username
+ _, body, _ = self.legacy.create_token_response(
+ self.token_uri, body=body)
+ self.assertEqual(json.loads(body)['access_token'], 'abc')
+
+ def test_backend_application(self):
+ body = 'grant_type=client_credentials'
+ self.assertRaises(ValueError,
+ self.backend.create_token_response,
+ self.token_uri, body=body)
+
+ self.validator.authenticate_client.side_effect = self.set_user_from_credentials
+ _, body, _ = self.backend.create_token_response(
+ self.token_uri, body=body)
+ self.assertEqual(json.loads(body)['access_token'], 'abc')
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py
new file mode 100644
index 0000000000..338dbd91fa
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+from json import loads
+from unittest.mock import MagicMock
+
+from oauthlib.common import urlencode
+from oauthlib.oauth2 import RequestValidator, RevocationEndpoint
+
+from tests.unittest import TestCase
+
+
+class RevocationEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.client_authentication_required.return_value = True
+ self.validator.authenticate_client.return_value = True
+ self.validator.revoke_token.return_value = True
+ self.endpoint = RevocationEndpoint(self.validator)
+
+ self.uri = 'https://example.com/revoke_token'
+ self.headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+ self.resp_h = {
+ 'Cache-Control': 'no-store',
+ 'Content-Type': 'application/json',
+ 'Pragma': 'no-cache'
+ }
+
+ def test_revoke_token(self):
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type)])
+ h, b, s = self.endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {})
+ self.assertEqual(b, '')
+ self.assertEqual(s, 200)
+
+ # don't specify token_type_hint
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {})
+ self.assertEqual(b, '')
+ self.assertEqual(s, 200)
+
+ def test_revoke_token_client_authentication_failed(self):
+ self.validator.authenticate_client.return_value = False
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = self.endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer error="invalid_client"'
+ })
+ self.assertEqual(loads(b)['error'], 'invalid_client')
+ self.assertEqual(s, 401)
+
+ def test_revoke_token_public_client_authentication(self):
+ self.validator.client_authentication_required.return_value = False
+ self.validator.authenticate_client_id.return_value = True
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type)])
+ h, b, s = self.endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {})
+ self.assertEqual(b, '')
+ self.assertEqual(s, 200)
+
+ def test_revoke_token_public_client_authentication_failed(self):
+ self.validator.client_authentication_required.return_value = False
+ self.validator.authenticate_client_id.return_value = False
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = self.endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer error="invalid_client"'
+ })
+ self.assertEqual(loads(b)['error'], 'invalid_client')
+ self.assertEqual(s, 401)
+
+ def test_revoke_with_callback(self):
+ endpoint = RevocationEndpoint(self.validator, enable_jsonp=True)
+ callback = 'package.hello_world'
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type),
+ ('callback', callback)])
+ h, b, s = endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {})
+ self.assertEqual(b, callback + '();')
+ self.assertEqual(s, 200)
+
+ def test_revoke_unsupported_token(self):
+ endpoint = RevocationEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'refresh_token')])
+ h, b, s = endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'unsupported_token_type')
+ self.assertEqual(s, 400)
+
+ h, b, s = endpoint.create_revocation_response(self.uri,
+ headers=self.headers, body='')
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertEqual(s, 400)
+
+ def test_revoke_invalid_request_method(self):
+ endpoint = RevocationEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ test_methods = ['GET', 'pUt', 'dEleTe', 'paTcH']
+ test_methods = test_methods + [x.lower() for x in test_methods] + [x.upper() for x in test_methods]
+ for method in test_methods:
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'refresh_token')])
+ h, b, s = endpoint.create_revocation_response(self.uri,
+ http_method = method, headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertIn('Unsupported request method', loads(b)['error_description'])
+ self.assertEqual(s, 400)
+
+ def test_revoke_bad_post_request(self):
+ endpoint = RevocationEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ for param in ['token', 'secret', 'code', 'foo']:
+ uri = 'http://some.endpoint?' + urlencode([(param, 'secret')])
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = endpoint.create_revocation_response(uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertIn('query parameters are not allowed', loads(b)['error_description'])
+ self.assertEqual(s, 400)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_scope_handling.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
new file mode 100644
index 0000000000..4c87d9c7c8
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
@@ -0,0 +1,193 @@
+"""Ensure scope is preserved across authorization.
+
+Fairly trivial in all grants except the Authorization Code Grant where scope
+need to be persisted temporarily in an authorization code.
+"""
+import json
+from unittest import mock
+
+from oauthlib.oauth2 import (
+ BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer,
+ RequestValidator, Server, WebApplicationServer,
+)
+
+from tests.unittest import TestCase
+
+from .test_utils import get_fragment_credentials, get_query_credentials
+
+
+class TestScopeHandling(TestCase):
+
+ DEFAULT_REDIRECT_URI = 'http://i.b./path'
+
+ def set_scopes(self, scopes):
+ def set_request_scopes(client_id, code, client, request):
+ request.scopes = scopes
+ return True
+ return set_request_scopes
+
+ def set_user(self, request):
+ request.user = 'foo'
+ request.client_id = 'bar'
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_default_redirect_uri.return_value = TestScopeHandling.DEFAULT_REDIRECT_URI
+ self.validator.get_code_challenge.return_value = None
+ self.validator.authenticate_client.side_effect = self.set_client
+ self.server = Server(self.validator)
+ self.web = WebApplicationServer(self.validator)
+ self.mobile = MobileApplicationServer(self.validator)
+ self.legacy = LegacyApplicationServer(self.validator)
+ self.backend = BackendApplicationServer(self.validator)
+
+ def test_scope_extraction(self):
+ scopes = (
+ ('images', ['images']),
+ ('images+videos', ['images', 'videos']),
+ ('images+videos+openid', ['images', 'videos', 'openid']),
+ ('http%3A%2f%2fa.b%2fvideos', ['http://a.b/videos']),
+ ('http%3A%2f%2fa.b%2fvideos+pics', ['http://a.b/videos', 'pics']),
+ ('pics+http%3A%2f%2fa.b%2fvideos', ['pics', 'http://a.b/videos']),
+ ('http%3A%2f%2fa.b%2fvideos+https%3A%2f%2fc.d%2Fsecret', ['http://a.b/videos', 'https://c.d/secret']),
+ )
+
+ uri = 'http://example.com/path?client_id=abc&scope=%s&response_type=%s'
+ for scope, correct_scopes in scopes:
+ scopes, _ = self.web.validate_authorization_request(
+ uri % (scope, 'code'))
+ self.assertCountEqual(scopes, correct_scopes)
+ scopes, _ = self.mobile.validate_authorization_request(
+ uri % (scope, 'token'))
+ self.assertCountEqual(scopes, correct_scopes)
+ scopes, _ = self.server.validate_authorization_request(
+ uri % (scope, 'code'))
+ self.assertCountEqual(scopes, correct_scopes)
+
+ def test_scope_preservation(self):
+ scope = 'pics+http%3A%2f%2fa.b%2fvideos'
+ decoded_scope = 'pics http://a.b/videos'
+ auth_uri = 'http://example.com/path?client_id=abc&response_type='
+ token_uri = 'http://example.com/path'
+
+ # authorization grant
+ for backend_server_type in ['web', 'server']:
+ h, _, s = getattr(self, backend_server_type).create_authorization_response(
+ auth_uri + 'code', scopes=decoded_scope.split(' '))
+ self.validator.validate_code.side_effect = self.set_scopes(decoded_scope.split(' '))
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ code = get_query_credentials(h['Location'])['code'][0]
+ _, body, _ = getattr(self, backend_server_type).create_token_response(token_uri,
+ body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code)
+ self.assertEqual(json.loads(body)['scope'], decoded_scope)
+
+ # implicit grant
+ for backend_server_type in ['mobile', 'server']:
+ h, _, s = getattr(self, backend_server_type).create_authorization_response(
+ auth_uri + 'token', scopes=decoded_scope.split(' '))
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertEqual(get_fragment_credentials(h['Location'])['scope'][0], decoded_scope)
+
+ # resource owner password credentials grant
+ for backend_server_type in ['legacy', 'server']:
+ body = 'grant_type=password&username=abc&password=secret&scope=%s'
+
+ _, body, _ = getattr(self, backend_server_type).create_token_response(token_uri,
+ body=body % scope)
+ self.assertEqual(json.loads(body)['scope'], decoded_scope)
+
+ # client credentials grant
+ for backend_server_type in ['backend', 'server']:
+ body = 'grant_type=client_credentials&scope=%s'
+ self.validator.authenticate_client.side_effect = self.set_user
+ _, body, _ = getattr(self, backend_server_type).create_token_response(token_uri,
+ body=body % scope)
+ self.assertEqual(json.loads(body)['scope'], decoded_scope)
+
+ def test_scope_changed(self):
+ scope = 'pics+http%3A%2f%2fa.b%2fvideos'
+ scopes = ['images', 'http://a.b/videos']
+ decoded_scope = 'images http://a.b/videos'
+ auth_uri = 'http://example.com/path?client_id=abc&response_type='
+ token_uri = 'http://example.com/path'
+
+ # authorization grant
+ h, _, s = self.web.create_authorization_response(
+ auth_uri + 'code', scopes=scopes)
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ code = get_query_credentials(h['Location'])['code'][0]
+ self.validator.validate_code.side_effect = self.set_scopes(scopes)
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=%s' % code)
+ self.assertEqual(json.loads(body)['scope'], decoded_scope)
+
+ # implicit grant
+ self.validator.validate_scopes.side_effect = self.set_scopes(scopes)
+ h, _, s = self.mobile.create_authorization_response(
+ auth_uri + 'token', scopes=scopes)
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertEqual(get_fragment_credentials(h['Location'])['scope'][0], decoded_scope)
+
+ # resource owner password credentials grant
+ self.validator.validate_scopes.side_effect = self.set_scopes(scopes)
+ body = 'grant_type=password&username=abc&password=secret&scope=%s'
+ _, body, _ = self.legacy.create_token_response(token_uri,
+ body=body % scope)
+ self.assertEqual(json.loads(body)['scope'], decoded_scope)
+
+ # client credentials grant
+ self.validator.validate_scopes.side_effect = self.set_scopes(scopes)
+ self.validator.authenticate_client.side_effect = self.set_user
+ body = 'grant_type=client_credentials&scope=%s'
+ _, body, _ = self.backend.create_token_response(token_uri,
+ body=body % scope)
+
+ self.assertEqual(json.loads(body)['scope'], decoded_scope)
+
+ def test_invalid_scope(self):
+ scope = 'pics+http%3A%2f%2fa.b%2fvideos'
+ auth_uri = 'http://example.com/path?client_id=abc&response_type='
+ token_uri = 'http://example.com/path'
+
+ self.validator.validate_scopes.return_value = False
+
+ # authorization grant
+ h, _, s = self.web.create_authorization_response(
+ auth_uri + 'code', scopes=['invalid'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ error = get_query_credentials(h['Location'])['error'][0]
+ self.assertEqual(error, 'invalid_scope')
+
+ # implicit grant
+ h, _, s = self.mobile.create_authorization_response(
+ auth_uri + 'token', scopes=['invalid'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ error = get_fragment_credentials(h['Location'])['error'][0]
+ self.assertEqual(error, 'invalid_scope')
+
+ # resource owner password credentials grant
+ body = 'grant_type=password&username=abc&password=secret&scope=%s'
+ _, body, _ = self.legacy.create_token_response(token_uri,
+ body=body % scope)
+ self.assertEqual(json.loads(body)['error'], 'invalid_scope')
+
+ # client credentials grant
+ self.validator.authenticate_client.side_effect = self.set_user
+ body = 'grant_type=client_credentials&scope=%s'
+ _, body, _ = self.backend.create_token_response(token_uri,
+ body=body % scope)
+ self.assertEqual(json.loads(body)['error'], 'invalid_scope')
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_utils.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_utils.py
new file mode 100644
index 0000000000..5eae1956f4
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/endpoints/test_utils.py
@@ -0,0 +1,11 @@
+import urllib.parse as urlparse
+
+
+def get_query_credentials(uri):
+ return urlparse.parse_qs(urlparse.urlparse(uri).query,
+ keep_blank_values=True)
+
+
+def get_fragment_credentials(uri):
+ return urlparse.parse_qs(urlparse.urlparse(uri).fragment,
+ keep_blank_values=True)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/__init__.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_authorization_code.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_authorization_code.py
new file mode 100644
index 0000000000..77e1a81b46
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_authorization_code.py
@@ -0,0 +1,382 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.grant_types import (
+ AuthorizationCodeGrant, authorization_code,
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from tests.unittest import TestCase
+
+
+class AuthorizationCodeGrantTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'world')
+ self.request.expires_in = 1800
+ self.request.client = 'batman'
+ self.request.client_id = 'abcdef'
+ self.request.code = '1234'
+ self.request.response_type = 'code'
+ self.request.grant_type = 'authorization_code'
+ self.request.redirect_uri = 'https://a.b/cb'
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.is_pkce_required.return_value = False
+ self.mock_validator.get_code_challenge.return_value = None
+ self.mock_validator.is_origin_allowed.return_value = False
+ self.mock_validator.authenticate_client.side_effect = self.set_client
+ self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator)
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def setup_validators(self):
+ self.authval1, self.authval2 = mock.Mock(), mock.Mock()
+ self.authval1.return_value = {}
+ self.authval2.return_value = {}
+ self.tknval1, self.tknval2 = mock.Mock(), mock.Mock()
+ self.tknval1.return_value = None
+ self.tknval2.return_value = None
+ self.auth.custom_validators.pre_token.append(self.tknval1)
+ self.auth.custom_validators.post_token.append(self.tknval2)
+ self.auth.custom_validators.pre_auth.append(self.authval1)
+ self.auth.custom_validators.post_auth.append(self.authval2)
+
+ def test_custom_auth_validators(self):
+ self.setup_validators()
+
+ bearer = BearerToken(self.mock_validator)
+ self.auth.create_authorization_response(self.request, bearer)
+ self.assertTrue(self.authval1.called)
+ self.assertTrue(self.authval2.called)
+ self.assertFalse(self.tknval1.called)
+ self.assertFalse(self.tknval2.called)
+
+ def test_custom_token_validators(self):
+ self.setup_validators()
+
+ bearer = BearerToken(self.mock_validator)
+ self.auth.create_token_response(self.request, bearer)
+ self.assertTrue(self.tknval1.called)
+ self.assertTrue(self.tknval2.called)
+ self.assertFalse(self.authval1.called)
+ self.assertFalse(self.authval2.called)
+
+ def test_create_authorization_grant(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ grant = dict(Request(h['Location']).uri_query_params)
+ self.assertIn('code', grant)
+ self.assertTrue(self.mock_validator.validate_redirect_uri.called)
+ self.assertTrue(self.mock_validator.validate_response_type.called)
+ self.assertTrue(self.mock_validator.validate_scopes.called)
+
+ def test_create_authorization_grant_no_scopes(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ self.request.scopes = []
+ self.auth.create_authorization_response(self.request, bearer)
+
+ def test_create_authorization_grant_state(self):
+ self.request.state = 'abc'
+ self.request.redirect_uri = None
+ self.request.response_mode = 'query'
+ self.mock_validator.get_default_redirect_uri.return_value = 'https://a.b/cb'
+ bearer = BearerToken(self.mock_validator)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ grant = dict(Request(h['Location']).uri_query_params)
+ self.assertIn('code', grant)
+ self.assertIn('state', grant)
+ self.assertFalse(self.mock_validator.validate_redirect_uri.called)
+ self.assertTrue(self.mock_validator.get_default_redirect_uri.called)
+ self.assertTrue(self.mock_validator.validate_response_type.called)
+ self.assertTrue(self.mock_validator.validate_scopes.called)
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_create_authorization_response(self, generate_token):
+ generate_token.return_value = 'abc'
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], 'https://a.b/cb?code=abc')
+ self.request.response_mode = 'fragment'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], 'https://a.b/cb#code=abc')
+
+ def test_create_token_response(self):
+ bearer = BearerToken(self.mock_validator)
+
+ h, token, s = self.auth.create_token_response(self.request, bearer)
+ token = json.loads(token)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('scope', token)
+ self.assertTrue(self.mock_validator.client_authentication_required.called)
+ self.assertTrue(self.mock_validator.authenticate_client.called)
+ self.assertTrue(self.mock_validator.validate_code.called)
+ self.assertTrue(self.mock_validator.confirm_redirect_uri.called)
+ self.assertTrue(self.mock_validator.validate_grant_type.called)
+ self.assertTrue(self.mock_validator.invalidate_authorization_code.called)
+
+ def test_create_token_response_without_refresh_token(self):
+ self.auth.refresh_token = False # Not to issue refresh token.
+
+ bearer = BearerToken(self.mock_validator)
+ h, token, s = self.auth.create_token_response(self.request, bearer)
+ token = json.loads(token)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertNotIn('refresh_token', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('scope', token)
+ self.assertTrue(self.mock_validator.client_authentication_required.called)
+ self.assertTrue(self.mock_validator.authenticate_client.called)
+ self.assertTrue(self.mock_validator.validate_code.called)
+ self.assertTrue(self.mock_validator.confirm_redirect_uri.called)
+ self.assertTrue(self.mock_validator.validate_grant_type.called)
+ self.assertTrue(self.mock_validator.invalidate_authorization_code.called)
+
+ def test_invalid_request(self):
+ del self.request.code
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_token_request,
+ self.request)
+
+ def test_invalid_request_duplicates(self):
+ request = mock.MagicMock(wraps=self.request)
+ request.grant_type = 'authorization_code'
+ request.duplicate_params = ['client_id']
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_token_request,
+ request)
+
+ def test_authentication_required(self):
+ """
+ ensure client_authentication_required() is properly called
+ """
+ self.auth.validate_token_request(self.request)
+ self.mock_validator.client_authentication_required.assert_called_once_with(self.request)
+
+ def test_authenticate_client(self):
+ self.mock_validator.authenticate_client.side_effect = None
+ self.mock_validator.authenticate_client.return_value = False
+ self.assertRaises(errors.InvalidClientError, self.auth.validate_token_request,
+ self.request)
+
+ def test_client_id_missing(self):
+ self.mock_validator.authenticate_client.side_effect = None
+ request = mock.MagicMock(wraps=self.request)
+ request.grant_type = 'authorization_code'
+ del request.client.client_id
+ self.assertRaises(NotImplementedError, self.auth.validate_token_request,
+ request)
+
+ def test_invalid_grant(self):
+ self.request.client = 'batman'
+ self.mock_validator.authenticate_client = self.set_client
+ self.mock_validator.validate_code.return_value = False
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_invalid_grant_type(self):
+ self.request.grant_type = 'foo'
+ self.assertRaises(errors.UnsupportedGrantTypeError,
+ self.auth.validate_token_request, self.request)
+
+ def test_authenticate_client_id(self):
+ self.mock_validator.client_authentication_required.return_value = False
+ self.mock_validator.authenticate_client_id.return_value = False
+ self.request.state = 'abc'
+ self.assertRaises(errors.InvalidClientError,
+ self.auth.validate_token_request, self.request)
+
+ def test_invalid_redirect_uri(self):
+ self.mock_validator.confirm_redirect_uri.return_value = False
+ self.assertRaises(errors.MismatchingRedirectURIError,
+ self.auth.validate_token_request, self.request)
+
+ # PKCE validate_authorization_request
+ def test_pkce_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.assertRaises(errors.MissingCodeChallengeError,
+ self.auth.validate_authorization_request, self.request)
+
+ def test_pkce_default_method(self):
+ for required in [True, False]:
+ self.mock_validator.is_pkce_required.return_value = required
+ self.request.code_challenge = "present"
+ _, ri = self.auth.validate_authorization_request(self.request)
+ self.assertIn("code_challenge", ri)
+ self.assertIn("code_challenge_method", ri)
+ self.assertEqual(ri["code_challenge"], "present")
+ self.assertEqual(ri["code_challenge_method"], "plain")
+
+ def test_pkce_wrong_method(self):
+ for required in [True, False]:
+ self.mock_validator.is_pkce_required.return_value = required
+ self.request.code_challenge = "present"
+ self.request.code_challenge_method = "foobar"
+ self.assertRaises(errors.UnsupportedCodeChallengeMethodError,
+ self.auth.validate_authorization_request, self.request)
+
+ # PKCE validate_token_request
+ def test_pkce_verifier_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ # PKCE validate_token_request
+ def test_pkce_required_verifier_missing_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = None
+ self.mock_validator.get_code_challenge.return_value = None
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_missing_challenge_valid(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = None
+ self.mock_validator.get_code_challenge.return_value = "foo"
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_valid_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "foobar"
+ self.mock_validator.get_code_challenge.return_value = None
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_valid_challenge_valid_method_valid(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "foobar"
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.mock_validator.get_code_challenge_method.return_value = "plain"
+ self.auth.validate_token_request(self.request)
+
+ def test_pkce_required_verifier_invalid_challenge_valid_method_valid(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "foobar"
+ self.mock_validator.get_code_challenge.return_value = "raboof"
+ self.mock_validator.get_code_challenge_method.return_value = "plain"
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_valid_challenge_valid_method_wrong(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "present"
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.mock_validator.get_code_challenge_method.return_value = "cryptic_method"
+ self.assertRaises(errors.ServerError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_verifier_valid_challenge_valid_method_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "present"
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.mock_validator.get_code_challenge_method.return_value = None
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_optional_verifier_valid_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = False
+ self.request.code_verifier = "present"
+ self.mock_validator.get_code_challenge.return_value = None
+ self.auth.validate_token_request(self.request)
+
+ def test_pkce_optional_verifier_missing_challenge_valid(self):
+ self.mock_validator.is_pkce_required.return_value = False
+ self.request.code_verifier = None
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ # PKCE functions
+ def test_wrong_code_challenge_method_plain(self):
+ self.assertFalse(authorization_code.code_challenge_method_plain("foo", "bar"))
+
+ def test_correct_code_challenge_method_plain(self):
+ self.assertTrue(authorization_code.code_challenge_method_plain("foo", "foo"))
+
+ def test_wrong_code_challenge_method_s256(self):
+ self.assertFalse(authorization_code.code_challenge_method_s256("foo", "bar"))
+
+ def test_correct_code_challenge_method_s256(self):
+ # "abcd" as verifier gives a '+' to base64
+ self.assertTrue(
+ authorization_code.code_challenge_method_s256("abcd",
+ "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o-Fhk28DFYk")
+ )
+ # "/" as verifier gives a '/' and '+' to base64
+ self.assertTrue(
+ authorization_code.code_challenge_method_s256("/",
+ "il7asoJjJEMhngUeSt4tHVu8Zxx4EFG_FDeJfL3-oPE")
+ )
+ # Example from PKCE RFCE
+ self.assertTrue(
+ authorization_code.code_challenge_method_s256("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
+ "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
+ )
+
+ def test_code_modifier_called(self):
+ bearer = BearerToken(self.mock_validator)
+ code_modifier = mock.MagicMock(wraps=lambda grant, *a: grant)
+ self.auth.register_code_modifier(code_modifier)
+ self.auth.create_authorization_response(self.request, bearer)
+ code_modifier.assert_called_once()
+
+ def test_hybrid_token_save(self):
+ bearer = BearerToken(self.mock_validator)
+ self.auth.register_code_modifier(
+ lambda grant, *a: dict(list(grant.items()) + [('access_token', 1)])
+ )
+ self.auth.create_authorization_response(self.request, bearer)
+ self.mock_validator.save_token.assert_called_once()
+
+ # CORS
+
+ def test_create_cors_headers(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'https://foo.bar'
+ self.mock_validator.is_origin_allowed.return_value = True
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertEqual(
+ headers['Access-Control-Allow-Origin'], 'https://foo.bar'
+ )
+ self.mock_validator.is_origin_allowed.assert_called_once_with(
+ 'abcdef', 'https://foo.bar', self.request
+ )
+
+ def test_create_cors_headers_no_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_not_called()
+
+ def test_create_cors_headers_insecure_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'http://foo.bar'
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_not_called()
+
+ def test_create_cors_headers_invalid_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'https://foo.bar'
+ self.mock_validator.is_origin_allowed.return_value = False
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_called_once_with(
+ 'abcdef', 'https://foo.bar', self.request
+ )
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_client_credentials.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_client_credentials.py
new file mode 100644
index 0000000000..e9559c7931
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_client_credentials.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.grant_types import ClientCredentialsGrant
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from tests.unittest import TestCase
+
+
+class ClientCredentialsGrantTest(TestCase):
+
+ def setUp(self):
+ mock_client = mock.MagicMock()
+ mock_client.user.return_value = 'mocked user'
+ self.request = Request('http://a.b/path')
+ self.request.grant_type = 'client_credentials'
+ self.request.client = mock_client
+ self.request.scopes = ('mocked', 'scopes')
+ self.mock_validator = mock.MagicMock()
+ self.auth = ClientCredentialsGrant(
+ request_validator=self.mock_validator)
+
+ def test_custom_auth_validators_unsupported(self):
+ authval1, authval2 = mock.Mock(), mock.Mock()
+ expected = ('ClientCredentialsGrant does not support authorization '
+ 'validators. Use token validators instead.')
+ with self.assertRaises(ValueError) as caught:
+ ClientCredentialsGrant(self.mock_validator, pre_auth=[authval1])
+ self.assertEqual(caught.exception.args[0], expected)
+ with self.assertRaises(ValueError) as caught:
+ ClientCredentialsGrant(self.mock_validator, post_auth=[authval2])
+ self.assertEqual(caught.exception.args[0], expected)
+ with self.assertRaises(AttributeError):
+ self.auth.custom_validators.pre_auth.append(authval1)
+ with self.assertRaises(AttributeError):
+ self.auth.custom_validators.pre_auth.append(authval2)
+
+ def test_custom_token_validators(self):
+ tknval1, tknval2 = mock.Mock(), mock.Mock()
+ self.auth.custom_validators.pre_token.append(tknval1)
+ self.auth.custom_validators.post_token.append(tknval2)
+
+ bearer = BearerToken(self.mock_validator)
+ self.auth.create_token_response(self.request, bearer)
+ self.assertTrue(tknval1.called)
+ self.assertTrue(tknval2.called)
+
+ def test_create_token_response(self):
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('Content-Type', headers)
+ self.assertEqual(headers['Content-Type'], 'application/json')
+
+ def test_error_response(self):
+ bearer = BearerToken(self.mock_validator)
+ self.mock_validator.authenticate_client.return_value = False
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+ error_msg = json.loads(body)
+ self.assertIn('error', error_msg)
+ self.assertEqual(error_msg['error'], 'invalid_client')
+ self.assertIn('Content-Type', headers)
+ self.assertEqual(headers['Content-Type'], 'application/json')
+
+ def test_validate_token_response(self):
+ # wrong grant type, scope
+ pass
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_implicit.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_implicit.py
new file mode 100644
index 0000000000..1fb71a1dc9
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_implicit.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.grant_types import ImplicitGrant
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from tests.unittest import TestCase
+
+
+class ImplicitGrantTest(TestCase):
+
+ def setUp(self):
+ mock_client = mock.MagicMock()
+ mock_client.user.return_value = 'mocked user'
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'world')
+ self.request.client = mock_client
+ self.request.client_id = 'abcdef'
+ self.request.response_type = 'token'
+ self.request.state = 'xyz'
+ self.request.redirect_uri = 'https://b.c/p'
+
+ self.mock_validator = mock.MagicMock()
+ self.auth = ImplicitGrant(request_validator=self.mock_validator)
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_create_token_response(self, generate_token):
+ generate_token.return_value = '1234'
+ bearer = BearerToken(self.mock_validator, expires_in=1800)
+ h, b, s = self.auth.create_token_response(self.request, bearer)
+ correct_uri = 'https://b.c/p#access_token=1234&token_type=Bearer&expires_in=1800&state=xyz&scope=hello+world'
+ self.assertEqual(s, 302)
+ self.assertURLEqual(h['Location'], correct_uri, parse_fragment=True)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+
+ correct_uri = 'https://b.c/p?access_token=1234&token_type=Bearer&expires_in=1800&state=xyz&scope=hello+world'
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_token_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], correct_uri)
+
+ def test_custom_validators(self):
+ self.authval1, self.authval2 = mock.Mock(), mock.Mock()
+ self.tknval1, self.tknval2 = mock.Mock(), mock.Mock()
+ for val in (self.authval1, self.authval2):
+ val.return_value = {}
+ for val in (self.tknval1, self.tknval2):
+ val.return_value = None
+ self.auth.custom_validators.pre_token.append(self.tknval1)
+ self.auth.custom_validators.post_token.append(self.tknval2)
+ self.auth.custom_validators.pre_auth.append(self.authval1)
+ self.auth.custom_validators.post_auth.append(self.authval2)
+
+ bearer = BearerToken(self.mock_validator)
+ self.auth.create_token_response(self.request, bearer)
+ self.assertTrue(self.tknval1.called)
+ self.assertTrue(self.tknval2.called)
+ self.assertTrue(self.authval1.called)
+ self.assertTrue(self.authval2.called)
+
+ def test_error_response(self):
+ pass
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
new file mode 100644
index 0000000000..581f2a4d6a
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.grant_types import RefreshTokenGrant
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from tests.unittest import TestCase
+
+
+class RefreshTokenGrantTest(TestCase):
+
+ def setUp(self):
+ mock_client = mock.MagicMock()
+ mock_client.user.return_value = 'mocked user'
+ self.request = Request('http://a.b/path')
+ self.request.grant_type = 'refresh_token'
+ self.request.refresh_token = 'lsdkfhj230'
+ self.request.client_id = 'abcdef'
+ self.request.client = mock_client
+ self.request.scope = 'foo'
+ self.mock_validator = mock.MagicMock()
+ self.auth = RefreshTokenGrant(
+ request_validator=self.mock_validator)
+
+ def test_create_token_response(self):
+ self.mock_validator.get_original_scopes.return_value = ['foo', 'bar']
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertEqual(token['scope'], 'foo')
+
+ def test_custom_auth_validators_unsupported(self):
+ authval1, authval2 = mock.Mock(), mock.Mock()
+ expected = ('RefreshTokenGrant does not support authorization '
+ 'validators. Use token validators instead.')
+ with self.assertRaises(ValueError) as caught:
+ RefreshTokenGrant(self.mock_validator, pre_auth=[authval1])
+ self.assertEqual(caught.exception.args[0], expected)
+ with self.assertRaises(ValueError) as caught:
+ RefreshTokenGrant(self.mock_validator, post_auth=[authval2])
+ self.assertEqual(caught.exception.args[0], expected)
+ with self.assertRaises(AttributeError):
+ self.auth.custom_validators.pre_auth.append(authval1)
+ with self.assertRaises(AttributeError):
+ self.auth.custom_validators.pre_auth.append(authval2)
+
+ def test_custom_token_validators(self):
+ tknval1, tknval2 = mock.Mock(), mock.Mock()
+ self.auth.custom_validators.pre_token.append(tknval1)
+ self.auth.custom_validators.post_token.append(tknval2)
+
+ bearer = BearerToken(self.mock_validator)
+ self.auth.create_token_response(self.request, bearer)
+ self.assertTrue(tknval1.called)
+ self.assertTrue(tknval2.called)
+
+ def test_create_token_inherit_scope(self):
+ self.request.scope = None
+ self.mock_validator.get_original_scopes.return_value = ['foo', 'bar']
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertEqual(token['scope'], 'foo bar')
+
+ def test_create_token_within_original_scope(self):
+ self.mock_validator.get_original_scopes.return_value = ['baz']
+ self.mock_validator.is_within_original_scope.return_value = True
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertEqual(token['scope'], 'foo')
+
+ def test_invalid_scope(self):
+ self.mock_validator.get_original_scopes.return_value = ['baz']
+ self.mock_validator.is_within_original_scope.return_value = False
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+ self.assertEqual(token['error'], 'invalid_scope')
+ self.assertEqual(status_code, 400)
+
+ def test_invalid_token(self):
+ self.mock_validator.validate_refresh_token.return_value = False
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+ self.assertEqual(token['error'], 'invalid_grant')
+ self.assertEqual(status_code, 400)
+
+ def test_invalid_client(self):
+ self.mock_validator.authenticate_client.return_value = False
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+ self.assertEqual(token['error'], 'invalid_client')
+ self.assertEqual(status_code, 401)
+
+ def test_authentication_required(self):
+ """
+ ensure client_authentication_required() is properly called
+ """
+ self.mock_validator.authenticate_client.return_value = False
+ self.mock_validator.authenticate_client_id.return_value = False
+ self.request.code = 'waffles'
+ self.assertRaises(errors.InvalidClientError, self.auth.validate_token_request,
+ self.request)
+ self.mock_validator.client_authentication_required.assert_called_once_with(self.request)
+
+ def test_invalid_grant_type(self):
+ self.request.grant_type = 'wrong_type'
+ self.assertRaises(errors.UnsupportedGrantTypeError,
+ self.auth.validate_token_request, self.request)
+
+ def test_authenticate_client_id(self):
+ self.mock_validator.client_authentication_required.return_value = False
+ self.request.refresh_token = mock.MagicMock()
+ self.mock_validator.authenticate_client_id.return_value = False
+ self.assertRaises(errors.InvalidClientError,
+ self.auth.validate_token_request, self.request)
+
+ def test_invalid_refresh_token(self):
+ # invalid refresh token
+ self.mock_validator.authenticate_client_id.return_value = True
+ self.mock_validator.validate_refresh_token.return_value = False
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+ # no token provided
+ del self.request.refresh_token
+ self.assertRaises(errors.InvalidRequestError,
+ self.auth.validate_token_request, self.request)
+
+ def test_invalid_scope_original_scopes_empty(self):
+ self.mock_validator.validate_refresh_token.return_value = True
+ self.mock_validator.is_within_original_scope.return_value = False
+ self.assertRaises(errors.InvalidScopeError,
+ self.auth.validate_token_request, self.request)
+
+ def test_valid_token_request(self):
+ self.request.scope = 'foo bar'
+ self.mock_validator.get_original_scopes = mock.Mock()
+ self.mock_validator.get_original_scopes.return_value = 'foo bar baz'
+ self.auth.validate_token_request(self.request)
+ self.assertEqual(self.request.scopes, self.request.scope.split())
+ # all ok but without request.scope
+ del self.request.scope
+ self.auth.validate_token_request(self.request)
+ self.assertEqual(self.request.scopes, 'foo bar baz'.split())
+
+ # CORS
+
+ def test_create_cors_headers(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'https://foo.bar'
+ self.mock_validator.is_origin_allowed.return_value = True
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertEqual(
+ headers['Access-Control-Allow-Origin'], 'https://foo.bar'
+ )
+ self.mock_validator.is_origin_allowed.assert_called_once_with(
+ 'abcdef', 'https://foo.bar', self.request
+ )
+
+ def test_create_cors_headers_no_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_not_called()
+
+ def test_create_cors_headers_insecure_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'http://foo.bar'
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_not_called()
+
+ def test_create_cors_headers_invalid_origin(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.headers['origin'] = 'https://foo.bar'
+ self.mock_validator.is_origin_allowed.return_value = False
+
+ headers = self.auth.create_token_response(self.request, bearer)[0]
+ self.assertNotIn('Access-Control-Allow-Origin', headers)
+ self.mock_validator.is_origin_allowed.assert_called_once_with(
+ 'abcdef', 'https://foo.bar', self.request
+ )
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py
new file mode 100644
index 0000000000..294e27be35
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.grant_types import (
+ ResourceOwnerPasswordCredentialsGrant,
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from tests.unittest import TestCase
+
+
+class ResourceOwnerPasswordCredentialsGrantTest(TestCase):
+
+ def setUp(self):
+ mock_client = mock.MagicMock()
+ mock_client.user.return_value = 'mocked user'
+ self.request = Request('http://a.b/path')
+ self.request.grant_type = 'password'
+ self.request.username = 'john'
+ self.request.password = 'doe'
+ self.request.client = mock_client
+ self.request.scopes = ('mocked', 'scopes')
+ self.mock_validator = mock.MagicMock()
+ self.auth = ResourceOwnerPasswordCredentialsGrant(
+ request_validator=self.mock_validator)
+
+ def set_client(self, request, *args, **kwargs):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def test_create_token_response(self):
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('refresh_token', token)
+ # ensure client_authentication_required() is properly called
+ self.mock_validator.client_authentication_required.assert_called_once_with(self.request)
+ # fail client authentication
+ self.mock_validator.reset_mock()
+ self.mock_validator.validate_user.return_value = True
+ self.mock_validator.authenticate_client.return_value = False
+ status_code = self.auth.create_token_response(self.request, bearer)[2]
+ self.assertEqual(status_code, 401)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+
+ # mock client_authentication_required() returning False then fail
+ self.mock_validator.reset_mock()
+ self.mock_validator.client_authentication_required.return_value = False
+ self.mock_validator.authenticate_client_id.return_value = False
+ status_code = self.auth.create_token_response(self.request, bearer)[2]
+ self.assertEqual(status_code, 401)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+
+ def test_create_token_response_without_refresh_token(self):
+ # self.auth.refresh_token = False so we don't generate a refresh token
+ self.auth = ResourceOwnerPasswordCredentialsGrant(
+ request_validator=self.mock_validator, refresh_token=False)
+ bearer = BearerToken(self.mock_validator)
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer)
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ # ensure no refresh token is generated
+ self.assertNotIn('refresh_token', token)
+ # ensure client_authentication_required() is properly called
+ self.mock_validator.client_authentication_required.assert_called_once_with(self.request)
+ # fail client authentication
+ self.mock_validator.reset_mock()
+ self.mock_validator.validate_user.return_value = True
+ self.mock_validator.authenticate_client.return_value = False
+ status_code = self.auth.create_token_response(self.request, bearer)[2]
+ self.assertEqual(status_code, 401)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+ # mock client_authentication_required() returning False then fail
+ self.mock_validator.reset_mock()
+ self.mock_validator.client_authentication_required.return_value = False
+ self.mock_validator.authenticate_client_id.return_value = False
+ status_code = self.auth.create_token_response(self.request, bearer)[2]
+ self.assertEqual(status_code, 401)
+ self.assertEqual(self.mock_validator.save_token.call_count, 0)
+
+ def test_custom_auth_validators_unsupported(self):
+ authval1, authval2 = mock.Mock(), mock.Mock()
+ expected = ('ResourceOwnerPasswordCredentialsGrant does not '
+ 'support authorization validators. Use token '
+ 'validators instead.')
+ with self.assertRaises(ValueError) as caught:
+ ResourceOwnerPasswordCredentialsGrant(self.mock_validator,
+ pre_auth=[authval1])
+ self.assertEqual(caught.exception.args[0], expected)
+ with self.assertRaises(ValueError) as caught:
+ ResourceOwnerPasswordCredentialsGrant(self.mock_validator,
+ post_auth=[authval2])
+ self.assertEqual(caught.exception.args[0], expected)
+ with self.assertRaises(AttributeError):
+ self.auth.custom_validators.pre_auth.append(authval1)
+ with self.assertRaises(AttributeError):
+ self.auth.custom_validators.pre_auth.append(authval2)
+
+ def test_custom_token_validators(self):
+ tknval1, tknval2 = mock.Mock(), mock.Mock()
+ self.auth.custom_validators.pre_token.append(tknval1)
+ self.auth.custom_validators.post_token.append(tknval2)
+
+ bearer = BearerToken(self.mock_validator)
+ self.auth.create_token_response(self.request, bearer)
+ self.assertTrue(tknval1.called)
+ self.assertTrue(tknval2.called)
+
+ def test_error_response(self):
+ pass
+
+ def test_scopes(self):
+ pass
+
+ def test_invalid_request_missing_params(self):
+ del self.request.grant_type
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_token_request,
+ self.request)
+
+ def test_invalid_request_duplicates(self):
+ request = mock.MagicMock(wraps=self.request)
+ request.duplicate_params = ['scope']
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_token_request,
+ request)
+
+ def test_invalid_grant_type(self):
+ self.request.grant_type = 'foo'
+ self.assertRaises(errors.UnsupportedGrantTypeError,
+ self.auth.validate_token_request, self.request)
+
+ def test_invalid_user(self):
+ self.mock_validator.validate_user.return_value = False
+ self.assertRaises(errors.InvalidGrantError, self.auth.validate_token_request,
+ self.request)
+
+ def test_client_id_missing(self):
+ del self.request.client.client_id
+ self.assertRaises(NotImplementedError, self.auth.validate_token_request,
+ self.request)
+
+ def test_valid_token_request(self):
+ self.mock_validator.validate_grant_type.return_value = True
+ self.auth.validate_token_request(self.request)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/test_parameters.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_parameters.py
new file mode 100644
index 0000000000..cd8c9e952b
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_parameters.py
@@ -0,0 +1,304 @@
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2.rfc6749.errors import *
+from oauthlib.oauth2.rfc6749.parameters import *
+
+from tests.unittest import TestCase
+
+
+@patch('time.time', new=lambda: 1000)
+class ParameterTests(TestCase):
+
+ state = 'xyz'
+ auth_base = {
+ 'uri': 'https://server.example.com/authorize',
+ 'client_id': 's6BhdRkqt3',
+ 'redirect_uri': 'https://client.example.com/cb',
+ 'state': state,
+ 'scope': 'photos'
+ }
+ list_scope = ['list', 'of', 'scopes']
+
+ auth_grant = {'response_type': 'code'}
+ auth_grant_pkce = {'response_type': 'code', 'code_challenge': "code_challenge",
+ 'code_challenge_method': 'code_challenge_method'}
+ auth_grant_list_scope = {}
+ auth_implicit = {'response_type': 'token', 'extra': 'extra'}
+ auth_implicit_list_scope = {}
+
+ def setUp(self):
+ self.auth_grant.update(self.auth_base)
+ self.auth_grant_pkce.update(self.auth_base)
+ self.auth_implicit.update(self.auth_base)
+ self.auth_grant_list_scope.update(self.auth_grant)
+ self.auth_grant_list_scope['scope'] = self.list_scope
+ self.auth_implicit_list_scope.update(self.auth_implicit)
+ self.auth_implicit_list_scope['scope'] = self.list_scope
+
+ auth_base_uri = ('https://server.example.com/authorize?response_type={0}'
+ '&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2F'
+ 'client.example.com%2Fcb&scope={1}&state={2}{3}')
+
+ auth_base_uri_pkce = ('https://server.example.com/authorize?response_type={0}'
+ '&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2F'
+ 'client.example.com%2Fcb&scope={1}&state={2}{3}&code_challenge={4}'
+ '&code_challenge_method={5}')
+
+ auth_grant_uri = auth_base_uri.format('code', 'photos', state, '')
+ auth_grant_uri_pkce = auth_base_uri_pkce.format('code', 'photos', state, '', 'code_challenge',
+ 'code_challenge_method')
+ auth_grant_uri_list_scope = auth_base_uri.format('code', 'list+of+scopes', state, '')
+ auth_implicit_uri = auth_base_uri.format('token', 'photos', state, '&extra=extra')
+ auth_implicit_uri_list_scope = auth_base_uri.format('token', 'list+of+scopes', state, '&extra=extra')
+
+ grant_body = {
+ 'grant_type': 'authorization_code',
+ 'code': 'SplxlOBeZQQYbYS6WxSbIA',
+ 'redirect_uri': 'https://client.example.com/cb'
+ }
+ grant_body_pkce = {
+ 'grant_type': 'authorization_code',
+ 'code': 'SplxlOBeZQQYbYS6WxSbIA',
+ 'redirect_uri': 'https://client.example.com/cb',
+ 'code_verifier': 'code_verifier'
+ }
+ grant_body_scope = {'scope': 'photos'}
+ grant_body_list_scope = {'scope': list_scope}
+ auth_grant_body = ('grant_type=authorization_code&'
+ 'code=SplxlOBeZQQYbYS6WxSbIA&'
+ 'redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb')
+ auth_grant_body_pkce = ('grant_type=authorization_code&'
+ 'code=SplxlOBeZQQYbYS6WxSbIA&'
+ 'redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb'
+ '&code_verifier=code_verifier')
+ auth_grant_body_scope = auth_grant_body + '&scope=photos'
+ auth_grant_body_list_scope = auth_grant_body + '&scope=list+of+scopes'
+
+ pwd_body = {
+ 'grant_type': 'password',
+ 'username': 'johndoe',
+ 'password': 'A3ddj3w'
+ }
+ password_body = 'grant_type=password&username=johndoe&password=A3ddj3w'
+
+ cred_grant = {'grant_type': 'client_credentials'}
+ cred_body = 'grant_type=client_credentials'
+
+ grant_response = 'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz'
+ grant_dict = {'code': 'SplxlOBeZQQYbYS6WxSbIA', 'state': state}
+
+ error_nocode = 'https://client.example.com/cb?state=xyz'
+ error_nostate = 'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA'
+ error_wrongstate = 'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=abc'
+ error_denied = 'https://client.example.com/cb?error=access_denied&state=xyz'
+ error_invalid = 'https://client.example.com/cb?error=invalid_request&state=xyz'
+
+ implicit_base = 'https://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&scope=abc&'
+ implicit_response = implicit_base + 'state={}&token_type=example&expires_in=3600'.format(state)
+ implicit_notype = implicit_base + 'state={}&expires_in=3600'.format(state)
+ implicit_wrongstate = implicit_base + 'state={}&token_type=exampleexpires_in=3600'.format('invalid')
+ implicit_nostate = implicit_base + 'token_type=example&expires_in=3600'
+ implicit_notoken = 'https://example.com/cb#state=xyz&token_type=example&expires_in=3600'
+
+ implicit_dict = {
+ 'access_token': '2YotnFZFEjr1zCsicMWpAA',
+ 'state': state,
+ 'token_type': 'example',
+ 'expires_in': 3600,
+ 'expires_at': 4600,
+ 'scope': ['abc']
+ }
+
+ json_response = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type": "example",'
+ ' "expires_in": 3600,'
+ ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter": "example_value",'
+ ' "scope":"abc def"}')
+ json_response_noscope = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type": "example",'
+ ' "expires_in": 3600,'
+ ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter": "example_value" }')
+ json_response_noexpire = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type": "example",'
+ ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter": "example_value"}')
+ json_response_expirenull = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ ' "token_type": "example",'
+ ' "expires_in": null,'
+ ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter": "example_value"}')
+
+ json_custom_error = '{ "error": "incorrect_client_credentials" }'
+ json_error = '{ "error": "access_denied" }'
+
+ json_notoken = ('{ "token_type": "example",'
+ ' "expires_in": 3600,'
+ ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter": "example_value" }')
+
+ json_notype = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ ' "expires_in": 3600,'
+ ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ ' "example_parameter": "example_value" }')
+
+ json_dict = {
+ 'access_token': '2YotnFZFEjr1zCsicMWpAA',
+ 'token_type': 'example',
+ 'expires_in': 3600,
+ 'expires_at': 4600,
+ 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA',
+ 'example_parameter': 'example_value',
+ 'scope': ['abc', 'def']
+ }
+
+ json_noscope_dict = {
+ 'access_token': '2YotnFZFEjr1zCsicMWpAA',
+ 'token_type': 'example',
+ 'expires_in': 3600,
+ 'expires_at': 4600,
+ 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA',
+ 'example_parameter': 'example_value'
+ }
+
+ json_noexpire_dict = {
+ 'access_token': '2YotnFZFEjr1zCsicMWpAA',
+ 'token_type': 'example',
+ 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA',
+ 'example_parameter': 'example_value'
+ }
+
+ json_notype_dict = {
+ 'access_token': '2YotnFZFEjr1zCsicMWpAA',
+ 'expires_in': 3600,
+ 'expires_at': 4600,
+ 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA',
+ 'example_parameter': 'example_value',
+ }
+
+ url_encoded_response = ('access_token=2YotnFZFEjr1zCsicMWpAA'
+ '&token_type=example'
+ '&expires_in=3600'
+ '&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA'
+ '&example_parameter=example_value'
+ '&scope=abc def')
+
+ url_encoded_error = 'error=access_denied'
+
+ url_encoded_notoken = ('token_type=example'
+ '&expires_in=3600'
+ '&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA'
+ '&example_parameter=example_value')
+
+
+ def test_prepare_grant_uri(self):
+ """Verify correct authorization URI construction."""
+ self.assertURLEqual(prepare_grant_uri(**self.auth_grant), self.auth_grant_uri)
+ self.assertURLEqual(prepare_grant_uri(**self.auth_grant_list_scope), self.auth_grant_uri_list_scope)
+ self.assertURLEqual(prepare_grant_uri(**self.auth_implicit), self.auth_implicit_uri)
+ self.assertURLEqual(prepare_grant_uri(**self.auth_implicit_list_scope), self.auth_implicit_uri_list_scope)
+ self.assertURLEqual(prepare_grant_uri(**self.auth_grant_pkce), self.auth_grant_uri_pkce)
+
+ def test_prepare_token_request(self):
+ """Verify correct access token request body construction."""
+ self.assertFormBodyEqual(prepare_token_request(**self.grant_body), self.auth_grant_body)
+ self.assertFormBodyEqual(prepare_token_request(**self.pwd_body), self.password_body)
+ self.assertFormBodyEqual(prepare_token_request(**self.cred_grant), self.cred_body)
+ self.assertFormBodyEqual(prepare_token_request(**self.grant_body_pkce), self.auth_grant_body_pkce)
+
+ def test_grant_response(self):
+ """Verify correct parameter parsing and validation for auth code responses."""
+ params = parse_authorization_code_response(self.grant_response)
+ self.assertEqual(params, self.grant_dict)
+ params = parse_authorization_code_response(self.grant_response, state=self.state)
+ self.assertEqual(params, self.grant_dict)
+
+ self.assertRaises(MissingCodeError, parse_authorization_code_response,
+ self.error_nocode)
+ self.assertRaises(AccessDeniedError, parse_authorization_code_response,
+ self.error_denied)
+ self.assertRaises(InvalidRequestFatalError, parse_authorization_code_response,
+ self.error_invalid)
+ self.assertRaises(MismatchingStateError, parse_authorization_code_response,
+ self.error_nostate, state=self.state)
+ self.assertRaises(MismatchingStateError, parse_authorization_code_response,
+ self.error_wrongstate, state=self.state)
+
+ def test_implicit_token_response(self):
+ """Verify correct parameter parsing and validation for implicit responses."""
+ self.assertEqual(parse_implicit_response(self.implicit_response),
+ self.implicit_dict)
+ self.assertRaises(MissingTokenError, parse_implicit_response,
+ self.implicit_notoken)
+ self.assertRaises(ValueError, parse_implicit_response,
+ self.implicit_nostate, state=self.state)
+ self.assertRaises(ValueError, parse_implicit_response,
+ self.implicit_wrongstate, state=self.state)
+
+ def test_custom_json_error(self):
+ self.assertRaises(CustomOAuth2Error, parse_token_response, self.json_custom_error)
+
+ def test_json_token_response(self):
+ """Verify correct parameter parsing and validation for token responses. """
+ self.assertEqual(parse_token_response(self.json_response), self.json_dict)
+ self.assertRaises(AccessDeniedError, parse_token_response, self.json_error)
+ self.assertRaises(MissingTokenError, parse_token_response, self.json_notoken)
+
+ self.assertEqual(parse_token_response(self.json_response_noscope,
+ scope=['all', 'the', 'scopes']), self.json_noscope_dict)
+ self.assertEqual(parse_token_response(self.json_response_noexpire), self.json_noexpire_dict)
+ self.assertEqual(parse_token_response(self.json_response_expirenull), self.json_noexpire_dict)
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ parse_token_response(self.json_response, scope='aaa')
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ for scope in new + old:
+ self.assertIn(scope, message)
+ self.assertEqual(old, ['aaa'])
+ self.assertEqual(set(new), {'abc', 'def'})
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
+
+
+ def test_json_token_notype(self):
+ """Verify strict token type parsing only when configured. """
+ self.assertEqual(parse_token_response(self.json_notype), self.json_notype_dict)
+ try:
+ os.environ['OAUTHLIB_STRICT_TOKEN_TYPE'] = '1'
+ self.assertRaises(MissingTokenTypeError, parse_token_response, self.json_notype)
+ finally:
+ del os.environ['OAUTHLIB_STRICT_TOKEN_TYPE']
+
+ def test_url_encoded_token_response(self):
+ """Verify fallback parameter parsing and validation for token responses. """
+ self.assertEqual(parse_token_response(self.url_encoded_response), self.json_dict)
+ self.assertRaises(AccessDeniedError, parse_token_response, self.url_encoded_error)
+ self.assertRaises(MissingTokenError, parse_token_response, self.url_encoded_notoken)
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ token = parse_token_response(self.url_encoded_response, scope='aaa')
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ for scope in new + old:
+ self.assertIn(scope, message)
+ self.assertEqual(old, ['aaa'])
+ self.assertEqual(set(new), {'abc', 'def'})
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
+ del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/test_request_validator.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_request_validator.py
new file mode 100644
index 0000000000..7a8d06b668
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_request_validator.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+from oauthlib.oauth2 import RequestValidator
+
+from tests.unittest import TestCase
+
+
+class RequestValidatorTest(TestCase):
+
+ def test_method_contracts(self):
+ v = RequestValidator()
+ self.assertRaises(NotImplementedError, v.authenticate_client, 'r')
+ self.assertRaises(NotImplementedError, v.authenticate_client_id,
+ 'client_id', 'r')
+ self.assertRaises(NotImplementedError, v.confirm_redirect_uri,
+ 'client_id', 'code', 'redirect_uri', 'client', 'request')
+ self.assertRaises(NotImplementedError, v.get_default_redirect_uri,
+ 'client_id', 'request')
+ self.assertRaises(NotImplementedError, v.get_default_scopes,
+ 'client_id', 'request')
+ self.assertRaises(NotImplementedError, v.get_original_scopes,
+ 'refresh_token', 'request')
+ self.assertFalse(v.is_within_original_scope(
+ ['scope'], 'refresh_token', 'request'))
+ self.assertRaises(NotImplementedError, v.invalidate_authorization_code,
+ 'client_id', 'code', 'request')
+ self.assertRaises(NotImplementedError, v.save_authorization_code,
+ 'client_id', 'code', 'request')
+ self.assertRaises(NotImplementedError, v.save_bearer_token,
+ 'token', 'request')
+ self.assertRaises(NotImplementedError, v.validate_bearer_token,
+ 'token', 'scopes', 'request')
+ self.assertRaises(NotImplementedError, v.validate_client_id,
+ 'client_id', 'request')
+ self.assertRaises(NotImplementedError, v.validate_code,
+ 'client_id', 'code', 'client', 'request')
+ self.assertRaises(NotImplementedError, v.validate_grant_type,
+ 'client_id', 'grant_type', 'client', 'request')
+ self.assertRaises(NotImplementedError, v.validate_redirect_uri,
+ 'client_id', 'redirect_uri', 'request')
+ self.assertRaises(NotImplementedError, v.validate_refresh_token,
+ 'refresh_token', 'client', 'request')
+ self.assertRaises(NotImplementedError, v.validate_response_type,
+ 'client_id', 'response_type', 'client', 'request')
+ self.assertRaises(NotImplementedError, v.validate_scopes,
+ 'client_id', 'scopes', 'client', 'request')
+ self.assertRaises(NotImplementedError, v.validate_user,
+ 'username', 'password', 'client', 'request')
+ self.assertTrue(v.client_authentication_required('r'))
+ self.assertFalse(
+ v.is_origin_allowed('client_id', 'https://foo.bar', 'r')
+ )
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/test_server.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_server.py
new file mode 100644
index 0000000000..94af37e56b
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_server.py
@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib import common
+from oauthlib.oauth2.rfc6749 import errors, tokens
+from oauthlib.oauth2.rfc6749.endpoints import Server
+from oauthlib.oauth2.rfc6749.endpoints.authorization import (
+ AuthorizationEndpoint,
+)
+from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint
+from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
+from oauthlib.oauth2.rfc6749.grant_types import (
+ AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant,
+ ResourceOwnerPasswordCredentialsGrant,
+)
+
+from tests.unittest import TestCase
+
+
+class AuthorizationEndpointTest(TestCase):
+
+ def setUp(self):
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ auth_code = AuthorizationCodeGrant(
+ request_validator=self.mock_validator)
+ auth_code.save_authorization_code = mock.MagicMock()
+ implicit = ImplicitGrant(
+ request_validator=self.mock_validator)
+ implicit.save_token = mock.MagicMock()
+
+ response_types = {
+ 'code': auth_code,
+ 'token': implicit,
+ 'none': auth_code
+ }
+ self.expires_in = 1800
+ token = tokens.BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = AuthorizationEndpoint(
+ default_response_type='code',
+ default_token_type=token,
+ response_types=response_types
+ )
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz')
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_implicit_grant(self):
+ uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True)
+
+ def test_none_grant(self):
+ uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True)
+ self.assertIsNone(body)
+ self.assertEqual(status_code, 302)
+
+ # and without the state parameter
+ uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me', parse_fragment=True)
+ self.assertIsNone(body)
+ self.assertEqual(status_code, 302)
+
+ def test_missing_type(self):
+ uri = 'http://i.b/l?client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ self.mock_validator.validate_request = mock.MagicMock(
+ side_effect=errors.InvalidRequestError())
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.')
+
+ def test_invalid_type(self):
+ uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ self.mock_validator.validate_request = mock.MagicMock(
+ side_effect=errors.UnsupportedResponseTypeError())
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type')
+
+
+class TokenEndpointTest(TestCase):
+
+ def setUp(self):
+ def set_user(request):
+ request.user = mock.MagicMock()
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked_client_id'
+ return True
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = set_user
+ self.mock_validator.get_code_challenge.return_value = None
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ auth_code = AuthorizationCodeGrant(
+ request_validator=self.mock_validator)
+ password = ResourceOwnerPasswordCredentialsGrant(
+ request_validator=self.mock_validator)
+ client = ClientCredentialsGrant(
+ request_validator=self.mock_validator)
+ supported_types = {
+ 'authorization_code': auth_code,
+ 'password': password,
+ 'client_credentials': client,
+ }
+ self.expires_in = 1800
+ token = tokens.BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = TokenEndpoint(
+ 'authorization_code',
+ default_token_type=token,
+ grant_types=supported_types
+ )
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ body = 'grant_type=authorization_code&code=abc&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc',
+ 'scope': 'all of them'
+ }
+ self.assertEqual(json.loads(body), token)
+
+ body = 'grant_type=authorization_code&code=abc'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc'
+ }
+ self.assertEqual(json.loads(body), token)
+
+ # try with additional custom variables
+ body = 'grant_type=authorization_code&code=abc&state=foobar'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ self.assertEqual(json.loads(body), token)
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_password_grant(self):
+ body = 'grant_type=password&username=a&password=hello&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc',
+ 'scope': 'all of them',
+ }
+ self.assertEqual(json.loads(body), token)
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_client_grant(self):
+ body = 'grant_type=client_credentials&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'scope': 'all of them',
+ }
+ self.assertEqual(json.loads(body), token)
+
+ def test_missing_type(self):
+ _, body, _ = self.endpoint.create_token_response('', body='')
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
+
+ def test_invalid_type(self):
+ body = 'grant_type=invalid'
+ _, body, _ = self.endpoint.create_token_response('', body=body)
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
+
+
+class SignedTokenEndpointTest(TestCase):
+
+ def setUp(self):
+ self.expires_in = 1800
+
+ def set_user(request):
+ request.user = mock.MagicMock()
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked_client_id'
+ return True
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.mock_validator.authenticate_client.side_effect = set_user
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+
+ self.private_pem = """
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA6TtDhWGwzEOWZP6m/zHoZnAPLABfetvoMPmxPGjFjtDuMRPv
+EvI1sbixZBjBtdnc5rTtHUUQ25Am3JzwPRGo5laMGbj1pPyCPxlVi9LK82HQNX0B
+YK7tZtVfDHElQA7F4v3j9d3rad4O9/n+lyGIQ0tT7yQcBm2A8FEaP0bZYCLMjwMN
+WfaVLE8eXHyv+MfpNNLI9wttLxygKYM48I3NwsFuJgOa/KuodXaAmf8pJnx8t1Wn
+nxvaYXFiUn/TxmhM/qhemPa6+0nqq+aWV5eT7xn4K/ghLgNs09v6Yge0pmPl9Oz+
++bjJ+aKRnAmwCOY8/5U5EilAiUOeBoO9+8OXtwIDAQABAoIBAGFTTbXXMkPK4HN8
+oItVdDlrAanG7hECuz3UtFUVE3upS/xG6TjqweVLwRqYCh2ssDXFwjy4mXRGDzF4
+e/e/6s9Txlrlh/w1MtTJ6ZzTdcViR9RKOczysjZ7S5KRlI3KnGFAuWPcG2SuOWjZ
+dZfzcj1Crd/ZHajBAVFHRsCo/ATVNKbTRprFfb27xKpQ2BwH/GG781sLE3ZVNIhs
+aRRaED4622kI1E/WXws2qQMqbFKzo0m1tPbLb3Z89WgZJ/tRQwuDype1Vfm7k6oX
+xfbp3948qSe/yWKRlMoPkleji/WxPkSIalzWSAi9ziN/0Uzhe65FURgrfHL3XR1A
+B8UR+aECgYEA7NPQZV4cAikk02Hv65JgISofqV49P8MbLXk8sdnI1n7Mj10TgzU3
+lyQGDEX4hqvT0bTXe4KAOxQZx9wumu05ejfzhdtSsEm6ptGHyCdmYDQeV0C/pxDX
+JNCK8XgMku2370XG0AnyBCT7NGlgtDcNCQufcesF2gEuoKiXg6Zjo7sCgYEA/Bzs
+9fWGZZnSsMSBSW2OYbFuhF3Fne0HcxXQHipl0Rujc/9g0nccwqKGizn4fGOE7a8F
+usQgJoeGcinL7E9OEP/uQ9VX1C9RNVjIxP1O5/Guw1zjxQQYetOvbPhN2QhD1Ye7
+0TRKrW1BapcjwLpFQlVg1ZeTPOi5lv24W/wX9jUCgYEAkrMSX/hPuTbrTNVZ3L6r
+NV/2hN+PaTPeXei/pBuXwOaCqDurnpcUfFcgN/IP5LwDVd+Dq0pHTFFDNv45EFbq
+R77o5n3ZVsIVEMiyJ1XgoK8oLDw7e61+15smtjT69Piz+09pu+ytMcwGn4y3Dmsb
+dALzHYnL8iLRU0ubrz0ec4kCgYAJiVKRTzNBPptQom49h85d9ac3jJCAE8o3WTjh
+Gzt0uHXrWlqgO280EY/DTnMOyXjqwLcXxHlu26uDP/99tdY/IF8z46sJ1KxetzgI
+84f7kBHLRAU9m5UNeFpnZdEUB5MBTbwWAsNcYgiabpMkpCcghjg+fBhOsoLqqjhC
+CnwhjQKBgQDkv0QTdyBU84TE8J0XY3eLQwXbrvG2yD5A2ntN3PyxGEneX5WTJGMZ
+xJxwaFYQiDS3b9E7b8Q5dg8qa5Y1+epdhx3cuQAWPm+AoHKshDfbRve4txBDQAqh
+c6MxSWgsa+2Ld5SWSNbGtpPcmEM3Fl5ttMCNCKtNc0UE16oHwaPAIw==
+-----END RSA PRIVATE KEY-----
+ """
+
+ self.public_pem = """
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6TtDhWGwzEOWZP6m/zHo
+ZnAPLABfetvoMPmxPGjFjtDuMRPvEvI1sbixZBjBtdnc5rTtHUUQ25Am3JzwPRGo
+5laMGbj1pPyCPxlVi9LK82HQNX0BYK7tZtVfDHElQA7F4v3j9d3rad4O9/n+lyGI
+Q0tT7yQcBm2A8FEaP0bZYCLMjwMNWfaVLE8eXHyv+MfpNNLI9wttLxygKYM48I3N
+wsFuJgOa/KuodXaAmf8pJnx8t1WnnxvaYXFiUn/TxmhM/qhemPa6+0nqq+aWV5eT
+7xn4K/ghLgNs09v6Yge0pmPl9Oz++bjJ+aKRnAmwCOY8/5U5EilAiUOeBoO9+8OX
+twIDAQAB
+-----END PUBLIC KEY-----
+ """
+
+ signed_token = tokens.signed_token_generator(self.private_pem,
+ user_id=123)
+ self.endpoint = Server(
+ self.mock_validator,
+ token_expires_in=self.expires_in,
+ token_generator=signed_token,
+ refresh_token_generator=tokens.random_token_generator
+ )
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ body = json.loads(body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': body['access_token'],
+ 'refresh_token': 'abc',
+ 'scope': 'all of them'
+ }
+ self.assertEqual(body, token)
+
+ body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ body = json.loads(body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': body['access_token'],
+ 'refresh_token': 'abc'
+ }
+ self.assertEqual(body, token)
+
+ # try with additional custom variables
+ body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=foobar'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ body = json.loads(body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': body['access_token'],
+ 'refresh_token': 'abc'
+ }
+ self.assertEqual(body, token)
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_password_grant(self):
+ body = 'grant_type=password&username=a&password=hello&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ body = json.loads(body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': body['access_token'],
+ 'refresh_token': 'abc',
+ 'scope': 'all of them',
+ }
+ self.assertEqual(body, token)
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_scopes_and_user_id_stored_in_access_token(self):
+ body = 'grant_type=password&username=a&password=hello&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+
+ access_token = json.loads(body)['access_token']
+
+ claims = common.verify_signed_token(self.public_pem, access_token)
+
+ self.assertEqual(claims['scope'], 'all of them')
+ self.assertEqual(claims['user_id'], 123)
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_client_grant(self):
+ body = 'grant_type=client_credentials&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ body = json.loads(body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': body['access_token'],
+ 'scope': 'all of them',
+ }
+ self.assertEqual(body, token)
+
+ def test_missing_type(self):
+ _, body, _ = self.endpoint.create_token_response('', body='client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&code=abc')
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
+
+ def test_invalid_type(self):
+ body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=invalid&code=abc'
+ _, body, _ = self.endpoint.create_token_response('', body=body)
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
+
+
+class ResourceEndpointTest(TestCase):
+
+ def setUp(self):
+ self.mock_validator = mock.MagicMock()
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ token = tokens.BearerToken(request_validator=self.mock_validator)
+ self.endpoint = ResourceEndpoint(
+ default_token='Bearer',
+ token_types={'Bearer': token}
+ )
+
+ def test_defaults(self):
+ uri = 'http://a.b/path?some=query'
+ self.mock_validator.validate_bearer_token.return_value = False
+ valid, request = self.endpoint.verify_request(uri)
+ self.assertFalse(valid)
+ self.assertEqual(request.token_type, 'Bearer')
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/test_tokens.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_tokens.py
new file mode 100644
index 0000000000..fa6b1c092c
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_tokens.py
@@ -0,0 +1,170 @@
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.tokens import (
+ BearerToken, prepare_bearer_body, prepare_bearer_headers,
+ prepare_bearer_uri, prepare_mac_header,
+)
+
+from tests.unittest import TestCase
+
+
+class TokenTest(TestCase):
+
+ # MAC without body/payload or extension
+ mac_plain = {
+ 'token': 'h480djs93hd8',
+ 'uri': 'http://example.com/resource/1?b=1&a=2',
+ 'key': '489dks293j39',
+ 'http_method': 'GET',
+ 'nonce': '264095:dj83hs9s',
+ 'hash_algorithm': 'hmac-sha-1'
+ }
+ auth_plain = {
+ 'Authorization': 'MAC id="h480djs93hd8", nonce="264095:dj83hs9s",'
+ ' mac="SLDJd4mg43cjQfElUs3Qub4L6xE="'
+ }
+
+ # MAC with body/payload, no extension
+ mac_body = {
+ 'token': 'jd93dh9dh39D',
+ 'uri': 'http://example.com/request',
+ 'key': '8yfrufh348h',
+ 'http_method': 'POST',
+ 'nonce': '273156:di3hvdf8',
+ 'hash_algorithm': 'hmac-sha-1',
+ 'body': 'hello=world%21'
+ }
+ auth_body = {
+ 'Authorization': 'MAC id="jd93dh9dh39D", nonce="273156:di3hvdf8",'
+ ' bodyhash="k9kbtCIy0CkI3/FEfpS/oIDjk6k=", mac="W7bdMZbv9UWOTadASIQHagZyirA="'
+ }
+
+ # MAC with body/payload and extension
+ mac_both = {
+ 'token': 'h480djs93hd8',
+ 'uri': 'http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q',
+ 'key': '489dks293j39',
+ 'http_method': 'GET',
+ 'nonce': '264095:7d8f3e4a',
+ 'hash_algorithm': 'hmac-sha-1',
+ 'body': 'Hello World!',
+ 'ext': 'a,b,c'
+ }
+ auth_both = {
+ 'Authorization': 'MAC id="h480djs93hd8", nonce="264095:7d8f3e4a",'
+ ' bodyhash="Lve95gjOVATpfV8EL5X4nxwjKHE=", ext="a,b,c",'
+ ' mac="Z3C2DojEopRDIC88/imW8Ez853g="'
+ }
+
+ # Bearer
+ token = 'vF9dft4qmT'
+ uri = 'http://server.example.com/resource'
+ bearer_headers = {
+ 'Authorization': 'Bearer vF9dft4qmT'
+ }
+ valid_bearer_header_lowercase = {"Authorization": "bearer vF9dft4qmT"}
+ fake_bearer_headers = [
+ {'Authorization': 'Beaver vF9dft4qmT'},
+ {'Authorization': 'BeavervF9dft4qmT'},
+ {'Authorization': 'Beaver vF9dft4qmT'},
+ {'Authorization': 'BearerF9dft4qmT'},
+ {'Authorization': 'Bearer vF9d ft4qmT'},
+ ]
+ valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'}
+ bearer_body = 'access_token=vF9dft4qmT'
+ bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT'
+
+ def _mocked_validate_bearer_token(self, token, scopes, request):
+ if not token:
+ return False
+ return True
+
+ def test_prepare_mac_header(self):
+ """Verify mac signatures correctness
+
+ TODO: verify hmac-sha-256
+ """
+ self.assertEqual(prepare_mac_header(**self.mac_plain), self.auth_plain)
+ self.assertEqual(prepare_mac_header(**self.mac_body), self.auth_body)
+ self.assertEqual(prepare_mac_header(**self.mac_both), self.auth_both)
+
+ def test_prepare_bearer_request(self):
+ """Verify proper addition of bearer tokens to requests.
+
+ They may be represented as query components in body or URI or
+ in a Bearer authorization header.
+ """
+ self.assertEqual(prepare_bearer_headers(self.token), self.bearer_headers)
+ self.assertEqual(prepare_bearer_body(self.token), self.bearer_body)
+ self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri)
+
+ def test_valid_bearer_is_validated(self):
+ request_validator = mock.MagicMock()
+ request_validator.validate_bearer_token = self._mocked_validate_bearer_token
+
+ request = Request("/", headers=self.bearer_headers)
+ result = BearerToken(request_validator=request_validator).validate_request(
+ request
+ )
+ self.assertTrue(result)
+
+ def test_lowercase_bearer_is_validated(self):
+ request_validator = mock.MagicMock()
+ request_validator.validate_bearer_token = self._mocked_validate_bearer_token
+
+ request = Request("/", headers=self.valid_bearer_header_lowercase)
+ result = BearerToken(request_validator=request_validator).validate_request(
+ request
+ )
+ self.assertTrue(result)
+
+ def test_fake_bearer_is_not_validated(self):
+ request_validator = mock.MagicMock()
+ request_validator.validate_bearer_token = self._mocked_validate_bearer_token
+
+ for fake_header in self.fake_bearer_headers:
+ request = Request("/", headers=fake_header)
+ result = BearerToken(request_validator=request_validator).validate_request(
+ request
+ )
+
+ self.assertFalse(result)
+
+ def test_header_with_multispaces_is_validated(self):
+ request_validator = mock.MagicMock()
+ request_validator.validate_bearer_token = self._mocked_validate_bearer_token
+
+ request = Request("/", headers=self.valid_header_with_multiple_spaces)
+ result = BearerToken(request_validator=request_validator).validate_request(
+ request
+ )
+
+ self.assertTrue(result)
+
+ def test_estimate_type(self):
+ request_validator = mock.MagicMock()
+ request_validator.validate_bearer_token = self._mocked_validate_bearer_token
+ request = Request("/", headers=self.bearer_headers)
+ result = BearerToken(request_validator=request_validator).estimate_type(request)
+ self.assertEqual(result, 9)
+
+ def test_estimate_type_with_fake_header_returns_type_0(self):
+ request_validator = mock.MagicMock()
+ request_validator.validate_bearer_token = self._mocked_validate_bearer_token
+
+ for fake_header in self.fake_bearer_headers:
+ request = Request("/", headers=fake_header)
+ result = BearerToken(request_validator=request_validator).estimate_type(
+ request
+ )
+
+ if (
+ fake_header["Authorization"].count(" ") == 2
+ and fake_header["Authorization"].split()[0] == "Bearer"
+ ):
+ # If we're dealing with the header containing 2 spaces, it will be recognized
+ # as a Bearer valid header, the token itself will be invalid by the way.
+ self.assertEqual(result, 9)
+ else:
+ self.assertEqual(result, 0)
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc6749/test_utils.py b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_utils.py
new file mode 100644
index 0000000000..3299591926
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc6749/test_utils.py
@@ -0,0 +1,100 @@
+import datetime
+import os
+
+from oauthlib.oauth2.rfc6749.utils import (
+ escape, generate_age, host_from_uri, is_secure_transport, list_to_scope,
+ params_from_uri, scope_to_list,
+)
+
+from tests.unittest import TestCase
+
+
+class ScopeObject:
+ """
+ Fixture for testing list_to_scope()/scope_to_list() with objects other
+ than regular strings.
+ """
+ def __init__(self, scope):
+ self.scope = scope
+
+ def __str__(self):
+ return self.scope
+
+
+class UtilsTests(TestCase):
+
+ def test_escape(self):
+ """Assert that we are only escaping unicode"""
+ self.assertRaises(ValueError, escape, b"I am a string type. Not a unicode type.")
+ self.assertEqual(escape("I am a unicode type."), "I%20am%20a%20unicode%20type.")
+
+ def test_host_from_uri(self):
+ """Test if hosts and ports are properly extracted from URIs.
+
+ This should be done according to the MAC Authentication spec.
+ Defaults ports should be provided when none is present in the URI.
+ """
+ self.assertEqual(host_from_uri('http://a.b-c.com:8080'), ('a.b-c.com', '8080'))
+ self.assertEqual(host_from_uri('https://a.b.com:8080'), ('a.b.com', '8080'))
+ self.assertEqual(host_from_uri('http://www.example.com'), ('www.example.com', '80'))
+ self.assertEqual(host_from_uri('https://www.example.com'), ('www.example.com', '443'))
+
+ def test_is_secure_transport(self):
+ """Test check secure uri."""
+ if 'OAUTHLIB_INSECURE_TRANSPORT' in os.environ:
+ del os.environ['OAUTHLIB_INSECURE_TRANSPORT']
+
+ self.assertTrue(is_secure_transport('https://example.com'))
+ self.assertFalse(is_secure_transport('http://example.com'))
+
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
+ self.assertTrue(is_secure_transport('http://example.com'))
+ del os.environ['OAUTHLIB_INSECURE_TRANSPORT']
+
+ def test_params_from_uri(self):
+ self.assertEqual(params_from_uri('http://i.b/?foo=bar&g&scope=a+d'),
+ {'foo': 'bar', 'g': '', 'scope': ['a', 'd']})
+
+ def test_generate_age(self):
+ issue_time = datetime.datetime.now() - datetime.timedelta(
+ days=3, minutes=1, seconds=4)
+ self.assertGreater(float(generate_age(issue_time)), 259263.0)
+
+ def test_list_to_scope(self):
+ expected = 'foo bar baz'
+
+ string_list = ['foo', 'bar', 'baz']
+ self.assertEqual(list_to_scope(string_list), expected)
+
+ string_tuple = ('foo', 'bar', 'baz')
+ self.assertEqual(list_to_scope(string_tuple), expected)
+
+ obj_list = [ScopeObject('foo'), ScopeObject('bar'), ScopeObject('baz')]
+ self.assertEqual(list_to_scope(obj_list), expected)
+
+ set_list = set(string_list)
+ set_scope = list_to_scope(set_list)
+ assert len(set_scope.split(' ')) == 3
+ for x in string_list:
+ assert x in set_scope
+
+ self.assertRaises(ValueError, list_to_scope, object())
+
+ def test_scope_to_list(self):
+ expected = ['foo', 'bar', 'baz']
+
+ string_scopes = 'foo bar baz '
+ self.assertEqual(scope_to_list(string_scopes), expected)
+
+ string_list_scopes = ['foo', 'bar', 'baz']
+ self.assertEqual(scope_to_list(string_list_scopes), expected)
+
+ tuple_list_scopes = ('foo', 'bar', 'baz')
+ self.assertEqual(scope_to_list(tuple_list_scopes), expected)
+
+ obj_list_scopes = [ScopeObject('foo'), ScopeObject('bar'), ScopeObject('baz')]
+ self.assertEqual(scope_to_list(obj_list_scopes), expected)
+
+ set_list_scopes = set(string_list_scopes)
+ set_list = scope_to_list(set_list_scopes)
+ self.assertEqual(sorted(set_list), sorted(string_list_scopes))
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc8628/__init__.py b/contrib/python/oauthlib/tests/oauth2/rfc8628/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc8628/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc8628/clients/__init__.py b/contrib/python/oauthlib/tests/oauth2/rfc8628/clients/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc8628/clients/__init__.py
diff --git a/contrib/python/oauthlib/tests/oauth2/rfc8628/clients/test_device.py b/contrib/python/oauthlib/tests/oauth2/rfc8628/clients/test_device.py
new file mode 100644
index 0000000000..725dea2a92
--- /dev/null
+++ b/contrib/python/oauthlib/tests/oauth2/rfc8628/clients/test_device.py
@@ -0,0 +1,63 @@
+import os
+from unittest.mock import patch
+
+from oauthlib import signals
+from oauthlib.oauth2 import DeviceClient
+
+from tests.unittest import TestCase
+
+
+class DeviceClientTest(TestCase):
+
+ client_id = "someclientid"
+ kwargs = {
+ "some": "providers",
+ "require": "extra arguments"
+ }
+
+ client_secret = "asecret"
+
+ device_code = "somedevicecode"
+
+ scope = ["profile", "email"]
+
+ body = "not=empty"
+
+ body_up = "not=empty&grant_type=urn:ietf:params:oauth:grant-type:device_code"
+ body_code = body_up + "&device_code=somedevicecode"
+ body_kwargs = body_code + "&some=providers&require=extra+arguments"
+
+ uri = "https://example.com/path?query=world"
+ uri_id = uri + "&client_id=" + client_id
+ uri_grant = uri_id + "&grant_type=urn:ietf:params:oauth:grant-type:device_code"
+ uri_secret = uri_grant + "&client_secret=asecret"
+ uri_scope = uri_secret + "&scope=profile+email"
+
+ def test_request_body(self):
+ client = DeviceClient(self.client_id)
+
+ # Basic, no extra arguments
+ body = client.prepare_request_body(self.device_code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ rclient = DeviceClient(self.client_id)
+ body = rclient.prepare_request_body(self.device_code, body=self.body)
+ self.assertFormBodyEqual(body, self.body_code)
+
+ # With extra parameters
+ body = client.prepare_request_body(
+ self.device_code, body=self.body, **self.kwargs)
+ self.assertFormBodyEqual(body, self.body_kwargs)
+
+ def test_request_uri(self):
+ client = DeviceClient(self.client_id)
+
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_grant)
+
+ client = DeviceClient(self.client_id, client_secret=self.client_secret)
+ uri = client.prepare_request_uri(self.uri)
+ self.assertURLEqual(uri, self.uri_secret)
+
+ uri = client.prepare_request_uri(self.uri, scope=self.scope)
+ self.assertURLEqual(uri, self.uri_scope)
diff --git a/contrib/python/oauthlib/tests/openid/__init__.py b/contrib/python/oauthlib/tests/openid/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/__init__.py
diff --git a/contrib/python/oauthlib/tests/openid/connect/__init__.py b/contrib/python/oauthlib/tests/openid/connect/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/__init__.py
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/__init__.py b/contrib/python/oauthlib/tests/openid/connect/core/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/__init__.py
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/endpoints/__init__.py b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/__init__.py
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_claims_handling.py b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_claims_handling.py
new file mode 100644
index 0000000000..301ed1aa44
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_claims_handling.py
@@ -0,0 +1,107 @@
+"""Ensure OpenID Connect Authorization Request 'claims' are preserved across authorization.
+
+The claims parameter is an optional query param for the Authorization Request endpoint
+ but if it is provided and is valid it needs to be deserialized (from urlencoded JSON)
+ and persisted with the authorization code itself, then in the subsequent Access Token
+ request the claims should be transferred (via the oauthlib request) to be persisted
+ with the Access Token when it is created.
+"""
+from unittest import mock
+
+from oauthlib.openid import RequestValidator
+from oauthlib.openid.connect.core.endpoints.pre_configured import Server
+
+from __tests__.oauth2.rfc6749.endpoints.test_utils import get_query_credentials
+from tests.unittest import TestCase
+
+
+class TestClaimsHandling(TestCase):
+
+ DEFAULT_REDIRECT_URI = 'http://i.b./path'
+
+ def set_scopes(self, scopes):
+ def set_request_scopes(client_id, code, client, request):
+ request.scopes = scopes
+ return True
+ return set_request_scopes
+
+ def set_user(self, request):
+ request.user = 'foo'
+ request.client_id = 'bar'
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def save_claims_with_code(self, client_id, code, request, *args, **kwargs):
+ # a real validator would save the claims with the code during save_authorization_code()
+ self.claims_from_auth_code_request = request.claims
+ self.scopes = request.scopes.split()
+
+ def retrieve_claims_saved_with_code(self, client_id, code, client, request, *args, **kwargs):
+ request.claims = self.claims_from_auth_code_request
+ request.scopes = self.scopes
+
+ return True
+
+ def save_claims_with_bearer_token(self, token, request, *args, **kwargs):
+ # a real validator would save the claims with the access token during save_bearer_token()
+ self.claims_saved_with_bearer_token = request.claims
+
+ def setUp(self):
+ self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_code_challenge.return_value = None
+ self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI
+ self.validator.authenticate_client.side_effect = self.set_client
+
+ self.validator.save_authorization_code.side_effect = self.save_claims_with_code
+ self.validator.validate_code.side_effect = self.retrieve_claims_saved_with_code
+ self.validator.save_token.side_effect = self.save_claims_with_bearer_token
+
+ self.server = Server(self.validator)
+
+ def test_claims_stored_on_code_creation(self):
+
+ claims = {
+ "id_token": {
+ "claim_1": None,
+ "claim_2": {
+ "essential": True
+ }
+ },
+ "userinfo": {
+ "claim_3": {
+ "essential": True
+ },
+ "claim_4": None
+ }
+ }
+
+ claims_urlquoted = '%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D'
+ uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s'
+
+ h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope')
+
+ self.assertDictEqual(self.claims_from_auth_code_request, claims)
+
+ code = get_query_credentials(h['Location'])['code'][0]
+ token_uri = 'http://example.com/path'
+ _, body, _ = self.server.create_token_response(
+ token_uri,
+ body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code
+ )
+
+ self.assertDictEqual(self.claims_saved_with_bearer_token, claims)
+
+ def test_invalid_claims(self):
+ uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=this-is-not-json'
+
+ h, b, s = self.server.create_authorization_response(uri, scopes='openid test_scope')
+ error = get_query_credentials(h['Location'])['error'][0]
+ error_desc = get_query_credentials(h['Location'])['error_description'][0]
+ self.assertEqual(error, 'invalid_request')
+ self.assertEqual(error_desc, "Malformed claims parameter")
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py
new file mode 100644
index 0000000000..c55136fbf1
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py
@@ -0,0 +1,78 @@
+from unittest import mock
+from urllib.parse import urlencode
+
+from oauthlib.oauth2 import InvalidRequestError
+from oauthlib.oauth2.rfc6749.endpoints.authorization import (
+ AuthorizationEndpoint,
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types import AuthorizationCodeGrant
+
+from tests.unittest import TestCase
+
+
+class OpenIDConnectEndpointTest(TestCase):
+
+ def setUp(self):
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = self.set_client
+ grant = AuthorizationCodeGrant(request_validator=self.mock_validator)
+ bearer = BearerToken(self.mock_validator)
+ self.endpoint = AuthorizationEndpoint(grant, bearer,
+ response_types={'code': grant})
+ params = {
+ 'prompt': 'consent',
+ 'display': 'touch',
+ 'nonce': 'abcd',
+ 'state': 'abc',
+ 'redirect_uri': 'https://a.b/cb',
+ 'response_type': 'code',
+ 'client_id': 'abcdef',
+ 'scope': 'hello openid'
+ }
+ self.url = 'http://a.b/path?' + urlencode(params)
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_authorization_endpoint_handles_prompt(self, generate_token):
+ generate_token.return_value = "MOCK_CODE"
+ # In the GET view:
+ scopes, creds = self.endpoint.validate_authorization_request(self.url)
+ # In the POST view:
+ creds['scopes'] = scopes
+ h, b, s = self.endpoint.create_authorization_response(self.url,
+ credentials=creds)
+ expected = 'https://a.b/cb?state=abc&code=MOCK_CODE'
+ self.assertURLEqual(h['Location'], expected)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ def test_prompt_none_exclusiveness(self):
+ """
+ Test that prompt=none can't be used with another prompt value.
+ """
+ params = {
+ 'prompt': 'none consent',
+ 'state': 'abc',
+ 'redirect_uri': 'https://a.b/cb',
+ 'response_type': 'code',
+ 'client_id': 'abcdef',
+ 'scope': 'hello openid'
+ }
+ url = 'http://a.b/path?' + urlencode(params)
+ with self.assertRaises(InvalidRequestError):
+ self.endpoint.validate_authorization_request(url)
+
+ def test_oidc_params_preservation(self):
+ """
+ Test that the nonce parameter is passed through.
+ """
+ scopes, creds = self.endpoint.validate_authorization_request(self.url)
+
+ self.assertEqual(creds['prompt'], {'consent'})
+ self.assertEqual(creds['nonce'], 'abcd')
+ self.assertEqual(creds['display'], 'touch')
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py
new file mode 100644
index 0000000000..4833485195
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.openid import RequestValidator, UserInfoEndpoint
+
+from tests.unittest import TestCase
+
+
+def set_scopes_valid(token, scopes, request):
+ request.scopes = ["openid", "bar"]
+ return True
+
+
+class UserInfoEndpointTest(TestCase):
+ def setUp(self):
+ self.claims = {
+ "sub": "john",
+ "fruit": "banana"
+ }
+ # Can't use MagicMock/wraps below.
+ # Triggers error when endpoint copies to self.bearer.request_validator
+ self.validator = RequestValidator()
+ self.validator.validate_bearer_token = mock.Mock()
+ self.validator.validate_bearer_token.side_effect = set_scopes_valid
+ self.validator.get_userinfo_claims = mock.Mock()
+ self.validator.get_userinfo_claims.return_value = self.claims
+ self.endpoint = UserInfoEndpoint(self.validator)
+
+ self.uri = 'should_not_matter'
+ self.headers = {
+ 'Authorization': 'Bearer eyJxx'
+ }
+
+ def test_userinfo_no_auth(self):
+ self.endpoint.create_userinfo_response(self.uri)
+
+ def test_userinfo_wrong_auth(self):
+ self.headers['Authorization'] = 'Basic foifoifoi'
+ self.endpoint.create_userinfo_response(self.uri, headers=self.headers)
+
+ def test_userinfo_token_expired(self):
+ self.validator.validate_bearer_token.return_value = False
+ self.endpoint.create_userinfo_response(self.uri, headers=self.headers)
+
+ def test_userinfo_token_no_openid_scope(self):
+ def set_scopes_invalid(token, scopes, request):
+ request.scopes = ["foo", "bar"]
+ return True
+ self.validator.validate_bearer_token.side_effect = set_scopes_invalid
+ with self.assertRaises(errors.InsufficientScopeError) as context:
+ self.endpoint.create_userinfo_response(self.uri)
+
+ def test_userinfo_json_response(self):
+ h, b, s = self.endpoint.create_userinfo_response(self.uri)
+ self.assertEqual(s, 200)
+ body_json = json.loads(b)
+ self.assertEqual(self.claims, body_json)
+ self.assertEqual("application/json", h['Content-Type'])
+
+ def test_userinfo_jwt_response(self):
+ self.validator.get_userinfo_claims.return_value = "eyJzzzzz"
+ h, b, s = self.endpoint.create_userinfo_response(self.uri)
+ self.assertEqual(s, 200)
+ self.assertEqual(b, "eyJzzzzz")
+ self.assertEqual("application/jwt", h['Content-Type'])
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/__init__.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/__init__.py
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_authorization_code.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_authorization_code.py
new file mode 100644
index 0000000000..49b03a7f7d
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_authorization_code.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.errors import (
+ ConsentRequired, InvalidRequestError, LoginRequired,
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types.authorization_code import (
+ AuthorizationCodeGrant,
+)
+
+from __tests__.oauth2.rfc6749.grant_types.test_authorization_code import (
+ AuthorizationCodeGrantTest,
+)
+from tests.unittest import TestCase
+
+
+def get_id_token_mock(token, token_handler, request):
+ return "MOCKED_TOKEN"
+
+
+class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super().setUp()
+ self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator)
+
+
+class OpenIDAuthCodeTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'openid')
+ self.request.expires_in = 1800
+ self.request.client_id = 'abcdef'
+ self.request.code = '1234'
+ self.request.response_type = 'code'
+ self.request.grant_type = 'authorization_code'
+ self.request.redirect_uri = 'https://a.b/cb'
+ self.request.state = 'abc'
+ self.request.nonce = None
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = self.set_client
+ self.mock_validator.get_code_challenge.return_value = None
+ self.mock_validator.get_id_token.side_effect = get_id_token_mock
+ self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator)
+
+ self.url_query = 'https://a.b/cb?code=abc&state=abc'
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc'
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_authorization(self, generate_token):
+
+ scope, info = self.auth.validate_authorization_request(self.request)
+
+ generate_token.return_value = 'abc'
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ self.request.response_mode = 'fragment'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_no_prompt_authorization(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.prompt = 'none'
+
+ bearer = BearerToken(self.mock_validator)
+
+ self.request.response_mode = 'query'
+ self.request.id_token_hint = 'me@email.com'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ # Test alternative response modes
+ self.request.response_mode = 'fragment'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+
+ # Ensure silent authentication and authorization is done
+ self.mock_validator.validate_silent_login.return_value = False
+ self.mock_validator.validate_silent_authorization.return_value = True
+ self.assertRaises(LoginRequired,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ self.mock_validator.validate_silent_login.return_value = True
+ self.mock_validator.validate_silent_authorization.return_value = False
+ self.assertRaises(ConsentRequired,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=consent_required', h['Location'])
+
+ # ID token hint must match logged in user
+ self.mock_validator.validate_silent_authorization.return_value = True
+ self.mock_validator.validate_user_match.return_value = False
+ self.assertRaises(LoginRequired,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ def test_none_multi_prompt(self):
+ bearer = BearerToken(self.mock_validator)
+
+ self.request.prompt = 'none login'
+ self.assertRaises(InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ self.request.prompt = 'none consent'
+ self.assertRaises(InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ self.request.prompt = 'none select_account'
+ self.assertRaises(InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ self.request.prompt = 'consent none login'
+ self.assertRaises(InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ def set_scopes(self, client_id, code, client, request):
+ request.scopes = self.request.scopes
+ request.user = 'bob'
+ return True
+
+ def test_create_token_response(self):
+ self.request.response_type = None
+ self.mock_validator.validate_code.side_effect = self.set_scopes
+
+ bearer = BearerToken(self.mock_validator)
+
+ h, token, s = self.auth.create_token_response(self.request, bearer)
+ token = json.loads(token)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('scope', token)
+ self.assertIn('id_token', token)
+ self.assertIn('openid', token['scope'])
+
+ self.mock_validator.reset_mock()
+
+ self.request.scopes = ('hello', 'world')
+ h, token, s = self.auth.create_token_response(self.request, bearer)
+ token = json.loads(token)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('scope', token)
+ self.assertNotIn('id_token', token)
+ self.assertNotIn('openid', token['scope'])
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_optional_nonce(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.nonce = 'xyz'
+ scope, info = self.auth.validate_authorization_request(self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_base.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_base.py
new file mode 100644
index 0000000000..a88834b807
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_base.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+import time
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.openid.connect.core.grant_types.base import GrantTypeBase
+
+from tests.unittest import TestCase
+
+
+class GrantBase(GrantTypeBase):
+ """Class to test GrantTypeBase"""
+ def __init__(self, request_validator=None, **kwargs):
+ self.request_validator = request_validator
+
+
+class IDTokenTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'openid')
+ self.request.expires_in = 1800
+ self.request.client_id = 'abcdef'
+ self.request.code = '1234'
+ self.request.response_type = 'id_token'
+ self.request.grant_type = 'authorization_code'
+ self.request.redirect_uri = 'https://a.b/cb'
+ self.request.state = 'abc'
+ self.request.nonce = None
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_id_token.return_value = None
+ self.mock_validator.finalize_id_token.return_value = "eyJ.body.signature"
+ self.token = {}
+
+ self.grant = GrantBase(request_validator=self.mock_validator)
+
+ self.url_query = 'https://a.b/cb?code=abc&state=abc'
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc'
+
+ def test_id_token_hash(self):
+ self.assertEqual(self.grant.id_token_hash(
+ "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk",
+ ), "LDktKdoQak3Pk0cnXxCltA", "hash differs from RFC")
+
+ def test_get_id_token_no_openid(self):
+ self.request.scopes = ('hello')
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertNotIn("id_token", token)
+
+ self.request.scopes = None
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertNotIn("id_token", token)
+
+ self.request.scopes = ()
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertNotIn("id_token", token)
+
+ def test_get_id_token(self):
+ self.mock_validator.get_id_token.return_value = "toto"
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertIn("id_token", token)
+ self.assertEqual(token["id_token"], "toto")
+
+ def test_finalize_id_token(self):
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertIn("id_token", token)
+ self.assertEqual(token["id_token"], "eyJ.body.signature")
+ id_token = self.mock_validator.finalize_id_token.call_args[0][0]
+ self.assertEqual(id_token['aud'], 'abcdef')
+ self.assertGreaterEqual(int(time.time()), id_token['iat'])
+
+ def test_finalize_id_token_with_nonce(self):
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request, "my_nonce")
+ self.assertIn("id_token", token)
+ self.assertEqual(token["id_token"], "eyJ.body.signature")
+ id_token = self.mock_validator.finalize_id_token.call_args[0][0]
+ self.assertEqual(id_token['nonce'], 'my_nonce')
+
+ def test_finalize_id_token_with_at_hash(self):
+ self.token["access_token"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk"
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertIn("id_token", token)
+ self.assertEqual(token["id_token"], "eyJ.body.signature")
+ id_token = self.mock_validator.finalize_id_token.call_args[0][0]
+ self.assertEqual(id_token['at_hash'], 'LDktKdoQak3Pk0cnXxCltA')
+
+ def test_finalize_id_token_with_c_hash(self):
+ self.token["code"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk"
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertIn("id_token", token)
+ self.assertEqual(token["id_token"], "eyJ.body.signature")
+ id_token = self.mock_validator.finalize_id_token.call_args[0][0]
+ self.assertEqual(id_token['c_hash'], 'LDktKdoQak3Pk0cnXxCltA')
+
+ def test_finalize_id_token_with_c_and_at_hash(self):
+ self.token["code"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk"
+ self.token["access_token"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk"
+ token = self.grant.add_id_token(self.token, "token_handler_mock", self.request)
+ self.assertIn("id_token", token)
+ self.assertEqual(token["id_token"], "eyJ.body.signature")
+ id_token = self.mock_validator.finalize_id_token.call_args[0][0]
+ self.assertEqual(id_token['at_hash'], 'LDktKdoQak3Pk0cnXxCltA')
+ self.assertEqual(id_token['c_hash'], 'LDktKdoQak3Pk0cnXxCltA')
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_dispatchers.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_dispatchers.py
new file mode 100644
index 0000000000..ccbada490d
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_dispatchers.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.grant_types import (
+ AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
+ ImplicitGrant as OAuth2ImplicitGrant,
+)
+from oauthlib.openid.connect.core.grant_types.authorization_code import (
+ AuthorizationCodeGrant,
+)
+from oauthlib.openid.connect.core.grant_types.dispatchers import (
+ AuthorizationTokenGrantDispatcher, ImplicitTokenGrantDispatcher,
+)
+from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
+
+from tests.unittest import TestCase
+
+
+class ImplicitTokenGrantDispatcherTest(TestCase):
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ request_validator = mock.MagicMock()
+ implicit_grant = OAuth2ImplicitGrant(request_validator)
+ openid_connect_implicit = ImplicitGrant(request_validator)
+
+ self.dispatcher = ImplicitTokenGrantDispatcher(
+ default_grant=implicit_grant,
+ oidc_grant=openid_connect_implicit
+ )
+
+ def test_create_authorization_response_openid(self):
+ self.request.scopes = ('hello', 'openid')
+ self.request.response_type = 'id_token'
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, ImplicitGrant)
+
+ def test_validate_authorization_request_openid(self):
+ self.request.scopes = ('hello', 'openid')
+ self.request.response_type = 'id_token'
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, ImplicitGrant)
+
+ def test_create_authorization_response_oauth(self):
+ self.request.scopes = ('hello', 'world')
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2ImplicitGrant)
+
+ def test_validate_authorization_request_oauth(self):
+ self.request.scopes = ('hello', 'world')
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2ImplicitGrant)
+
+
+class DispatcherTest(TestCase):
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.decoded_body = (
+ ("client_id", "me"),
+ ("code", "code"),
+ ("redirect_url", "https://a.b/cb"),
+ )
+
+ self.request_validator = mock.MagicMock()
+ self.auth_grant = OAuth2AuthorizationCodeGrant(self.request_validator)
+ self.openid_connect_auth = AuthorizationCodeGrant(self.request_validator)
+
+
+class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest):
+
+ def setUp(self):
+ super().setUp()
+ self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid')
+ self.dispatcher = AuthorizationTokenGrantDispatcher(
+ self.request_validator,
+ default_grant=self.auth_grant,
+ oidc_grant=self.openid_connect_auth
+ )
+
+ def test_create_token_response_openid(self):
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, AuthorizationCodeGrant)
+ self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called)
+
+
+class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest):
+
+ def setUp(self):
+ super().setUp()
+ self.request.decoded_body = (
+ ("client_id", "me"),
+ ("code", ""),
+ ("redirect_url", "https://a.b/cb"),
+ )
+ self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid')
+ self.dispatcher = AuthorizationTokenGrantDispatcher(
+ self.request_validator,
+ default_grant=self.auth_grant,
+ oidc_grant=self.openid_connect_auth
+ )
+
+ def test_create_token_response_openid_without_code(self):
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2AuthorizationCodeGrant)
+ self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called)
+
+
+class AuthTokenGrantDispatcherOAuthTest(DispatcherTest):
+
+ def setUp(self):
+ super().setUp()
+ self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world')
+ self.dispatcher = AuthorizationTokenGrantDispatcher(
+ self.request_validator,
+ default_grant=self.auth_grant,
+ oidc_grant=self.openid_connect_auth
+ )
+
+ def test_create_token_response_oauth(self):
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2AuthorizationCodeGrant)
+ self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called)
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_hybrid.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_hybrid.py
new file mode 100644
index 0000000000..111c8c5c4b
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_hybrid.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+from unittest import mock
+
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant
+
+from __tests__.oauth2.rfc6749.grant_types.test_authorization_code import (
+ AuthorizationCodeGrantTest,
+)
+
+from .test_authorization_code import OpenIDAuthCodeTest
+
+
+class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super().setUp()
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+
+
+class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest):
+
+ def setUp(self):
+ super().setUp()
+ self.request.response_type = 'code token'
+ self.request.nonce = None
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+ self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc'
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc'
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_optional_nonce(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.nonce = 'xyz'
+ scope, info = self.auth.validate_authorization_request(self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+
+class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest):
+
+ def setUp(self):
+ super().setUp()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.request.response_type = 'code id_token'
+ self.request.nonce = 'zxc'
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_required_nonce(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.nonce = None
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ def test_id_token_contains_nonce(self):
+ token = {}
+ self.mock_validator.get_id_token.side_effect = None
+ self.mock_validator.get_id_token.return_value = None
+ token = self.auth.add_id_token(token, None, self.request)
+ assert self.mock_validator.finalize_id_token.call_count == 1
+ claims = self.mock_validator.finalize_id_token.call_args[0][0]
+ assert "nonce" in claims
+
+
+class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest):
+
+ def setUp(self):
+ super().setUp()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.request.response_type = 'code id_token token'
+ self.request.nonce = 'xyz'
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_required_nonce(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.nonce = None
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_implicit.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_implicit.py
new file mode 100644
index 0000000000..825093138c
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_implicit.py
@@ -0,0 +1,170 @@
+# -*- coding: utf-8 -*-
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
+
+from __tests__.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest
+from tests.unittest import TestCase
+
+from .test_authorization_code import get_id_token_mock
+
+
+class OpenIDImplicitInterferenceTest(ImplicitGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super().setUp()
+ self.auth = ImplicitGrant(request_validator=self.mock_validator)
+
+
+class OpenIDImplicitTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'openid')
+ self.request.expires_in = 1800
+ self.request.client_id = 'abcdef'
+ self.request.response_type = 'id_token token'
+ self.request.redirect_uri = 'https://a.b/cb'
+ self.request.state = 'abc'
+ self.request.nonce = 'xyz'
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_id_token.side_effect = get_id_token_mock
+ self.auth = ImplicitGrant(request_validator=self.mock_validator)
+
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_authorization(self, generate_token):
+ scope, info = self.auth.validate_authorization_request(self.request)
+
+ generate_token.return_value = 'abc'
+ bearer = BearerToken(self.mock_validator)
+
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ self.request.response_type = 'id_token'
+ token = 'MOCKED_TOKEN'
+ url = 'https://a.b/cb#state=abc&id_token=%s' % token
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], url, parse_fragment=True)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_no_prompt_authorization(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.prompt = 'none'
+
+ bearer = BearerToken(self.mock_validator)
+
+ self.request.response_mode = 'query'
+ self.request.id_token_hint = 'me@email.com'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+ # Test alternative response modes
+ self.request.response_mode = 'fragment'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+
+ # Ensure silent authentication and authorization is done
+ self.mock_validator.validate_silent_login.return_value = False
+ self.mock_validator.validate_silent_authorization.return_value = True
+ self.assertRaises(errors.LoginRequired,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ self.mock_validator.validate_silent_login.return_value = True
+ self.mock_validator.validate_silent_authorization.return_value = False
+ self.assertRaises(errors.ConsentRequired,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=consent_required', h['Location'])
+
+ # ID token hint must match logged in user
+ self.mock_validator.validate_silent_authorization.return_value = True
+ self.mock_validator.validate_user_match.return_value = False
+ self.assertRaises(errors.LoginRequired,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ def test_none_multi_prompt(self):
+ bearer = BearerToken(self.mock_validator)
+
+ self.request.prompt = 'none login'
+ self.assertRaises(errors.InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ self.request.prompt = 'none consent'
+ self.assertRaises(errors.InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ self.request.prompt = 'none select_account'
+ self.assertRaises(errors.InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ self.request.prompt = 'consent none login'
+ self.assertRaises(errors.InvalidRequestError,
+ self.auth.validate_authorization_request,
+ self.request)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_required_nonce(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.nonce = None
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
+
+
+class OpenIDImplicitNoAccessTokenTest(OpenIDImplicitTest):
+ def setUp(self):
+ super().setUp()
+ self.request.response_type = 'id_token'
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?state=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#state=abc&id_token=%s' % token
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_required_nonce(self, generate_token):
+ generate_token.return_value = 'abc'
+ self.request.nonce = None
+ self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+ self.assertIsNone(b)
+ self.assertEqual(s, 302)
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_refresh_token.py b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_refresh_token.py
new file mode 100644
index 0000000000..2e363fef1a
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/grant_types/test_refresh_token.py
@@ -0,0 +1,105 @@
+import json
+from unittest import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types import RefreshTokenGrant
+
+from __tests__.oauth2.rfc6749.grant_types.test_refresh_token import (
+ RefreshTokenGrantTest,
+)
+from tests.unittest import TestCase
+
+
+def get_id_token_mock(token, token_handler, request):
+ return "MOCKED_TOKEN"
+
+
+class OpenIDRefreshTokenInterferenceTest(RefreshTokenGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super().setUp()
+ self.auth = RefreshTokenGrant(request_validator=self.mock_validator)
+
+
+class OpenIDRefreshTokenTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.grant_type = 'refresh_token'
+ self.request.refresh_token = 'lsdkfhj230'
+ self.request.scope = ('hello', 'openid')
+ self.mock_validator = mock.MagicMock()
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = self.set_client
+ self.mock_validator.get_id_token.side_effect = get_id_token_mock
+ self.auth = RefreshTokenGrant(request_validator=self.mock_validator)
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ def test_refresh_id_token(self):
+ self.mock_validator.get_original_scopes.return_value = [
+ 'hello', 'openid'
+ ]
+ bearer = BearerToken(self.mock_validator)
+
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer
+ )
+
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('id_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertEqual(token['scope'], 'hello openid')
+ self.mock_validator.refresh_id_token.assert_called_once_with(
+ self.request
+ )
+
+ def test_refresh_id_token_false(self):
+ self.mock_validator.refresh_id_token.return_value = False
+ self.mock_validator.get_original_scopes.return_value = [
+ 'hello', 'openid'
+ ]
+ bearer = BearerToken(self.mock_validator)
+
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer
+ )
+
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertEqual(token['scope'], 'hello openid')
+ self.assertNotIn('id_token', token)
+ self.mock_validator.refresh_id_token.assert_called_once_with(
+ self.request
+ )
+
+ def test_refresh_token_without_openid_scope(self):
+ self.request.scope = "hello"
+ bearer = BearerToken(self.mock_validator)
+
+ headers, body, status_code = self.auth.create_token_response(
+ self.request, bearer
+ )
+
+ token = json.loads(body)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('token_type', token)
+ self.assertIn('expires_in', token)
+ self.assertNotIn('id_token', token)
+ self.assertEqual(token['scope'], 'hello')
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/test_request_validator.py b/contrib/python/oauthlib/tests/openid/connect/core/test_request_validator.py
new file mode 100644
index 0000000000..6a800d41ca
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/test_request_validator.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+from oauthlib.openid import RequestValidator
+
+from tests.unittest import TestCase
+
+
+class RequestValidatorTest(TestCase):
+
+ def test_method_contracts(self):
+ v = RequestValidator()
+ self.assertRaises(
+ NotImplementedError,
+ v.get_authorization_code_scopes,
+ 'client_id', 'code', 'redirect_uri', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.get_jwt_bearer_token,
+ 'token', 'token_handler', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.finalize_id_token,
+ 'id_token', 'token', 'token_handler', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_jwt_bearer_token,
+ 'token', 'scopes', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_id_token,
+ 'token', 'scopes', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_silent_authorization,
+ 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_silent_login,
+ 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_user_match,
+ 'id_token_hint', 'scopes', 'claims', 'request'
+ )
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/test_server.py b/contrib/python/oauthlib/tests/openid/connect/core/test_server.py
new file mode 100644
index 0000000000..47f0ecc842
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/test_server.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+import json
+from unittest import mock
+
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.endpoints.authorization import (
+ AuthorizationEndpoint,
+)
+from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types.authorization_code import (
+ AuthorizationCodeGrant,
+)
+from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant
+from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
+
+from tests.unittest import TestCase
+
+
+class AuthorizationEndpointTest(TestCase):
+
+ def setUp(self):
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ auth_code = AuthorizationCodeGrant(request_validator=self.mock_validator)
+ auth_code.save_authorization_code = mock.MagicMock()
+ implicit = ImplicitGrant(
+ request_validator=self.mock_validator)
+ implicit.save_token = mock.MagicMock()
+ hybrid = HybridGrant(self.mock_validator)
+
+ response_types = {
+ 'code': auth_code,
+ 'token': implicit,
+ 'id_token': implicit,
+ 'id_token token': implicit,
+ 'code token': hybrid,
+ 'code id_token': hybrid,
+ 'code token id_token': hybrid,
+ 'none': auth_code
+ }
+ self.expires_in = 1800
+ token = BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = AuthorizationEndpoint(
+ default_response_type='code',
+ default_token_type=token,
+ response_types=response_types
+ )
+
+ # TODO: Add hybrid grant test
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz')
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_implicit_grant(self):
+ uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True)
+
+ def test_none_grant(self):
+ uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True)
+ self.assertIsNone(body)
+ self.assertEqual(status_code, 302)
+
+ # and without the state parameter
+ uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me', parse_fragment=True)
+ self.assertIsNone(body)
+ self.assertEqual(status_code, 302)
+
+ def test_missing_type(self):
+ uri = 'http://i.b/l?client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ self.mock_validator.validate_request = mock.MagicMock(
+ side_effect=errors.InvalidRequestError())
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.')
+
+ def test_invalid_type(self):
+ uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ self.mock_validator.validate_request = mock.MagicMock(
+ side_effect=errors.UnsupportedResponseTypeError())
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type')
+
+
+class TokenEndpointTest(TestCase):
+
+ def setUp(self):
+ def set_user(request):
+ request.user = mock.MagicMock()
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked_client_id'
+ return True
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = set_user
+ self.mock_validator.get_code_challenge.return_value = None
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ auth_code = AuthorizationCodeGrant(
+ request_validator=self.mock_validator)
+ supported_types = {
+ 'authorization_code': auth_code,
+ }
+ self.expires_in = 1800
+ token = BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = TokenEndpoint(
+ 'authorization_code',
+ default_token_type=token,
+ grant_types=supported_types
+ )
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ body = 'grant_type=authorization_code&code=abc&scope=all+of+them'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc',
+ 'scope': 'all of them'
+ }
+ self.assertEqual(json.loads(body), token)
+
+ body = 'grant_type=authorization_code&code=abc'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc'
+ }
+ self.assertEqual(json.loads(body), token)
+
+ # ignore useless fields
+ body = 'grant_type=authorization_code&code=abc&state=foobar'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ self.assertEqual(json.loads(body), token)
+
+ def test_missing_type(self):
+ _, body, _ = self.endpoint.create_token_response('', body='')
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
+
+ def test_invalid_type(self):
+ body = 'grant_type=invalid'
+ _, body, _ = self.endpoint.create_token_response('', body=body)
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
diff --git a/contrib/python/oauthlib/tests/openid/connect/core/test_tokens.py b/contrib/python/oauthlib/tests/openid/connect/core/test_tokens.py
new file mode 100644
index 0000000000..fe90142bb8
--- /dev/null
+++ b/contrib/python/oauthlib/tests/openid/connect/core/test_tokens.py
@@ -0,0 +1,157 @@
+from unittest import mock
+
+from oauthlib.openid.connect.core.tokens import JWTToken
+
+from tests.unittest import TestCase
+
+
+class JWTTokenTestCase(TestCase):
+
+ def test_create_token_callable_expires_in(self):
+ """
+ Test retrieval of the expires in value by calling the callable expires_in property
+ """
+
+ expires_in_mock = mock.MagicMock()
+ request_mock = mock.MagicMock()
+
+ token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock())
+ token.create_token(request=request_mock)
+
+ expires_in_mock.assert_called_once_with(request_mock)
+
+ def test_create_token_non_callable_expires_in(self):
+ """
+ When a non callable expires in is set this should just be set to the request
+ """
+
+ expires_in_mock = mock.NonCallableMagicMock()
+ request_mock = mock.MagicMock()
+
+ token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock())
+ token.create_token(request=request_mock)
+
+ self.assertFalse(expires_in_mock.called)
+ self.assertEqual(request_mock.expires_in, expires_in_mock)
+
+ def test_create_token_calls_get_id_token(self):
+ """
+ When create_token is called the call should be forwarded to the get_id_token on the token validator
+ """
+ request_mock = mock.MagicMock()
+
+ with mock.patch('oauthlib.openid.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+
+ request_validator = RequestValidatorMock()
+
+ token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator)
+ token.create_token(request=request_mock)
+
+ request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock)
+
+ def test_validate_request_token_from_headers(self):
+ """
+ Bearer token get retrieved from headers.
+ """
+
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \
+ mock.patch('oauthlib.openid.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+ request_validator_mock = RequestValidatorMock()
+
+ token = JWTToken(request_validator=request_validator_mock)
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.scopes = mock.MagicMock()
+ request.headers = {
+ 'Authorization': 'Bearer some-token-from-header'
+ }
+
+ token.validate_request(request=request)
+
+ request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header',
+ request.scopes,
+ request)
+
+ def test_validate_request_token_from_headers_basic(self):
+ """
+ Wrong kind of token (Basic) retrieved from headers. Confirm token is not parsed.
+ """
+
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \
+ mock.patch('oauthlib.openid.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+ request_validator_mock = RequestValidatorMock()
+
+ token = JWTToken(request_validator=request_validator_mock)
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.scopes = mock.MagicMock()
+ request.headers = {
+ 'Authorization': 'Basic some-token-from-header'
+ }
+
+ token.validate_request(request=request)
+
+ request_validator_mock.validate_jwt_bearer_token.assert_called_once_with(None,
+ request.scopes,
+ request)
+
+ def test_validate_token_from_request(self):
+ """
+ Token get retrieved from request object.
+ """
+
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \
+ mock.patch('oauthlib.openid.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+ request_validator_mock = RequestValidatorMock()
+
+ token = JWTToken(request_validator=request_validator_mock)
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.scopes = mock.MagicMock()
+ request.access_token = 'some-token-from-request-object'
+ request.headers = {}
+
+ token.validate_request(request=request)
+
+ request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object',
+ request.scopes,
+ request)
+
+ def test_estimate_type(self):
+ """
+ Estimate type results for a jwt token
+ """
+
+ def test_token(token, expected_result):
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock:
+ jwt_token = JWTToken()
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.headers = {
+ 'Authorization': 'Bearer {}'.format(token)
+ }
+
+ result = jwt_token.estimate_type(request=request)
+
+ self.assertEqual(result, expected_result)
+
+ test_items = (
+ ('eyfoo.foo.foo', 10),
+ ('eyfoo.foo.foo.foo.foo', 10),
+ ('eyfoobar', 0)
+ )
+
+ for token, expected_result in test_items:
+ test_token(token, expected_result)
diff --git a/contrib/python/oauthlib/tests/test_common.py b/contrib/python/oauthlib/tests/test_common.py
new file mode 100644
index 0000000000..7f0e35bc9c
--- /dev/null
+++ b/contrib/python/oauthlib/tests/test_common.py
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+import oauthlib
+from oauthlib.common import (
+ CaseInsensitiveDict, Request, add_params_to_uri, extract_params,
+ generate_client_id, generate_nonce, generate_timestamp, generate_token,
+ urldecode,
+)
+
+from tests.unittest import TestCase
+
+PARAMS_DICT = {'foo': 'bar', 'baz': '123', }
+PARAMS_TWOTUPLE = [('foo', 'bar'), ('baz', '123')]
+PARAMS_FORMENCODED = 'foo=bar&baz=123'
+URI = 'http://www.someuri.com'
+
+
+class EncodingTest(TestCase):
+
+ def test_urldecode(self):
+ self.assertCountEqual(urldecode(''), [])
+ self.assertCountEqual(urldecode('='), [('', '')])
+ self.assertCountEqual(urldecode('%20'), [(' ', '')])
+ self.assertCountEqual(urldecode('+'), [(' ', '')])
+ self.assertCountEqual(urldecode('c2'), [('c2', '')])
+ self.assertCountEqual(urldecode('c2='), [('c2', '')])
+ self.assertCountEqual(urldecode('foo=bar'), [('foo', 'bar')])
+ self.assertCountEqual(urldecode('foo_%20~=.bar-'),
+ [('foo_ ~', '.bar-')])
+ self.assertCountEqual(urldecode('foo=1,2,3'), [('foo', '1,2,3')])
+ self.assertCountEqual(urldecode('foo=(1,2,3)'), [('foo', '(1,2,3)')])
+ self.assertCountEqual(urldecode('foo=bar.*'), [('foo', 'bar.*')])
+ self.assertCountEqual(urldecode('foo=bar@spam'), [('foo', 'bar@spam')])
+ self.assertCountEqual(urldecode('foo=bar/baz'), [('foo', 'bar/baz')])
+ self.assertCountEqual(urldecode('foo=bar?baz'), [('foo', 'bar?baz')])
+ self.assertCountEqual(urldecode('foo=bar\'s'), [('foo', 'bar\'s')])
+ self.assertCountEqual(urldecode('foo=$'), [('foo', '$')])
+ self.assertRaises(ValueError, urldecode, 'foo bar')
+ self.assertRaises(ValueError, urldecode, '%R')
+ self.assertRaises(ValueError, urldecode, '%RA')
+ self.assertRaises(ValueError, urldecode, '%AR')
+ self.assertRaises(ValueError, urldecode, '%RR')
+
+
+class ParameterTest(TestCase):
+
+ def test_extract_params_dict(self):
+ self.assertCountEqual(extract_params(PARAMS_DICT), PARAMS_TWOTUPLE)
+
+ def test_extract_params_twotuple(self):
+ self.assertCountEqual(extract_params(PARAMS_TWOTUPLE), PARAMS_TWOTUPLE)
+
+ def test_extract_params_formencoded(self):
+ self.assertCountEqual(extract_params(PARAMS_FORMENCODED),
+ PARAMS_TWOTUPLE)
+
+ def test_extract_params_blank_string(self):
+ self.assertCountEqual(extract_params(''), [])
+
+ def test_extract_params_empty_list(self):
+ self.assertCountEqual(extract_params([]), [])
+
+ def test_extract_non_formencoded_string(self):
+ self.assertIsNone(extract_params('not a formencoded string'))
+
+ def test_extract_invalid(self):
+ self.assertIsNone(extract_params(object()))
+ self.assertIsNone(extract_params([('')]))
+
+ def test_add_params_to_uri(self):
+ correct = '{}?{}'.format(URI, PARAMS_FORMENCODED)
+ self.assertURLEqual(add_params_to_uri(URI, PARAMS_DICT), correct)
+ self.assertURLEqual(add_params_to_uri(URI, PARAMS_TWOTUPLE), correct)
+
+
+class GeneratorTest(TestCase):
+
+ def test_generate_timestamp(self):
+ timestamp = generate_timestamp()
+ self.assertIsInstance(timestamp, str)
+ self.assertTrue(int(timestamp))
+ self.assertGreater(int(timestamp), 1331672335)
+
+ def test_generate_nonce(self):
+ """Ping me (ib-lundgren) when you discover how to test randomness."""
+ nonce = generate_nonce()
+ for i in range(50):
+ self.assertNotEqual(nonce, generate_nonce())
+
+ def test_generate_token(self):
+ token = generate_token()
+ self.assertEqual(len(token), 30)
+
+ token = generate_token(length=44)
+ self.assertEqual(len(token), 44)
+
+ token = generate_token(length=6, chars="python")
+ self.assertEqual(len(token), 6)
+ for c in token:
+ self.assertIn(c, "python")
+
+ def test_generate_client_id(self):
+ client_id = generate_client_id()
+ self.assertEqual(len(client_id), 30)
+
+ client_id = generate_client_id(length=44)
+ self.assertEqual(len(client_id), 44)
+
+ client_id = generate_client_id(length=6, chars="python")
+ self.assertEqual(len(client_id), 6)
+ for c in client_id:
+ self.assertIn(c, "python")
+
+
+class RequestTest(TestCase):
+
+ def test_non_unicode_params(self):
+ r = Request(
+ b'http://a.b/path?query',
+ http_method=b'GET',
+ body=b'you=shall+pass',
+ headers={
+ b'a': b'b',
+ }
+ )
+ self.assertEqual(r.uri, 'http://a.b/path?query')
+ self.assertEqual(r.http_method, 'GET')
+ self.assertEqual(r.body, 'you=shall+pass')
+ self.assertEqual(r.decoded_body, [('you', 'shall pass')])
+ self.assertEqual(r.headers, {'a': 'b'})
+
+ def test_none_body(self):
+ r = Request(URI)
+ self.assertIsNone(r.decoded_body)
+
+ def test_empty_list_body(self):
+ r = Request(URI, body=[])
+ self.assertEqual(r.decoded_body, [])
+
+ def test_empty_dict_body(self):
+ r = Request(URI, body={})
+ self.assertEqual(r.decoded_body, [])
+
+ def test_empty_string_body(self):
+ r = Request(URI, body='')
+ self.assertEqual(r.decoded_body, [])
+
+ def test_non_formencoded_string_body(self):
+ body = 'foo bar'
+ r = Request(URI, body=body)
+ self.assertIsNone(r.decoded_body)
+
+ def test_param_free_sequence_body(self):
+ body = [1, 1, 2, 3, 5, 8, 13]
+ r = Request(URI, body=body)
+ self.assertIsNone(r.decoded_body)
+
+ def test_list_body(self):
+ r = Request(URI, body=PARAMS_TWOTUPLE)
+ self.assertCountEqual(r.decoded_body, PARAMS_TWOTUPLE)
+
+ def test_dict_body(self):
+ r = Request(URI, body=PARAMS_DICT)
+ self.assertCountEqual(r.decoded_body, PARAMS_TWOTUPLE)
+
+ def test_getattr_existing_attribute(self):
+ r = Request(URI, body='foo bar')
+ self.assertEqual('foo bar', getattr(r, 'body'))
+
+ def test_getattr_return_default(self):
+ r = Request(URI, body='')
+ actual_value = getattr(r, 'does_not_exist', 'foo bar')
+ self.assertEqual('foo bar', actual_value)
+
+ def test_getattr_raise_attribute_error(self):
+ r = Request(URI, body='foo bar')
+ with self.assertRaises(AttributeError):
+ getattr(r, 'does_not_exist')
+
+ def test_sanitizing_authorization_header(self):
+ r = Request(URI, headers={'Accept': 'application/json',
+ 'Authorization': 'Basic Zm9vOmJhcg=='}
+ )
+ self.assertNotIn('Zm9vOmJhcg==', repr(r))
+ self.assertIn('<SANITIZED>', repr(r))
+ # Double-check we didn't modify the underlying object:
+ self.assertEqual(r.headers['Authorization'], 'Basic Zm9vOmJhcg==')
+
+ def test_token_body(self):
+ payload = 'client_id=foo&refresh_token=bar'
+ r = Request(URI, body=payload)
+ self.assertNotIn('bar', repr(r))
+ self.assertIn('<SANITIZED>', repr(r))
+
+ payload = 'refresh_token=bar&client_id=foo'
+ r = Request(URI, body=payload)
+ self.assertNotIn('bar', repr(r))
+ self.assertIn('<SANITIZED>', repr(r))
+
+ def test_password_body(self):
+ payload = 'username=foo&password=bar'
+ r = Request(URI, body=payload)
+ self.assertNotIn('bar', repr(r))
+ self.assertIn('<SANITIZED>', repr(r))
+
+ payload = 'password=bar&username=foo'
+ r = Request(URI, body=payload)
+ self.assertNotIn('bar', repr(r))
+ self.assertIn('<SANITIZED>', repr(r))
+
+ def test_headers_params(self):
+ r = Request(URI, headers={'token': 'foobar'}, body='token=banana')
+ self.assertEqual(r.headers['token'], 'foobar')
+ self.assertEqual(r.token, 'banana')
+
+ def test_sanitized_request_non_debug_mode(self):
+ """make sure requests are sanitized when in non debug mode.
+ For the debug mode, the other tests checking sanitization should prove
+ that debug mode is working.
+ """
+ try:
+ oauthlib.set_debug(False)
+ r = Request(URI, headers={'token': 'foobar'}, body='token=banana')
+ self.assertNotIn('token', repr(r))
+ self.assertIn('SANITIZED', repr(r))
+ finally:
+ # set flag back for other tests
+ oauthlib.set_debug(True)
+
+
+class CaseInsensitiveDictTest(TestCase):
+
+ def test_basic(self):
+ cid = CaseInsensitiveDict({})
+ cid['a'] = 'b'
+ cid['c'] = 'd'
+ del cid['c']
+ self.assertEqual(cid['A'], 'b')
+ self.assertEqual(cid['a'], 'b')
+
+ def test_update(self):
+ cid = CaseInsensitiveDict({})
+ cid.update({'KeY': 'value'})
+ self.assertEqual(cid['kEy'], 'value')
diff --git a/contrib/python/oauthlib/tests/test_uri_validate.py b/contrib/python/oauthlib/tests/test_uri_validate.py
new file mode 100644
index 0000000000..6a9f8ea60b
--- /dev/null
+++ b/contrib/python/oauthlib/tests/test_uri_validate.py
@@ -0,0 +1,84 @@
+import unittest
+from oauthlib.uri_validate import is_absolute_uri
+
+from tests.unittest import TestCase
+
+
+class UriValidateTest(TestCase):
+
+ def test_is_absolute_uri(self):
+ self.assertIsNotNone(is_absolute_uri('schema://example.com/path'))
+ self.assertIsNotNone(is_absolute_uri('https://example.com/path'))
+ self.assertIsNotNone(is_absolute_uri('https://example.com'))
+ self.assertIsNotNone(is_absolute_uri('https://example.com:443/path'))
+ self.assertIsNotNone(is_absolute_uri('https://example.com:443/'))
+ self.assertIsNotNone(is_absolute_uri('https://example.com:443'))
+ self.assertIsNotNone(is_absolute_uri('http://example.com'))
+ self.assertIsNotNone(is_absolute_uri('http://example.com/path'))
+ self.assertIsNotNone(is_absolute_uri('http://example.com:80/path'))
+
+ def test_query(self):
+ self.assertIsNotNone(is_absolute_uri('http://example.com:80/path?foo'))
+ self.assertIsNotNone(is_absolute_uri('http://example.com:80/path?foo=bar'))
+ self.assertIsNotNone(is_absolute_uri('http://example.com:80/path?foo=bar&fruit=banana'))
+
+ def test_fragment_forbidden(self):
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path#foo'))
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path#foo=bar'))
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path#foo=bar&fruit=banana'))
+
+ def test_combined_forbidden(self):
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path?foo#bar'))
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path?foo&bar#fruit'))
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path?foo=1&bar#fruit=banana'))
+ self.assertIsNone(is_absolute_uri('http://example.com:80/path?foo=1&bar=2#fruit=banana&bar=foo'))
+
+ def test_custom_scheme(self):
+ self.assertIsNotNone(is_absolute_uri('com.example.bundle.id://'))
+
+ def test_ipv6_bracket(self):
+ self.assertIsNotNone(is_absolute_uri('http://[::1]:38432/path'))
+ self.assertIsNotNone(is_absolute_uri('http://[::1]/path'))
+ self.assertIsNotNone(is_absolute_uri('http://[fd01:0001::1]/path'))
+ self.assertIsNotNone(is_absolute_uri('http://[fd01:1::1]/path'))
+ self.assertIsNotNone(is_absolute_uri('http://[0123:4567:89ab:cdef:0123:4567:89ab:cdef]/path'))
+ self.assertIsNotNone(is_absolute_uri('http://[0123:4567:89ab:cdef:0123:4567:89ab:cdef]:8080/path'))
+
+ @unittest.skip("ipv6 edge-cases not supported")
+ def test_ipv6_edge_cases(self):
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8::'))
+ self.assertIsNotNone(is_absolute_uri('http://::1234:5678'))
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8::1234:5678'))
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8:3333:4444:5555:6666:7777:8888'))
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'))
+ self.assertIsNotNone(is_absolute_uri('http://0123:4567:89ab:cdef:0123:4567:89ab:cdef/path'))
+ self.assertIsNotNone(is_absolute_uri('http://::'))
+ self.assertIsNotNone(is_absolute_uri('http://2001:0db8:0001:0000:0000:0ab9:C0A8:0102'))
+
+ @unittest.skip("ipv6 dual ipv4 not supported")
+ def test_ipv6_dual(self):
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8:3333:4444:5555:6666:1.2.3.4'))
+ self.assertIsNotNone(is_absolute_uri('http://::11.22.33.44'))
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8::123.123.123.123'))
+ self.assertIsNotNone(is_absolute_uri('http://::1234:5678:91.123.4.56'))
+ self.assertIsNotNone(is_absolute_uri('http://::1234:5678:1.2.3.4'))
+ self.assertIsNotNone(is_absolute_uri('http://2001:db8::1234:5678:5.6.7.8'))
+
+ def test_ipv4(self):
+ self.assertIsNotNone(is_absolute_uri('http://127.0.0.1:38432/'))
+ self.assertIsNotNone(is_absolute_uri('http://127.0.0.1:38432/'))
+ self.assertIsNotNone(is_absolute_uri('http://127.1:38432/'))
+
+ def test_failures(self):
+ self.assertIsNone(is_absolute_uri('http://example.com:notaport/path'))
+ self.assertIsNone(is_absolute_uri('wrong'))
+ self.assertIsNone(is_absolute_uri('http://[:1]:38432/path'))
+ self.assertIsNone(is_absolute_uri('http://[abcd:efgh::1]/'))
+
+ def test_recursive_regex(self):
+ from datetime import datetime
+ t0 = datetime.now()
+ is_absolute_uri('http://[::::::::::::::::::::::::::]/path')
+ t1 = datetime.now()
+ spent = t1 - t0
+ self.assertGreater(0.1, spent.total_seconds(), "possible recursive loop detected")
diff --git a/contrib/python/oauthlib/tests/unittest/__init__.py b/contrib/python/oauthlib/tests/unittest/__init__.py
new file mode 100644
index 0000000000..f94f35c664
--- /dev/null
+++ b/contrib/python/oauthlib/tests/unittest/__init__.py
@@ -0,0 +1,32 @@
+import urllib.parse as urlparse
+from unittest import TestCase
+
+
+# URL comparison where query param order is insignificant
+def url_equals(self, a, b, parse_fragment=False):
+ parsed_a = urlparse.urlparse(a, allow_fragments=parse_fragment)
+ parsed_b = urlparse.urlparse(b, allow_fragments=parse_fragment)
+ query_a = urlparse.parse_qsl(parsed_a.query)
+ query_b = urlparse.parse_qsl(parsed_b.query)
+ if parse_fragment:
+ fragment_a = urlparse.parse_qsl(parsed_a.fragment)
+ fragment_b = urlparse.parse_qsl(parsed_b.fragment)
+ self.assertCountEqual(fragment_a, fragment_b)
+ else:
+ self.assertEqual(parsed_a.fragment, parsed_b.fragment)
+ self.assertEqual(parsed_a.scheme, parsed_b.scheme)
+ self.assertEqual(parsed_a.netloc, parsed_b.netloc)
+ self.assertEqual(parsed_a.path, parsed_b.path)
+ self.assertEqual(parsed_a.params, parsed_b.params)
+ self.assertEqual(parsed_a.username, parsed_b.username)
+ self.assertEqual(parsed_a.password, parsed_b.password)
+ self.assertEqual(parsed_a.hostname, parsed_b.hostname)
+ self.assertEqual(parsed_a.port, parsed_b.port)
+ self.assertCountEqual(query_a, query_b)
+
+
+TestCase.assertURLEqual = url_equals
+
+# Form body comparison where order is insignificant
+TestCase.assertFormBodyEqual = lambda self, a, b: self.assertCountEqual(
+ urlparse.parse_qsl(a), urlparse.parse_qsl(b))
diff --git a/contrib/python/oauthlib/tests/ya.make b/contrib/python/oauthlib/tests/ya.make
new file mode 100644
index 0000000000..b207e5ea63
--- /dev/null
+++ b/contrib/python/oauthlib/tests/ya.make
@@ -0,0 +1,88 @@
+PY3TEST()
+
+PEERDIR(
+ contrib/python/oauthlib
+ contrib/python/mock
+ contrib/python/PyJWT
+ contrib/python/blinker
+)
+
+PY_SRCS(
+ NAMESPACE tests
+ unittest/__init__.py
+)
+
+TEST_SRCS(
+ __init__.py
+ oauth1/__init__.py
+ oauth1/rfc5849/__init__.py
+ oauth1/rfc5849/endpoints/__init__.py
+ oauth1/rfc5849/endpoints/test_access_token.py
+ oauth1/rfc5849/endpoints/test_authorization.py
+ oauth1/rfc5849/endpoints/test_base.py
+ oauth1/rfc5849/endpoints/test_request_token.py
+ oauth1/rfc5849/endpoints/test_resource.py
+ oauth1/rfc5849/endpoints/test_signature_only.py
+ oauth1/rfc5849/test_client.py
+ oauth1/rfc5849/test_parameters.py
+ oauth1/rfc5849/test_request_validator.py
+ oauth1/rfc5849/test_signatures.py
+ oauth1/rfc5849/test_utils.py
+ oauth2/__init__.py
+ oauth2/rfc6749/__init__.py
+ oauth2/rfc6749/clients/__init__.py
+ oauth2/rfc6749/clients/test_backend_application.py
+ oauth2/rfc6749/clients/test_base.py
+ oauth2/rfc6749/clients/test_legacy_application.py
+ oauth2/rfc6749/clients/test_mobile_application.py
+ oauth2/rfc6749/clients/test_service_application.py
+ oauth2/rfc6749/clients/test_web_application.py
+ oauth2/rfc6749/endpoints/__init__.py
+ oauth2/rfc6749/endpoints/test_base_endpoint.py
+ oauth2/rfc6749/endpoints/test_client_authentication.py
+ oauth2/rfc6749/endpoints/test_credentials_preservation.py
+ oauth2/rfc6749/endpoints/test_error_responses.py
+ oauth2/rfc6749/endpoints/test_extra_credentials.py
+ oauth2/rfc6749/endpoints/test_introspect_endpoint.py
+ oauth2/rfc6749/endpoints/test_metadata.py
+ oauth2/rfc6749/endpoints/test_resource_owner_association.py
+ oauth2/rfc6749/endpoints/test_revocation_endpoint.py
+ oauth2/rfc6749/endpoints/test_scope_handling.py
+ oauth2/rfc6749/endpoints/test_utils.py
+ oauth2/rfc6749/grant_types/__init__.py
+ oauth2/rfc6749/grant_types/test_authorization_code.py
+ oauth2/rfc6749/grant_types/test_client_credentials.py
+ oauth2/rfc6749/grant_types/test_implicit.py
+ oauth2/rfc6749/grant_types/test_refresh_token.py
+ oauth2/rfc6749/grant_types/test_resource_owner_password.py
+ oauth2/rfc6749/test_parameters.py
+ oauth2/rfc6749/test_request_validator.py
+ oauth2/rfc6749/test_server.py
+ oauth2/rfc6749/test_tokens.py
+ oauth2/rfc6749/test_utils.py
+ oauth2/rfc8628/__init__.py
+ oauth2/rfc8628/clients/__init__.py
+ oauth2/rfc8628/clients/test_device.py
+ openid/__init__.py
+ openid/connect/__init__.py
+ openid/connect/core/__init__.py
+ openid/connect/core/endpoints/__init__.py
+ openid/connect/core/endpoints/test_claims_handling.py
+ openid/connect/core/endpoints/test_openid_connect_params_handling.py
+ openid/connect/core/endpoints/test_userinfo_endpoint.py
+ openid/connect/core/grant_types/__init__.py
+ openid/connect/core/grant_types/test_authorization_code.py
+ openid/connect/core/grant_types/test_base.py
+ openid/connect/core/grant_types/test_dispatchers.py
+ openid/connect/core/grant_types/test_hybrid.py
+ openid/connect/core/grant_types/test_implicit.py
+ openid/connect/core/test_request_validator.py
+ openid/connect/core/test_server.py
+ openid/connect/core/test_tokens.py
+ test_common.py
+ test_uri_validate.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/oauthlib/ya.make b/contrib/python/oauthlib/ya.make
new file mode 100644
index 0000000000..31a9686ead
--- /dev/null
+++ b/contrib/python/oauthlib/ya.make
@@ -0,0 +1,93 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(3.2.2)
+
+LICENSE(BSD-3-Clause)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ oauthlib/__init__.py
+ oauthlib/common.py
+ oauthlib/oauth1/__init__.py
+ oauthlib/oauth1/rfc5849/__init__.py
+ oauthlib/oauth1/rfc5849/endpoints/__init__.py
+ oauthlib/oauth1/rfc5849/endpoints/access_token.py
+ oauthlib/oauth1/rfc5849/endpoints/authorization.py
+ oauthlib/oauth1/rfc5849/endpoints/base.py
+ oauthlib/oauth1/rfc5849/endpoints/pre_configured.py
+ oauthlib/oauth1/rfc5849/endpoints/request_token.py
+ oauthlib/oauth1/rfc5849/endpoints/resource.py
+ oauthlib/oauth1/rfc5849/endpoints/signature_only.py
+ oauthlib/oauth1/rfc5849/errors.py
+ oauthlib/oauth1/rfc5849/parameters.py
+ oauthlib/oauth1/rfc5849/request_validator.py
+ oauthlib/oauth1/rfc5849/signature.py
+ oauthlib/oauth1/rfc5849/utils.py
+ oauthlib/oauth2/__init__.py
+ oauthlib/oauth2/rfc6749/__init__.py
+ oauthlib/oauth2/rfc6749/clients/__init__.py
+ oauthlib/oauth2/rfc6749/clients/backend_application.py
+ oauthlib/oauth2/rfc6749/clients/base.py
+ oauthlib/oauth2/rfc6749/clients/legacy_application.py
+ oauthlib/oauth2/rfc6749/clients/mobile_application.py
+ oauthlib/oauth2/rfc6749/clients/service_application.py
+ oauthlib/oauth2/rfc6749/clients/web_application.py
+ oauthlib/oauth2/rfc6749/endpoints/__init__.py
+ oauthlib/oauth2/rfc6749/endpoints/authorization.py
+ oauthlib/oauth2/rfc6749/endpoints/base.py
+ oauthlib/oauth2/rfc6749/endpoints/introspect.py
+ oauthlib/oauth2/rfc6749/endpoints/metadata.py
+ oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
+ oauthlib/oauth2/rfc6749/endpoints/resource.py
+ oauthlib/oauth2/rfc6749/endpoints/revocation.py
+ oauthlib/oauth2/rfc6749/endpoints/token.py
+ oauthlib/oauth2/rfc6749/errors.py
+ oauthlib/oauth2/rfc6749/grant_types/__init__.py
+ oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
+ oauthlib/oauth2/rfc6749/grant_types/base.py
+ oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
+ oauthlib/oauth2/rfc6749/grant_types/implicit.py
+ oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
+ oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
+ oauthlib/oauth2/rfc6749/parameters.py
+ oauthlib/oauth2/rfc6749/request_validator.py
+ oauthlib/oauth2/rfc6749/tokens.py
+ oauthlib/oauth2/rfc6749/utils.py
+ oauthlib/oauth2/rfc8628/__init__.py
+ oauthlib/oauth2/rfc8628/clients/__init__.py
+ oauthlib/oauth2/rfc8628/clients/device.py
+ oauthlib/openid/__init__.py
+ oauthlib/openid/connect/__init__.py
+ oauthlib/openid/connect/core/__init__.py
+ oauthlib/openid/connect/core/endpoints/__init__.py
+ oauthlib/openid/connect/core/endpoints/pre_configured.py
+ oauthlib/openid/connect/core/endpoints/userinfo.py
+ oauthlib/openid/connect/core/exceptions.py
+ oauthlib/openid/connect/core/grant_types/__init__.py
+ oauthlib/openid/connect/core/grant_types/authorization_code.py
+ oauthlib/openid/connect/core/grant_types/base.py
+ oauthlib/openid/connect/core/grant_types/dispatchers.py
+ oauthlib/openid/connect/core/grant_types/hybrid.py
+ oauthlib/openid/connect/core/grant_types/implicit.py
+ oauthlib/openid/connect/core/grant_types/refresh_token.py
+ oauthlib/openid/connect/core/request_validator.py
+ oauthlib/openid/connect/core/tokens.py
+ oauthlib/signals.py
+ oauthlib/uri_validate.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/oauthlib/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)