Import Upstream version 3.1.0
This commit is contained in:
commit
a68ee70966
|
@ -0,0 +1,431 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
3.1.0 (TBD)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
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: BackendApplication.Client.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
|
||||||
|
|
||||||
|
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 overriden 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 infact 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 aquire 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.
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2019 The OAuthlib Community
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of this project nor the names of its contributors may
|
||||||
|
be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,2 @@
|
||||||
|
include README.rst LICENSE CHANGELOG.rst
|
||||||
|
recursive-include tests *.py
|
|
@ -0,0 +1,167 @@
|
||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: oauthlib
|
||||||
|
Version: 3.1.0
|
||||||
|
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
|
||||||
|
Description: OAuthLib - Python Framework for OAuth1 & OAuth2
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
*A generic, spec-compliant, thorough implementation of the OAuth request-signing
|
||||||
|
logic for Python 2.7 and 3.4+.*
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/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
|
||||||
|
|
||||||
|
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 eachother, 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``.
|
||||||
|
|
||||||
|
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 :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.4
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: Implementation
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||||
|
Provides-Extra: signedtoken
|
||||||
|
Provides-Extra: signals
|
||||||
|
Provides-Extra: rsa
|
|
@ -0,0 +1,131 @@
|
||||||
|
OAuthLib - Python Framework for OAuth1 & OAuth2
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
*A generic, spec-compliant, thorough implementation of the OAuth request-signing
|
||||||
|
logic for Python 2.7 and 3.4+.*
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/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
|
||||||
|
|
||||||
|
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 eachother, 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``.
|
|
@ -0,0 +1,167 @@
|
||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: oauthlib
|
||||||
|
Version: 3.1.0
|
||||||
|
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
|
||||||
|
Description: OAuthLib - Python Framework for OAuth1 & OAuth2
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
*A generic, spec-compliant, thorough implementation of the OAuth request-signing
|
||||||
|
logic for Python 2.7 and 3.4+.*
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/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
|
||||||
|
|
||||||
|
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 eachother, 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``.
|
||||||
|
|
||||||
|
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 :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.4
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: Implementation
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||||
|
Provides-Extra: signedtoken
|
||||||
|
Provides-Extra: signals
|
||||||
|
Provides-Extra: rsa
|
|
@ -0,0 +1,141 @@
|
||||||
|
CHANGELOG.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/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/exceptions.py
|
||||||
|
oauthlib/openid/connect/core/grant_types/hybrid.py
|
||||||
|
oauthlib/openid/connect/core/grant_types/implicit.py
|
||||||
|
tests/__init__.py
|
||||||
|
tests/test_common.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/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/unittest/__init__.py
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
[rsa]
|
||||||
|
cryptography
|
||||||
|
|
||||||
|
[signals]
|
||||||
|
blinker
|
||||||
|
|
||||||
|
[signedtoken]
|
||||||
|
cryptography
|
||||||
|
pyjwt>=1.0.0
|
|
@ -0,0 +1 @@
|
||||||
|
oauthlib
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""
|
||||||
|
oauthlib
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
A generic, spec-compliant, thorough implementation of the OAuth
|
||||||
|
request-signing logic.
|
||||||
|
|
||||||
|
:copyright: (c) 2019 by The OAuthlib Community
|
||||||
|
:license: BSD, see LICENSE for details.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from logging import NullHandler
|
||||||
|
|
||||||
|
__author__ = 'The OAuthlib Community'
|
||||||
|
__version__ = '3.1.0'
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,468 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.common
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module provides data structures and utilities common
|
||||||
|
to all implementations of OAuth.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from . import get_debug
|
||||||
|
|
||||||
|
try:
|
||||||
|
from secrets import randbits
|
||||||
|
from secrets import SystemRandom
|
||||||
|
except ImportError:
|
||||||
|
from random import getrandbits as randbits
|
||||||
|
from random import SystemRandom
|
||||||
|
try:
|
||||||
|
from urllib import quote as _quote
|
||||||
|
from urllib import unquote as _unquote
|
||||||
|
from urllib import urlencode as _urlencode
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import quote as _quote
|
||||||
|
from urllib.parse import unquote as _unquote
|
||||||
|
from urllib.parse import urlencode as _urlencode
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
unicode_type = str
|
||||||
|
else:
|
||||||
|
unicode_type = unicode
|
||||||
|
|
||||||
|
|
||||||
|
# '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, unicode_type) 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, unicode_type): # PY3 returns unicode
|
||||||
|
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, unicode_type) else k,
|
||||||
|
v.encode('utf-8') if isinstance(v, unicode_type) 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 encode to utf-8 prior to parsing because parse_qsl behaves
|
||||||
|
# differently on unicode input in python 2 and 3.
|
||||||
|
# Python 2.7
|
||||||
|
# >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
|
||||||
|
# u'\xe5\x95\xa6\xe5\x95\xa6'
|
||||||
|
# Python 2.7, non unicode input gives the same
|
||||||
|
# >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
|
||||||
|
# '\xe5\x95\xa6\xe5\x95\xa6'
|
||||||
|
# but now we can decode it to unicode
|
||||||
|
# >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
|
||||||
|
# u'\u5566\u5566'
|
||||||
|
# Python 3.3 however
|
||||||
|
# >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
|
||||||
|
# u'\u5566\u5566'
|
||||||
|
query = query.encode(
|
||||||
|
'utf-8') if not PY3 and isinstance(query, unicode_type) else query
|
||||||
|
# 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, unicode_type)):
|
||||||
|
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 unicode_type(unicode_type(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 unicode_type(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, unicode_type):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
return unicode_type(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 dict(((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 = dict((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(CaseInsensitiveDict, self).__delitem__(key)
|
||||||
|
del self.proxy[k.lower()]
|
||||||
|
|
||||||
|
def __getitem__(self, k):
|
||||||
|
key = self.proxy[k.lower()]
|
||||||
|
return super(CaseInsensitiveDict, self).__getitem__(key)
|
||||||
|
|
||||||
|
def get(self, k, default=None):
|
||||||
|
return self[k] if k in self else default
|
||||||
|
|
||||||
|
def __setitem__(self, k, v):
|
||||||
|
super(CaseInsensitiveDict, self).__setitem__(k, v)
|
||||||
|
self.proxy[k.lower()] = k
|
||||||
|
|
||||||
|
def update(self, *args, **kwargs):
|
||||||
|
super(CaseInsensitiveDict, self).update(*args, **kwargs)
|
||||||
|
for k in dict(*args, **kwargs):
|
||||||
|
self.proxy[k.lower()] = k
|
||||||
|
|
||||||
|
|
||||||
|
class Request(object):
|
||||||
|
|
||||||
|
"""A malleable representation of a signable HTTP request.
|
||||||
|
|
||||||
|
Body argument may contain any data, but parameters will only be decoded if
|
||||||
|
they are one of:
|
||||||
|
|
||||||
|
* urlencoded query string
|
||||||
|
* dict
|
||||||
|
* list of 2-tuples
|
||||||
|
|
||||||
|
Anything else will be treated as raw body data to be passed through
|
||||||
|
unmolested.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, uri, http_method='GET', body=None, headers=None,
|
||||||
|
encoding='utf-8'):
|
||||||
|
# Convert to unicode using encoding if given, else assume unicode
|
||||||
|
encode = lambda x: to_unicode(x, encoding) if encoding else x
|
||||||
|
|
||||||
|
self.uri = encode(uri)
|
||||||
|
self.http_method = encode(http_method)
|
||||||
|
self.headers = CaseInsensitiveDict(encode(headers or {}))
|
||||||
|
self.body = encode(body)
|
||||||
|
self.decoded_body = extract_params(self.body)
|
||||||
|
self.oauth_params = []
|
||||||
|
self.validator_log = {}
|
||||||
|
|
||||||
|
self._params = {
|
||||||
|
"access_token": None,
|
||||||
|
"client": None,
|
||||||
|
"client_id": None,
|
||||||
|
"client_secret": None,
|
||||||
|
"code": None,
|
||||||
|
"code_challenge": None,
|
||||||
|
"code_challenge_method": None,
|
||||||
|
"code_verifier": None,
|
||||||
|
"extra_credentials": None,
|
||||||
|
"grant_type": None,
|
||||||
|
"redirect_uri": None,
|
||||||
|
"refresh_token": None,
|
||||||
|
"request_token": None,
|
||||||
|
"response_type": None,
|
||||||
|
"scope": None,
|
||||||
|
"scopes": None,
|
||||||
|
"state": None,
|
||||||
|
"token": None,
|
||||||
|
"user": None,
|
||||||
|
"token_type_hint": None,
|
||||||
|
|
||||||
|
# OpenID Connect
|
||||||
|
"response_mode": None,
|
||||||
|
"nonce": None,
|
||||||
|
"display": None,
|
||||||
|
"prompt": None,
|
||||||
|
"claims": None,
|
||||||
|
"max_age": None,
|
||||||
|
"ui_locales": None,
|
||||||
|
"id_token_hint": None,
|
||||||
|
"login_hint": None,
|
||||||
|
"acr_values": None
|
||||||
|
}
|
||||||
|
self._params.update(dict(urldecode(self.uri_query)))
|
||||||
|
self._params.update(dict(self.decoded_body or []))
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name in self._params:
|
||||||
|
return self._params[name]
|
||||||
|
else:
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not get_debug():
|
||||||
|
return "<oauthlib.Request SANITIZED>"
|
||||||
|
body = self.body
|
||||||
|
headers = self.headers.copy()
|
||||||
|
if body:
|
||||||
|
body = SANITIZE_PATTERN.sub('\1<SANITIZED>', str(body))
|
||||||
|
if 'Authorization' in headers:
|
||||||
|
headers['Authorization'] = '<SANITIZED>'
|
||||||
|
return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
|
||||||
|
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]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth1
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is a wrapper for the most recent implementation of OAuth 1.0 Client
|
||||||
|
and Server classes.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .rfc5849 import Client
|
||||||
|
from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
|
||||||
|
from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
|
||||||
|
from .rfc5849 import SIGNATURE_TYPE_BODY
|
||||||
|
from .rfc5849.request_validator import RequestValidator
|
||||||
|
from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
|
||||||
|
from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
|
||||||
|
from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
|
||||||
|
from .rfc5849.errors import InsecureTransportError, InvalidClientError, InvalidRequestError, InvalidSignatureMethodError, OAuth1Error
|
|
@ -0,0 +1,327 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth1.rfc5849
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
from oauthlib.common import Request, urlencode, generate_nonce
|
||||||
|
from oauthlib.common import generate_timestamp, to_unicode
|
||||||
|
from . import parameters, signature
|
||||||
|
|
||||||
|
SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
|
||||||
|
SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
|
||||||
|
SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1
|
||||||
|
SIGNATURE_RSA = "RSA-SHA1"
|
||||||
|
SIGNATURE_PLAINTEXT = "PLAINTEXT"
|
||||||
|
SIGNATURE_METHODS = (SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, 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(object):
|
||||||
|
|
||||||
|
"""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_RSA: signature.sign_rsa_sha1_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('%s=%s' % (k, v) for k, v in attrs.items())
|
||||||
|
return '<%s %s>' % (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: {0}".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: {0}".format(normalized_params))
|
||||||
|
log.debug("Normalized URI: {0}".format(normalized_uri))
|
||||||
|
|
||||||
|
base_string = signature.signature_base_string(request.http_method,
|
||||||
|
normalized_uri, normalized_params)
|
||||||
|
|
||||||
|
log.debug("Signing: signature base string: {0}".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: {0}".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 {0} "
|
||||||
|
"instead of {1}".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
|
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from .base import BaseEndpoint
|
||||||
|
from .request_token import RequestTokenEndpoint
|
||||||
|
from .authorization import AuthorizationEndpoint
|
||||||
|
from .access_token import AccessTokenEndpoint
|
||||||
|
from .resource import ResourceEndpoint
|
||||||
|
from .signature_only import SignatureOnlyEndpoint
|
||||||
|
from .pre_configured import WebApplicationServer
|
|
@ -0,0 +1,217 @@
|
||||||
|
# -*- 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.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,163 @@
|
||||||
|
# -*- 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 __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import Request, add_params_to_uri
|
||||||
|
|
||||||
|
from .. import errors
|
||||||
|
from .base import BaseEndpoint
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib import urlencode
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
|
||||||
|
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}
|
|
@ -0,0 +1,216 @@
|
||||||
|
# -*- 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.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oauthlib.common import CaseInsensitiveDict, Request, generate_token
|
||||||
|
|
||||||
|
from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA,
|
||||||
|
SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY,
|
||||||
|
SIGNATURE_TYPE_QUERY, errors, signature, utils)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEndpoint(object):
|
||||||
|
|
||||||
|
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, %s not in %r." % (
|
||||||
|
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:
|
||||||
|
# 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)
|
||||||
|
valid_signature = signature.verify_rsa_sha1(request, rsa_key)
|
||||||
|
|
||||||
|
# ---- HMAC or Plaintext Signature verification ----
|
||||||
|
else:
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
valid_signature = signature.verify_plaintext(request,
|
||||||
|
client_secret, resource_owner_secret)
|
||||||
|
return valid_signature
|
|
@ -0,0 +1,14 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from . import (AccessTokenEndpoint, AuthorizationEndpoint,
|
||||||
|
RequestTokenEndpoint, ResourceEndpoint)
|
||||||
|
|
||||||
|
|
||||||
|
class WebApplicationServer(RequestTokenEndpoint, AuthorizationEndpoint,
|
||||||
|
AccessTokenEndpoint, ResourceEndpoint):
|
||||||
|
|
||||||
|
def __init__(self, request_validator):
|
||||||
|
RequestTokenEndpoint.__init__(self, request_validator)
|
||||||
|
AuthorizationEndpoint.__init__(self, request_validator)
|
||||||
|
AccessTokenEndpoint.__init__(self, request_validator)
|
||||||
|
ResourceEndpoint.__init__(self, request_validator)
|
|
@ -0,0 +1,211 @@
|
||||||
|
# -*- 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.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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 %s. Allowed are %r.' % (
|
||||||
|
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 interepreted 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 abscence 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
|
|
@ -0,0 +1,165 @@
|
||||||
|
# -*- 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.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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 interepreted 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 abscence 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
|
|
@ -0,0 +1,84 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth1.rfc5849.endpoints.signature_only
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of the signing logic of OAuth 1.0 RFC 5849.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,79 @@
|
||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
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 __future__ import unicode_literals
|
||||||
|
|
||||||
|
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 = '(%s) %s' % (self.error, self.description)
|
||||||
|
if request:
|
||||||
|
message += ' ' + repr(request)
|
||||||
|
super(OAuth1Error, self).__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'
|
|
@ -0,0 +1,139 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
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 __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import extract_params, urlencode
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse, urlunparse
|
||||||
|
except ImportError: # noqa
|
||||||
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = '{0}="{1}"'.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))
|
|
@ -0,0 +1,854 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth1.rfc5849
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import SIGNATURE_METHODS, utils
|
||||||
|
|
||||||
|
|
||||||
|
class RequestValidator(object):
|
||||||
|
|
||||||
|
"""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 usefull 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")
|
|
@ -0,0 +1,743 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth1.rfc5849.signature
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module represents a direct implementation of `section 3.4`_ of the spec.
|
||||||
|
|
||||||
|
Terminology:
|
||||||
|
* Client: software interfacing with an OAuth API
|
||||||
|
* Server: the API provider
|
||||||
|
* Resource Owner: the user who is granting authorization to the client
|
||||||
|
|
||||||
|
Steps for signing a request:
|
||||||
|
|
||||||
|
1. Collect parameters from the uri query, auth header, & body
|
||||||
|
2. Normalize those parameters
|
||||||
|
3. Normalize the uri
|
||||||
|
4. Pass the normalized uri, normalized parameters, and http method to
|
||||||
|
construct the base string
|
||||||
|
5. Pass the base string and any keys needed to a signing function
|
||||||
|
|
||||||
|
.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauthlib.common import (extract_params, safe_string_equals,
|
||||||
|
unicode_type, urldecode)
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def signature_base_string(http_method, base_str_uri,
|
||||||
|
normalized_encoded_request_parameters):
|
||||||
|
"""**Construct the signature base string.**
|
||||||
|
Per `section 3.4.1.1`_ of the spec.
|
||||||
|
|
||||||
|
For example, the HTTP request::
|
||||||
|
|
||||||
|
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: OAuth realm="Example",
|
||||||
|
oauth_consumer_key="9djdj82h48djs9d2",
|
||||||
|
oauth_token="kkk9d7dh3k39sjv7",
|
||||||
|
oauth_signature_method="HMAC-SHA1",
|
||||||
|
oauth_timestamp="137131201",
|
||||||
|
oauth_nonce="7d8f3e4a",
|
||||||
|
oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
|
||||||
|
|
||||||
|
c2&a3=2+q
|
||||||
|
|
||||||
|
is represented by the following signature base string (line breaks
|
||||||
|
are for display purposes only)::
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
.. _`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.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.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`).
|
||||||
|
#
|
||||||
|
# .. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
|
||||||
|
# .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6
|
||||||
|
base_string += utils.escape(normalized_encoded_request_parameters)
|
||||||
|
|
||||||
|
return base_string
|
||||||
|
|
||||||
|
|
||||||
|
def base_string_uri(uri, host=None):
|
||||||
|
"""**Base String URI**
|
||||||
|
Per `section 3.4.1.2`_ of RFC 5849.
|
||||||
|
|
||||||
|
For example, the HTTP request::
|
||||||
|
|
||||||
|
GET /r%20v/X?id=123 HTTP/1.1
|
||||||
|
Host: EXAMPLE.COM:80
|
||||||
|
|
||||||
|
is represented by the base string URI: "http://example.com/r%20v/X".
|
||||||
|
|
||||||
|
In another example, the HTTPS request::
|
||||||
|
|
||||||
|
GET /?q=1 HTTP/1.1
|
||||||
|
Host: www.example.net:8080
|
||||||
|
|
||||||
|
is represented by the base string URI: "https://www.example.net:8080/".
|
||||||
|
|
||||||
|
.. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||||
|
|
||||||
|
The host argument overrides the netloc part of the uri argument.
|
||||||
|
"""
|
||||||
|
if not isinstance(uri, unicode_type):
|
||||||
|
raise ValueError('uri must be a unicode object.')
|
||||||
|
|
||||||
|
# FIXME: urlparse does not support unicode
|
||||||
|
scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
|
||||||
|
|
||||||
|
# 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 or not netloc:
|
||||||
|
raise ValueError('uri must include a scheme and netloc')
|
||||||
|
|
||||||
|
# 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 section 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()
|
||||||
|
netloc = netloc.lower()
|
||||||
|
|
||||||
|
# 2. The host and port values MUST match the content of the HTTP
|
||||||
|
# request "Host" header field.
|
||||||
|
if host is not None:
|
||||||
|
netloc = host.lower()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
default_ports = (
|
||||||
|
('http', '80'),
|
||||||
|
('https', '443'),
|
||||||
|
)
|
||||||
|
if ':' in netloc:
|
||||||
|
host, port = netloc.split(':', 1)
|
||||||
|
if (scheme, port) in default_ports:
|
||||||
|
netloc = host
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
# ** Request Parameters **
|
||||||
|
#
|
||||||
|
# Per `section 3.4.1.3`_ of the spec.
|
||||||
|
#
|
||||||
|
# In order to guarantee a consistent and reproducible representation of
|
||||||
|
# the request parameters, the parameters are collected and decoded to
|
||||||
|
# their original decoded form. They are then sorted and encoded in a
|
||||||
|
# particular manner that is often different from their original
|
||||||
|
# encoding scheme, and concatenated into a single string.
|
||||||
|
#
|
||||||
|
# .. _`section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3
|
||||||
|
|
||||||
|
def collect_parameters(uri_query='', body=[], headers=None,
|
||||||
|
exclude_oauth_signature=True, with_realm=False):
|
||||||
|
"""**Parameter Sources**
|
||||||
|
|
||||||
|
Parameters starting with `oauth_` will be unescaped.
|
||||||
|
|
||||||
|
Body parameters must be supplied as a dict, a list of 2-tuples, or a
|
||||||
|
formencoded query string.
|
||||||
|
|
||||||
|
Headers must be supplied as a dict.
|
||||||
|
|
||||||
|
Per `section 3.4.1.3.1`_ of the spec.
|
||||||
|
|
||||||
|
For example, the HTTP request::
|
||||||
|
|
||||||
|
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: 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"
|
||||||
|
|
||||||
|
c2&a3=2+q
|
||||||
|
|
||||||
|
contains the following (fully decoded) parameters used in the
|
||||||
|
signature base sting::
|
||||||
|
|
||||||
|
+------------------------+------------------+
|
||||||
|
| Name | Value |
|
||||||
|
+------------------------+------------------+
|
||||||
|
| 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 |
|
||||||
|
+------------------------+------------------+
|
||||||
|
|
||||||
|
Note that the value of "b5" is "=%3D" and not "==". Both "c@" and
|
||||||
|
"c2" have empty values. While the encoding rules specified in this
|
||||||
|
specification for the purpose of constructing the signature base
|
||||||
|
string exclude the use of a "+" character (ASCII code 43) to
|
||||||
|
represent an encoded space character (ASCII code 32), this practice
|
||||||
|
is widely used in "application/x-www-form-urlencoded" encoded values,
|
||||||
|
and MUST be properly decoded, as demonstrated by one of the "a3"
|
||||||
|
parameter instances (the "a3" parameter is used twice in this
|
||||||
|
request).
|
||||||
|
|
||||||
|
.. _`section 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
|
||||||
|
"""
|
||||||
|
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`_, Section 17.13.4.
|
||||||
|
#
|
||||||
|
# .. _`RFC3986, Section 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4
|
||||||
|
# .. _`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.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 = dict((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`_.
|
||||||
|
|
||||||
|
# * The HTTP request entity-header includes the "Content-Type"
|
||||||
|
# header field set to "application/x-www-form-urlencoded".
|
||||||
|
#
|
||||||
|
# .._`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.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):
|
||||||
|
"""**Parameters Normalization**
|
||||||
|
Per `section 3.4.1.3.2`_ of the spec.
|
||||||
|
|
||||||
|
For example, the list of parameters from the previous section would
|
||||||
|
be normalized as follows:
|
||||||
|
|
||||||
|
Encoded::
|
||||||
|
|
||||||
|
+------------------------+------------------+
|
||||||
|
| Name | Value |
|
||||||
|
+------------------------+------------------+
|
||||||
|
| b5 | %3D%253D |
|
||||||
|
| a3 | a |
|
||||||
|
| c%40 | |
|
||||||
|
| a2 | r%20b |
|
||||||
|
| oauth_consumer_key | 9djdj82h48djs9d2 |
|
||||||
|
| oauth_token | kkk9d7dh3k39sjv7 |
|
||||||
|
| oauth_signature_method | HMAC-SHA1 |
|
||||||
|
| oauth_timestamp | 137131201 |
|
||||||
|
| oauth_nonce | 7d8f3e4a |
|
||||||
|
| c2 | |
|
||||||
|
| a3 | 2%20q |
|
||||||
|
+------------------------+------------------+
|
||||||
|
|
||||||
|
Sorted::
|
||||||
|
|
||||||
|
+------------------------+------------------+
|
||||||
|
| Name | Value |
|
||||||
|
+------------------------+------------------+
|
||||||
|
| a2 | r%20b |
|
||||||
|
| a3 | 2%20q |
|
||||||
|
| a3 | a |
|
||||||
|
| b5 | %3D%253D |
|
||||||
|
| c%40 | |
|
||||||
|
| c2 | |
|
||||||
|
| oauth_consumer_key | 9djdj82h48djs9d2 |
|
||||||
|
| oauth_nonce | 7d8f3e4a |
|
||||||
|
| oauth_signature_method | HMAC-SHA1 |
|
||||||
|
| oauth_timestamp | 137131201 |
|
||||||
|
| oauth_token | kkk9d7dh3k39sjv7 |
|
||||||
|
+------------------------+------------------+
|
||||||
|
|
||||||
|
Concatenated Pairs::
|
||||||
|
|
||||||
|
+-------------------------------------+
|
||||||
|
| Name=Value |
|
||||||
|
+-------------------------------------+
|
||||||
|
| a2=r%20b |
|
||||||
|
| a3=2%20q |
|
||||||
|
| a3=a |
|
||||||
|
| b5=%3D%253D |
|
||||||
|
| c%40= |
|
||||||
|
| c2= |
|
||||||
|
| oauth_consumer_key=9djdj82h48djs9d2 |
|
||||||
|
| oauth_nonce=7d8f3e4a |
|
||||||
|
| oauth_signature_method=HMAC-SHA1 |
|
||||||
|
| oauth_timestamp=137131201 |
|
||||||
|
| oauth_token=kkk9d7dh3k39sjv7 |
|
||||||
|
+-------------------------------------+
|
||||||
|
|
||||||
|
and concatenated together into a single string (line breaks are for
|
||||||
|
display purposes only)::
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
.. _`section 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 = ['{0}={1}'.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)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_hmac_sha1_with_client(base_string, client):
|
||||||
|
return sign_hmac_sha1(base_string,
|
||||||
|
client.client_secret,
|
||||||
|
client.resource_owner_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
|
||||||
|
"""**HMAC-SHA1**
|
||||||
|
|
||||||
|
The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
|
||||||
|
algorithm as defined in `RFC2104`_::
|
||||||
|
|
||||||
|
digest = HMAC-SHA1 (key, text)
|
||||||
|
|
||||||
|
Per `section 3.4.2`_ of the spec.
|
||||||
|
|
||||||
|
.. _`RFC2104`: https://tools.ietf.org/html/rfc2104
|
||||||
|
.. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The HMAC-SHA1 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 = base_string
|
||||||
|
|
||||||
|
# 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 '')
|
||||||
|
|
||||||
|
# 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, hashlib.sha1)
|
||||||
|
|
||||||
|
# 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, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
|
||||||
|
return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def sign_hmac_sha256_with_client(base_string, client):
|
||||||
|
return sign_hmac_sha256(base_string,
|
||||||
|
client.client_secret,
|
||||||
|
client.resource_owner_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_hmac_sha256(base_string, client_secret, resource_owner_secret):
|
||||||
|
"""**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 = base_string
|
||||||
|
|
||||||
|
# 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 '')
|
||||||
|
|
||||||
|
# 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, hashlib.sha256)
|
||||||
|
|
||||||
|
# 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, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
|
||||||
|
return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
|
||||||
|
|
||||||
|
_jwtrs1 = None
|
||||||
|
|
||||||
|
#jwt has some nice pycrypto/cryptography abstractions
|
||||||
|
def _jwt_rs1_signing_algorithm():
|
||||||
|
global _jwtrs1
|
||||||
|
if _jwtrs1 is None:
|
||||||
|
import jwt.algorithms as jwtalgo
|
||||||
|
_jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1)
|
||||||
|
return _jwtrs1
|
||||||
|
|
||||||
|
def sign_rsa_sha1(base_string, rsa_private_key):
|
||||||
|
"""**RSA-SHA1**
|
||||||
|
|
||||||
|
Per `section 3.4.3`_ of the spec.
|
||||||
|
|
||||||
|
The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
|
||||||
|
algorithm as defined in `RFC3447, Section 8.2`_ (also known as
|
||||||
|
PKCS#1), using SHA-1 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
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(base_string, unicode_type):
|
||||||
|
base_string = base_string.encode('utf-8')
|
||||||
|
# TODO: finish RSA documentation
|
||||||
|
alg = _jwt_rs1_signing_algorithm()
|
||||||
|
key = _prepare_key_plus(alg, rsa_private_key)
|
||||||
|
s=alg.sign(base_string, key)
|
||||||
|
return binascii.b2a_base64(s)[:-1].decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def sign_rsa_sha1_with_client(base_string, client):
|
||||||
|
if not client.rsa_key:
|
||||||
|
raise ValueError('rsa_key is required when using RSA signature method.')
|
||||||
|
return sign_rsa_sha1(base_string, client.rsa_key)
|
||||||
|
|
||||||
|
|
||||||
|
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 sign_plaintext_with_client(base_string, client):
|
||||||
|
return sign_plaintext(client.client_secret, client.resource_owner_secret)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_hmac_sha1(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_sha1(sig_base_str, client_secret,
|
||||||
|
resource_owner_secret)
|
||||||
|
match = safe_string_equals(signature, request.signature)
|
||||||
|
if not match:
|
||||||
|
log.debug('Verify HMAC-SHA1 failed: signature base string: %s',
|
||||||
|
sig_base_str)
|
||||||
|
return match
|
||||||
|
|
||||||
|
|
||||||
|
def verify_hmac_sha256(request, client_secret=None,
|
||||||
|
resource_owner_secret=None):
|
||||||
|
"""Verify a HMAC-SHA256 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_sha256(sig_base_str, client_secret,
|
||||||
|
resource_owner_secret)
|
||||||
|
match = safe_string_equals(signature, request.signature)
|
||||||
|
if not match:
|
||||||
|
log.debug('Verify HMAC-SHA256 failed: signature base string: %s',
|
||||||
|
sig_base_str)
|
||||||
|
return match
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_key_plus(alg, keystr):
|
||||||
|
if isinstance(keystr, bytes):
|
||||||
|
keystr = keystr.decode('utf-8')
|
||||||
|
return alg.prepare_key(keystr)
|
||||||
|
|
||||||
|
def verify_rsa_sha1(request, rsa_public_key):
|
||||||
|
"""Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.
|
||||||
|
|
||||||
|
Per `section 3.4.3`_ of the spec.
|
||||||
|
|
||||||
|
Note this method requires the jwt and cryptography libraries.
|
||||||
|
|
||||||
|
.. _`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 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).encode('utf-8')
|
||||||
|
sig = binascii.a2b_base64(request.signature.encode('utf-8'))
|
||||||
|
|
||||||
|
alg = _jwt_rs1_signing_algorithm()
|
||||||
|
key = _prepare_key_plus(alg, rsa_public_key)
|
||||||
|
|
||||||
|
verify_ok = alg.verify(sig_base_str, key, sig)
|
||||||
|
if not verify_ok:
|
||||||
|
log.debug('Verify RSA-SHA1 failed: signature base string: %s',
|
||||||
|
sig_base_str)
|
||||||
|
return verify_ok
|
||||||
|
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,90 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.utils
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module contains utility methods used by various parts of the OAuth
|
||||||
|
spec.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import quote, unicode_type, unquote
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib2
|
||||||
|
except ImportError:
|
||||||
|
import urllib.request as urllib2
|
||||||
|
|
||||||
|
|
||||||
|
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, unicode_type):
|
||||||
|
raise ValueError('Only unicode objects are escapable. ' +
|
||||||
|
'Got %r of type %s.' % (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, unicode_type):
|
||||||
|
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')
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is a wrapper for the most recent implementation of OAuth 2.0 Client
|
||||||
|
and Server classes.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .rfc6749.clients import Client
|
||||||
|
from .rfc6749.clients import WebApplicationClient
|
||||||
|
from .rfc6749.clients import MobileApplicationClient
|
||||||
|
from .rfc6749.clients import LegacyApplicationClient
|
||||||
|
from .rfc6749.clients import BackendApplicationClient
|
||||||
|
from .rfc6749.clients import ServiceApplicationClient
|
||||||
|
from .rfc6749.endpoints import AuthorizationEndpoint
|
||||||
|
from .rfc6749.endpoints import IntrospectEndpoint
|
||||||
|
from .rfc6749.endpoints import MetadataEndpoint
|
||||||
|
from .rfc6749.endpoints import TokenEndpoint
|
||||||
|
from .rfc6749.endpoints import ResourceEndpoint
|
||||||
|
from .rfc6749.endpoints import RevocationEndpoint
|
||||||
|
from .rfc6749.endpoints import Server
|
||||||
|
from .rfc6749.endpoints import WebApplicationServer
|
||||||
|
from .rfc6749.endpoints import MobileApplicationServer
|
||||||
|
from .rfc6749.endpoints import LegacyApplicationServer
|
||||||
|
from .rfc6749.endpoints import BackendApplicationServer
|
||||||
|
from .rfc6749.errors import AccessDeniedError, OAuth2Error, FatalClientError, InsecureTransportError, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError
|
||||||
|
from .rfc6749.grant_types import AuthorizationCodeGrant
|
||||||
|
from .rfc6749.grant_types import ImplicitGrant
|
||||||
|
from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant
|
||||||
|
from .rfc6749.grant_types import ClientCredentialsGrant
|
||||||
|
from .rfc6749.grant_types import RefreshTokenGrant
|
||||||
|
from .rfc6749.request_validator import RequestValidator
|
||||||
|
from .rfc6749.tokens import BearerToken, OAuth2Token
|
||||||
|
from .rfc6749.utils import is_secure_transport
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .endpoints.base import BaseEndpoint
|
||||||
|
from .endpoints.base import catch_errors_and_unavailability
|
||||||
|
from .errors import TemporarilyUnavailableError, ServerError
|
||||||
|
from .errors import FatalClientError, OAuth2Error
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
|
@ -0,0 +1,16 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .base import Client, AUTH_HEADER, URI_QUERY, BODY
|
||||||
|
from .web_application import WebApplicationClient
|
||||||
|
from .mobile_application import MobileApplicationClient
|
||||||
|
from .legacy_application import LegacyApplicationClient
|
||||||
|
from .backend_application import BackendApplicationClient
|
||||||
|
from .service_application import ServiceApplicationClient
|
|
@ -0,0 +1,76 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from ..parameters import parse_token_response, 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 paramters. 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)
|
|
@ -0,0 +1,512 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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(object):
|
||||||
|
"""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,
|
||||||
|
**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`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = 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 = dict(
|
||||||
|
(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:
|
||||||
|
|
||||||
|
: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
|
||||||
|
self.scope = scope or self.scope
|
||||||
|
auth_url = self.prepare_request_uri(
|
||||||
|
authorization_url, redirect_uri=self.redirect_url,
|
||||||
|
scope=self.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 successfull
|
||||||
|
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 paramters. 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 paramters. Default ''.
|
||||||
|
|
||||||
|
:param scope: List of scopes to request. Must be equal to
|
||||||
|
or a subset of the scopes granted when obtaining the refresh
|
||||||
|
token.
|
||||||
|
|
||||||
|
: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()
|
||||||
|
|
||||||
|
self.scope = scope or self.scope
|
||||||
|
body = self.prepare_refresh_body(body=body,
|
||||||
|
refresh_token=refresh_token, scope=self.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: http
|
||||||
|
|
||||||
|
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.
|
||||||
|
:return: Dictionary of token parameters.
|
||||||
|
:raises: Warning if scope has changed. 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
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
|
||||||
|
grant_type
|
||||||
|
REQUIRED. Value MUST be set to "refresh_token".
|
||||||
|
refresh_token
|
||||||
|
REQUIRED. The refresh token issued to the client.
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
refresh_token = refresh_token or self.refresh_token
|
||||||
|
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 _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:
|
||||||
|
self._expires_at = int(response.get('expires_at'))
|
||||||
|
|
||||||
|
if 'mac_key' in response:
|
||||||
|
self.mac_key = response.get('mac_key')
|
||||||
|
|
||||||
|
if 'mac_algorithm' in response:
|
||||||
|
self.mac_algorithm = response.get('mac_algorithm')
|
|
@ -0,0 +1,85 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from ..parameters import parse_token_response, 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(LegacyApplicationClient, self).__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 paramters. 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
|
||||||
|
return prepare_token_request(self.grant_type, body=body, username=username,
|
||||||
|
password=password, scope=scope, **kwargs)
|
|
@ -0,0 +1,174 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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 registerd 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
|
||||||
|
"""
|
||||||
|
return prepare_grant_uri(uri, self.client_id, self.response_type,
|
||||||
|
redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
|
||||||
|
|
||||||
|
def parse_request_uri_response(self, uri, state=None, scope=None):
|
||||||
|
"""Parse the response URI fragment.
|
||||||
|
|
||||||
|
If the resource owner grants the access request, the authorization
|
||||||
|
server issues an access token and delivers it to the client by adding
|
||||||
|
the following parameters to the fragment component of the redirection
|
||||||
|
URI using the "application/x-www-form-urlencoded" format:
|
||||||
|
|
||||||
|
:param uri: The callback URI that resulted from the user being redirected
|
||||||
|
back from the provider to you, the client.
|
||||||
|
:param state: The state provided in the authorization request.
|
||||||
|
:param scope: The scopes provided in the authorization request.
|
||||||
|
:return: Dictionary of token parameters.
|
||||||
|
:raises: OAuth2Error if response is invalid.
|
||||||
|
|
||||||
|
A successful response should always contain
|
||||||
|
|
||||||
|
**access_token**
|
||||||
|
The access token issued by the authorization server. Often
|
||||||
|
a random string.
|
||||||
|
|
||||||
|
**token_type**
|
||||||
|
The type of the token issued as described in `Section 7.1`_.
|
||||||
|
Commonly ``Bearer``.
|
||||||
|
|
||||||
|
**state**
|
||||||
|
If you provided the state parameter in the authorization phase, then
|
||||||
|
the provider is required to include that exact state value in the
|
||||||
|
response.
|
||||||
|
|
||||||
|
While it is not mandated it is recommended that the provider include
|
||||||
|
|
||||||
|
**expires_in**
|
||||||
|
The lifetime in seconds of the access token. For
|
||||||
|
example, the value "3600" denotes that the access token will
|
||||||
|
expire in one hour from the time the response was generated.
|
||||||
|
If omitted, the authorization server SHOULD provide the
|
||||||
|
expiration time via other means or document the default value.
|
||||||
|
|
||||||
|
**scope**
|
||||||
|
Providers may supply this in all responses but are required to only
|
||||||
|
if it has changed since the authorization request.
|
||||||
|
|
||||||
|
A few example responses can be seen below::
|
||||||
|
|
||||||
|
>>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world'
|
||||||
|
>>> from oauthlib.oauth2 import MobileApplicationClient
|
||||||
|
>>> client = MobileApplicationClient('your_id')
|
||||||
|
>>> client.parse_request_uri_response(response_uri)
|
||||||
|
{
|
||||||
|
'access_token': 'sdlfkj452',
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'state': 'ss345asyht',
|
||||||
|
'scope': [u'hello', u'world']
|
||||||
|
}
|
||||||
|
>>> client.parse_request_uri_response(response_uri, state='other')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response
|
||||||
|
**scope**
|
||||||
|
File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response
|
||||||
|
raise ValueError("Mismatching or missing state in params.")
|
||||||
|
ValueError: Mismatching or missing state in params.
|
||||||
|
>>> def alert_scope_changed(message, old, new):
|
||||||
|
... print(message, old, new)
|
||||||
|
...
|
||||||
|
>>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
|
||||||
|
>>> client.parse_request_body_response(response_body, scope=['other'])
|
||||||
|
('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
|
||||||
|
|
||||||
|
.. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
|
||||||
|
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||||
|
"""
|
||||||
|
self.token = parse_implicit_response(uri, state=state, scope=scope)
|
||||||
|
self.populate_token_attributes(self.token)
|
||||||
|
return self.token
|
|
@ -0,0 +1,190 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oauthlib.common import to_unicode
|
||||||
|
|
||||||
|
from ..parameters import parse_token_response, 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):
|
||||||
|
"""Initalize 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(ServiceApplicationClient, self).__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 paramters. 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
|
||||||
|
return prepare_token_request(self.grant_type,
|
||||||
|
body=body,
|
||||||
|
assertion=assertion,
|
||||||
|
scope=scope,
|
||||||
|
**kwargs)
|
|
@ -0,0 +1,205 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from ..parameters import (parse_authorization_code_response,
|
||||||
|
parse_token_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(WebApplicationClient, self).__init__(client_id, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
|
||||||
|
state=None, **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 registerd 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 ``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', 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
|
||||||
|
"""
|
||||||
|
return prepare_grant_uri(uri, self.client_id, 'code',
|
||||||
|
redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
|
||||||
|
|
||||||
|
def prepare_request_body(self, code=None, redirect_uri=None, body='',
|
||||||
|
include_client_id=True, **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 paramters. 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 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='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, **kwargs)
|
||||||
|
|
||||||
|
def parse_request_uri_response(self, uri, state=None):
|
||||||
|
"""Parse the URI query for code and state.
|
||||||
|
|
||||||
|
If the resource owner grants the access request, the authorization
|
||||||
|
server issues an authorization code and delivers it to the client by
|
||||||
|
adding the following parameters to the query component of the
|
||||||
|
redirection URI using the "application/x-www-form-urlencoded" format:
|
||||||
|
|
||||||
|
:param uri: The callback URI that resulted from the user being redirected
|
||||||
|
back from the provider to you, the client.
|
||||||
|
:param state: The state provided in the authorization request.
|
||||||
|
|
||||||
|
**code**
|
||||||
|
The authorization code generated by the authorization server.
|
||||||
|
The authorization code MUST expire shortly after it is issued
|
||||||
|
to mitigate the risk of leaks. A maximum authorization code
|
||||||
|
lifetime of 10 minutes is RECOMMENDED. The client MUST NOT
|
||||||
|
use the authorization code more than once. If an authorization
|
||||||
|
code is used more than once, the authorization server MUST deny
|
||||||
|
the request and SHOULD revoke (when possible) all tokens
|
||||||
|
previously issued based on that authorization code.
|
||||||
|
The authorization code is bound to the client identifier and
|
||||||
|
redirection URI.
|
||||||
|
|
||||||
|
**state**
|
||||||
|
If the "state" parameter was present in the authorization request.
|
||||||
|
|
||||||
|
This method is mainly intended to enforce strict state checking with
|
||||||
|
the added benefit of easily extracting parameters from the URI::
|
||||||
|
|
||||||
|
>>> from oauthlib.oauth2 import WebApplicationClient
|
||||||
|
>>> client = WebApplicationClient('your_id')
|
||||||
|
>>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45'
|
||||||
|
>>> client.parse_request_uri_response(uri, state='sfetw45')
|
||||||
|
{'state': 'sfetw45', 'code': 'sdfkjh345'}
|
||||||
|
>>> client.parse_request_uri_response(uri, state='other')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response
|
||||||
|
back from the provider to you, the client.
|
||||||
|
File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response
|
||||||
|
raise MismatchingStateError()
|
||||||
|
oauthlib.oauth2.rfc6749.errors.MismatchingStateError
|
||||||
|
"""
|
||||||
|
response = parse_authorization_code_response(uri, state=state)
|
||||||
|
self.populate_code_attributes(response)
|
||||||
|
return response
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .authorization import AuthorizationEndpoint
|
||||||
|
from .introspect import IntrospectEndpoint
|
||||||
|
from .metadata import MetadataEndpoint
|
||||||
|
from .token import TokenEndpoint
|
||||||
|
from .resource import ResourceEndpoint
|
||||||
|
from .revocation import RevocationEndpoint
|
||||||
|
from .pre_configured import Server
|
||||||
|
from .pre_configured import WebApplicationServer
|
||||||
|
from .pre_configured import MobileApplicationServer
|
||||||
|
from .pre_configured import LegacyApplicationServer
|
||||||
|
from .pre_configured import BackendApplicationServer
|
|
@ -0,0 +1,117 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,117 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..errors import (FatalClientError, OAuth2Error, ServerError,
|
||||||
|
TemporarilyUnavailableError, InvalidRequestError,
|
||||||
|
InvalidClientError, UnsupportedTokenTypeError)
|
||||||
|
|
||||||
|
from oauthlib.common import CaseInsensitiveDict, urldecode
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEndpoint(object):
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,125 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.endpoint.introspect
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
An implementation of the OAuth 2.0 `Token Introspection`.
|
||||||
|
|
||||||
|
.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauthlib.common import Request
|
||||||
|
|
||||||
|
from ..errors import OAuth2Error, UnsupportedTokenTypeError
|
||||||
|
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)
|
|
@ -0,0 +1,242 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ....common import unicode_type
|
||||||
|
from .base import BaseEndpoint, catch_errors_and_unavailability
|
||||||
|
from .authorization import AuthorizationEndpoint
|
||||||
|
from .introspect import IntrospectEndpoint
|
||||||
|
from .token import TokenEndpoint
|
||||||
|
from .revocation import RevocationEndpoint
|
||||||
|
from .. import grant_types
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
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 array[key].startswith("https"):
|
||||||
|
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, unicode_type):
|
||||||
|
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
|
|
@ -0,0 +1,220 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.endpoints.pre_configured
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various endpoints needed
|
||||||
|
for providing OAuth 2.0 RFC6749 servers.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
auth_grant = AuthorizationCodeGrant(request_validator)
|
||||||
|
implicit_grant = ImplicitGrant(request_validator)
|
||||||
|
password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||||
|
request_validator)
|
||||||
|
credentials_grant = ClientCredentialsGrant(request_validator)
|
||||||
|
refresh_grant = RefreshTokenGrant(request_validator)
|
||||||
|
|
||||||
|
bearer = BearerToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
|
||||||
|
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||||
|
response_types={
|
||||||
|
'code': auth_grant,
|
||||||
|
'token': implicit_grant,
|
||||||
|
'none': auth_grant
|
||||||
|
},
|
||||||
|
default_token_type=bearer)
|
||||||
|
|
||||||
|
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||||
|
grant_types={
|
||||||
|
'authorization_code': auth_grant,
|
||||||
|
'password': password_grant,
|
||||||
|
'client_credentials': credentials_grant,
|
||||||
|
'refresh_token': refresh_grant,
|
||||||
|
},
|
||||||
|
default_token_type=bearer)
|
||||||
|
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||||
|
token_types={'Bearer': 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.
|
||||||
|
"""
|
||||||
|
auth_grant = AuthorizationCodeGrant(request_validator)
|
||||||
|
refresh_grant = RefreshTokenGrant(request_validator)
|
||||||
|
bearer = BearerToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||||
|
response_types={'code': auth_grant},
|
||||||
|
default_token_type=bearer)
|
||||||
|
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||||
|
grant_types={
|
||||||
|
'authorization_code': auth_grant,
|
||||||
|
'refresh_token': refresh_grant,
|
||||||
|
},
|
||||||
|
default_token_type=bearer)
|
||||||
|
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||||
|
token_types={'Bearer': 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.
|
||||||
|
"""
|
||||||
|
implicit_grant = ImplicitGrant(request_validator)
|
||||||
|
bearer = BearerToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
AuthorizationEndpoint.__init__(self, default_response_type='token',
|
||||||
|
response_types={
|
||||||
|
'token': implicit_grant},
|
||||||
|
default_token_type=bearer)
|
||||||
|
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||||
|
token_types={'Bearer': 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.
|
||||||
|
"""
|
||||||
|
password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||||
|
request_validator)
|
||||||
|
refresh_grant = RefreshTokenGrant(request_validator)
|
||||||
|
bearer = BearerToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
TokenEndpoint.__init__(self, default_grant_type='password',
|
||||||
|
grant_types={
|
||||||
|
'password': password_grant,
|
||||||
|
'refresh_token': refresh_grant,
|
||||||
|
},
|
||||||
|
default_token_type=bearer)
|
||||||
|
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||||
|
token_types={'Bearer': 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.
|
||||||
|
"""
|
||||||
|
credentials_grant = ClientCredentialsGrant(request_validator)
|
||||||
|
bearer = BearerToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
TokenEndpoint.__init__(self, default_grant_type='client_credentials',
|
||||||
|
grant_types={
|
||||||
|
'client_credentials': credentials_grant},
|
||||||
|
default_token_type=bearer)
|
||||||
|
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||||
|
token_types={'Bearer': bearer})
|
||||||
|
RevocationEndpoint.__init__(self, request_validator,
|
||||||
|
supported_token_types=['access_token'])
|
||||||
|
IntrospectEndpoint.__init__(self, request_validator,
|
||||||
|
supported_token_types=['access_token'])
|
|
@ -0,0 +1,87 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,129 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauthlib.common import Request
|
||||||
|
|
||||||
|
from ..errors import OAuth2Error, UnsupportedTokenTypeError
|
||||||
|
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 sucessfully 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 = '%s(%s);' % (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 accross 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)
|
|
@ -0,0 +1,123 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OAuth 2.0 RFC6749.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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 authorization endpoint result in user
|
||||||
|
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)
|
|
@ -0,0 +1,406 @@
|
||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
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 __future__ import unicode_literals
|
||||||
|
|
||||||
|
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 = '(%s) %s' % (self.error, self.description)
|
||||||
|
if request:
|
||||||
|
message += ' ' + repr(request)
|
||||||
|
super(OAuth2Error, self).__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 = [
|
||||||
|
"Bearer",
|
||||||
|
'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": ", ".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(CustomOAuth2Error, self).__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)
|
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from .authorization_code import AuthorizationCodeGrant
|
||||||
|
from .implicit import ImplicitGrant
|
||||||
|
from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
|
||||||
|
from .client_credentials import ClientCredentialsGrant
|
||||||
|
from .refresh_token import RefreshTokenGrant
|
|
@ -0,0 +1,548 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauthlib import common
|
||||||
|
|
||||||
|
from .. import errors
|
||||||
|
from .base import GrantTypeBase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def code_challenge_method_s256(verifier, challenge):
|
||||||
|
"""
|
||||||
|
If the "code_challenge_method" from `Section 4.3`_ was "S256", the
|
||||||
|
received "code_verifier" is hashed by SHA-256, base64url-encoded, and
|
||||||
|
then compared to the "code_challenge", i.e.:
|
||||||
|
|
||||||
|
BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
|
||||||
|
|
||||||
|
How to implement a base64url-encoding
|
||||||
|
function without padding, based upon the standard base64-encoding
|
||||||
|
function that uses padding.
|
||||||
|
|
||||||
|
To be concrete, example C# code implementing these functions is shown
|
||||||
|
below. Similar code could be used in other languages.
|
||||||
|
|
||||||
|
static string base64urlencode(byte [] arg)
|
||||||
|
{
|
||||||
|
string s = Convert.ToBase64String(arg); // Regular base64 encoder
|
||||||
|
s = s.Split('=')[0]; // Remove any trailing '='s
|
||||||
|
s = s.Replace('+', '-'); // 62nd char of encoding
|
||||||
|
s = s.Replace('/', '_'); // 63rd char of encoding
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
In python urlsafe_b64encode is already replacing '+' and '/', but preserve
|
||||||
|
the trailing '='. So we have to remove it.
|
||||||
|
|
||||||
|
.. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
|
||||||
|
"""
|
||||||
|
return base64.urlsafe_b64encode(
|
||||||
|
hashlib.sha256(verifier.encode()).digest()
|
||||||
|
).decode().rstrip('=') == challenge
|
||||||
|
|
||||||
|
|
||||||
|
def code_challenge_method_plain(verifier, challenge):
|
||||||
|
"""
|
||||||
|
If the "code_challenge_method" from `Section 4.3`_ was "plain", they are
|
||||||
|
compared directly, i.e.:
|
||||||
|
|
||||||
|
code_verifier == code_challenge.
|
||||||
|
|
||||||
|
.. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
|
||||||
|
"""
|
||||||
|
return verifier == challenge
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationCodeGrant(GrantTypeBase):
|
||||||
|
|
||||||
|
"""`Authorization Code Grant`_
|
||||||
|
|
||||||
|
The authorization code grant type is used to obtain both access
|
||||||
|
tokens and refresh tokens and is optimized for confidential clients.
|
||||||
|
Since this is a redirection-based flow, the client must be capable of
|
||||||
|
interacting with the resource owner's user-agent (typically a web
|
||||||
|
browser) and capable of receiving incoming requests (via redirection)
|
||||||
|
from the authorization server::
|
||||||
|
|
||||||
|
+----------+
|
||||||
|
| Resource |
|
||||||
|
| Owner |
|
||||||
|
| |
|
||||||
|
+----------+
|
||||||
|
^
|
||||||
|
|
|
||||||
|
(B)
|
||||||
|
+----|-----+ Client Identifier +---------------+
|
||||||
|
| -+----(A)-- & Redirection URI ---->| |
|
||||||
|
| User- | | Authorization |
|
||||||
|
| Agent -+----(B)-- User authenticates --->| Server |
|
||||||
|
| | | |
|
||||||
|
| -+----(C)-- Authorization Code ---<| |
|
||||||
|
+-|----|---+ +---------------+
|
||||||
|
| | ^ v
|
||||||
|
(A) (C) | |
|
||||||
|
| | | |
|
||||||
|
^ v | |
|
||||||
|
+---------+ | |
|
||||||
|
| |>---(D)-- Authorization Code ---------' |
|
||||||
|
| Client | & Redirection URI |
|
||||||
|
| | |
|
||||||
|
| |<---(E)----- Access Token -------------------'
|
||||||
|
+---------+ (w/ Optional Refresh Token)
|
||||||
|
|
||||||
|
Note: The lines illustrating steps (A), (B), and (C) are broken into
|
||||||
|
two parts as they pass through the user-agent.
|
||||||
|
|
||||||
|
Figure 3: Authorization Code Flow
|
||||||
|
|
||||||
|
The flow illustrated in Figure 3 includes the following steps:
|
||||||
|
|
||||||
|
(A) The client initiates the flow by directing the resource owner's
|
||||||
|
user-agent to the authorization endpoint. The client includes
|
||||||
|
its client identifier, requested scope, local state, and a
|
||||||
|
redirection URI to which the authorization server will send the
|
||||||
|
user-agent back once access is granted (or denied).
|
||||||
|
|
||||||
|
(B) The authorization server authenticates the resource owner (via
|
||||||
|
the user-agent) and establishes whether the resource owner
|
||||||
|
grants or denies the client's access request.
|
||||||
|
|
||||||
|
(C) Assuming the resource owner grants access, the authorization
|
||||||
|
server redirects the user-agent back to the client using the
|
||||||
|
redirection URI provided earlier (in the request or during
|
||||||
|
client registration). The redirection URI includes an
|
||||||
|
authorization code and any local state provided by the client
|
||||||
|
earlier.
|
||||||
|
|
||||||
|
(D) The client requests an access token from the authorization
|
||||||
|
server's token endpoint by including the authorization code
|
||||||
|
received in the previous step. When making the request, the
|
||||||
|
client authenticates with the authorization server. The client
|
||||||
|
includes the redirection URI used to obtain the authorization
|
||||||
|
code for verification.
|
||||||
|
|
||||||
|
(E) The authorization server authenticates the client, validates the
|
||||||
|
authorization code, and ensures that the redirection URI
|
||||||
|
received matches the URI used to redirect the client in
|
||||||
|
step (C). If valid, the authorization server responds back with
|
||||||
|
an access token and, optionally, a refresh token.
|
||||||
|
|
||||||
|
OAuth 2.0 public clients utilizing the Authorization Code Grant are
|
||||||
|
susceptible to the authorization code interception attack.
|
||||||
|
|
||||||
|
A technique to mitigate against the threat through the use of Proof Key for Code
|
||||||
|
Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
.. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
|
||||||
|
.. _`PKCE`: https://tools.ietf.org/html/rfc7636
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_response_mode = 'query'
|
||||||
|
response_types = ['code']
|
||||||
|
|
||||||
|
# This dict below is private because as RFC mention it:
|
||||||
|
# "S256" is Mandatory To Implement (MTI) on the server.
|
||||||
|
#
|
||||||
|
_code_challenge_methods = {
|
||||||
|
'plain': code_challenge_method_plain,
|
||||||
|
'S256': code_challenge_method_s256
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_authorization_code(self, request):
|
||||||
|
"""
|
||||||
|
Generates an authorization grant represented as a dictionary.
|
||||||
|
|
||||||
|
:param request: OAuthlib request.
|
||||||
|
:type request: oauthlib.common.Request
|
||||||
|
"""
|
||||||
|
grant = {'code': common.generate_token()}
|
||||||
|
if hasattr(request, 'state') and request.state:
|
||||||
|
grant['state'] = request.state
|
||||||
|
log.debug('Created authorization code grant %r for request %r.',
|
||||||
|
grant, request)
|
||||||
|
return grant
|
||||||
|
|
||||||
|
def create_authorization_response(self, request, token_handler):
|
||||||
|
"""
|
||||||
|
The client constructs the request URI by adding the following
|
||||||
|
parameters to the query component of the authorization endpoint URI
|
||||||
|
using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
|
||||||
|
|
||||||
|
response_type
|
||||||
|
REQUIRED. Value MUST be set to "code" for standard OAuth2
|
||||||
|
authorization flow. For OpenID Connect it must be one of
|
||||||
|
"code token", "code id_token", or "code token id_token" - we
|
||||||
|
essentially test that "code" appears in the response_type.
|
||||||
|
client_id
|
||||||
|
REQUIRED. The client identifier as described in `Section 2.2`_.
|
||||||
|
redirect_uri
|
||||||
|
OPTIONAL. As described in `Section 3.1.2`_.
|
||||||
|
scope
|
||||||
|
OPTIONAL. The scope of the access request as described by
|
||||||
|
`Section 3.3`_.
|
||||||
|
state
|
||||||
|
RECOMMENDED. An opaque value used by the client to maintain
|
||||||
|
state between the request and callback. The authorization
|
||||||
|
server includes this value when redirecting the user-agent back
|
||||||
|
to the client. The parameter SHOULD be used for preventing
|
||||||
|
cross-site request forgery as described in `Section 10.12`_.
|
||||||
|
|
||||||
|
The client directs the resource owner to the constructed URI using an
|
||||||
|
HTTP redirection response, or by other means available to it via the
|
||||||
|
user-agent.
|
||||||
|
|
||||||
|
:param request: OAuthlib request.
|
||||||
|
:type request: oauthlib.common.Request
|
||||||
|
:param token_handler: A token handler instance, for example of type
|
||||||
|
oauthlib.oauth2.BearerToken.
|
||||||
|
:returns: headers, body, status
|
||||||
|
:raises: FatalClientError on invalid redirect URI or client id.
|
||||||
|
|
||||||
|
A few examples::
|
||||||
|
|
||||||
|
>>> from your_validator import your_validator
|
||||||
|
>>> request = Request('https://example.com/authorize?client_id=valid'
|
||||||
|
... '&redirect_uri=http%3A%2F%2Fclient.com%2F')
|
||||||
|
>>> from oauthlib.common import Request
|
||||||
|
>>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
|
||||||
|
>>> token = BearerToken(your_validator)
|
||||||
|
>>> grant = AuthorizationCodeGrant(your_validator)
|
||||||
|
>>> request.scopes = ['authorized', 'in', 'some', 'form']
|
||||||
|
>>> grant.create_authorization_response(request, token)
|
||||||
|
(u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
|
||||||
|
>>> request = Request('https://example.com/authorize?client_id=valid'
|
||||||
|
... '&redirect_uri=http%3A%2F%2Fclient.com%2F'
|
||||||
|
... '&response_type=code')
|
||||||
|
>>> request.scopes = ['authorized', 'in', 'some', 'form']
|
||||||
|
>>> grant.create_authorization_response(request, token)
|
||||||
|
(u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
|
||||||
|
>>> # If the client id or redirect uri fails validation
|
||||||
|
>>> grant.create_authorization_response(request, token)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
|
||||||
|
>>> grant.create_authorization_response(request, token)
|
||||||
|
File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
|
||||||
|
oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
|
||||||
|
|
||||||
|
.. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
|
||||||
|
.. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
|
||||||
|
.. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
|
||||||
|
.. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
|
||||||
|
.. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.validate_authorization_request(request)
|
||||||
|
log.debug('Pre resource owner authorization validation ok for %r.',
|
||||||
|
request)
|
||||||
|
|
||||||
|
# If the request fails due to a missing, invalid, or mismatching
|
||||||
|
# redirection URI, or if the client identifier is missing or invalid,
|
||||||
|
# the authorization server SHOULD inform the resource owner of the
|
||||||
|
# error and MUST NOT automatically redirect the user-agent to the
|
||||||
|
# invalid redirection URI.
|
||||||
|
except errors.FatalClientError as e:
|
||||||
|
log.debug('Fatal client error during validation of %r. %r.',
|
||||||
|
request, e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# If the resource owner denies the access request or if the request
|
||||||
|
# fails for reasons other than a missing or invalid redirection URI,
|
||||||
|
# the authorization server informs the client by adding the following
|
||||||
|
# parameters to the query component of the redirection URI using the
|
||||||
|
# "application/x-www-form-urlencoded" format, per Appendix B:
|
||||||
|
# https://tools.ietf.org/html/rfc6749#appendix-B
|
||||||
|
except errors.OAuth2Error as e:
|
||||||
|
log.debug('Client error during validation of %r. %r.', request, e)
|
||||||
|
request.redirect_uri = request.redirect_uri or self.error_uri
|
||||||
|
redirect_uri = common.add_params_to_uri(
|
||||||
|
request.redirect_uri, e.twotuples,
|
||||||
|
fragment=request.response_mode == "fragment")
|
||||||
|
return {'Location': redirect_uri}, None, 302
|
||||||
|
|
||||||
|
grant = self.create_authorization_code(request)
|
||||||
|
for modifier in self._code_modifiers:
|
||||||
|
grant = modifier(grant, token_handler, request)
|
||||||
|
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)
|
||||||
|
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)
|
|
@ -0,0 +1,253 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from oauthlib.common import add_params_to_uri
|
||||||
|
from oauthlib.uri_validate import is_absolute_uri
|
||||||
|
from oauthlib.oauth2.rfc6749 import errors, utils
|
||||||
|
|
||||||
|
from ..request_validator import RequestValidator
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatorsContainer(object):
|
||||||
|
"""
|
||||||
|
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(object):
|
||||||
|
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)
|
|
@ -0,0 +1,127 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .. import errors
|
||||||
|
from ..request_validator import RequestValidator
|
||||||
|
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)
|
||||||
|
|
||||||
|
log.debug('Authorizing access to user %r.', request.user)
|
||||||
|
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)
|
|
@ -0,0 +1,379 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,139 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .. import errors, utils
|
||||||
|
from ..request_validator import RequestValidator
|
||||||
|
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(RefreshTokenGrant, self).__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)
|
||||||
|
|
||||||
|
self.request_validator.save_token(token, request)
|
||||||
|
|
||||||
|
log.debug('Issuing new 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
|
||||||
|
"""
|
||||||
|
# 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)
|
|
@ -0,0 +1,203 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .. import errors
|
||||||
|
from ..request_validator import RequestValidator
|
||||||
|
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)
|
|
@ -0,0 +1,461 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
|
||||||
|
scope=None, state=None, **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 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
|
||||||
|
&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))
|
||||||
|
|
||||||
|
for k in kwargs:
|
||||||
|
if kwargs[k]:
|
||||||
|
params.append((unicode_type(k), kwargs[k]))
|
||||||
|
|
||||||
|
return add_params_to_uri(uri, params)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_token_request(grant_type, body='', include_client_id=True, **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 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((unicode_type('client_id'), client_id))
|
||||||
|
|
||||||
|
# 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((unicode_type('client_secret'), client_secret))
|
||||||
|
|
||||||
|
# this handles: `code`, `redirect_uri`, and other undocumented params
|
||||||
|
for k in kwargs:
|
||||||
|
if kwargs[k]:
|
||||||
|
params.append((unicode_type(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((unicode_type(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 precence, 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
|
|
@ -0,0 +1,643 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oauth2.rfc6749.request_validator
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestValidator(object):
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
: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
|
||||||
|
efficency, 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
|
||||||
|
persistance 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.')
|
|
@ -0,0 +1,361 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
from binascii import b2a_base64
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from oauthlib import common
|
||||||
|
from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Token(dict):
|
||||||
|
|
||||||
|
def __init__(self, params, old_scope=None):
|
||||||
|
super(OAuth2Token, self).__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 '{0}:{1}'.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, unicode_type):
|
||||||
|
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(object):
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,94 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.utils
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module contains utility methods used by various parts of the OAuth 2 spec.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oauthlib.common import unicode_type, urldecode
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib import quote
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import quote
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def list_to_scope(scope):
|
||||||
|
"""Convert a list of scopes to a space separated string."""
|
||||||
|
if isinstance(scope, unicode_type) or scope is None:
|
||||||
|
return scope
|
||||||
|
elif isinstance(scope, (set, tuple, list)):
|
||||||
|
return " ".join([unicode_type(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 [unicode_type(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, unicode_type):
|
||||||
|
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 unicode_type(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://')
|
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .connect.core.endpoints import Server
|
||||||
|
from .connect.core.endpoints import UserInfoEndpoint
|
||||||
|
from .connect.core.request_validator import RequestValidator
|
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.oopenid.core
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various logic needed
|
||||||
|
for consuming and providing OpenID Connect
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from .pre_configured import Server
|
||||||
|
from .userinfo import UserInfoEndpoint
|
|
@ -0,0 +1,109 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.endpoints.pre_configured
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of various endpoints needed
|
||||||
|
for providing OpenID Connect servers.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.oauth2.rfc6749.endpoints import (
|
||||||
|
AuthorizationEndpoint,
|
||||||
|
IntrospectEndpoint,
|
||||||
|
ResourceEndpoint,
|
||||||
|
RevocationEndpoint,
|
||||||
|
TokenEndpoint
|
||||||
|
)
|
||||||
|
from oauthlib.oauth2.rfc6749.grant_types import (
|
||||||
|
AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
|
||||||
|
ImplicitGrant as OAuth2ImplicitGrant,
|
||||||
|
ClientCredentialsGrant,
|
||||||
|
RefreshTokenGrant,
|
||||||
|
ResourceOwnerPasswordCredentialsGrant
|
||||||
|
)
|
||||||
|
from oauthlib.oauth2.rfc6749.tokens import BearerToken
|
||||||
|
from ..grant_types import (
|
||||||
|
AuthorizationCodeGrant,
|
||||||
|
ImplicitGrant,
|
||||||
|
HybridGrant,
|
||||||
|
)
|
||||||
|
from ..grant_types.dispatchers import (
|
||||||
|
AuthorizationCodeGrantDispatcher,
|
||||||
|
ImplicitTokenGrantDispatcher,
|
||||||
|
AuthorizationTokenGrantDispatcher
|
||||||
|
)
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
auth_grant = OAuth2AuthorizationCodeGrant(request_validator)
|
||||||
|
implicit_grant = OAuth2ImplicitGrant(request_validator)
|
||||||
|
password_grant = ResourceOwnerPasswordCredentialsGrant(
|
||||||
|
request_validator)
|
||||||
|
credentials_grant = ClientCredentialsGrant(request_validator)
|
||||||
|
refresh_grant = RefreshTokenGrant(request_validator)
|
||||||
|
openid_connect_auth = AuthorizationCodeGrant(request_validator)
|
||||||
|
openid_connect_implicit = ImplicitGrant(request_validator)
|
||||||
|
openid_connect_hybrid = HybridGrant(request_validator)
|
||||||
|
|
||||||
|
bearer = BearerToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
|
||||||
|
jwt = JWTToken(request_validator, token_generator,
|
||||||
|
token_expires_in, refresh_token_generator)
|
||||||
|
|
||||||
|
auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=auth_grant, oidc_grant=openid_connect_auth)
|
||||||
|
implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=implicit_grant, oidc_grant=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': auth_grant_choice,
|
||||||
|
'token': implicit_grant_choice,
|
||||||
|
'id_token': openid_connect_implicit,
|
||||||
|
'id_token token': openid_connect_implicit,
|
||||||
|
'code token': openid_connect_hybrid,
|
||||||
|
'code id_token': openid_connect_hybrid,
|
||||||
|
'code id_token token': openid_connect_hybrid,
|
||||||
|
'none': auth_grant
|
||||||
|
},
|
||||||
|
default_token_type=bearer)
|
||||||
|
|
||||||
|
token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=auth_grant, oidc_grant=openid_connect_auth)
|
||||||
|
|
||||||
|
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||||
|
grant_types={
|
||||||
|
'authorization_code': token_grant_choice,
|
||||||
|
'password': password_grant,
|
||||||
|
'client_credentials': credentials_grant,
|
||||||
|
'refresh_token': refresh_grant,
|
||||||
|
},
|
||||||
|
default_token_type=bearer)
|
||||||
|
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||||
|
token_types={'Bearer': bearer, 'JWT': jwt})
|
||||||
|
RevocationEndpoint.__init__(self, request_validator)
|
||||||
|
IntrospectEndpoint.__init__(self, request_validator)
|
||||||
|
UserInfoEndpoint.__init__(self, request_validator)
|
|
@ -0,0 +1,102 @@
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.endpoints.userinfo
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is an implementation of userinfo endpoint.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauthlib.common import Request
|
||||||
|
from oauthlib.common import unicode_type
|
||||||
|
from oauthlib.oauth2.rfc6749.endpoints.base import BaseEndpoint
|
||||||
|
from oauthlib.oauth2.rfc6749.endpoints.base import catch_errors_and_unavailability
|
||||||
|
from oauthlib.oauth2.rfc6749.tokens import BearerToken
|
||||||
|
from oauthlib.oauth2.rfc6749 import errors
|
||||||
|
|
||||||
|
|
||||||
|
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, unicode_type):
|
||||||
|
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:
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
WWW-Authenticate: Bearer error="invalid_token",
|
||||||
|
error_description="The Access Token expired"
|
||||||
|
"""
|
||||||
|
if not self.bearer.validate_request(request):
|
||||||
|
raise errors.InvalidTokenError()
|
||||||
|
if "openid" not in request.scopes:
|
||||||
|
raise errors.InsufficientScopeError()
|
|
@ -0,0 +1,152 @@
|
||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
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 __future__ import unicode_literals
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from .authorization_code import AuthorizationCodeGrant
|
||||||
|
from .implicit import ImplicitGrant
|
||||||
|
from .base import GrantTypeBase
|
||||||
|
from .hybrid import HybridGrant
|
||||||
|
from .exceptions import OIDCNoPrompt
|
||||||
|
from .dispatchers import (
|
||||||
|
AuthorizationCodeGrantDispatcher,
|
||||||
|
ImplicitTokenGrantDispatcher,
|
||||||
|
AuthorizationTokenGrantDispatcher
|
||||||
|
)
|
|
@ -0,0 +1,44 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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(AuthorizationCodeGrant, self).add_id_token(token, token_handler, request, nonce=nonce)
|
|
@ -0,0 +1,332 @@
|
||||||
|
from .exceptions import OIDCNoPrompt
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from json import loads
|
||||||
|
|
||||||
|
from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GrantTypeBase(object):
|
||||||
|
|
||||||
|
# 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 = set(('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)
|
||||||
|
"""
|
||||||
|
# If request.prompt is 'none' then no login/authorization form should
|
||||||
|
# be presented to the user. Instead, a silent login/authorization
|
||||||
|
# should be performed.
|
||||||
|
if request.prompt == 'none':
|
||||||
|
raise OIDCNoPrompt()
|
||||||
|
else:
|
||||||
|
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
|
|
@ -0,0 +1,91 @@
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Dispatcher(object):
|
||||||
|
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):
|
||||||
|
return self._handler_for_request(request).create_authorization_response(request, token_handler)
|
||||||
|
|
||||||
|
def validate_authorization_request(self, request):
|
||||||
|
return self._handler_for_request(request).validate_authorization_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
class ImplicitTokenGrantDispatcher(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 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):
|
||||||
|
return self._handler_for_request(request).create_authorization_response(request, token_handler)
|
||||||
|
|
||||||
|
def validate_authorization_request(self, request):
|
||||||
|
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 pressent fallback to `default_grant` wich 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):
|
||||||
|
handler = self._handler_for_request(request)
|
||||||
|
return handler.create_token_response(request, token_handler)
|
|
@ -0,0 +1,32 @@
|
||||||
|
class OIDCNoPrompt(Exception):
|
||||||
|
"""Exception used to inform users that no explicit authorization is needed.
|
||||||
|
|
||||||
|
Normally users authorize requests after validation of the request is done.
|
||||||
|
Then post-authorization validation is again made and a response containing
|
||||||
|
an auth code or token is created. However, when OIDC clients request
|
||||||
|
no prompting of user authorization the final response is created directly.
|
||||||
|
|
||||||
|
Example (without the shortcut for no prompt)
|
||||||
|
|
||||||
|
scopes, req_info = endpoint.validate_authorization_request(url, ...)
|
||||||
|
authorization_view = create_fancy_auth_form(scopes, req_info)
|
||||||
|
return authorization_view
|
||||||
|
|
||||||
|
Example (with the no prompt shortcut)
|
||||||
|
try:
|
||||||
|
scopes, req_info = endpoint.validate_authorization_request(url, ...)
|
||||||
|
authorization_view = create_fancy_auth_form(scopes, req_info)
|
||||||
|
return authorization_view
|
||||||
|
except OIDCNoPrompt:
|
||||||
|
# Note: Location will be set for you
|
||||||
|
headers, body, status = endpoint.create_authorization_response(url, ...)
|
||||||
|
redirect_view = create_redirect(headers, body, status)
|
||||||
|
return redirect_view
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
msg = ("OIDC request for no user interaction received. Do not ask user "
|
||||||
|
"for authorization, it should been done using silent "
|
||||||
|
"authentication through create_authorization_response. "
|
||||||
|
"See OIDCNoPrompt.__doc__ for more details.")
|
||||||
|
super(OIDCNoPrompt, self).__init__(msg)
|
|
@ -0,0 +1,61 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant
|
||||||
|
from oauthlib.oauth2.rfc6749.errors import InvalidRequestError
|
||||||
|
|
||||||
|
from .base import GrantTypeBase
|
||||||
|
from ..request_validator import RequestValidator
|
||||||
|
|
||||||
|
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 openid_authorization_validator(self, request):
|
||||||
|
"""Additional validation when following the Authorization Code flow.
|
||||||
|
"""
|
||||||
|
request_info = super(HybridGrant, self).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
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.grant_types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .base import GrantTypeBase
|
||||||
|
|
||||||
|
from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant
|
||||||
|
from oauthlib.oauth2.rfc6749.errors import InvalidRequestError
|
||||||
|
|
||||||
|
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(ImplicitGrant, self).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(ImplicitGrant, self).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
|
|
@ -0,0 +1,309 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
oauthlib.openid.connect.core.request_validator
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""
|
||||||
|
authlib.openid.connect.core.tokens
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module contains methods for adding JWT tokens to requests.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
from oauthlib.oauth2.rfc6749.tokens import TokenBase, 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 = None
|
||||||
|
if 'Authorization' in request.headers:
|
||||||
|
token = request.headers.get('Authorization')[7:]
|
||||||
|
else:
|
||||||
|
token = request.access_token
|
||||||
|
return self.request_validator.validate_jwt_bearer_token(
|
||||||
|
token, request.scopes, request)
|
||||||
|
|
||||||
|
def estimate_type(self, request):
|
||||||
|
token = request.headers.get('Authorization', '')[7:]
|
||||||
|
if token.startswith('ey') and token.count('.') in (2, 4):
|
||||||
|
return 10
|
||||||
|
else:
|
||||||
|
return 0
|
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
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(object):
|
||||||
|
def signal(self, name, doc=None):
|
||||||
|
return _FakeSignal(name, doc)
|
||||||
|
|
||||||
|
class _FakeSignal(object):
|
||||||
|
"""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')
|
|
@ -0,0 +1,216 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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(
|
||||||
|
)
|
||||||
|
|
||||||
|
# h16 = 1*4HEXDIG
|
||||||
|
h16 = r"(?: %(HEXDIG)s ){1,4}" % locals()
|
||||||
|
|
||||||
|
# ls32 = ( h16 ":" h16 ) / IPv4address
|
||||||
|
ls32 = r"(?: (?: %(h16)s : %(h16)s ) | %(IPv4address)s )" % locals()
|
||||||
|
|
||||||
|
# IPv6address = 6( h16 ":" ) ls32
|
||||||
|
# / "::" 5( h16 ":" ) ls32
|
||||||
|
# / [ h16 ] "::" 4( h16 ":" ) ls32
|
||||||
|
# / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
|
||||||
|
# / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
|
||||||
|
# / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
|
||||||
|
# / [ *4( h16 ":" ) h16 ] "::" ls32
|
||||||
|
# / [ *5( h16 ":" ) h16 ] "::" h16
|
||||||
|
# / [ *6( h16 ":" ) h16 ] "::"
|
||||||
|
IPv6address = r"""(?: (?: %(h16)s : ){6} %(ls32)s |
|
||||||
|
:: (?: %(h16)s : ){5} %(ls32)s |
|
||||||
|
%(h16)s :: (?: %(h16)s : ){4} %(ls32)s |
|
||||||
|
(?: %(h16)s : ) %(h16)s :: (?: %(h16)s : ){3} %(ls32)s |
|
||||||
|
(?: %(h16)s : ){2} %(h16)s :: (?: %(h16)s : ){2} %(ls32)s |
|
||||||
|
(?: %(h16)s : ){3} %(h16)s :: %(h16)s : %(ls32)s |
|
||||||
|
(?: %(h16)s : ){4} %(h16)s :: %(ls32)s |
|
||||||
|
(?: %(h16)s : ){5} %(h16)s :: %(h16)s |
|
||||||
|
(?: %(h16)s : ){6} %(h16)s ::
|
||||||
|
)
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
|
||||||
|
IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals()
|
||||||
|
|
||||||
|
# IP-literal = "[" ( IPv6address / IPvFuture ) "]"
|
||||||
|
IP_literal = r"\[ (?: %(IPv6address)s | %(IPvFuture)s ) \]" % locals()
|
||||||
|
|
||||||
|
# reg-name = *( unreserved / pct-encoded / sub-delims )
|
||||||
|
reg_name = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s )*" % locals()
|
||||||
|
|
||||||
|
# userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
|
||||||
|
userinfo = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : )" % locals(
|
||||||
|
)
|
||||||
|
|
||||||
|
# host = IP-literal / IPv4address / reg-name
|
||||||
|
host = r"(?: %(IP_literal)s | %(IPv4address)s | %(reg_name)s )" % locals()
|
||||||
|
|
||||||
|
# port = *DIGIT
|
||||||
|
port = r"(?: %(DIGIT)s )*" % locals()
|
||||||
|
|
||||||
|
# authority = [ userinfo "@" ] host [ ":" port ]
|
||||||
|
authority = r"(?: %(userinfo)s @)? %(host)s (?: : %(port)s)?" % locals()
|
||||||
|
|
||||||
|
# Path
|
||||||
|
|
||||||
|
# segment = *pchar
|
||||||
|
segment = r"%(pchar)s*" % locals()
|
||||||
|
|
||||||
|
# segment-nz = 1*pchar
|
||||||
|
segment_nz = r"%(pchar)s+" % locals()
|
||||||
|
|
||||||
|
# segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
|
||||||
|
# ; non-zero-length segment without any colon ":"
|
||||||
|
segment_nz_nc = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | @ )+" % locals()
|
||||||
|
|
||||||
|
# path-abempty = *( "/" segment )
|
||||||
|
path_abempty = r"(?: / %(segment)s )*" % locals()
|
||||||
|
|
||||||
|
# path-absolute = "/" [ segment-nz *( "/" segment ) ]
|
||||||
|
path_absolute = r"/ (?: %(segment_nz)s (?: / %(segment)s )* )?" % locals()
|
||||||
|
|
||||||
|
# path-noscheme = segment-nz-nc *( "/" segment )
|
||||||
|
path_noscheme = r"%(segment_nz_nc)s (?: / %(segment)s )*" % locals()
|
||||||
|
|
||||||
|
# path-rootless = segment-nz *( "/" segment )
|
||||||
|
path_rootless = r"%(segment_nz)s (?: / %(segment)s )*" % locals()
|
||||||
|
|
||||||
|
# path-empty = 0<pchar>
|
||||||
|
path_empty = r"" # FIXME
|
||||||
|
|
||||||
|
# path = path-abempty ; begins with "/" or is empty
|
||||||
|
# / path-absolute ; begins with "/" but not "//"
|
||||||
|
# / path-noscheme ; begins with a non-colon segment
|
||||||
|
# / path-rootless ; begins with a segment
|
||||||
|
# / path-empty ; zero characters
|
||||||
|
path = r"""(?: %(path_abempty)s |
|
||||||
|
%(path_absolute)s |
|
||||||
|
%(path_noscheme)s |
|
||||||
|
%(path_rootless)s |
|
||||||
|
%(path_empty)s
|
||||||
|
)
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
### Query and Fragment
|
||||||
|
|
||||||
|
# query = *( pchar / "/" / "?" )
|
||||||
|
query = r"(?: %(pchar)s | / | \? )*" % locals()
|
||||||
|
|
||||||
|
# fragment = *( pchar / "/" / "?" )
|
||||||
|
fragment = r"(?: %(pchar)s | / | \? )*" % locals()
|
||||||
|
|
||||||
|
# URIs
|
||||||
|
|
||||||
|
# hier-part = "//" authority path-abempty
|
||||||
|
# / path-absolute
|
||||||
|
# / path-rootless
|
||||||
|
# / path-empty
|
||||||
|
hier_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
|
||||||
|
%(path_absolute)s |
|
||||||
|
%(path_rootless)s |
|
||||||
|
%(path_empty)s
|
||||||
|
)
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
# relative-part = "//" authority path-abempty
|
||||||
|
# / path-absolute
|
||||||
|
# / path-noscheme
|
||||||
|
# / path-empty
|
||||||
|
relative_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
|
||||||
|
%(path_absolute)s |
|
||||||
|
%(path_noscheme)s |
|
||||||
|
%(path_empty)s
|
||||||
|
)
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
# relative-ref = relative-part [ "?" query ] [ "#" fragment ]
|
||||||
|
relative_ref = r"%(relative_part)s (?: \? %(query)s)? (?: \# %(fragment)s)?" % locals(
|
||||||
|
)
|
||||||
|
|
||||||
|
# URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
|
||||||
|
URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s )? )$" % locals(
|
||||||
|
)
|
||||||
|
|
||||||
|
# URI-reference = URI / relative-ref
|
||||||
|
URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
|
||||||
|
|
||||||
|
# absolute-URI = scheme ":" hier-part [ "?" query ]
|
||||||
|
absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals(
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_uri(uri):
|
||||||
|
return re.match(URI, uri, re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
def is_uri_reference(uri):
|
||||||
|
return re.match(URI_reference, uri, re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
def is_absolute_uri(uri):
|
||||||
|
return re.match(absolute_URI, uri, re.VERBOSE)
|
|
@ -0,0 +1,10 @@
|
||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
license_file = LICENSE
|
||||||
|
|
||||||
|
[egg_info]
|
||||||
|
tag_build =
|
||||||
|
tag_date = 0
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Hack because logging + setuptools sucks.
|
||||||
|
try:
|
||||||
|
import multiprocessing
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import sys
|
||||||
|
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']
|
||||||
|
signedtoken_require = ['cryptography', 'pyjwt>=1.0.0']
|
||||||
|
signals_require = ['blinker']
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='oauthlib',
|
||||||
|
version=oauthlib.__version__,
|
||||||
|
description='A generic, spec-compliant, thorough implementation of the OAuth request-signing logic',
|
||||||
|
long_description=fread('README.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='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||||
|
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 :: 2',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: Implementation',
|
||||||
|
'Programming Language :: Python :: Implementation :: CPython',
|
||||||
|
'Programming Language :: Python :: Implementation :: PyPy',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
import oauthlib
|
||||||
|
|
||||||
|
oauthlib.set_debug(True)
|
|
@ -0,0 +1,93 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from mock import ANY, MagicMock
|
||||||
|
|
||||||
|
from oauthlib.oauth1 import RequestValidator
|
||||||
|
from oauthlib.oauth1.rfc5849 import Client
|
||||||
|
from oauthlib.oauth1.rfc5849.endpoints import AccessTokenEndpoint
|
||||||
|
|
||||||
|
from ....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)
|
|
@ -0,0 +1,56 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from 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)
|
|
@ -0,0 +1,407 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from re import sub
|
||||||
|
|
||||||
|
from 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 ....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))
|
|
@ -0,0 +1,92 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from mock import ANY, MagicMock
|
||||||
|
|
||||||
|
from oauthlib.oauth1 import RequestValidator
|
||||||
|
from oauthlib.oauth1.rfc5849 import Client
|
||||||
|
from oauthlib.oauth1.rfc5849.endpoints import RequestTokenEndpoint
|
||||||
|
|
||||||
|
from ....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)
|
|
@ -0,0 +1,104 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from mock import ANY, MagicMock
|
||||||
|
|
||||||
|
from oauthlib.oauth1 import RequestValidator
|
||||||
|
from oauthlib.oauth1.rfc5849 import Client
|
||||||
|
from oauthlib.oauth1.rfc5849.endpoints import ResourceEndpoint
|
||||||
|
|
||||||
|
from ....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()))
|
|
@ -0,0 +1,52 @@
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from mock import ANY, MagicMock
|
||||||
|
|
||||||
|
from oauthlib.oauth1 import RequestValidator
|
||||||
|
from oauthlib.oauth1.rfc5849 import Client
|
||||||
|
from oauthlib.oauth1.rfc5849.endpoints import SignatureOnlyEndpoint
|
||||||
|
|
||||||
|
from ....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)
|
|
@ -0,0 +1,270 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import Request
|
||||||
|
from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_HMAC_SHA1,
|
||||||
|
SIGNATURE_HMAC_SHA256, SIGNATURE_RSA,
|
||||||
|
SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY)
|
||||||
|
from oauthlib.oauth1.rfc5849 import Client
|
||||||
|
|
||||||
|
from ...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.assertTrue('PIZZA' in 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'])
|
|
@ -0,0 +1,92 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import urlencode
|
||||||
|
from oauthlib.oauth1.rfc5849.parameters import (_append_params,
|
||||||
|
prepare_form_encoded_body,
|
||||||
|
prepare_headers,
|
||||||
|
prepare_request_uri_query)
|
||||||
|
|
||||||
|
from ...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)
|
|
@ -0,0 +1,70 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.oauth1 import RequestValidator
|
||||||
|
|
||||||
|
from ...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']))
|
|
@ -0,0 +1,373 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import unicode_type
|
||||||
|
from oauthlib.oauth1.rfc5849.signature import (collect_parameters,
|
||||||
|
signature_base_string,
|
||||||
|
base_string_uri,
|
||||||
|
normalize_parameters,
|
||||||
|
sign_hmac_sha1,
|
||||||
|
sign_hmac_sha1_with_client,
|
||||||
|
sign_plaintext,
|
||||||
|
sign_plaintext_with_client,
|
||||||
|
sign_rsa_sha1,
|
||||||
|
sign_rsa_sha1_with_client)
|
||||||
|
|
||||||
|
from ...unittest import TestCase
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib import quote
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureTests(TestCase):
|
||||||
|
class MockClient(dict):
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return self[name]
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
self[name] = value
|
||||||
|
|
||||||
|
def decode(self):
|
||||||
|
for k, v in self.items():
|
||||||
|
self[k] = v.decode('utf-8')
|
||||||
|
|
||||||
|
uri_query = "b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2=&a3=2+q"
|
||||||
|
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()
|
||||||
|
body = "content=This+is+being+the+body+of+things"
|
||||||
|
http_method = b"post"
|
||||||
|
base_string_url = quote("http://example.com/request?b5=%3D%253D"
|
||||||
|
"&a3=a&c%40=&a2=r%20b").encode('utf-8')
|
||||||
|
normalized_encoded_request_parameters = quote(
|
||||||
|
'OAuth realm="Example",'
|
||||||
|
'oauth_consumer_key="9djdj82h48djs9d2",'
|
||||||
|
'oauth_token="kkk9d7dh3k39sjv7",'
|
||||||
|
'oauth_signature_method="HMAC-SHA1",'
|
||||||
|
'oauth_timestamp="137131201",'
|
||||||
|
'oauth_nonce="7d8f3e4a",'
|
||||||
|
'oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"'
|
||||||
|
).encode('utf-8')
|
||||||
|
client_secret = b"ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ"
|
||||||
|
resource_owner_secret = b"just-a-string asdasd"
|
||||||
|
control_base_string = (
|
||||||
|
"POST&http%253A%2F%2Fexample.com%2Frequest%253F"
|
||||||
|
"b5%253D%25253D%2525253D%2526"
|
||||||
|
"a3%253D"
|
||||||
|
"a%2526"
|
||||||
|
"c%252540%253D%2526"
|
||||||
|
"a2%253D"
|
||||||
|
"r%252520b&"
|
||||||
|
"OAuth%2520realm%253D%2522Example%2522%252C"
|
||||||
|
"oauth_consumer_key%253D%25229djdj82h48djs9d2%2522%252C"
|
||||||
|
"oauth_token%253D%2522kkk9d7dh3k39sjv7%2522%252C"
|
||||||
|
"oauth_signature_method%253D%2522HMAC-SHA1%2522%252C"
|
||||||
|
"oauth_timestamp%253D%2522137131201%2522%252C"
|
||||||
|
"oauth_nonce%253D%25227d8f3e4a%2522%252C"
|
||||||
|
"oauth_signature%253D%2522bYT5CMsGcbgUdFHObYMEfcx6bsw%25253D%2522")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = self.MockClient(
|
||||||
|
client_secret = self.client_secret,
|
||||||
|
resource_owner_secret = self.resource_owner_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_signature_base_string(self):
|
||||||
|
"""
|
||||||
|
Example text to be turned into a base string::
|
||||||
|
|
||||||
|
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: OAuth realm="Example",
|
||||||
|
oauth_consumer_key="9djdj82h48djs9d2",
|
||||||
|
oauth_token="kkk9d7dh3k39sjv7",
|
||||||
|
oauth_signature_method="HMAC-SHA1",
|
||||||
|
oauth_timestamp="137131201",
|
||||||
|
oauth_nonce="7d8f3e4a",
|
||||||
|
oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
|
||||||
|
|
||||||
|
Sample Base string generated and tested against::
|
||||||
|
|
||||||
|
POST&http%253A%2F%2Fexample.com%2Frequest%253Fb5%253D%25253D%252525
|
||||||
|
3D%2526a3%253Da%2526c%252540%253D%2526a2%253Dr%252520b&OAuth%2520re
|
||||||
|
alm%253D%2522Example%2522%252Coauth_consumer_key%253D%25229djdj82h4
|
||||||
|
8djs9d2%2522%252Coauth_token%253D%2522kkk9d7dh3k39sjv7%2522%252Coau
|
||||||
|
th_signature_method%253D%2522HMAC-SHA1%2522%252Coauth_timestamp%253
|
||||||
|
D%2522137131201%2522%252Coauth_nonce%253D%25227d8f3e4a%2522%252Coau
|
||||||
|
th_signature%253D%2522bYT5CMsGcbgUdFHObYMEfcx6bsw%25253D%2522
|
||||||
|
"""
|
||||||
|
self.assertRaises(ValueError, signature_base_string,
|
||||||
|
self.http_method,
|
||||||
|
self.base_string_url,
|
||||||
|
self.normalized_encoded_request_parameters)
|
||||||
|
self.assertRaises(ValueError, signature_base_string,
|
||||||
|
self.http_method.decode('utf-8'),
|
||||||
|
self.base_string_url,
|
||||||
|
self.normalized_encoded_request_parameters)
|
||||||
|
self.assertRaises(ValueError, signature_base_string,
|
||||||
|
self.http_method.decode('utf-8'),
|
||||||
|
self.base_string_url.decode('utf-8'),
|
||||||
|
self.normalized_encoded_request_parameters)
|
||||||
|
|
||||||
|
base_string = signature_base_string(
|
||||||
|
self.http_method.decode('utf-8'),
|
||||||
|
self.base_string_url.decode('utf-8'),
|
||||||
|
self.normalized_encoded_request_parameters.decode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.control_base_string, base_string)
|
||||||
|
|
||||||
|
def test_base_string_uri(self):
|
||||||
|
"""
|
||||||
|
Example text to be turned into a normalized base string uri::
|
||||||
|
|
||||||
|
GET /?q=1 HTTP/1.1
|
||||||
|
Host: www.example.net:8080
|
||||||
|
|
||||||
|
Sample string generated::
|
||||||
|
|
||||||
|
https://www.example.net:8080/
|
||||||
|
"""
|
||||||
|
|
||||||
|
# test first example from RFC 5849 section 3.4.1.2.
|
||||||
|
# Note: there is a space between "r" and "v"
|
||||||
|
uri = 'http://EXAMPLE.COM:80/r v/X?id=123'
|
||||||
|
self.assertEqual(base_string_uri(uri),
|
||||||
|
'http://example.com/r%20v/X')
|
||||||
|
|
||||||
|
# test second example from RFC 5849 section 3.4.1.2.
|
||||||
|
uri = 'https://www.example.net:8080/?q=1'
|
||||||
|
self.assertEqual(base_string_uri(uri),
|
||||||
|
'https://www.example.net:8080/')
|
||||||
|
|
||||||
|
# test for unicode failure
|
||||||
|
uri = b"www.example.com:8080"
|
||||||
|
self.assertRaises(ValueError, base_string_uri, uri)
|
||||||
|
|
||||||
|
# test for missing scheme
|
||||||
|
uri = "www.example.com:8080"
|
||||||
|
self.assertRaises(ValueError, base_string_uri, uri)
|
||||||
|
|
||||||
|
# test a URI with the default port
|
||||||
|
uri = "http://www.example.com:80/"
|
||||||
|
self.assertEqual(base_string_uri(uri),
|
||||||
|
"http://www.example.com/")
|
||||||
|
|
||||||
|
# test a URI missing a path
|
||||||
|
uri = "http://www.example.com"
|
||||||
|
self.assertEqual(base_string_uri(uri),
|
||||||
|
"http://www.example.com/")
|
||||||
|
|
||||||
|
# test a relative URI
|
||||||
|
uri = "/a-host-relative-uri"
|
||||||
|
host = "www.example.com"
|
||||||
|
self.assertRaises(ValueError, base_string_uri, (uri, host))
|
||||||
|
|
||||||
|
# test overriding the URI's netloc with a host argument
|
||||||
|
uri = "http://www.example.com/a-path"
|
||||||
|
host = "alternatehost.example.com"
|
||||||
|
self.assertEqual(base_string_uri(uri, host),
|
||||||
|
"http://alternatehost.example.com/a-path")
|
||||||
|
|
||||||
|
def test_collect_parameters(self):
|
||||||
|
"""We check against parameters multiple times in case things change
|
||||||
|
after more parameters are added.
|
||||||
|
"""
|
||||||
|
self.assertEqual(collect_parameters(), [])
|
||||||
|
|
||||||
|
# Check against uri_query
|
||||||
|
parameters = collect_parameters(uri_query=self.uri_query)
|
||||||
|
correct_parameters = [('b5', '=%3D'),
|
||||||
|
('a3', 'a'),
|
||||||
|
('c@', ''),
|
||||||
|
('a2', 'r b'),
|
||||||
|
('c2', ''),
|
||||||
|
('a3', '2 q')]
|
||||||
|
self.assertEqual(sorted(parameters), sorted(correct_parameters))
|
||||||
|
|
||||||
|
headers = {'Authorization': self.authorization_header}
|
||||||
|
# check against authorization header as well
|
||||||
|
parameters = collect_parameters(
|
||||||
|
uri_query=self.uri_query, headers=headers)
|
||||||
|
parameters_with_realm = collect_parameters(
|
||||||
|
uri_query=self.uri_query, headers=headers, with_realm=True)
|
||||||
|
# Redo the checks against all the parameters. Duplicated code but
|
||||||
|
# better safety
|
||||||
|
correct_parameters += [
|
||||||
|
('oauth_nonce', '7d8f3e4a'),
|
||||||
|
('oauth_timestamp', '137131201'),
|
||||||
|
('oauth_consumer_key', '9djdj82h48djs9d2'),
|
||||||
|
('oauth_signature_method', 'HMAC-SHA1'),
|
||||||
|
('oauth_token', 'kkk9d7dh3k39sjv7')]
|
||||||
|
correct_parameters_with_realm = (
|
||||||
|
correct_parameters + [('realm', 'Example')])
|
||||||
|
self.assertEqual(sorted(parameters), sorted(correct_parameters))
|
||||||
|
self.assertEqual(sorted(parameters_with_realm),
|
||||||
|
sorted(correct_parameters_with_realm))
|
||||||
|
|
||||||
|
# Add in the body.
|
||||||
|
# TODO: Add more content for the body. Daniel Greenfeld 2012/03/12
|
||||||
|
# Redo again the checks against all the parameters. Duplicated code
|
||||||
|
# but better safety
|
||||||
|
parameters = collect_parameters(
|
||||||
|
uri_query=self.uri_query, body=self.body, headers=headers)
|
||||||
|
correct_parameters += [
|
||||||
|
('content', 'This is being the body of things')]
|
||||||
|
self.assertEqual(sorted(parameters), sorted(correct_parameters))
|
||||||
|
|
||||||
|
def test_normalize_parameters(self):
|
||||||
|
""" We copy some of the variables from the test method above."""
|
||||||
|
|
||||||
|
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, unicode_type)
|
||||||
|
|
||||||
|
# Lets see if things are in order
|
||||||
|
# check to see that querystring keys come in alphanumeric order:
|
||||||
|
querystring_keys = ['a2', 'a3', 'b5', 'content', '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)
|
||||||
|
|
||||||
|
# Control signature created using openssl:
|
||||||
|
# echo -n $(cat <message>) | openssl dgst -binary -hmac <key> | base64
|
||||||
|
control_signature = "Uau4O9Kpd2k6rvh7UZN/RN+RG7Y="
|
||||||
|
|
||||||
|
def test_sign_hmac_sha1(self):
|
||||||
|
"""Verifying HMAC-SHA1 signature against one created by OpenSSL."""
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, sign_hmac_sha1, self.control_base_string,
|
||||||
|
self.client_secret, self.resource_owner_secret)
|
||||||
|
|
||||||
|
sign = sign_hmac_sha1(self.control_base_string,
|
||||||
|
self.client_secret.decode('utf-8'),
|
||||||
|
self.resource_owner_secret.decode('utf-8'))
|
||||||
|
self.assertEqual(len(sign), 28)
|
||||||
|
self.assertEqual(sign, self.control_signature)
|
||||||
|
|
||||||
|
def test_sign_hmac_sha1_with_client(self):
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
sign_hmac_sha1_with_client,
|
||||||
|
self.control_base_string,
|
||||||
|
self.client)
|
||||||
|
|
||||||
|
self.client.decode()
|
||||||
|
sign = sign_hmac_sha1_with_client(
|
||||||
|
self.control_base_string, self.client)
|
||||||
|
|
||||||
|
self.assertEqual(len(sign), 28)
|
||||||
|
self.assertEqual(sign, self.control_signature)
|
||||||
|
|
||||||
|
|
||||||
|
control_base_string_rsa_sha1 = (
|
||||||
|
b"POST&http%253A%2F%2Fexample.com%2Frequest%253Fb5%253D"
|
||||||
|
b"%25253D%2525253D%2526a3%253Da%2526c%252540%253D%2526"
|
||||||
|
b"a2%253Dr%252520b&OAuth%2520realm%253D%2522Example%25"
|
||||||
|
b"22%252Coauth_consumer_key%253D%25229djdj82h48djs9d2"
|
||||||
|
b"%2522%252Coauth_token%253D%2522kkk9d7dh3k39sjv7%2522"
|
||||||
|
b"%252Coauth_signature_method%253D%2522HMAC-SHA1%2522"
|
||||||
|
b"%252Coauth_timestamp%253D%2522137131201%2522%252Coau"
|
||||||
|
b"th_nonce%253D%25227d8f3e4a%2522%252Coauth_signature"
|
||||||
|
b"%253D%2522bYT5CMsGcbgUdFHObYMEfcx6bsw%25253D%2522")
|
||||||
|
|
||||||
|
# Generated using: $ openssl genrsa -out <key>.pem 1024
|
||||||
|
# PEM encoding requires the key to be concatenated with
|
||||||
|
# linebreaks.
|
||||||
|
rsa_private_key = b"""-----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-----
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def control_signature_rsa_sha1(self):
|
||||||
|
# Base string saved in "<message>". Signature obtained using:
|
||||||
|
# $ echo -n $(cat <message>) | openssl dgst -sign <key>.pem | base64
|
||||||
|
# where echo -n suppresses the last linebreak.
|
||||||
|
return (
|
||||||
|
"zV5g8ArdMuJuOXlH8XOqfLHS11XdthfIn4HReDm7jz8JmgLabHGmVBqCkCfZoFJPH"
|
||||||
|
"dka7tLvCplK/jsV4FUOnftrJOQhbXguuBdi87/hmxOFKLmQYqqlEW7BdXmwKLZcki"
|
||||||
|
"qq3qE5XziBgKSAFRkxJ4gmJAymvJBtrJYN9728rK8="
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_rsa_sha1(self):
|
||||||
|
"""Verify RSA-SHA1 signature against one created by OpenSSL."""
|
||||||
|
base_string = self.control_base_string_rsa_sha1
|
||||||
|
|
||||||
|
private_key = self.rsa_private_key
|
||||||
|
|
||||||
|
control_signature = self.control_signature_rsa_sha1
|
||||||
|
|
||||||
|
sign = sign_rsa_sha1(base_string, private_key)
|
||||||
|
self.assertEqual(sign, control_signature)
|
||||||
|
sign = sign_rsa_sha1(base_string.decode('utf-8'), private_key)
|
||||||
|
self.assertEqual(sign, control_signature)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_rsa_sha1_with_client(self):
|
||||||
|
base_string = self.control_base_string_rsa_sha1
|
||||||
|
|
||||||
|
self.client.rsa_key = self.rsa_private_key
|
||||||
|
|
||||||
|
control_signature = self.control_signature_rsa_sha1
|
||||||
|
|
||||||
|
sign = sign_rsa_sha1_with_client(base_string, self.client)
|
||||||
|
|
||||||
|
self.assertEqual(sign, control_signature)
|
||||||
|
|
||||||
|
self.client.decode() ## Decode `rsa_private_key` from UTF-8
|
||||||
|
|
||||||
|
sign = sign_rsa_sha1_with_client(base_string, self.client)
|
||||||
|
|
||||||
|
self.assertEqual(sign, control_signature)
|
||||||
|
|
||||||
|
|
||||||
|
control_signature_plaintext = (
|
||||||
|
"ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ&"
|
||||||
|
"just-a-string%20%20%20%20asdasd")
|
||||||
|
|
||||||
|
def test_sign_plaintext(self):
|
||||||
|
""" """
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, sign_plaintext, self.client_secret,
|
||||||
|
self.resource_owner_secret)
|
||||||
|
sign = sign_plaintext(self.client_secret.decode('utf-8'),
|
||||||
|
self.resource_owner_secret.decode('utf-8'))
|
||||||
|
self.assertEqual(sign, self.control_signature_plaintext)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_plaintext_with_client(self):
|
||||||
|
self.assertRaises(ValueError, sign_plaintext_with_client,
|
||||||
|
None, self.client)
|
||||||
|
|
||||||
|
self.client.decode()
|
||||||
|
|
||||||
|
sign = sign_plaintext_with_client(None, self.client)
|
||||||
|
|
||||||
|
self.assertEqual(sign, self.control_signature_plaintext)
|
|
@ -0,0 +1,141 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from oauthlib.common import unicode_type
|
||||||
|
from oauthlib.oauth1.rfc5849.utils import *
|
||||||
|
|
||||||
|
from ...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."), unicode_type)
|
||||||
|
|
||||||
|
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."), unicode_type)
|
||||||
|
|
||||||
|
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, unicode_type)
|
||||||
|
self.assertIsInstance(v, unicode_type)
|
||||||
|
|
||||||
|
# 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)
|
|
@ -0,0 +1,89 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from oauthlib import signals
|
||||||
|
from oauthlib.oauth2 import BackendApplicationClient
|
||||||
|
|
||||||
|
from ....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']
|
|
@ -0,0 +1,305 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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 ....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=%s&refresh_token=%s' % (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=%s&refresh_token=%s' % (scope, token))
|
|
@ -0,0 +1,148 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from oauthlib import signals
|
||||||
|
from oauthlib.oauth2 import LegacyApplicationClient
|
||||||
|
|
||||||
|
from ....unittest import TestCase
|
||||||
|
|
||||||
|
# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
|
||||||
|
@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=%s&password=%s" % (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=%s&password=%s' % (self.username, self.password, ),
|
||||||
|
'grant_type=password&password=%s&username=%s' % (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)
|
|
@ -0,0 +1,114 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from oauthlib import signals
|
||||||
|
from oauthlib.oauth2 import MobileApplicationClient
|
||||||
|
|
||||||
|
from ....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']
|
|
@ -0,0 +1,187 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from oauthlib.common import Request
|
||||||
|
from oauthlib.oauth2 import ServiceApplicationClient
|
||||||
|
|
||||||
|
from ....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']
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue