diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml new file mode 100644 index 000000000..3b3be00e6 --- /dev/null +++ b/.github/workflows/lint_python.yml @@ -0,0 +1,25 @@ +name: lint_python +on: [pull_request, push] +jobs: + lint_python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install --upgrade pip wheel + - run: pip install bandit black codespell flake8 flake8-2020 flake8-bugbear + flake8-comprehensions isort mypy pytest pyupgrade safety + - run: bandit --recursive --skip B101 . || true # B101 is assert statements + - run: black --check . || true + - run: codespell || true # --ignore-words-list="" --skip="*.css,*.js,*.lock" + - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 + --show-source --statistics + - run: isort --check-only --profile black . || true + - run: pip install -r requirements.txt || pip install --editable . || true + - run: mkdir --parents --verbose .mypy_cache + - run: mypy --ignore-missing-imports --install-types --non-interactive . || true + - run: pytest . || true + - run: pytest --doctest-modules . || true + - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true + - run: safety check diff --git a/.travis.yml b/.travis.yml index b2dad7a0c..6b5e6e304 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,8 @@ jobs: allow_failures: - python: "3.11-dev" before_install: - - python -m pip install --upgrade pip setuptools + - sudo apt-get install graphviz + - python -m pip install --upgrade pip "setuptools<60.9" - python -m pip install tox coveralls - if [ "$TOXENV" == "pypy3" ]; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source $HOME/.cargo/env ; fi script: tox diff --git a/AUTHORS b/AUTHORS index c820d6d91..0c622451a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -49,3 +49,10 @@ Scott Gifford Hugo van Kemenade Richard Connon Karim Kanso +Kian-Meng Ang +Tim Gates +Dariusz Smigiel +Nemanja Tozic +Kohki Yamagiwa +Arie Bovenberg +Sebastian Chnelik diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d7882e965..9e150100d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,19 @@ Changelog ========= +3.2.1 (2022-09-09) +------------------ +OAuth2.0 Provider: +* #803: Metadata endpoint support of non-HTTPS +* CVE-2022-36087 + +OAuth1.0: +* #818: Allow IPv6 being parsed by signature + +General: +* Improved and fixed documentation warnings. +* Cosmetic changes based on isort + 3.2.0 (2022-01-29) ------------------ OAuth2.0 Client: @@ -146,7 +159,7 @@ OAuth1.0 Client: General fixes: * $ and ' are allowed to be unencoded in query strings #564 -* Request attributes are no longer overriden by HTTP Headers #409 +* Request attributes are no longer overridden by HTTP Headers #409 * Removed unnecessary code for handling python2.6 * Add support of python3.7 #621 * Several minors updates to setup.py and tox @@ -204,7 +217,7 @@ General fixes: * 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). + However, in that case, the public client should still be checked that is in fact a public client (authenticate_client_id). * Improved prompt parameter validation. * Added two error codes from RFC 6750. * Hybrid response types are now be fragment-encoded. @@ -354,7 +367,7 @@ Quick fix. OAuth 1 client repr in 0.6.2 overwrote secrets when scrubbing for pri 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. + is trying to acquire a new set of scopes that are a subset of the original scope. * (OAuth 2 Provider) expires_in token lifetime can be set per request. diff --git a/README.rst b/README.rst index f2003fd7c..eb8c452d0 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ OAuthLib - Python Framework for OAuth1 & OAuth2 *A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for Python 3.6+.* -.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master - :target: https://travis-ci.org/oauthlib/oauthlib +.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master + :target: https://app.travis-ci.com/oauthlib/oauthlib :alt: Travis .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/oauthlib/oauthlib @@ -103,7 +103,7 @@ 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 =) +have the pleasure to run into each other, please send a docs pull request =) .. _`Gitter community`: https://gitter.im/oauthlib/Lobby diff --git a/docs/Makefile b/docs/Makefile index d134c96fb..c2ee5d5d3 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = -v +SPHINXOPTS = -v -W SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build diff --git a/docs/conf.py b/docs/conf.py index 91b5de464..f4b92c477 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -127,7 +127,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/docs/contributing.rst b/docs/contributing.rst index eed38665b..19ff9c9cf 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -272,8 +272,12 @@ the autogenerated documentation read more cleanly: Consolidated example +.. code-block:: python + def foo(self, request, client, bar=None, key=None): """ + This method defines framework for `MAC Access Authentication`_ RFC. + This method checks the `key` against the `client`. The `request` is passed to maintain context. @@ -283,15 +287,18 @@ Consolidated example nonce="1336363200:dj83hs9s", mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM=" - .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 - :param request: OAuthlib request. :type request: oauthlib.common.Request - :param client: Client object set by you, see ``.authenticate_client``. - :param bar: - :param key: MAC given provided by token endpoint. + :param client: User's defined Client object, see ``.authenticate_client``. + :param bar: Another example. + :param key: Another param. + :return: Explanation of return value and type + + .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 """ + + How pull requests are checked, tested, and done =============================================== diff --git a/docs/faq.rst b/docs/faq.rst index 4814dcda1..e47e3e0e7 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -48,7 +48,7 @@ What does ValueError `Error trying to decode a non urlencoded string` mean? include non percent encoded characters such as `£`. Which could be because it has already been decoded by your web framework. - If you believe it contains characters that should be excempt from this + If you believe it contains characters that should be exempt from this check please open an issue and state why. diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index f9309f97d..4f7bec549 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -30,7 +30,7 @@ the request. OAuth 2.0 ......... -OAuth 2.0 client and provider support for: +OAuth 2.0 full client and provider supports for: - `RFC 6749 section-4.1`_: Authorization Code Grant - `RFC 6749 section-4.2`_: Implicit Grant @@ -40,23 +40,28 @@ OAuth 2.0 client and provider support for: - `RFC 6750`_: Bearer Tokens - `RFC 7009`_: Token Revocation - `RFC 7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE) -- `RFC 8628`_: OAuth2.0 Device Authorization Grant - `RFC Draft`_ Message Authentication Code (MAC) Tokens -Partial implementations (any help/PR are welcomed to complete the list): +Only OAuth2.0 Provider has been implemented: + +- `OpenID Connect Core`_ +- `RFC 7662`_: Token Introspection +- `RFC 8414`_: Authorization Server Metadata + +Only OAuth2.0 Client has been implemented: + +- `RFC 8628`_: Device Authorization Grant + +Missing features: -- OAuth2.0 Provider: `OpenID Connect Core`_ -- OAuth2.0 Provider: `RFC 7662`_: Token Introspection -- OAuth2.0 Provider: `RFC 8414`_: Authorization Server Metadata -- OAuth2.0 **Client**: `OpenID Connect Core`_ -- OAuth2.0 **Client**: `RFC 7662`_: Token Introspection -- OAuth2.0 **Client**: `RFC 8414`_: Authorization Server Metadata - SAML2 - Bearer JWT as Client Authentication - Dynamic client registration - OpenID Discovery - OpenID Session Management -- ...and more + +Any help are welcomed and will be carefully reviewed and integrated to the project. Don't hesitate to be part of the community ! + Platforms ......... diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst index df1e2a0e1..d8b7d6b5a 100644 --- a/docs/oauth1/security.rst +++ b/docs/oauth1/security.rst @@ -5,7 +5,7 @@ A few important facts regarding OAuth security SSL for all interactions both with your API as well as for setting up tokens. An example of when it's especially bad is when sending POST requests with form data, this data is not accounted for in the OAuth - signature and a successfull man-in-the-middle attacker could swap your + signature and a successful man-in-the-middle attacker could swap your form data (or files) to whatever he pleases without invalidating the signature. This is an even bigger issue if you fail to check nonce/timestamp pairs for each request, allowing an attacker who @@ -20,7 +20,7 @@ A few important facts regarding OAuth security for Python 3.6 and later. The ``secrets`` module is designed for generating cryptographically strong random numbers. For earlier versions of Python, use ``random.SystemRandom`` which is based on ``os.urandom`` - rather than the default ``random`` based on the effecient but not truly + rather than the default ``random`` based on the efficient but not truly random Mersenne Twister. Predictable tokens allow attackers to bypass virtually all defences OAuth provides. diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index 2f30c65b7..2c01ab71b 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -59,7 +59,7 @@ The client interested in accessing protected resources. **Client secret**: Required for HMAC-SHA1 and PLAINTEXT. The secret the client will use when - verifying requests during the OAuth workflow. Has to be accesible as + verifying requests during the OAuth workflow. Has to be accessible as plaintext (i.e. not hashed) since it is used to recreate and validate request signatured:: @@ -175,7 +175,7 @@ you should consider expiring them as it increases security dramatically. The user and realms will need to be transferred from the request token to the access token. It is possible that the list of authorized realms is smaller than the list of requested realms. Clients can observe whether this is the case -by comparing the `oauth_realms` parameter given in the token reponse. This way +by comparing the `oauth_realms` parameter given in the token response. This way of indicating change of realms is backported from OAuth2 scope behaviour and is not in the OAuth 1 spec. diff --git a/docs/oauth2/clients/client.rst b/docs/oauth2/clients/client.rst index 9a5a4ffe7..4f4176a7a 100644 --- a/docs/oauth2/clients/client.rst +++ b/docs/oauth2/clients/client.rst @@ -14,6 +14,7 @@ to use them please browse the documentation for each client type below. mobileapplicationclient legacyapplicationclient backendapplicationclient + deviceclient **Existing libraries** If you are using the `requests`_ HTTP library you may be interested in using diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 0dd2da0f0..f05c44b65 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -3,7 +3,7 @@ Provider Endpoints Endpoints in OAuth 2 are targets with a specific responsibility and often associated with a particular URL. Because of this the word endpoint might be -used interchangably from the endpoint url. +used interchangeably from the endpoint url. The main three responsibilities in an OAuth 2 flow is to authorize access to a certain users resources to a client, to supply said client with a token diff --git a/docs/oauth2/endpoints/resource.rst b/docs/oauth2/endpoints/resource.rst index a5ff88573..6bd4d6b8a 100644 --- a/docs/oauth2/endpoints/resource.rst +++ b/docs/oauth2/endpoints/resource.rst @@ -5,7 +5,7 @@ Resource authorization Resource endpoints verify that the token presented is valid and granted access to the scopes associated with the resource in question. -**Request Verfication** +**Request Verification** Each view may set certain scopes under which it is bound. Only requests that present an access token bound to the correct scopes may access the view. Access tokens are commonly embedded in the authorization header but diff --git a/docs/oauth2/grants/custom_grant.rst b/docs/oauth2/grants/custom_grant.rst index 8c4571cbe..c639420b7 100644 --- a/docs/oauth2/grants/custom_grant.rst +++ b/docs/oauth2/grants/custom_grant.rst @@ -37,7 +37,7 @@ existing ones. 3. Associate it with Endpoints ------------------------------ -Then, once implemented, you have to instanciate the grant object and +Then, once implemented, you have to instantiate the grant object and bind it to your endpoint. Either :py:class:`AuthorizationEndpoint`, :py:class:`TokenEndpoint` or both. @@ -48,7 +48,7 @@ This example shows how to add a simple extension to the `Token endpoint`: * creation of a new class ``MyCustomGrant``, and implement ``create_token_response``. * do basics and custom request validations, then call a custom method of `Request Validator` to extend the interface for the implementor. -* instanciate the new grant, and bind it with an existing ``Server``. +* instantiate the new grant, and bind it with an existing ``Server``. .. code-block:: python diff --git a/docs/oauth2/grants/custom_validators.rst b/docs/oauth2/grants/custom_validators.rst index 9917dd713..f295e536a 100644 --- a/docs/oauth2/grants/custom_validators.rst +++ b/docs/oauth2/grants/custom_validators.rst @@ -3,7 +3,7 @@ Custom Validators The Custom validators are useful when you want to change a particular behavior of an existing grant. That is often needed because of the -diversity of the identity softwares and to let the oauthlib framework to be +diversity of the identity software and to let the oauthlib framework to be flexible as possible. However, if you are looking into writing a custom grant type, please diff --git a/docs/oauth2/oidc/grants.rst b/docs/oauth2/oidc/grants.rst index aa1f70f93..4cbcf42ae 100644 --- a/docs/oauth2/oidc/grants.rst +++ b/docs/oauth2/oidc/grants.rst @@ -39,3 +39,4 @@ checks the presence of `openid` scope in the parameters. authcode implicit hybrid + refresh_token diff --git a/docs/oauth2/oidc/id_tokens.rst b/docs/oauth2/oidc/id_tokens.rst index a1bf7cf35..753c5a154 100644 --- a/docs/oauth2/oidc/id_tokens.rst +++ b/docs/oauth2/oidc/id_tokens.rst @@ -11,7 +11,8 @@ See examples below. .. _`ID Tokens`: http://openid.net/specs/openid-connect-core-1_0.html#IDToken -.. autoclass:: oauthlib.oauth2.RequestValidator +.. autoclass:: oauthlib.openid.RequestValidator + :noindex: :members: finalize_id_token diff --git a/docs/oauth2/oidc/refresh_token.rst b/docs/oauth2/oidc/refresh_token.rst index 01d2d7f17..9b5987e70 100644 --- a/docs/oauth2/oidc/refresh_token.rst +++ b/docs/oauth2/oidc/refresh_token.rst @@ -1,4 +1,4 @@ -OpenID Authorization Code +OpenID Refresh Grant ------------------------- .. autoclass:: oauthlib.openid.connect.core.grant_types.RefreshTokenGrant diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst index a04e12e13..51bb1abb2 100644 --- a/docs/oauth2/oidc/validator.rst +++ b/docs/oauth2/oidc/validator.rst @@ -1,17 +1,17 @@ Creating a Provider -============================================= +=================== .. contents:: :depth: 2 1. Create an OIDC provider ------------------------ +-------------------------- If you don't have an OAuth2.0 Provider, you can follow the instructions at :doc:`OAuth2.0 Creating a Provider `. Then, follow the migration step below. 2. Migrate your OAuth2.0 provider into an OIDC provider ----------------------------------------------------- +------------------------------------------------------- If you have a OAuth2.0 provider running and want to upgrade to OIDC, you can upgrade it by replacing one line of code: diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 15420f3f2..922189bf9 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -447,7 +447,7 @@ The example using Django but should be transferable to any framework. response[k] = v return response - def response_from_error(e) + def response_from_error(e): return HttpResponseBadRequest('Evil client is unable to send a proper request. Error is: ' + e.description) diff --git a/docs/oauth2/tokens/bearer.rst b/docs/oauth2/tokens/bearer.rst index 0776db8b7..c23efabc3 100644 --- a/docs/oauth2/tokens/bearer.rst +++ b/docs/oauth2/tokens/bearer.rst @@ -79,7 +79,7 @@ And you will find all claims in its decoded form: Sometime you may want to generate custom `access_token` with a reference from a database (as text) or use a HASH signature in JWT or use JWE (encrypted content). -Also, note that you can declare the generate function in your instanciated +Also, note that you can declare the generate function in your instantiated validator to benefit of the `self` variables. See the example below: diff --git a/docs/release_process.rst b/docs/release_process.rst index 9ee987c2f..2796f29c3 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -26,7 +26,7 @@ changes and pings the primary contacts for each downstream project. Please respond within those 2 days if you have major concerns. How to get on the notifications list ------------------------------------ +------------------------------------ Which projects and the instructions for testing each will be defined in OAuthLibs ``Makefile``. To add your project, simply open a pull request or diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 5dbffc96b..9b7eff2f1 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.2.0' +__version__ = '3.2.1' logging.getLogger('oauthlib').addHandler(NullHandler()) diff --git a/oauthlib/common.py b/oauthlib/common.py index b5fbf52e7..395e75efc 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -18,11 +18,9 @@ from . import get_debug try: - from secrets import randbits - from secrets import SystemRandom + from secrets import SystemRandom, randbits except ImportError: - from random import getrandbits as randbits - from random import SystemRandom + from random import SystemRandom, getrandbits as randbits UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py index 07ef42260..9caf12a90 100644 --- a/oauthlib/oauth1/__init__.py +++ b/oauthlib/oauth1/__init__.py @@ -5,24 +5,19 @@ This module is a wrapper for the most recent implementation of OAuth 1.0 Client and Server classes. """ -from .rfc5849 import Client -from .rfc5849 import (SIGNATURE_HMAC, - SIGNATURE_HMAC_SHA1, - SIGNATURE_HMAC_SHA256, - SIGNATURE_HMAC_SHA512, - SIGNATURE_RSA, - SIGNATURE_RSA_SHA1, - SIGNATURE_RSA_SHA256, - SIGNATURE_RSA_SHA512, - SIGNATURE_PLAINTEXT) -from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY -from .rfc5849 import SIGNATURE_TYPE_BODY +from .rfc5849 import ( + SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, + SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA, + SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, + SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, + Client, +) +from .rfc5849.endpoints import ( + AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint, + ResourceEndpoint, SignatureOnlyEndpoint, WebApplicationServer, +) +from .rfc5849.errors import ( + InsecureTransportError, InvalidClientError, InvalidRequestError, + InvalidSignatureMethodError, OAuth1Error, +) from .rfc5849.request_validator import RequestValidator -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) diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index 3a8c26773..7831be7c5 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -11,12 +11,11 @@ from oauthlib.common import CaseInsensitiveDict, Request, generate_token from .. import ( - CONTENT_TYPE_FORM_URLENCODED, - SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_HMAC_SHA512, - SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, - SIGNATURE_PLAINTEXT, - SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, - SIGNATURE_TYPE_QUERY, errors, signature, utils) + CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, + SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1, + SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, SIGNATURE_TYPE_AUTH_HEADER, + SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, errors, signature, utils, +) class BaseEndpoint: diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py index bb67e71ec..0323cfb84 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py @@ -152,7 +152,7 @@ def validate_request_token_request(self, request): 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. + # it should be interpreted is not included in the OAuth spec. # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. @@ -164,7 +164,7 @@ def validate_request_token_request(self, request): # 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. + # flag and absence of realm. # # Clients obtaining an access token will not supply a realm and it will # not be checked. Instead the previously requested realm should be diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py index 45bdaaacd..8641152e4 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/resource.py +++ b/oauthlib/oauth1/rfc5849/endpoints/resource.py @@ -113,7 +113,7 @@ def validate_protected_resource_request(self, uri, http_method='GET', 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. + # it should be interpreted is not included in the OAuth spec. # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. @@ -125,7 +125,7 @@ def validate_protected_resource_request(self, uri, http_method='GET', # 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. + # flag and absence of realm. # # Clients obtaining an access token will not supply a realm and it will # not be checked. Instead the previously requested realm should be diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py index dc5bf0ebb..e937aabf4 100644 --- a/oauthlib/oauth1/rfc5849/request_validator.py +++ b/oauthlib/oauth1/rfc5849/request_validator.py @@ -19,7 +19,7 @@ class RequestValidator: 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. + but blacklisting can be useful to spot malicious activity. The following have methods a default implementation: - check_client_key @@ -443,7 +443,7 @@ def invalidate_request_token(self, client_key, request_token, request): :type request: oauthlib.common.Request :returns: None - Per `Section 2.3`__ of the spec: + Per `Section 2.3`_ of the spec: "The server MUST (...) ensure that the temporary credentials have not expired or been used before." @@ -831,7 +831,7 @@ 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 + :param verifier: A dictionary containing the oauth_verifier and oauth_token :param request: OAuthlib request. :type request: oauthlib.common.Request diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index a370ccd67..5ec123ad3 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -38,14 +38,13 @@ import hashlib import hmac import logging +import urllib.parse as urlparse import warnings from oauthlib.common import extract_params, safe_string_equals, urldecode -import urllib.parse as urlparse from . import utils - log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 0e2a8299d..e11e8fae3 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -39,7 +39,7 @@ def prepare_request_body(self, body='', scope=None, 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 ''. + into. This may contain extra parameters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index bb4c13385..d5eb0cc15 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -6,12 +6,12 @@ This module is an implementation of various logic needed for consuming OAuth 2.0 RFC6749. """ +import base64 +import hashlib +import re +import secrets import time import warnings -import secrets -import re -import hashlib -import base64 from oauthlib.common import generate_token from oauthlib.oauth2.rfc6749 import tokens @@ -228,26 +228,21 @@ def prepare_authorization_request(self, authorization_url, state=None, 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``. - + 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. - + after authorization. Must be provided unless previously setup with + the provider. If provided then it must also be provided in the + token request. :param scope: List of scopes to request. Must be equal to - or a subset of the scopes granted when obtaining the refresh - token. If none is provided, the ones provided in the constructor are - used. - + or a subset of the scopes granted when obtaining the refresh + token. If none is provided, the ones provided in the constructor are + used. :param kwargs: Additional parameters to included in the request. - :returns: The prepared request tuple with (url, headers, body). """ if not is_secure_transport(authorization_url): @@ -271,22 +266,16 @@ def prepare_token_request(self, token_url, authorization_response=None, 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. - + the location to which the user was redirected after successful + authorization. Used to mine credentials needed to obtain a token + in this step, such as authorization code. :param redirect_url: The redirect_url supplied with the authorization - request (if there was one). - + 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 ''. - + into. This may contain extra parameters. Default ''. :param kwargs: Additional parameters to included in the request. - :returns: The prepared request tuple with (url, headers, body). """ if not is_secure_transport(token_url): @@ -312,19 +301,14 @@ def prepare_refresh_token_request(self, token_url, refresh_token=None, 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 ''. - + into. This may contain extra parameters. Default ''. :param scope: List of scopes to request. Must be equal to - or a subset of the scopes granted when obtaining the refresh - token. If none is provided, the ones provided in the constructor are - used. - + or a subset of the scopes granted when obtaining the refresh + token. If none is provided, the ones provided in the constructor are + used. :param kwargs: Additional parameters to included in the request. - :returns: The prepared request tuple with (url, headers, body). """ if not is_secure_transport(token_url): @@ -341,20 +325,14 @@ def prepare_token_revocation_request(self, revocation_url, token, """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``. - + ``"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. - + 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 @@ -362,7 +340,7 @@ def prepare_token_revocation_request(self, revocation_url, token, An example of a revocation request - .. code-block: http + .. code-block:: http POST /revoke HTTP/1.1 Host: server.example.com @@ -373,7 +351,7 @@ def prepare_token_revocation_request(self, revocation_url, token, An example of a jsonp revocation request - .. code-block: http + .. code-block:: http GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1 Host: server.example.com @@ -382,9 +360,9 @@ def prepare_token_revocation_request(self, revocation_url, token, and an error response - .. code-block: http + .. code-block:: javascript - package.myCallback({"error":"unsupported_token_type"}); + 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 @@ -408,9 +386,10 @@ def parse_request_body_response(self, body, scope=None, **kwargs): :param body: The response body from the token request. :param scope: Scopes originally requested. If none is provided, the ones - provided in the constructor are used. + provided in the constructor are used. :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. + :raises: Warning if scope has changed. :py:class:`oauthlib.oauth2.errors.OAuth2Error` + if response is invalid. These response are json encoded and could easily be parsed without the assistance of OAuthLib. However, there are a few subtle issues @@ -436,7 +415,7 @@ def parse_request_body_response(self, body, scope=None, **kwargs): If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value. - **scope** + **scope** Providers may supply this in all responses but are required to only if it has changed since the authorization request. @@ -454,20 +433,16 @@ def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs 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" + 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. Note that if none is provided, the ones provided - in the constructor are used if any. + :param refresh_token: REQUIRED. The refresh token issued to the client. + :param scope: OPTIONAL. The scope of the access request as described by + Section 3.3. The requested scope MUST NOT include any scope + not originally granted by the resource owner, and if omitted is + treated as equal to the scope originally granted by the + resource owner. Note that if none is provided, the ones provided + in the constructor are used if any. """ refresh_token = refresh_token or self.refresh_token scope = self.scope if scope is None else scope @@ -492,18 +467,21 @@ def _add_bearer_token(self, uri, http_method='GET', body=None, def create_code_verifier(self, length): """Create PKCE **code_verifier** used in computing **code_challenge**. + See `RFC7636 Section 4.1`_ - :param length: REQUIRED. The length of the code_verifier. + :param length: REQUIRED. The length of the code_verifier. - The client first creates a code verifier, "code_verifier", for each - OAuth 2.0 [RFC6749] Authorization Request, in the following manner: + The client first creates a code verifier, "code_verifier", for each + OAuth 2.0 [RFC6749] Authorization Request, in the following manner: - code_verifier = high-entropy cryptographic random STRING using the - unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" - from Section 2.3 of [RFC3986], with a minimum length of 43 characters - and a maximum length of 128 characters. - - .. _`Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1 + .. code-block:: text + + code_verifier = high-entropy cryptographic random STRING using the + unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" + from Section 2.3 of [RFC3986], with a minimum length of 43 characters + and a maximum length of 128 characters. + + .. _`RFC7636 Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1 """ code_verifier = None @@ -525,33 +503,30 @@ def create_code_verifier(self, length): def create_code_challenge(self, code_verifier, code_challenge_method=None): """Create PKCE **code_challenge** derived from the **code_verifier**. + See `RFC7636 Section 4.2`_ - :param code_verifier: REQUIRED. The **code_verifier** generated from create_code_verifier(). - :param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable - values include "S256". DEFAULT is "plain". + :param code_verifier: REQUIRED. The **code_verifier** generated from `create_code_verifier()`. + :param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable values include `S256`. DEFAULT is `plain`. - - The client then creates a code challenge derived from the code + The client then creates a code challenge derived from the code verifier by using one of the following transformations on the code - verifier: - - plain - code_challenge = code_verifier + verifier:: - S256 - code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) + plain + code_challenge = code_verifier + S256 + code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) - If the client is capable of using "S256", it MUST use "S256", as - "S256" is Mandatory To Implement (MTI) on the server. Clients are - permitted to use "plain" only if they cannot support "S256" for some + If the client is capable of using `S256`, it MUST use `S256`, as + `S256` is Mandatory To Implement (MTI) on the server. Clients are + permitted to use `plain` only if they cannot support `S256` for some technical reason and know via out-of-band configuration that the - server supports "plain". + server supports `plain`. The plain transformation is for compatibility with existing - deployments and for constrained environments that can't use the S256 - transformation. + deployments and for constrained environments that can't use the S256 transformation. - .. _`Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2 + .. _`RFC7636 Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2 """ code_challenge = None diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index 7af68f348..9920981d2 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -49,7 +49,7 @@ def prepare_request_body(self, username, password, body='', scope=None, :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 ''. + into. This may contain extra parameters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. :param include_client_id: `True` to send the `client_id` in the diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index cd325f4e0..b10b41ced 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -55,7 +55,7 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, 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 + and it should have been registered with the OAuth provider prior to use. As described in `Section 3.1.2`_. :param scope: OPTIONAL. The scope of the access request as described by diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index c751c8b0d..8fb173776 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -31,7 +31,7 @@ class ServiceApplicationClient(Client): 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. + """Initialize a JWT client with defaults for implicit use later. :param client_id: Client identifier given by the OAuth provider upon registration. @@ -99,7 +99,7 @@ def prepare_request_body(self, :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 ''. + into. This may contain extra parameters. Default ''. :param scope: The scope of the access request. diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 1d3b2b5bf..50890fbf8 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -49,7 +49,7 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, 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 + and it should have been registered with the OAuth provider prior to use. As described in `Section 3.1.2`_. :param scope: OPTIONAL. The scope of the access request as described by @@ -117,7 +117,7 @@ def prepare_request_body(self, code=None, redirect_uri=None, body='', values MUST be identical. :param body: Existing request body (URL encoded string) to embed parameters - into. This may contain extra paramters. Default ''. + 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 diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 63570d9cb..3cc61e662 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -86,9 +86,9 @@ def validate_introspect_request(self, request): an HTTP POST request with parameters sent as "application/x-www-form-urlencoded". - token REQUIRED. The string value of the token. + * token REQUIRED. The string value of the token. + * token_type_hint OPTIONAL. - 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 @@ -96,11 +96,9 @@ def validate_introspect_request(self, request): 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`_ + * 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 diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index d43a82471..a2820f28a 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -10,7 +10,7 @@ import json import logging -from .. import grant_types +from .. import grant_types, utils from .authorization import AuthorizationEndpoint from .base import BaseEndpoint, catch_errors_and_unavailability from .introspect import IntrospectEndpoint @@ -68,7 +68,7 @@ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url raise ValueError("key {} is a mandatory metadata.".format(key)) elif is_issuer: - if not array[key].startswith("https"): + if not utils.is_secure_transport(array[key]): raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key])) if "?" in array[key] or "&" in array[key] or "#" in array[key]: raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key])) diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 4aa5ec6ee..596d0860f 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -42,7 +42,7 @@ def create_revocation_response(self, uri, http_method='POST', body=None, The authorization server responds with HTTP status code 200 if the - token has been revoked sucessfully or if the client submitted an + token has been revoked successfully or if the client submitted an invalid token. Note: invalid tokens do not cause an error response since the client @@ -95,7 +95,7 @@ def validate_revocation_request(self, request): 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 + extend its search across all of its supported token types. An authorization server MAY ignore this parameter, particularly if it is able to detect the token type automatically. This specification defines two such values: diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index b799823ee..858855a17 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -10,7 +10,6 @@ from oauthlib import common from .. import errors -from ..utils import is_secure_transport from .base import GrantTypeBase log = logging.getLogger(__name__) @@ -547,20 +546,3 @@ 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) - - def _create_cors_headers(self, request): - """If CORS is allowed, create the appropriate headers.""" - if 'origin' not in request.headers: - return {} - - origin = request.headers['origin'] - if not is_secure_transport(origin): - log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin) - return {} - elif not self.request_validator.is_origin_allowed( - request.client_id, origin, request): - log.debug('Invalid origin "%s", CORS not allowed.', origin) - return {} - else: - log.debug('Valid origin "%s", injecting CORS headers.', origin) - return {'Access-Control-Allow-Origin': origin} diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index a64f168c6..ca343a119 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -10,6 +10,7 @@ from oauthlib.uri_validate import is_absolute_uri from ..request_validator import RequestValidator +from ..utils import is_secure_transport log = logging.getLogger(__name__) @@ -248,3 +249,20 @@ def _handle_redirects(self, request): raise errors.MissingRedirectURIError(request=request) if not is_absolute_uri(request.redirect_uri): raise errors.InvalidRedirectURIError(request=request) + + def _create_cors_headers(self, request): + """If CORS is allowed, create the appropriate headers.""" + if 'origin' not in request.headers: + return {} + + origin = request.headers['origin'] + if not is_secure_transport(origin): + log.debug('Origin "%s" is not HTTPS, CORS not allowed.', origin) + return {} + elif not self.request_validator.is_origin_allowed( + request.client_id, origin, request): + log.debug('Invalid origin "%s", CORS not allowed.', origin) + return {} + else: + log.debug('Valid origin "%s", injecting CORS headers.', origin) + return {'Access-Control-Allow-Origin': origin} diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index f801de4af..ce33df0e7 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -69,6 +69,7 @@ def create_token_response(self, request, token_handler): log.debug('Issuing new token to client id %r (%r), %r.', request.client_id, request.client, token) + headers.update(self._create_cors_headers(request)) return headers, json.dumps(token), 200 def validate_token_request(self, request): diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 44738bb47..8f6ce2c7f 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -45,7 +45,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in `Section 10.12`_. - :param code_challenge: PKCE paramater. A challenge derived from the + :param code_challenge: PKCE parameter. A challenge derived from the code_verifier that is sent in the authorization request, to be verified against later. :param code_challenge_method: PKCE parameter. A method that was used to derive the diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 610a708de..3910c0b91 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -191,6 +191,7 @@ def introspect_token(self, token, token_type_hint, request, *args, **kwargs): 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 @@ -204,10 +205,10 @@ def introspect_token(self, token, token_type_hint, request, *args, **kwargs): - 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`_. + 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. + efficiency, but must fallback to other types to be compliant with RFC. The dict of claims is added to request.token after this method. @@ -443,6 +444,7 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): - 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. @@ -451,6 +453,7 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): 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 @@ -561,7 +564,7 @@ def validate_user(self, username, password, client, request, *args, **kwargs): 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). + persistence method used (commonly, save_bearer_token). :param username: Unicode username. :param password: Unicode password. @@ -671,6 +674,7 @@ def is_origin_allowed(self, client_id, origin, request, *args, **kwargs): Method is used by: - Authorization Code Grant + - Refresh Token Grant """ return False diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 6284248d7..0757d07ea 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -257,6 +257,7 @@ def get_token_from_header(request): class TokenBase: + __slots__ = () def __call__(self, request, refresh_token=False): raise NotImplementedError('Subclasses must implement this method.') diff --git a/oauthlib/oauth2/rfc8628/clients/device.py b/oauthlib/oauth2/rfc8628/clients/device.py index 95c4f5a25..b9ba2150a 100644 --- a/oauthlib/oauth2/rfc8628/clients/device.py +++ b/oauthlib/oauth2/rfc8628/clients/device.py @@ -5,12 +5,11 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ - +from oauthlib.common import add_params_to_uri from oauthlib.oauth2 import BackendApplicationClient, Client from oauthlib.oauth2.rfc6749.errors import InsecureTransportError from oauthlib.oauth2.rfc6749.parameters import prepare_token_request from oauthlib.oauth2.rfc6749.utils import is_secure_transport, list_to_scope -from oauthlib.common import add_params_to_uri class DeviceClient(Client): @@ -62,7 +61,7 @@ def prepare_request_body(self, device_code, body='', scope=None, body. :param body: Existing request body (URL encoded string) to embed parameters - into. This may contain extra paramters. Default ''. + into. This may contain extra parameters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. @@ -84,6 +83,8 @@ def prepare_request_body(self, device_code, body='', scope=None, >>> client.prepare_request_body(scope=['hello', 'world']) 'grant_type=urn:ietf:params:oauth:grant-type:device_code&scope=hello+world' + .. _`Section 3.2.1`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1 + .. _`Section 3.3`: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3 .. _`Section 3.4`: https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 """ diff --git a/oauthlib/openid/connect/core/endpoints/userinfo.py b/oauthlib/openid/connect/core/endpoints/userinfo.py index 1c29cc558..7aa2bbe97 100644 --- a/oauthlib/openid/connect/core/endpoints/userinfo.py +++ b/oauthlib/openid/connect/core/endpoints/userinfo.py @@ -69,7 +69,7 @@ def validate_userinfo_request(self, request): 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 + 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 @@ -77,21 +77,28 @@ def validate_userinfo_request(self, request): The following is a non-normative example of a UserInfo Request: - GET /userinfo HTTP/1.1 - Host: server.example.com - Authorization: Bearer SlAV32hkKG + .. code-block:: http + + GET /userinfo HTTP/1.1 + Host: server.example.com + Authorization: Bearer SlAV32hkKG 5.3.3. UserInfo Error Response When an error condition occurs, the UserInfo Endpoint returns an Error - Response as defined in Section 3 of OAuth 2.0 Bearer Token Usage + 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", + .. code-block:: http + + HTTP/1.1 401 Unauthorized + WWW-Authenticate: Bearer error="invalid_token", error_description="The Access Token expired" + + .. _`Section 2`: https://datatracker.ietf.org/doc/html/rfc6750#section-2 + .. _`Section 3`: https://datatracker.ietf.org/doc/html/rfc6750#section-3 """ if not self.bearer.validate_request(request): raise errors.InvalidTokenError() diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 76173e6c6..33411dad7 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -8,7 +8,6 @@ ConsentRequired, InvalidRequestError, LoginRequired, ) - log = logging.getLogger(__name__) diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py index 2734c387e..5aa7d4698 100644 --- a/oauthlib/openid/connect/core/grant_types/dispatchers.py +++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py @@ -84,7 +84,7 @@ def _handler_for_request(self, request): code = parameters.get('code', None) redirect_uri = parameters.get('redirect_uri', None) - # If code is not pressent fallback to `default_grant` which will + # If code is not present fallback to `default_grant` which will # raise an error for the missing `code` in `create_token_response` step. if code: scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py index a312e2d2e..936ab52e3 100644 --- a/oauthlib/openid/connect/core/tokens.py +++ b/oauthlib/openid/connect/core/tokens.py @@ -4,7 +4,9 @@ This module contains methods for adding JWT tokens to requests. """ -from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator, get_token_from_header +from oauthlib.oauth2.rfc6749.tokens import ( + TokenBase, get_token_from_header, random_token_generator, +) class JWTToken(TokenBase): diff --git a/tests/oauth1/rfc5849/test_signatures.py b/tests/oauth1/rfc5849/test_signatures.py index 3e84f24b5..4e6d96203 100644 --- a/tests/oauth1/rfc5849/test_signatures.py +++ b/tests/oauth1/rfc5849/test_signatures.py @@ -1,26 +1,15 @@ # -*- coding: utf-8 -*- from oauthlib.oauth1.rfc5849.signature import ( - collect_parameters, - signature_base_string, - base_string_uri, - normalize_parameters, - sign_hmac_sha1_with_client, - sign_hmac_sha256_with_client, - sign_hmac_sha512_with_client, - sign_rsa_sha1_with_client, - sign_rsa_sha256_with_client, - sign_rsa_sha512_with_client, - sign_plaintext_with_client, - verify_hmac_sha1, - verify_hmac_sha256, - verify_hmac_sha512, - verify_rsa_sha1, - verify_rsa_sha256, - verify_rsa_sha512, - verify_plaintext + base_string_uri, collect_parameters, normalize_parameters, + sign_hmac_sha1_with_client, sign_hmac_sha256_with_client, + sign_hmac_sha512_with_client, sign_plaintext_with_client, + sign_rsa_sha1_with_client, sign_rsa_sha256_with_client, + sign_rsa_sha512_with_client, signature_base_string, verify_hmac_sha1, + verify_hmac_sha256, verify_hmac_sha512, verify_plaintext, verify_rsa_sha1, + verify_rsa_sha256, verify_rsa_sha512, ) -from tests.unittest import TestCase +from tests.unittest import TestCase # ################################################################ diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index f6b94497a..7a7112151 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -45,7 +45,7 @@ class WebApplicationClientTest(TestCase): body_code = "not=empty&grant_type=authorization_code&code={}&client_id={}".format(code, client_id) body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" - bode_code_verifier = body_code + "&code_verifier=code_verifier" + body_code_verifier = body_code + "&code_verifier=code_verifier" body_kwargs = body_code + "&some=providers&require=extra+arguments" response_uri = "https://client.example.com/cb?code=zzzzaaaa&state=xyz" @@ -115,7 +115,7 @@ def test_request_body(self): # With code verifier body = client.prepare_request_body(body=self.body, code_verifier=self.code_verifier) - self.assertFormBodyEqual(body, self.bode_code_verifier) + self.assertFormBodyEqual(body, self.body_code_verifier) # With extra parameters body = client.prepare_request_body(body=self.body, **self.kwargs) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index d93f849b5..1f5b91210 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +import json + from oauthlib.oauth2 import MetadataEndpoint, Server, TokenEndpoint -import json from tests.unittest import TestCase @@ -135,3 +136,13 @@ def sort_list(claims): sort_list(metadata.claims) sort_list(expected_claims) self.assertEqual(sorted(metadata.claims.items()), sorted(expected_claims.items())) + + def test_metadata_validate_issuer(self): + with self.assertRaises(ValueError): + endpoint = TokenEndpoint( + None, None, grant_types={"password": None}, + ) + metadata = MetadataEndpoint([endpoint], { + "issuer": 'http://foo.bar', + "token_endpoint": "https://foo.bar/token", + }) diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py index 1d3e77a0e..581f2a4d6 100644 --- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py +++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py @@ -18,6 +18,7 @@ def setUp(self): self.request = Request('http://a.b/path') self.request.grant_type = 'refresh_token' self.request.refresh_token = 'lsdkfhj230' + self.request.client_id = 'abcdef' self.request.client = mock_client self.request.scope = 'foo' self.mock_validator = mock.MagicMock() @@ -168,3 +169,43 @@ def test_valid_token_request(self): del self.request.scope self.auth.validate_token_request(self.request) self.assertEqual(self.request.scopes, 'foo bar baz'.split()) + + # CORS + + def test_create_cors_headers(self): + bearer = BearerToken(self.mock_validator) + self.request.headers['origin'] = 'https://foo.bar' + self.mock_validator.is_origin_allowed.return_value = True + + headers = self.auth.create_token_response(self.request, bearer)[0] + self.assertEqual( + headers['Access-Control-Allow-Origin'], 'https://foo.bar' + ) + self.mock_validator.is_origin_allowed.assert_called_once_with( + 'abcdef', 'https://foo.bar', self.request + ) + + def test_create_cors_headers_no_origin(self): + bearer = BearerToken(self.mock_validator) + headers = self.auth.create_token_response(self.request, bearer)[0] + self.assertNotIn('Access-Control-Allow-Origin', headers) + self.mock_validator.is_origin_allowed.assert_not_called() + + def test_create_cors_headers_insecure_origin(self): + bearer = BearerToken(self.mock_validator) + self.request.headers['origin'] = 'http://foo.bar' + + headers = self.auth.create_token_response(self.request, bearer)[0] + self.assertNotIn('Access-Control-Allow-Origin', headers) + self.mock_validator.is_origin_allowed.assert_not_called() + + def test_create_cors_headers_invalid_origin(self): + bearer = BearerToken(self.mock_validator) + self.request.headers['origin'] = 'https://foo.bar' + self.mock_validator.is_origin_allowed.return_value = False + + headers = self.auth.create_token_response(self.request, bearer)[0] + self.assertNotIn('Access-Control-Allow-Origin', headers) + self.mock_validator.is_origin_allowed.assert_called_once_with( + 'abcdef', 'https://foo.bar', self.request + ) diff --git a/tox.ini b/tox.ini index c07245092..4eb0813b2 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands= pytest --cov=oauthlib tests/ -# tox -e docs to mimick readthedocs build. +# tox -e docs to mimic readthedocs build. # as of today, RTD is using python3.7 and doesn't run "setup.py install" [testenv:docs] basepython=python3.7 @@ -20,7 +20,7 @@ changedir=docs whitelist_externals=make commands=make clean html -# tox -e readme to mimick PyPI long_description check +# tox -e readme to mimic PyPI long_description check [testenv:readme] basepython=python3.8 deps=twine>=1.12.0