././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.046814 oauthlib-3.2.2/0000755000175000001440000000000014323331223013117 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035744.0 oauthlib-3.2.2/CHANGELOG.rst0000644000175000001440000004155714323330040015150 0ustar00doomsdayusersChangelog ========= 3.2.2 (2022-10-17) ------------------ OAuth2.0 Provider: * CVE-2022-36087 3.2.1 (2022-09-09) ------------------ OAuth2.0 Provider: * #803: Metadata endpoint support of non-HTTPS OAuth1.0: * #818: Allow IPv6 being parsed by signature General: * Improved and fixed documentation warnings. * Cosmetic changes based on isort 3.2.0 (2022-01-29) ------------------ OAuth2.0 Client: * #795: Add Device Authorization Flow for Web Application * #786: Add PKCE support for Client * #783: Fallback to none in case of wrong expires_at format. OAuth2.0 Provider: * #790: Add support for CORS to metadata endpoint. * #791: Add support for CORS to token endpoint. * #787: Remove comma after Bearer in WWW-Authenticate OAuth2.0 Provider - OIDC: * #755: Call save_token in Hybrid code flow * #751: OIDC add support of refreshing ID Tokens with `refresh_id_token` * #751: The RefreshTokenGrant modifiers now take the same arguments as the AuthorizationCodeGrant modifiers (`token`, `token_handler`, `request`). General: * Added Python 3.9, 3.10, 3.11 * Improve Travis & Coverage 3.1.1 (2021-05-31) ------------------ OAuth2.0 Provider - Bugfixes * #753: Fix acceptance of valid IPv6 addresses in URI validation OAuth2.0 Client - Bugfixes * #730: Base OAuth2 Client now has a consistent way of managing the `scope`: it consistently relies on the `scope` provided in the constructor if any, except if overridden temporarily in a method call. Note that in particular providing a non-None `scope` in `prepare_authorization_request` or `prepare_refresh_token` does not override anymore `self.scope` forever, it is just used temporarily. * #726: MobileApplicationClient.prepare_request_uri and MobileApplicationClient.parse_request_uri_response, ServiceApplicationClient.prepare_request_body, and WebApplicationClient.prepare_request_uri now correctly use the default `scope` provided in constructor. * #725: LegacyApplicationClient.prepare_request_body now correctly uses the default `scope` provided in constructor OAuth2.0 Provider - Bugfixes * #711: client_credentials grant: fix log message * #746: OpenID Connect Hybrid - fix nonce not passed to add_id_token * #756: Different prompt values are now handled according to spec (e.g. prompt=none) * #759: OpenID Connect - fix Authorization: Basic parsing General * #716: improved skeleton validator for public vs private client * #720: replace mock library with standard unittest.mock * #727: build isort integration * #734: python2 code removal * #735, #750: add python3.8 support * #749: bump minimum versions of pyjwt and cryptography 3.1.0 (2019-08-06) ------------------ OAuth2.0 Provider - Features * #660: OIDC add support of `nonce`, `c_hash`, `at_hash fields` - New `RequestValidator.fill_id_token` method - Deprecated `RequestValidator.get_id_token` method * #677: OIDC add `UserInfo` endpoint - New `RequestValidator.get_userinfo_claims` method OAuth2.0 Provider - Security * #665: Enhance data leak to logs * New default to not expose request content in logs * New function `oauthlib.set_debug(True)` * #666: Disabling query parameters for POST requests OAuth2.0 Provider - Bugfixes * #670: Fix `validate_authorization_request` to return the new PKCE fields * #674: Fix `token_type` to be case-insensitive (`bearer` and `Bearer`) OAuth2.0 Client - Bugfixes * #290: Fix Authorization Code's errors processing * #603: BackendApplicationClient.prepare_request_body use the `scope` argument as intended. * #672: Fix edge case when `expires_in=Null` OAuth1.0 Client * #669: Add case-insensitive headers to oauth1 `BaseEndpoint` OAuth1.0 * #722: Added support for HMAC-SHA512, RSA-SHA256 and RSA-SHA512 signature methods. 3.0.2 (2019-07-04) ------------------ * #650: Fixed space encoding in base string URI used in the signature base string. * #652: Fixed OIDC /token response which wrongly returned "&state=None" * #654: Doc: The value `state` must not be stored by the AS, only returned in /authorize response. * #656: Fixed OIDC "nonce" checks: raise errors when it's mandatory 3.0.1 (2019-01-24) ------------------ * Fixed OAuth2.0 regression introduced in 3.0.0: Revocation with Basic auth no longer possible #644 3.0.0 (2019-01-01) ------------------ OAuth2.0 Provider - outstanding Features * OpenID Connect Core support * RFC7662 Introspect support * RFC8414 OAuth2.0 Authorization Server Metadata support (#605) * RFC7636 PKCE support (#617 #624) OAuth2.0 Provider - API/Breaking Changes * Add "request" to confirm_redirect_uri #504 * confirm_redirect_uri/get_default_redirect_uri has a bit changed #445 * invalid_client is now a FatalError #606 * Changed errors status code from 401 to 400: - invalid_grant: #264 - invalid_scope: #620 - access_denied/unauthorized_client/consent_required/login_required #623 - 401 must have WWW-Authenticate HTTP Header set. #623 OAuth2.0 Provider - Bugfixes * empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406 OAuth2.0 Client - Bugfixes / Changes: * expires_in in Implicit flow is now an integer #569 * expires is no longer overriding expires_in #506 * parse_request_uri_response is now required #499 * Unknown error=xxx raised by OAuth2 providers was not understood #431 * OAuth2's `prepare_token_request` supports sending an empty string for `client_id` (#585) * OAuth2's `WebApplicationClient.prepare_request_body` was refactored to better support sending or omitting the `client_id` via a new `include_client_id` kwarg. By default this is included. The method will also emit a DeprecationWarning if a `client_id` parameter is submitted; the already configured `self.client_id` is the preferred option. (#585) OAuth1.0 Client: * Support for HMAC-SHA256 #498 General fixes: * $ and ' are allowed to be unencoded in query strings #564 * Request attributes are no longer overridden by HTTP Headers #409 * Removed unnecessary code for handling python2.6 * Add support of python3.7 #621 * Several minors updates to setup.py and tox * Set pytest as the default unittest framework 2.1.0 (2018-05-21) ------------------ * Fixed some copy and paste typos (#535) * Use secrets module in Python 3.6 and later (#533) * Add request argument to confirm_redirect_uri (#504) * Avoid populating spurious token credentials (#542) * Make populate attributes API public (#546) 2.0.7 (2018-03-19) ------------------ * Moved oauthlib into new organization on GitHub. * Include license file in the generated wheel package. (#494) * When deploying a release to PyPI, include the wheel distribution. (#496) * Check access token in self.token dict. (#500) * Added bottle-oauthlib to docs. (#509) * Update repository location in Travis. (#514) * Updated docs for organization change. (#515) * Replace G+ with Gitter. (#517) * Update requirements. (#518) * Add shields for Python versions, license and RTD. (#520) * Fix ReadTheDocs build (#521). * Fixed "make" command to test upstream with local oauthlib. (#522) * Replace IRC notification with Gitter Hook. (#523) * Added Github Releases deploy provider. (#523) 2.0.6 (2017-10-20) ------------------ * 2.0.5 contains breaking changes. 2.0.5 (2017-10-19) ------------------ * Fix OAuth2Error.response_mode for #463. * Documentation improvement. 2.0.4 (2017-09-17) ------------------ * Fixed typo that caused OAuthlib to crash because of the fix in "Address missing OIDC errors and fix a typo in the AccountSelectionRequired exception". 2.0.3 (2017-09-07) ------------------ * Address missing OIDC errors and fix a typo in the AccountSelectionRequired exception. * Update proxy keys on CaseInsensitiveDict.update(). * Redirect errors according to OIDC's response_mode. * Added universal wheel support. * Added log statements to except clauses. * According to RC7009 Section 2.1, a client should include authentication credentials when revoking its tokens. As discussed in #339, this is not make sense for public clients. However, in that case, the public client should still be checked that is in fact a public client (authenticate_client_id). * Improved prompt parameter validation. * Added two error codes from RFC 6750. * Hybrid response types are now be fragment-encoded. * Added Python 3.6 to Travis CI testing and trove classifiers. * Fixed BytesWarning issued when using a string placeholder for bytes object. * Documented PyJWT dependency and improved logging and exception messages. * Documentation improvements and fixes. 2.0.2 (2017-03-19) ------------------ * Dropped support for Python 2.6, 3.2 & 3.3. * (FIX) `OpenIDConnector` will no longer raise an AttributeError when calling `openid_authorization_validator()` twice. 2.0.1 (2016-11-23) ------------------ * (FIX) Normalize handling of request.scopes list 2.0.0 (2016-09-03) ------------------ * (New Feature) **OpenID** support. * Documentation improvements and fixes. 1.1.2 (2016-06-01) ------------------ * (Fix) Query strings should be able to include colons. * (Fix) Cast body to a string to ensure that we can perform a regex substitution on it. 1.1.1 (2016-05-01) ------------------ * (Enhancement) Better sanitisation of Request objects __repr__. 1.1.0 (2016-04-11) ------------------ * (Fix) '(', ')', '/' and '?' are now safe characters in url encoded strings. * (Enhancement) Added support for specifying if refresh tokens should be created on authorization code grants. * (Fix) OAuth2Token now handles None scopes correctly. * (Fix) Request token is now available for OAuth 1. * (Enhancement) OAuth2Token is declared with __slots__ for smaller memory footprint. * (Enhancement) RefreshTokenGrant now allows to set issue_new_refresh_tokens. * Documentation improvements and fixes. 1.0.3 (2015-08-16) ------------------ * (Fix) Changed the documented return type of the ```invalidate_request_token()``` method from the RSA key to None since nobody is using the return type. * (Enhancement) Added a validator log that will store what the endpoint has computed for debugging and logging purposes (OAuth 1 only for now). 1.0.2 (2015-08-10) ------------------ * (Fix) Allow client secret to be null for public applications that do not mandate it's specification in the query parameters. * (Fix) Encode request body before hashing in order to prevent encoding errors in Python 3. 1.0.1 (2015-07-27) ------------------ * (Fix) Added token_type_hint to the list of default Request parameters. 1.0.0 (2015-07-19) ------------------ * (Breaking Change) Replace pycrypto with cryptography from https://cryptography.io * (Breaking Change) Update jwt to 1.0.0 (which is backwards incompatible) no oauthlib api changes were made. * (Breaking Change) Raise attribute error for non-existing attributes in the Request object. * (Fix) Strip whitespace off of scope string. * (Change) Don't require to return the state in the access token response. * (Change) Hide password in logs. * (Fix) Fix incorrect invocation of prepare_refresh_body in the OAuth2 client. * (Fix) Handle empty/non-parsable query strings. * (Fix) Check if an RSA key is actually needed before requiring it. * (Change) Allow tuples for list_to_scope as well as sets and lists. * (Change) Add code to determine if client authentication is required for OAuth2. * (Fix) Fix error message on invalid Content-Type header for OAtuh1 signing. * (Fix) Allow ! character in query strings. * (Fix) OAuth1 now includes the body hash for requests that specify any content-type that isn't x-www-form-urlencoded. * (Fix) Fixed error description in oauth1 endpoint. * (Fix) Revocation endpoint for oauth2 will now return an empty string in the response body instead of 'None'. * Increased test coverage. * Performance improvements. * Documentation improvements and fixes. 0.7.2 (2014-11-13) ------------------ * (Quick fix) Unpushed locally modified files got included in the PyPI 0.7.1 release. Doing a new clean release to address this. Please upgrade quickly and report any issues you are running into. 0.7.1 (2014-10-27) ------------------ * (Quick fix) Add oauthlib.common.log object back in for libraries using it. 0.7.0 (2014-10-27) ------------------ * (Change) OAuth2 clients will not raise a Warning on scope change if the environment variable ``OAUTHLIB_RELAX_TOKEN_SCOPE`` is set. The token will now be available as an attribute on the error, ``error.token``. Token changes will now also be announced using blinker. * (Fix/Feature) Automatic fixes of non-compliant OAuth2 provider responses (e.g. Facebook). * (Fix) Logging is now tiered (per file) as opposed to logging all under ``oauthlib``. * (Fix) Error messages should now include a description in their message. * (Fix/Feature) Optional support for jsonp callbacks after token revocation. * (Feature) Client side preparation of OAuth 2 token revocation requests. * (Feature) New OAuth2 client API methods for preparing full requests. * (Feature) OAuth1 SignatureOnlyEndpoint that only verifies signatures and client IDs. * (Fix/Feature) Refresh token grant now allow optional refresh tokens. * (Fix) add missing state param to OAuth2 errors. * (Fix) add_params_to_uri now properly parse fragment. * (Fix/Feature) All OAuth1 errors can now be imported from oauthlib.oauth1. * (Fix/Security) OAuth2 logs will now strip client provided password, if present. * Allow unescaped @ in urlencoded parameters. 0.6.3 (2014-06-10) ------------------ Quick fix. OAuth 1 client repr in 0.6.2 overwrote secrets when scrubbing for print. 0.6.2 (2014-06-06) ------------------ * Numerous OAuth2 provider errors now suggest a status code of 401 instead of 400 (#247. * Added support for JSON web tokens with oauthlib.common.generate_signed_token. Install extra dependency with oauthlib[signedtoken] (#237). * OAuth2 scopes can be arbitrary objects with __str__ defined (#240). * OAuth 1 Clients can now register custom signature methods (#239). * Exposed new method oauthlib.oauth2.is_secure_transport that checks whether the given URL is HTTPS. Checks using this method can be disabled by setting the environment variable OAUTHLIB_INSECURE_TRANSPORT (#249). * OAuth1 clients now has __repr__ and will be printed with secrets scrubbed. * OAuth1 Client.get_oauth_params now takes an oauthlib.Request as an argument. * urldecode will now raise a much more informative error message on incorrectly encoded strings. * Plenty of typo and other doc fixes. 0.6.1 (2014-01-20) ------------------ Draft revocation endpoint features and numerous fixes including: * (OAuth 2 Provider) is_within_original_scope to check whether a refresh token is trying to acquire a new set of scopes that are a subset of the original scope. * (OAuth 2 Provider) expires_in token lifetime can be set per request. * (OAuth 2 Provider) client_authentication_required method added to differentiate between public and confidential clients. * (OAuth 2 Provider) rotate_refresh_token now indicates whether a new refresh token should be generated during token refresh or if old should be kept. * (OAuth 2 Provider) returned JSON headers no longer include charset. * (OAuth 2 Provider) validate_authorizatoin_request now also includes the internal request object in the returned dictionary. Note that this is not meant to be relied upon heavily and its interface might change. * and many style and typo fixes. 0.6.0 ----- OAuth 1 & 2 provider API refactor with breaking changes: * All endpoint methods change contract to return 3 values instead of 4. The new signature is `headers`, `body`, `status code` where the initial `redirect_uri` has been relocated to its rightful place inside headers as `Location`. * OAuth 1 Access Token Endpoint has a new required validator method `invalidate_request_token`. * OAuth 1 Authorization Endpoint now returns a 200 response instead of 302 on `oob` callbacks. 0.5.1 ----- OAuth 1 provider fix for incorrect token param in nonce validation. 0.5.0 ----- OAuth 1 provider refactor. OAuth 2 refresh token validation fix. 0.4.2 ----- OAuth 2 draft to RFC. Removed OAuth 2 framework decorators. 0.4.1 ----- Documentation corrections and various small code fixes. 0.4.0 ----- OAuth 2 Provider support (experimental). 0.3.8 ----- OAuth 2 Client now uses custom errors and raise on expire. 0.3.7 ----- OAuth 1 optional encoding of Client.sign return values. 0.3.6 ----- Revert default urlencoding. 0.3.5 ----- Default unicode conversion (utf-8) and urlencoding of input. 0.3.4 ----- A number of small features and bug fixes. 0.3.3 ----- OAuth 1 Provider verify now return useful params. 0.3.2 ----- Fixed #62, all Python 3 tests pass. 0.3.1 ----- Python 3.1, 3.2, 3.3 support (experimental). 0.3.0 ----- Initial OAuth 2 client support. 0.2.1 ----- Exclude non urlencoded bodies during request verification. 0.2.0 ----- OAuth provider support. 0.1.4 ----- Soft dependency on PyCrypto. 0.1.3 ----- Use python-rsa instead of pycrypto. 0.1.1 / 0.1.2 ------------- Fix installation of pycrypto dependency. 0.1.0 ----- OAuth 1 client functionality seems to be working. Hooray! 0.0.x ----- In the beginning, there was the word. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/LICENSE0000644000175000001440000000277214055423025014140 0ustar00doomsdayusersCopyright (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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/MANIFEST.in0000644000175000001440000000010613253715251014662 0ustar00doomsdayusersinclude README.rst LICENSE CHANGELOG.rst recursive-include tests *.py ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.046814 oauthlib-3.2.2/PKG-INFO0000644000175000001440000001606714323331223014226 0ustar00doomsdayusersMetadata-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 Provides-Extra: rsa Provides-Extra: signedtoken Provides-Extra: signals License-File: LICENSE 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``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/README.rst0000644000175000001440000001310114305724435014615 0ustar00doomsdayusersOAuthLib - 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``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0301473 oauthlib-3.2.2/oauthlib/0000755000175000001440000000000014323331223014726 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035763.0 oauthlib-3.2.2/oauthlib/__init__.py0000644000175000001440000000125614323330063017044 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/common.py0000644000175000001440000003210114305724435016600 0ustar00doomsdayusers""" 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 "" body = self.body headers = self.headers.copy() if body: body = SANITIZE_PATTERN.sub('\1', str(body)) if 'Authorization' in headers: headers['Authorization'] = '' return ''.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] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0301473 oauthlib-3.2.2/oauthlib/oauth1/0000755000175000001440000000000014323331223016127 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth1/__init__.py0000644000175000001440000000146614305724435020262 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0301473 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/0000755000175000001440000000000014323331223017233 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/__init__.py0000644000175000001440000004055014055423025021354 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0334806 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/0000755000175000001440000000000014323331223021236 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/__init__.py0000644000175000001440000000050714055423025023355 0ustar00doomsdayusersfrom .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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/access_token.py0000644000175000001440000002220314055423025024254 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/authorization.py0000644000175000001440000001510414055423025024515 0ustar00doomsdayusers# -*- 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} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/base.py0000644000175000001440000002656514305724435022553 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py0000644000175000001440000000103714055423025024610 0ustar00doomsdayusersfrom . 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/request_token.py0000644000175000001440000002211314305724435024512 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/resource.py0000644000175000001440000001631614305724435023461 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/endpoints/signature_only.py0000644000175000001440000000637714055423025024673 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/errors.py0000644000175000001440000000465214055423025021134 0ustar00doomsdayusers""" 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' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/parameters.py0000644000175000001440000001130214055423025021751 0ustar00doomsdayusers""" 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/request_validator.py0000644000175000001440000007441314305724435023366 0ustar00doomsdayusers""" 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035457.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/signature.py0000644000175000001440000007651414323327401021626 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth1/rfc5849/utils.py0000644000175000001440000000506514055423025020757 0ustar00doomsdayusers""" 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') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0334806 oauthlib-3.2.2/oauthlib/oauth2/0000755000175000001440000000000014323331223016130 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/oauthlib/oauth2/__init__.py0000644000175000001440000000307514175325314020257 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0334806 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/0000755000175000001440000000000014323331223017234 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/__init__.py0000644000175000001440000000062414055423025021353 0ustar00doomsdayusers""" 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__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0334806 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/0000755000175000001440000000000014323331223020675 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/__init__.py0000644000175000001440000000077014055423025023016 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/backend_application.py0000644000175000001440000000623014305724435025235 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/base.py0000644000175000001440000006403414305724435022203 0ustar00doomsdayusers# -*- 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/legacy_application.py0000644000175000001440000000770014305724435025115 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/mobile_application.py0000644000175000001440000002125614305724435025122 0ustar00doomsdayusers# -*- 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 "", line 1, in 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/service_application.py0000644000175000001440000001720414305724435025311 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/clients/web_application.py0000644000175000001440000002747014305724435024434 0ustar00doomsdayusers# -*- 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 "", line 1, in 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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/0000755000175000001440000000000014323331223021237 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/__init__.py0000644000175000001440000000105114055423025023351 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/authorization.py0000644000175000001440000001075014055423025024520 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/base.py0000644000175000001440000001004214055423025022524 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/introspect.py0000644000175000001440000001152314305724435024020 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/metadata.py0000644000175000001440000002444214305724435023412 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py0000644000175000001440000002726214055423025024621 0ustar00doomsdayusers""" 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']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/resource.py0000644000175000001440000000626014055423025023450 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/revocation.py0000644000175000001440000001213414305724435023776 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/endpoints/token.py0000644000175000001440000001076314055423025022744 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035476.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/errors.py0000644000175000001440000003122314323327424021133 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/0000755000175000001440000000000014323331223021573 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/__init__.py0000644000175000001440000000056014055423025023711 0ustar00doomsdayusers""" 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, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1645216385.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py0000644000175000001440000006276614204001201026044 0ustar00doomsdayusers""" 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 "", line 1, in 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1645216385.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/base.py0000644000175000001440000002533114204001201023047 0ustar00doomsdayusers""" 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} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py0000644000175000001440000001172714055423025026014 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/implicit.py0000644000175000001440000004072414055423025023772 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1645216385.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py0000644000175000001440000001340314204001201024770 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py0000644000175000001440000002050414055423025031512 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/parameters.py0000644000175000001440000004511014305724435021765 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/request_validator.py0000644000175000001440000007026314305724435023366 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1645216385.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/tokens.py0000644000175000001440000002555414204001201021110 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/oauth2/rfc6749/utils.py0000644000175000001440000000423714055423025020760 0ustar00doomsdayusers""" 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://') ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/oauth2/rfc8628/0000755000175000001440000000000014323331223017232 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/oauthlib/oauth2/rfc8628/__init__.py0000644000175000001440000000035014175325314021352 0ustar00doomsdayusers""" 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__) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/oauth2/rfc8628/clients/0000755000175000001440000000000014323331223020673 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/oauthlib/oauth2/rfc8628/clients/__init__.py0000644000175000001440000000031114175325314023010 0ustar00doomsdayusers""" oauthlib.oauth2.rfc8628 ~~~~~~~~~~~~~~~~~~~~~~~ This module is an implementation of various logic needed for consuming OAuth 2.0 Device Authorization RFC8628. """ from .device import DeviceClient ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/oauth2/rfc8628/clients/device.py0000644000175000001440000000772414305724435022531 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/openid/0000755000175000001440000000000014323331223016204 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/__init__.py0000644000175000001440000000024214055423025020317 0ustar00doomsdayusers""" oauthlib.openid ~~~~~~~~~~~~~~ """ from .connect.core.endpoints import Server, UserInfoEndpoint from .connect.core.request_validator import RequestValidator ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/openid/connect/0000755000175000001440000000000014323331223017635 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/__init__.py0000644000175000001440000000000014055423025021740 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/openid/connect/core/0000755000175000001440000000000014323331223020565 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/core/__init__.py0000644000175000001440000000000014055423025022670 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/openid/connect/core/endpoints/0000755000175000001440000000000014323331223022570 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/core/endpoints/__init__.py0000644000175000001440000000034514055423025024707 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035476.0 oauthlib-3.2.2/oauthlib/openid/connect/core/endpoints/pre_configured.py0000644000175000001440000001246214323327424026152 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/openid/connect/core/endpoints/userinfo.py0000644000175000001440000001000014305724435024776 0ustar00doomsdayusers""" 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/core/exceptions.py0000644000175000001440000001126614055423025023332 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.036814 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/0000755000175000001440000000000014323331223023124 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/__init__.py0000644000175000001440000000065214175325314025251 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/authorization_code.py0000644000175000001440000000264114055423025027377 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/base.py0000644000175000001440000003603114305724435024426 0ustar00doomsdayusersimport 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/dispatchers.py0000644000175000001440000000761214305724435026030 0ustar00doomsdayusersimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/hybrid.py0000644000175000001440000000526614055423025024774 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/implicit.py0000644000175000001440000000366314055423025025324 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/oauthlib/openid/connect/core/grant_types/refresh_token.py0000644000175000001440000000201314175325314026341 0ustar00doomsdayusers""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035476.0 oauthlib-3.2.2/oauthlib/openid/connect/core/request_validator.py0000644000175000001440000003270714323327424024715 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/oauthlib/openid/connect/core/tokens.py0000644000175000001440000000310514305724435022454 0ustar00doomsdayusers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/oauthlib/signals.py0000644000175000001440000000272114055423025016746 0ustar00doomsdayusers""" 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035457.0 oauthlib-3.2.2/oauthlib/uri_validate.py0000644000175000001440000001374014323327401017760 0ustar00doomsdayusers""" 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 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0301473 oauthlib-3.2.2/oauthlib.egg-info/0000755000175000001440000000000014323331223016420 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666036370.0 oauthlib-3.2.2/oauthlib.egg-info/PKG-INFO0000644000175000001440000001606714323331222017526 0ustar00doomsdayusersMetadata-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 Provides-Extra: rsa Provides-Extra: signedtoken Provides-Extra: signals License-File: LICENSE 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``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666036370.0 oauthlib-3.2.2/oauthlib.egg-info/SOURCES.txt0000644000175000001440000001462614323331222020314 0ustar00doomsdayusersCHANGELOG.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py oauthlib/__init__.py oauthlib/common.py oauthlib/signals.py oauthlib/uri_validate.py oauthlib.egg-info/PKG-INFO oauthlib.egg-info/SOURCES.txt oauthlib.egg-info/dependency_links.txt oauthlib.egg-info/requires.txt oauthlib.egg-info/top_level.txt oauthlib/oauth1/__init__.py oauthlib/oauth1/rfc5849/__init__.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/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/oauth2/__init__.py oauthlib/oauth2/rfc6749/__init__.py oauthlib/oauth2/rfc6749/errors.py oauthlib/oauth2/rfc6749/parameters.py oauthlib/oauth2/rfc6749/request_validator.py oauthlib/oauth2/rfc6749/tokens.py oauthlib/oauth2/rfc6749/utils.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/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/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/exceptions.py oauthlib/openid/connect/core/request_validator.py oauthlib/openid/connect/core/tokens.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/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 tests/__init__.py tests/test_common.py tests/test_uri_validate.py tests/oauth1/__init__.py tests/oauth1/rfc5849/__init__.py tests/oauth1/rfc5849/test_client.py tests/oauth1/rfc5849/test_parameters.py tests/oauth1/rfc5849/test_request_validator.py tests/oauth1/rfc5849/test_signatures.py tests/oauth1/rfc5849/test_utils.py tests/oauth1/rfc5849/endpoints/__init__.py tests/oauth1/rfc5849/endpoints/test_access_token.py tests/oauth1/rfc5849/endpoints/test_authorization.py tests/oauth1/rfc5849/endpoints/test_base.py tests/oauth1/rfc5849/endpoints/test_request_token.py tests/oauth1/rfc5849/endpoints/test_resource.py tests/oauth1/rfc5849/endpoints/test_signature_only.py tests/oauth2/__init__.py tests/oauth2/rfc6749/__init__.py tests/oauth2/rfc6749/test_parameters.py tests/oauth2/rfc6749/test_request_validator.py tests/oauth2/rfc6749/test_server.py tests/oauth2/rfc6749/test_tokens.py tests/oauth2/rfc6749/test_utils.py tests/oauth2/rfc6749/clients/__init__.py tests/oauth2/rfc6749/clients/test_backend_application.py tests/oauth2/rfc6749/clients/test_base.py tests/oauth2/rfc6749/clients/test_legacy_application.py tests/oauth2/rfc6749/clients/test_mobile_application.py tests/oauth2/rfc6749/clients/test_service_application.py tests/oauth2/rfc6749/clients/test_web_application.py tests/oauth2/rfc6749/endpoints/__init__.py tests/oauth2/rfc6749/endpoints/test_base_endpoint.py tests/oauth2/rfc6749/endpoints/test_client_authentication.py tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py tests/oauth2/rfc6749/endpoints/test_error_responses.py tests/oauth2/rfc6749/endpoints/test_extra_credentials.py tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py tests/oauth2/rfc6749/endpoints/test_metadata.py tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py tests/oauth2/rfc6749/endpoints/test_scope_handling.py tests/oauth2/rfc6749/endpoints/test_utils.py tests/oauth2/rfc6749/grant_types/__init__.py tests/oauth2/rfc6749/grant_types/test_authorization_code.py tests/oauth2/rfc6749/grant_types/test_client_credentials.py tests/oauth2/rfc6749/grant_types/test_implicit.py tests/oauth2/rfc6749/grant_types/test_refresh_token.py tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py tests/oauth2/rfc8628/__init__.py tests/oauth2/rfc8628/clients/__init__.py tests/oauth2/rfc8628/clients/test_device.py tests/openid/__init__.py tests/openid/connect/__init__.py tests/openid/connect/core/__init__.py tests/openid/connect/core/test_request_validator.py tests/openid/connect/core/test_server.py tests/openid/connect/core/test_tokens.py tests/openid/connect/core/endpoints/__init__.py tests/openid/connect/core/endpoints/test_claims_handling.py tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py tests/openid/connect/core/endpoints/test_userinfo_endpoint.py tests/openid/connect/core/grant_types/__init__.py tests/openid/connect/core/grant_types/test_authorization_code.py tests/openid/connect/core/grant_types/test_base.py tests/openid/connect/core/grant_types/test_dispatchers.py tests/openid/connect/core/grant_types/test_hybrid.py tests/openid/connect/core/grant_types/test_implicit.py tests/openid/connect/core/grant_types/test_refresh_token.py tests/unittest/__init__.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666036370.0 oauthlib-3.2.2/oauthlib.egg-info/dependency_links.txt0000644000175000001440000000000114323331222022465 0ustar00doomsdayusers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666036370.0 oauthlib-3.2.2/oauthlib.egg-info/requires.txt0000644000175000001440000000015014323331222021013 0ustar00doomsdayusers [rsa] cryptography>=3.0.0 [signals] blinker>=1.4.0 [signedtoken] cryptography>=3.0.0 pyjwt<3,>=2.0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666036370.0 oauthlib-3.2.2/oauthlib.egg-info/top_level.txt0000644000175000001440000000001114323331222021141 0ustar00doomsdayusersoauthlib ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.046814 oauthlib-3.2.2/setup.cfg0000644000175000001440000000050014323331223014733 0ustar00doomsdayusers[metadata] license_file = LICENSE [isort] combine_as_imports = true default_section = THIRDPARTY include_trailing_comma = true known_first_party = oauthlib known_tests = tests sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER line_length = 79 multi_line_output = 5 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035476.0 oauthlib-3.2.2/setup.py0000755000175000001440000000422214323327424014644 0ustar00doomsdayusers# Hack because logging + setuptools sucks. try: import multiprocessing except ImportError: pass from os.path import dirname, join from setuptools import find_packages, setup import oauthlib def fread(fn): with open(join(dirname(__file__), fn), 'r') as f: return f.read() rsa_require = ['cryptography>=3.0.0'] signedtoken_require = ['cryptography>=3.0.0', 'pyjwt>=2.0.0,<3'] signals_require = ['blinker>=1.4.0'] setup( name='oauthlib', version=oauthlib.__version__, description='A generic, spec-compliant, thorough implementation of the OAuth request-signing logic', long_description=fread('README.rst'), long_description_content_type='text/x-rst', author='The OAuthlib Community', author_email='idan@gazit.me', maintainer='Ib Lundgren', maintainer_email='ib.lundgren@gmail.com', url='https://github.com/oauthlib/oauthlib', platforms='any', license='BSD', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), python_requires='>=3.6', extras_require={ 'rsa': rsa_require, 'signedtoken': signedtoken_require, 'signals': signals_require, }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved', 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/0000755000175000001440000000000014323331223014261 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/__init__.py0000644000175000001440000000005214055423025016373 0ustar00doomsdayusersimport oauthlib oauthlib.set_debug(True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/oauth1/0000755000175000001440000000000014323331223015462 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth1/__init__.py0000644000175000001440000000000013253715251017571 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/oauth1/rfc5849/0000755000175000001440000000000014323331223016566 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth1/rfc5849/__init__.py0000644000175000001440000000000013253715251020675 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/0000755000175000001440000000000014323331223020571 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/__init__.py0000644000175000001440000000000013253715251022700 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/test_access_token.py0000644000175000001440000000765314055423025024662 0ustar00doomsdayusersfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/test_authorization.py0000644000175000001440000000430614055423025025111 0ustar00doomsdayusersfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/test_base.py0000644000175000001440000004021014055423025023115 0ustar00doomsdayusersfrom 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/test_request_token.py0000644000175000001440000000740514055423025025104 0ustar00doomsdayusersfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/test_resource.py0000644000175000001440000001074014055423025024037 0ustar00doomsdayusersfrom 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())) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/endpoints/test_signature_only.py0000644000175000001440000000364514055423025025260 0ustar00doomsdayusersfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/test_client.py0000644000175000001440000003115314055423025021464 0ustar00doomsdayusers# -*- 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']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/test_parameters.py0000644000175000001440000000732314055423025022353 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/test_request_validator.py0000644000175000001440000000614714055423025023750 0ustar00doomsdayusers# -*- 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'])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035457.0 oauthlib-3.2.2/tests/oauth1/rfc5849/test_signatures.py0000644000175000001440000010604514323327401022374 0ustar00doomsdayusers# -*- 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth1/rfc5849/test_utils.py0000644000175000001440000001233414055423025021346 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/oauth2/0000755000175000001440000000000014323331223015463 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth2/__init__.py0000644000175000001440000000000013253715251017572 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/oauth2/rfc6749/0000755000175000001440000000000014323331223016567 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth2/rfc6749/__init__.py0000644000175000001440000000000013253715251020676 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0401473 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/0000755000175000001440000000000014323331223020230 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/__init__.py0000644000175000001440000000000013253715251022337 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/test_backend_application.py0000644000175000001440000000626014055423025025623 0ustar00doomsdayusers# -*- 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'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/test_base.py0000644000175000001440000003635614175325314022601 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/test_legacy_application.py0000644000175000001440000001443314055423025025501 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/test_mobile_application.py0000644000175000001440000001010214055423025025471 0ustar00doomsdayusers# -*- 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'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/test_service_application.py0000644000175000001440000001661214055423025025676 0ustar00doomsdayusers# -*- 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'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/tests/oauth2/rfc6749/clients/test_web_application.py0000644000175000001440000002746614305724435025033 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/0000755000175000001440000000000014323331223020572 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/__init__.py0000644000175000001440000000000013253715251022701 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py0000644000175000001440000000466614055423025025035 0ustar00doomsdayusers# -*- 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_client_authentication.py0000644000175000001440000001524514055423025026573 0ustar00doomsdayusers"""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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py0000644000175000001440000001251014055423025027304 0ustar00doomsdayusers"""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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_error_responses.py0000644000175000001440000005424014175325314025453 0ustar00doomsdayusers"""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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_extra_credentials.py0000644000175000001440000000506314055423025025713 0ustar00doomsdayusers"""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'}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py0000644000175000001440000001704614175325314026316 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1662495005.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_metadata.py0000644000175000001440000001312614305724435024001 0ustar00doomsdayusers# -*- 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", }) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py0000644000175000001440000001032714055423025027647 0ustar00doomsdayusers"""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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py0000644000175000001440000001524414175325314026273 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_scope_handling.py0000644000175000001440000002110014055423025025156 0ustar00doomsdayusers"""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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/endpoints/test_utils.py0000644000175000001440000000045014055423025023346 0ustar00doomsdayusersimport 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/0000755000175000001440000000000014323331223021126 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1521457833.0 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/__init__.py0000644000175000001440000000000013253715251023235 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/test_authorization_code.py0000644000175000001440000004353314175325314026452 0ustar00doomsdayusers# -*- 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/test_client_credentials.py0000644000175000001440000000632114055423025026400 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/test_implicit.py0000644000175000001440000000514614055423025024363 0ustar00doomsdayusers# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1645216385.0 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/test_refresh_token.py0000644000175000001440000002274714204001201025375 0ustar00doomsdayusers# -*- 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/grant_types/test_resource_owner_password.py0000644000175000001440000001610114055423025027525 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/test_parameters.py0000644000175000001440000003412114175325314022355 0ustar00doomsdayusersfrom 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'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc6749/test_request_validator.py0000644000175000001440000000512014175325314023744 0ustar00doomsdayusers# -*- 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') ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/test_server.py0000644000175000001440000004041314055423025021514 0ustar00doomsdayusers# -*- 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/test_tokens.py0000644000175000001440000001437314055423025021517 0ustar00doomsdayusersfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/oauth2/rfc6749/test_utils.py0000644000175000001440000000721614055423025021352 0ustar00doomsdayusersimport 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)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/oauth2/rfc8628/0000755000175000001440000000000014323331223016565 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc8628/__init__.py0000644000175000001440000000000014175325314020675 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/oauth2/rfc8628/clients/0000755000175000001440000000000014323331223020226 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc8628/clients/__init__.py0000644000175000001440000000000014175325314022336 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/oauth2/rfc8628/clients/test_device.py0000644000175000001440000000376014175325314023115 0ustar00doomsdayusersimport 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/openid/0000755000175000001440000000000014323331223015537 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/__init__.py0000644000175000001440000000000014055423025017642 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/openid/connect/0000755000175000001440000000000014323331223017170 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/__init__.py0000644000175000001440000000000014055423025021273 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/openid/connect/core/0000755000175000001440000000000014323331223020120 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/__init__.py0000644000175000001440000000000014055423025022223 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666036371.0434806 oauthlib-3.2.2/tests/openid/connect/core/endpoints/0000755000175000001440000000000014323331223022123 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/endpoints/__init__.py0000644000175000001440000000000014055423025024226 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/endpoints/test_claims_handling.py0000644000175000001440000001074314055423025026661 0ustar00doomsdayusers"""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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py0000644000175000001440000000555014055423025031743 0ustar00doomsdayusersfrom 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py0000644000175000001440000000473214055423025027300 0ustar00doomsdayusers# -*- 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']) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.046814 oauthlib-3.2.2/tests/openid/connect/core/grant_types/0000755000175000001440000000000014323331223022457 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/__init__.py0000644000175000001440000000000014055423025024562 0ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/test_authorization_code.py0000644000175000001440000002025514055423025027772 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/test_base.py0000644000175000001440000001126014055423025025006 0ustar00doomsdayusers# -*- 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/test_dispatchers.py0000644000175000001440000001125414055423025026410 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/test_hybrid.py0000644000175000001440000001046714055423025025365 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/test_implicit.py0000644000175000001440000001672514055423025025721 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643489996.0 oauthlib-3.2.2/tests/openid/connect/core/grant_types/test_refresh_token.py0000644000175000001440000000715714175325314026751 0ustar00doomsdayusersimport 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/test_request_validator.py0000644000175000001440000000263014055423025025273 0ustar00doomsdayusers# -*- 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' ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/test_server.py0000644000175000001440000001723414055423025023052 0ustar00doomsdayusers# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/openid/connect/core/test_tokens.py0000644000175000001440000001422014055423025023037 0ustar00doomsdayusersfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/test_common.py0000644000175000001440000002115014055423025017165 0ustar00doomsdayusers# -*- 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('', 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('', repr(r)) payload = 'refresh_token=bar&client_id=foo' r = Request(URI, body=payload) self.assertNotIn('bar', repr(r)) self.assertIn('', repr(r)) def test_password_body(self): payload = 'username=foo&password=bar' r = Request(URI, body=payload) self.assertNotIn('bar', repr(r)) self.assertIn('', repr(r)) payload = 'password=bar&username=foo' r = Request(URI, body=payload) self.assertNotIn('bar', repr(r)) self.assertIn('', 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1666035457.0 oauthlib-3.2.2/tests/test_uri_validate.py0000644000175000001440000001103314323327401020343 0ustar00doomsdayusersimport 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") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1666036371.046814 oauthlib-3.2.2/tests/unittest/0000755000175000001440000000000014323331223016140 5ustar00doomsdayusers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1622550037.0 oauthlib-3.2.2/tests/unittest/__init__.py0000644000175000001440000000254514055423025020263 0ustar00doomsdayusersimport 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))