From 511ea25363cdeb955d8945f9d93594da0a6b9b80 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 30 Aug 2022 10:13:24 +0600 Subject: [PATCH 001/115] create security policy --- SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ddb8632d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +following versions are currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 3.2.x | :white_check_mark: | +| 3.1.x | :x: | +| < 3.2.0 | :x: | + +## Reporting a Vulnerability +Contact auvipy@gmail.com for reporting any vulnerability. From 52599819d5778d19ad684b83f2d503d1475f4c81 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sat, 10 Sep 2022 15:11:38 +0600 Subject: [PATCH 002/115] draft CI --- .github/workflows/python-publish.yml | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 00000000..ec703542 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,39 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From a70e13ff19d74f7fc1e397742782c7b881e2d496 Mon Sep 17 00:00:00 2001 From: Maximilian Wirtz Date: Fri, 16 Sep 2022 13:28:20 +0200 Subject: [PATCH 003/115] Use proper SPDX identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0192458b..4c435f9a 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def fread(fn): maintainer_email='ib.lundgren@gmail.com', url='https://github.com/oauthlib/oauthlib', platforms='any', - license='BSD', + license='BSD-3-Clause', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), python_requires='>=3.6', extras_require={ From c3dda126005547184d2688d350585e940be996f5 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 6 Oct 2022 09:20:34 +0200 Subject: [PATCH 004/115] Upgrade GitHub Actions and make bandit, codespell, and pytest mandatory (#835) * Upgrade GitHub Actions * Fix typo discovered by codespell * Update lint_python.yml --- .github/workflows/lint_python.yml | 18 ++++++++++-------- .../openid/connect/core/request_validator.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 3b3be00e..ba4d6c92 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -4,22 +4,24 @@ jobs: lint_python: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x - 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: bandit --recursive --skip B101,B105,B106,B107,B324 . - run: black --check . || true - - run: codespell || true # --ignore-words-list="" --skip="*.css,*.js,*.lock" + - run: codespell # --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: pip install -r requirements-test.txt + - run: pip install --editable . - 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: pytest + - run: shopt -s globstar && pyupgrade --py37-plus **/*.py || true - run: safety check diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index 47c4cd94..e3cea79b 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -143,7 +143,7 @@ def finalize_id_token(self, id_token, token, token_handler, request): Token MUST NOT be accepted by the RP when performing authentication with the OP. - Additionals claims must be added, note that `request.scope` + Additional 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`_ From f1cc9c8d26c096b7ffee83fae5aae6c1249e7bf8 Mon Sep 17 00:00:00 2001 From: Tiphaine LAURENT <25824254+TiphaineLAURENT@users.noreply.github.com> Date: Tue, 3 May 2022 16:39:24 +0200 Subject: [PATCH 005/115] OAuth2Error: Allow falsy values as state The idea is to allow values like `0` to be used a state. The current implementation only checks for truthiness. --- oauthlib/oauth2/rfc6749/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index da24feab..6fcb7519 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -60,7 +60,7 @@ def __init__(self, description=None, uri=None, state=None, self.response_type = request.response_type self.response_mode = request.response_mode self.grant_type = request.grant_type - if not state: + if state is None: self.state = request.state else: self.redirect_uri = None From 7e69a15c01dd7d23edf0a958ced62c0684286ccb Mon Sep 17 00:00:00 2001 From: Burke Livingston Date: Thu, 6 Oct 2022 01:47:12 -0700 Subject: [PATCH 006/115] Update pre-configured OIDC server to use OIDC flavor of Refresh Token grant type (#838) * Modify pre-configured OIDC server to use OIDC Refresh Token grant type * Add test coverage for OIDC refresh token grant type * Use longer variable names --- .../connect/core/endpoints/pre_configured.py | 6 ++-- .../core/endpoints/test_refresh_token.py | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/openid/connect/core/endpoints/test_refresh_token.py diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 8ce8bee6..1f4370ad 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -12,11 +12,13 @@ from oauthlib.oauth2.rfc6749.grant_types import ( AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant, - RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, + ResourceOwnerPasswordCredentialsGrant, ) from oauthlib.oauth2.rfc6749.tokens import BearerToken -from ..grant_types import AuthorizationCodeGrant, HybridGrant, ImplicitGrant +from ..grant_types import ( + AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant +) from ..grant_types.dispatchers import ( AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher, ImplicitTokenGrantDispatcher, diff --git a/tests/openid/connect/core/endpoints/test_refresh_token.py b/tests/openid/connect/core/endpoints/test_refresh_token.py new file mode 100644 index 00000000..9161f5a5 --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_refresh_token.py @@ -0,0 +1,32 @@ +"""Ensure that the server correctly uses the OIDC flavor of +the Refresh token grant type when appropriate. + +When the OpenID scope is provided, the refresh token response +should include a fresh ID token. +""" +import json +from unittest import mock + +from oauthlib.openid import RequestValidator +from oauthlib.openid.connect.core.endpoints.pre_configured import Server + +from tests.unittest import TestCase + + +class TestRefreshToken(TestCase): + + def setUp(self): + self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.get_id_token.return_value='id_token' + + self.server = Server(self.validator) + + def test_refresh_token_with_openid(self): + request_body = 'scope=openid+test_scope&grant_type=refresh_token&refresh_token=abc' + headers, body, status = self.server.create_token_response('', body=request_body) + self.assertIn('id_token', json.loads(body)) + + def test_refresh_token_no_openid(self): + request_body = 'scope=test_scope&grant_type=refresh_token&refresh_token=abc' + headers, body, status = self.server.create_token_response('', body=request_body) + self.assertNotIn('id_token', json.loads(body)) From 564d526fdbdc32a936e4b5ddac186c26024f626b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 18 Oct 2022 09:43:17 +0200 Subject: [PATCH 007/115] Update setup.cfg to use license_files (#839) Fixes the following warning: > The license_file parameter is deprecated, use license_files instead. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ca59291b..286d6cbe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -license_file = LICENSE +license_files = LICENSE [isort] combine_as_imports = true From 7d7fe904af504c20f7d802650e54a78e3d0be8ba Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 17 Oct 2022 21:42:56 +0200 Subject: [PATCH 008/115] Add 3.2.2 version --- CHANGELOG.rst | 6 +++++- oauthlib/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e150100..82dbd75a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,15 @@ Changelog ========= +3.2.2 (2022-10-17) +------------------ +OAuth2.0 Provider: +* CVE-2022-36087 + 3.2.1 (2022-09-09) ------------------ OAuth2.0 Provider: * #803: Metadata endpoint support of non-HTTPS -* CVE-2022-36087 OAuth1.0: * #818: Allow IPv6 being parsed by signature diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 9b7eff2f..d9a5e38e 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.2.1' +__version__ = '3.2.2' logging.getLogger('oauthlib').addHandler(NullHandler()) From 80eda5d81f4da1d9d194480c65a4c01216ce95e1 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 18 May 2023 10:03:11 +0600 Subject: [PATCH 009/115] Create dependency-review.yml --- .github/workflows/dependency-review.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..fe461b42 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v2 From d4b6699f8ccb608152b764919e0bd3d38a7b171f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindri=20Gu=C3=B0mundsson?= Date: Mon, 22 Aug 2022 16:32:14 +0000 Subject: [PATCH 010/115] Ensure expires_at is always int As discussed in #745 --- oauthlib/oauth2/rfc6749/clients/base.py | 4 +-- oauthlib/oauth2/rfc6749/parameters.py | 5 ++- tests/oauth2/rfc6749/clients/test_base.py | 33 +++++++++++++++++++ .../clients/test_service_application.py | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index d5eb0cc1..1d12638e 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -589,11 +589,11 @@ def populate_token_attributes(self, response): if 'expires_in' in response: self.expires_in = response.get('expires_in') - self._expires_at = time.time() + int(self.expires_in) + self._expires_at = round(time.time()) + int(self.expires_in) if 'expires_at' in response: try: - self._expires_at = int(response.get('expires_at')) + self._expires_at = round(float(response.get('expires_at'))) except: self._expires_at = None diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 8f6ce2c7..0f0f423a 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -345,7 +345,7 @@ def parse_implicit_response(uri, state=None, scope=None): params['scope'] = scope_to_list(params['scope']) if 'expires_in' in params: - params['expires_at'] = time.time() + int(params['expires_in']) + params['expires_at'] = round(time.time()) + int(params['expires_in']) if state and params.get('state', None) != state: raise ValueError("Mismatching or missing state in params.") @@ -437,6 +437,9 @@ def parse_token_response(body, scope=None): else: params['expires_at'] = time.time() + int(params['expires_in']) + if isinstance(params.get('expires_at'), float): + params['expires_at'] = round(params['expires_at']) + params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) return params diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index 70a22834..7286b991 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import datetime +from unittest.mock import patch from oauthlib import common from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError @@ -353,3 +354,35 @@ def test_create_code_challenge_s256(self): code_verifier = client.create_code_verifier(length=128) code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256') self.assertEqual(code_challenge_s256, client.code_challenge) + + def test_parse_token_response_expires_at_is_int(self): + expected_expires_at = 1661185149 + token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_at":1661185148.6437678,' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}') + + client = Client(self.client_id) + + response = client.parse_request_body_response(token_json, scope=["/profile"]) + + self.assertEqual(response['expires_at'], expected_expires_at) + self.assertEqual(client._expires_at, expected_expires_at) + + @patch('time.time') + def test_parse_token_response_generated_expires_at_is_int(self, t): + t.return_value = 1661185148.6437678 + expected_expires_at = round(t.return_value) + 3600 + token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_in":3600,' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}') + + client = Client(self.client_id) + + response = client.parse_request_body_response(token_json, scope=["/profile"]) + + self.assertEqual(response['expires_at'], expected_expires_at) + self.assertEqual(client._expires_at, expected_expires_at) diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py index b97d8554..84361d8b 100644 --- a/tests/oauth2/rfc6749/clients/test_service_application.py +++ b/tests/oauth2/rfc6749/clients/test_service_application.py @@ -166,7 +166,7 @@ def test_request_body_no_initial_private_key(self, t): @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 + self.token['expires_at'] = self.token['expires_in'] + round(t.return_value) client = ServiceApplicationClient(self.client_id) From 88e7ae82d9cd7433e19cd1fa4fa449a3c40b2aff Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 23 Aug 2023 15:48:21 +0100 Subject: [PATCH 011/115] Update .github/workflows/python-publish.yml Co-authored-by: Christian Clauss --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ec703542..1a79796b 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies From a8d29ff192ff0c0bcb6fcee56ddea34034233efd Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 23 Aug 2023 22:07:27 +0600 Subject: [PATCH 012/115] Update .github/workflows/python-publish.yml Co-authored-by: Christian Clauss --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1a79796b..de1b8f15 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -28,7 +28,7 @@ jobs: python-version: '3.x' - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip setuptools pip install build - name: Build package run: python -m build From 7637284517a974cbe2ab71ad1de36fbd235ebc4b Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 23 Aug 2023 19:51:38 +0200 Subject: [PATCH 013/115] Fix failing GitHub Action lint_python.yml --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index ba4d6c92..7cb2b28e 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.x - - run: pip install --upgrade pip wheel + - run: pip install --upgrade pip setuptools wheel - run: pip install bandit black codespell flake8 flake8-2020 flake8-bugbear flake8-comprehensions isort mypy pytest pyupgrade safety - run: bandit --recursive --skip B101,B105,B106,B107,B324 . From 9139b72753cd1423d325cd7641f4b3c16ed27e43 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 24 Aug 2023 13:36:49 +0200 Subject: [PATCH 014/115] Initial build & publish rules with GH Actions --- .github/workflows/python-build.yml | 40 ++++++++++++++++++ .github/workflows/python-publish.yml | 61 ++++++++++++---------------- .travis.yml | 58 -------------------------- Makefile | 2 +- tox.ini | 18 ++++---- 5 files changed, 77 insertions(+), 102 deletions(-) create mode 100644 .github/workflows/python-build.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml new file mode 100644 index 00000000..e2d4dfdd --- /dev/null +++ b/.github/workflows/python-build.yml @@ -0,0 +1,40 @@ +name: Python Tests +run-name: Run Tests by ${{ github.actor }} +on: [push] +jobs: + tests: + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + python: ["3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + steps: + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Check out repository code + uses: actions/checkout@v3 + - name: Install prereq + run: pip install tox coveralls + - name: Run python tests + run: tox -e ${{ matrix.python }} + - name: Coveralls + run: coveralls + docs: + strategy: + matrix: + toxenv: ["docs", "readme"] + runs-on: ubuntu-latest + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Check out repository code + uses: actions/checkout@v3 + - name: Install prereq + run: pip install tox + - name: Run python tests + run: tox -e ${{ matrix.toxenv }} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index de1b8f15..89262709 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,39 +1,32 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - +name: Production deploy on: - release: - types: [published] - -permissions: - contents: read - + workflow_run: + workflows: ["Python Tests"] + types: + - completed jobs: - deploy: - + pypi-publish: + if: | + ${{ github.event.workflow_run.conclusion == 'success' }} && + ${{ github.ref_type == 'tag' }} + name: Upload release to PyPI runs-on: ubuntu-latest - + environment: + name: pypi + url: https://pypi.org/p/oauthlib + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - name: Check out repository code + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip setuptools - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + python-version: '3.10' + - name: Install prereq + run: pip install wheel + - name: Build python package + run: python setup.py build + - name: Package python package + run: python setup.py sdist bdist_wheel + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6b5e6e30..00000000 --- a/.travis.yml +++ /dev/null @@ -1,58 +0,0 @@ -language: python -python: 3.8 -os: linux -dist: bionic -cache: pip -jobs: - include: - - python: "3.6" - env: TOXENV=py36 - - python: "3.7" - env: TOXENV=py37,docs - - python: "3.8" - env: TOXENV=py38,bandit,readme - - python: "3.9" - env: TOXENV=py39 - - python: "3.10.2" - env: TOXENV=py310 - - python: "3.11-dev" - env: TOXENV=py311 - - python: "pypy3" - env: TOXENV=pypy3 - allow_failures: - - python: "3.11-dev" -before_install: - - 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 -after_success: COVERALLS_PARALLEL=true coveralls -notifications: - webhooks: - urls: - - https://coveralls.io/webhook - - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 - on_success: always - on_failure: always - on_start: never -deploy: - - provider: releases - token: - secure: "eqEWOzKWZCuvd1a77CA03OX/HCrsYlsu1/Sz/RhXQIEhKz6tKp10KGw9zr57bHAIl0OfJFK9k63lI2HOctAmwkKeeQ4HdNqw4pHFa8Gk3liGp31KSmshVtHX8Rtn0DuFA028Wm7w5n+fOVc8tJVU/UsKjsfsAzRHnQjMamckoXU=" - skip_cleanup: true - on: - tags: true - all_branches: true - condition: $TOXENV = py36 - repo: oauthlib/oauthlib - - provider: pypi - username: JonathanHuot - password: - secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ=" - distributions: sdist bdist_wheel - on: - tags: true - all_branches: true - condition: $TOXENV = py36 - repo: oauthlib/oauthlib diff --git a/Makefile b/Makefile index b1fbb39d..2d2e1ed7 100644 --- a/Makefile +++ b/Makefile @@ -81,4 +81,4 @@ dance: .DEFAULT_GOAL := all .PHONY: clean test bottle dance django flask requests -all: clean test bottle dance django flask requests +all: test bottle dance django flask requests diff --git a/tox.ini b/tox.ini index 4eb0813b..3195c771 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ +# require tox>=4 [tox] -envlist = py36,py37,py38,py39,py310,py311,pypy3,docs,readme,bandit,isort +envlist = py38,py39,py310,py311,pypy3,docs,readme,bandit,isort [testenv] deps= @@ -7,33 +8,32 @@ deps= commands= pytest --cov=oauthlib tests/ - # 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 +basepython=python3.11 skipsdist=True deps= sphinx sphinx_rtd_theme changedir=docs -whitelist_externals=make +allowlist_externals=make commands=make clean html # tox -e readme to mimic PyPI long_description check [testenv:readme] -basepython=python3.8 +basepython=python3.11 deps=twine>=1.12.0 -whitelist_externals=echo +allowlist_externals=echo commands= - twine check .tox/dist/* + twine check .tox/.pkg/dist/* [testenv:bandit] -basepython=python3.8 +basepython=python3.11 skipsdist=True deps=bandit commands=bandit -b bandit.json -r oauthlib/ -whitelist_externals=bandit +allowlist_externals=bandit [testenv:isort] basepython = python3.8 From d63d1aea5d3eb1e2240077096177687f018fc32a Mon Sep 17 00:00:00 2001 From: Maximilian Wirtz Date: Fri, 16 Sep 2022 13:28:20 +0200 Subject: [PATCH 015/115] Use proper SPDX identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0192458b..4c435f9a 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def fread(fn): maintainer_email='ib.lundgren@gmail.com', url='https://github.com/oauthlib/oauthlib', platforms='any', - license='BSD', + license='BSD-3-Clause', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), python_requires='>=3.6', extras_require={ From c21c56287d09f0d5f8e565ef201d841af5724004 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 6 Oct 2022 09:20:34 +0200 Subject: [PATCH 016/115] Upgrade GitHub Actions and make bandit, codespell, and pytest mandatory (#835) * Upgrade GitHub Actions * Fix typo discovered by codespell * Update lint_python.yml --- .github/workflows/lint_python.yml | 18 ++++++++++-------- .../openid/connect/core/request_validator.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 3b3be00e..ba4d6c92 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -4,22 +4,24 @@ jobs: lint_python: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x - 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: bandit --recursive --skip B101,B105,B106,B107,B324 . - run: black --check . || true - - run: codespell || true # --ignore-words-list="" --skip="*.css,*.js,*.lock" + - run: codespell # --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: pip install -r requirements-test.txt + - run: pip install --editable . - 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: pytest + - run: shopt -s globstar && pyupgrade --py37-plus **/*.py || true - run: safety check diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index 47c4cd94..e3cea79b 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -143,7 +143,7 @@ def finalize_id_token(self, id_token, token, token_handler, request): Token MUST NOT be accepted by the RP when performing authentication with the OP. - Additionals claims must be added, note that `request.scope` + Additional 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`_ From 97f5d05357187cad30a4f1e3833620c18a173118 Mon Sep 17 00:00:00 2001 From: Tiphaine LAURENT <25824254+TiphaineLAURENT@users.noreply.github.com> Date: Tue, 3 May 2022 16:39:24 +0200 Subject: [PATCH 017/115] OAuth2Error: Allow falsy values as state The idea is to allow values like `0` to be used a state. The current implementation only checks for truthiness. --- oauthlib/oauth2/rfc6749/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index da24feab..6fcb7519 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -60,7 +60,7 @@ def __init__(self, description=None, uri=None, state=None, self.response_type = request.response_type self.response_mode = request.response_mode self.grant_type = request.grant_type - if not state: + if state is None: self.state = request.state else: self.redirect_uri = None From 11833b611ed267e30c6746172815c7bc96f5777a Mon Sep 17 00:00:00 2001 From: Burke Livingston Date: Thu, 6 Oct 2022 01:47:12 -0700 Subject: [PATCH 018/115] Update pre-configured OIDC server to use OIDC flavor of Refresh Token grant type (#838) * Modify pre-configured OIDC server to use OIDC Refresh Token grant type * Add test coverage for OIDC refresh token grant type * Use longer variable names --- .../connect/core/endpoints/pre_configured.py | 6 ++-- .../core/endpoints/test_refresh_token.py | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/openid/connect/core/endpoints/test_refresh_token.py diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 8ce8bee6..1f4370ad 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -12,11 +12,13 @@ from oauthlib.oauth2.rfc6749.grant_types import ( AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant, - RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, + ResourceOwnerPasswordCredentialsGrant, ) from oauthlib.oauth2.rfc6749.tokens import BearerToken -from ..grant_types import AuthorizationCodeGrant, HybridGrant, ImplicitGrant +from ..grant_types import ( + AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant +) from ..grant_types.dispatchers import ( AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher, ImplicitTokenGrantDispatcher, diff --git a/tests/openid/connect/core/endpoints/test_refresh_token.py b/tests/openid/connect/core/endpoints/test_refresh_token.py new file mode 100644 index 00000000..9161f5a5 --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_refresh_token.py @@ -0,0 +1,32 @@ +"""Ensure that the server correctly uses the OIDC flavor of +the Refresh token grant type when appropriate. + +When the OpenID scope is provided, the refresh token response +should include a fresh ID token. +""" +import json +from unittest import mock + +from oauthlib.openid import RequestValidator +from oauthlib.openid.connect.core.endpoints.pre_configured import Server + +from tests.unittest import TestCase + + +class TestRefreshToken(TestCase): + + def setUp(self): + self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.get_id_token.return_value='id_token' + + self.server = Server(self.validator) + + def test_refresh_token_with_openid(self): + request_body = 'scope=openid+test_scope&grant_type=refresh_token&refresh_token=abc' + headers, body, status = self.server.create_token_response('', body=request_body) + self.assertIn('id_token', json.loads(body)) + + def test_refresh_token_no_openid(self): + request_body = 'scope=test_scope&grant_type=refresh_token&refresh_token=abc' + headers, body, status = self.server.create_token_response('', body=request_body) + self.assertNotIn('id_token', json.loads(body)) From 541297b344944d13c77f4ea0356b83bb3b381dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 18 Oct 2022 09:43:17 +0200 Subject: [PATCH 019/115] Update setup.cfg to use license_files (#839) Fixes the following warning: > The license_file parameter is deprecated, use license_files instead. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ca59291b..286d6cbe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -license_file = LICENSE +license_files = LICENSE [isort] combine_as_imports = true From 120c88e7aeae1a0f184c4aaf888cc0aef1c5f2b7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 17 Oct 2022 21:42:56 +0200 Subject: [PATCH 020/115] Add 3.2.2 version --- CHANGELOG.rst | 6 +++++- oauthlib/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e150100..82dbd75a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,15 @@ Changelog ========= +3.2.2 (2022-10-17) +------------------ +OAuth2.0 Provider: +* CVE-2022-36087 + 3.2.1 (2022-09-09) ------------------ OAuth2.0 Provider: * #803: Metadata endpoint support of non-HTTPS -* CVE-2022-36087 OAuth1.0: * #818: Allow IPv6 being parsed by signature diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 9b7eff2f..d9a5e38e 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.2.1' +__version__ = '3.2.2' logging.getLogger('oauthlib').addHandler(NullHandler()) From 761c1bf7b18c33abd24cdd7dd19b002a87f61866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindri=20Gu=C3=B0mundsson?= Date: Mon, 22 Aug 2022 16:32:14 +0000 Subject: [PATCH 021/115] Ensure expires_at is always int As discussed in #745 --- oauthlib/oauth2/rfc6749/clients/base.py | 4 +-- oauthlib/oauth2/rfc6749/parameters.py | 5 ++- tests/oauth2/rfc6749/clients/test_base.py | 33 +++++++++++++++++++ .../clients/test_service_application.py | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index d5eb0cc1..1d12638e 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -589,11 +589,11 @@ def populate_token_attributes(self, response): if 'expires_in' in response: self.expires_in = response.get('expires_in') - self._expires_at = time.time() + int(self.expires_in) + self._expires_at = round(time.time()) + int(self.expires_in) if 'expires_at' in response: try: - self._expires_at = int(response.get('expires_at')) + self._expires_at = round(float(response.get('expires_at'))) except: self._expires_at = None diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 8f6ce2c7..0f0f423a 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -345,7 +345,7 @@ def parse_implicit_response(uri, state=None, scope=None): params['scope'] = scope_to_list(params['scope']) if 'expires_in' in params: - params['expires_at'] = time.time() + int(params['expires_in']) + params['expires_at'] = round(time.time()) + int(params['expires_in']) if state and params.get('state', None) != state: raise ValueError("Mismatching or missing state in params.") @@ -437,6 +437,9 @@ def parse_token_response(body, scope=None): else: params['expires_at'] = time.time() + int(params['expires_in']) + if isinstance(params.get('expires_at'), float): + params['expires_at'] = round(params['expires_at']) + params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) return params diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index 70a22834..7286b991 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import datetime +from unittest.mock import patch from oauthlib import common from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError @@ -353,3 +354,35 @@ def test_create_code_challenge_s256(self): code_verifier = client.create_code_verifier(length=128) code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256') self.assertEqual(code_challenge_s256, client.code_challenge) + + def test_parse_token_response_expires_at_is_int(self): + expected_expires_at = 1661185149 + token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_at":1661185148.6437678,' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}') + + client = Client(self.client_id) + + response = client.parse_request_body_response(token_json, scope=["/profile"]) + + self.assertEqual(response['expires_at'], expected_expires_at) + self.assertEqual(client._expires_at, expected_expires_at) + + @patch('time.time') + def test_parse_token_response_generated_expires_at_is_int(self, t): + t.return_value = 1661185148.6437678 + expected_expires_at = round(t.return_value) + 3600 + token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_in":3600,' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}') + + client = Client(self.client_id) + + response = client.parse_request_body_response(token_json, scope=["/profile"]) + + self.assertEqual(response['expires_at'], expected_expires_at) + self.assertEqual(client._expires_at, expected_expires_at) diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py index b97d8554..84361d8b 100644 --- a/tests/oauth2/rfc6749/clients/test_service_application.py +++ b/tests/oauth2/rfc6749/clients/test_service_application.py @@ -166,7 +166,7 @@ def test_request_body_no_initial_private_key(self, t): @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 + self.token['expires_at'] = self.token['expires_in'] + round(t.return_value) client = ServiceApplicationClient(self.client_id) From ae242d89100be709e6b137594e45cf8df8eb634e Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 30 Aug 2022 10:13:24 +0600 Subject: [PATCH 022/115] create security policy --- SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ddb8632d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +following versions are currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 3.2.x | :white_check_mark: | +| 3.1.x | :x: | +| < 3.2.0 | :x: | + +## Reporting a Vulnerability +Contact auvipy@gmail.com for reporting any vulnerability. From 38b2b4198d2a6b6232b68dab347618d4bc0a6a35 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 23 Aug 2023 19:51:38 +0200 Subject: [PATCH 023/115] Fix failing GitHub Action lint_python.yml --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index ba4d6c92..7cb2b28e 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.x - - run: pip install --upgrade pip wheel + - run: pip install --upgrade pip setuptools wheel - run: pip install bandit black codespell flake8 flake8-2020 flake8-bugbear flake8-comprehensions isort mypy pytest pyupgrade safety - run: bandit --recursive --skip B101,B105,B106,B107,B324 . From 2c9811b7c9180270412b2420d0b54b4cb4afa791 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 24 Aug 2023 14:36:08 +0200 Subject: [PATCH 024/115] Add graphviz to generate dot graphs in docs --- .github/workflows/python-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index e2d4dfdd..d20d5b38 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -28,6 +28,8 @@ jobs: toxenv: ["docs", "readme"] runs-on: ubuntu-latest steps: + - name: Install dot + run: sudo apt install -y graphviz - name: Set up Python uses: actions/setup-python@v4 with: From a2c70e457a11287d0649a71877ed451c55821d34 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 24 Aug 2023 15:55:45 +0100 Subject: [PATCH 025/115] Remove unnecessary name Co-authored-by: Christian Clauss --- .github/workflows/python-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index d20d5b38..c7af97ad 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -28,8 +28,7 @@ jobs: toxenv: ["docs", "readme"] runs-on: ubuntu-latest steps: - - name: Install dot - run: sudo apt install -y graphviz + - run: sudo apt install -y graphviz - name: Set up Python uses: actions/setup-python@v4 with: From 8b33f83fdcd5aed1d319534161dcb4bca73e28ed Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 24 Aug 2023 17:45:46 +0200 Subject: [PATCH 026/115] Lint with ruff to replace bandit, flake8, isort, pyupgrade (#855) * Lint with ruff to replace bandit, flake8, isort, pyupgrade * Readability fixes --- .github/workflows/lint_python.yml | 11 +- docs/conf.py | 2 +- examples/__init__.py | 0 oauthlib/__init__.py | 20 ++-- oauthlib/common.py | 5 +- oauthlib/oauth1/rfc5849/__init__.py | 5 +- oauthlib/oauth1/rfc5849/endpoints/base.py | 11 +- oauthlib/oauth1/rfc5849/signature.py | 6 +- oauthlib/oauth1/rfc5849/utils.py | 5 +- oauthlib/oauth2/rfc6749/clients/base.py | 8 +- .../rfc6749/clients/mobile_application.py | 2 +- .../oauth2/rfc6749/clients/web_application.py | 6 +- oauthlib/oauth2/rfc6749/endpoints/base.py | 4 +- .../oauth2/rfc6749/endpoints/introspect.py | 2 +- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 4 +- oauthlib/oauth2/rfc6749/errors.py | 1 - .../rfc6749/grant_types/authorization_code.py | 7 +- oauthlib/oauth2/rfc6749/grant_types/base.py | 7 +- .../rfc6749/grant_types/client_credentials.py | 9 +- .../oauth2/rfc6749/grant_types/implicit.py | 5 +- .../resource_owner_password_credentials.py | 11 +- oauthlib/oauth2/rfc6749/parameters.py | 20 ++-- oauthlib/oauth2/rfc6749/request_validator.py | 4 +- oauthlib/oauth2/rfc6749/tokens.py | 10 +- oauthlib/openid/connect/core/exceptions.py | 4 +- .../openid/connect/core/grant_types/hybrid.py | 11 +- oauthlib/openid/connect/core/tokens.py | 5 +- oauthlib/signals.py | 5 +- oauthlib/uri_validate.py | 3 +- ruff.toml | 107 ++++++++++++++++++ setup.py | 7 +- tests/oauth1/rfc5849/endpoints/test_base.py | 2 +- tests/oauth1/rfc5849/test_signatures.py | 59 +++++----- .../oauth2/rfc6749/endpoints/test_metadata.py | 4 +- tests/oauth2/rfc6749/test_utils.py | 2 +- 35 files changed, 227 insertions(+), 147 deletions(-) create mode 100644 examples/__init__.py create mode 100644 ruff.toml diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 7cb2b28e..f50e8aa9 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -8,20 +8,15 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.x + check-latest: true - run: pip install --upgrade pip setuptools wheel - - run: pip install bandit black codespell flake8 flake8-2020 flake8-bugbear - flake8-comprehensions isort mypy pytest pyupgrade safety - - run: bandit --recursive --skip B101,B105,B106,B107,B324 . + - run: pip install black codespell mypy pytest ruff safety + - run: ruff --format=github . - run: black --check . || true - run: codespell # --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-test.txt - run: pip install --editable . - run: mkdir --parents --verbose .mypy_cache - run: mypy --ignore-missing-imports --install-types --non-interactive . || true - run: pytest - - run: shopt -s globstar && pyupgrade --py37-plus **/*.py || true - run: safety check diff --git a/docs/conf.py b/docs/conf.py index f4b92c47..05e93ee0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,7 +54,7 @@ # # The short X.Y version. -from oauthlib import __version__ as v +from oauthlib import __version__ as v # noqa: E402 version = v[:3] # The full version, including alpha/beta/rc tags. release = v diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index d9a5e38e..4f56ef14 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -19,16 +19,16 @@ _DEBUG = False def set_debug(debug_val): - """Set value of debug flag - + """Set value of debug flag + :param debug_val: Value to set. Must be a bool value. - """ - global _DEBUG - _DEBUG = debug_val + """ + global _DEBUG # noqa: PLW0603 + _DEBUG = debug_val def get_debug(): - """Get debug mode value. - - :return: `True` if debug mode is on, `False` otherwise - """ - return _DEBUG + """Get debug mode value. + + :return: `True` if debug mode is on, `False` otherwise + """ + return _DEBUG diff --git a/oauthlib/common.py b/oauthlib/common.py index 395e75ef..9d850b9a 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -34,7 +34,7 @@ always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' - '0123456789' '_.-') + '0123456789_.-') log = logging.getLogger('oauthlib') @@ -346,7 +346,8 @@ class Request: 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 + def encode(x): + return to_unicode(x, encoding) if encoding else x self.uri = encode(uri) self.http_method = encode(http_method) diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index c559251f..85e0b90b 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -121,7 +121,8 @@ def __init__(self, client_key, :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 + def encode(x): + return to_unicode(x, encoding) if encoding else x self.client_key = encode(client_key) self.client_secret = encode(client_secret) @@ -219,7 +220,7 @@ def get_oauth_params(self, request): 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'))) + params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) # noqa: S324 return params diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index 7831be7c..8d3d89c6 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -69,12 +69,10 @@ def _get_signature_type_and_params(self, request): 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"]): + if "Content-Type" in headers and CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]: # noqa: SIM108 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)) @@ -129,8 +127,7 @@ def _check_mandatory_parameters(self, request): # 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): + if (request.signature_method not in self.request_validator.allowed_signature_methods): raise errors.InvalidSignatureMethodError( description="Invalid signature, {} not in {!r}.".format( request.signature_method, @@ -180,9 +177,7 @@ def _check_mandatory_parameters(self, request): def _check_signature(self, request, is_token_request=False): # ---- RSA Signature verification ---- - if request.signature_method == SIGNATURE_RSA_SHA1 or \ - request.signature_method == SIGNATURE_RSA_SHA256 or \ - request.signature_method == SIGNATURE_RSA_SHA512: + if request.signature_method in {SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512}: # RSA-based signature method # The server verifies the signature per `[RFC3447] section 8.2.2`_ diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 9cb1a517..8916782b 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -45,6 +45,7 @@ from oauthlib.common import extract_params, safe_string_equals, urldecode from . import utils +import contextlib log = logging.getLogger(__name__) @@ -188,10 +189,9 @@ def base_string_uri(uri: str, host: str = None) -> str: raise ValueError('missing host') # NOTE: Try guessing if we're dealing with IP or hostname - try: + with contextlib.suppress(ValueError): hostname = ipaddress.ip_address(hostname) - except ValueError: - pass + if isinstance(hostname, ipaddress.IPv6Address): hostname = f"[{hostname}]" diff --git a/oauthlib/oauth1/rfc5849/utils.py b/oauthlib/oauth1/rfc5849/utils.py index 8fb8302e..0915105b 100644 --- a/oauthlib/oauth1/rfc5849/utils.py +++ b/oauthlib/oauth1/rfc5849/utils.py @@ -30,7 +30,8 @@ def wrapper(params, *args, **kwargs): 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_") + def is_oauth(kv): + return kv[0].startswith('oauth_') if isinstance(params, dict): return list(filter(is_oauth, list(params.items()))) else: @@ -59,7 +60,7 @@ def unescape(u): return unquote(u) -def parse_keqv_list(l): +def parse_keqv_list(l): # noqa: E741 """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) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 1d12638e..cdcfa55a 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -207,7 +207,7 @@ def add_token(self, uri, http_method='GET', body=None, headers=None, case_insensitive_token_types = { k.lower(): v for k, v in self.token_types.items()} - if not self.token_type.lower() in case_insensitive_token_types: + if self.token_type.lower() not 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')): @@ -466,7 +466,7 @@ def _add_bearer_token(self, uri, http_method='GET', body=None, return uri, headers, body def create_code_verifier(self, length): - """Create PKCE **code_verifier** used in computing **code_challenge**. + """Create PKCE **code_verifier** used in computing **code_challenge**. See `RFC7636 Section 4.1`_ :param length: REQUIRED. The length of the code_verifier. @@ -530,10 +530,10 @@ def create_code_challenge(self, code_verifier, code_challenge_method=None): """ code_challenge = None - if code_verifier == None: + if code_verifier is None: raise ValueError("Invalid code_verifier") - if code_challenge_method == None: + if code_challenge_method is None: code_challenge_method = "plain" self.code_challenge_method = code_challenge_method code_challenge = code_verifier diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index b10b41ce..023cf236 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -43,7 +43,7 @@ class MobileApplicationClient(Client): 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, diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 50890fbf..3bf94c4b 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -33,7 +33,7 @@ class WebApplicationClient(Client): 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): @@ -62,8 +62,8 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in `Section 10.12`_. - :param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced. - A challenge derived from the code_verifier that is sent in the + :param code_challenge: OPTIONAL. PKCE parameter. REQUIRED if PKCE is enforced. + A challenge derived from the code_verifier that is sent in the authorization request, to be verified against later. :param code_challenge_method: OPTIONAL. PKCE parameter. A method that was used to derive code challenge. diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index 3f239917..987fac6a 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -32,7 +32,7 @@ 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): @@ -40,7 +40,7 @@ def available(self): @available.setter def available(self, available): - self._available = available + self._available = available @property def catch_errors(self): diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 3cc61e66..ef73988d 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -74,7 +74,7 @@ def create_introspect_response(self, uri, http_method='POST', body=None, request ) if claims is None: - return resp_headers, json.dumps(dict(active=False)), 200 + return resp_headers, json.dumps({'active': False}), 200 if "active" in claims: claims.pop("active") return resp_headers, json.dumps(dict(active=True, **claims)), 200 diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index a2820f28..34274cba 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -38,9 +38,9 @@ class MetadataEndpoint(BaseEndpoint): """ def __init__(self, endpoints, claims={}, raise_errors=True): - assert isinstance(claims, dict) + assert isinstance(claims, dict) # noqa: S101 for endpoint in endpoints: - assert isinstance(endpoint, BaseEndpoint) + assert isinstance(endpoint, BaseEndpoint) # noqa: S101 BaseEndpoint.__init__(self) self.raise_errors = raise_errors diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 6fcb7519..3b415748 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -150,7 +150,6 @@ class FatalClientError(OAuth2Error): Instead the user should be informed of the error by the provider itself. """ - pass class InvalidRequestFatalError(FatalClientError): diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 858855a1..09dc6199 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -387,7 +387,7 @@ def validate_authorization_request(self, request): 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': + elif 'code' not 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, @@ -400,9 +400,8 @@ def validate_authorization_request(self, 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 self.request_validator.is_pkce_required(request.client_id, request) is True and request.code_challenge is None: + raise errors.MissingCodeChallengeError(request=request) if request.code_challenge is not None: request_info["code_challenge"] = request.code_challenge diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index ca343a11..d96a2db4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -143,7 +143,7 @@ def add_token(self, token, token_handler, 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"]: + if request.response_type not in ["token", "code token", "id_token token", "code id_token token"]: return token token.update(token_handler.create_token(request, refresh_token=False)) @@ -199,10 +199,7 @@ def prepare_authorization_response(self, request, token, headers, body, status): if request.response_type == 'none': state = token.get('state', None) - if state: - token_items = [('state', state)] - else: - token_items = [] + token_items = [('state', state)] if state else [] if request.response_mode == 'query': headers['Location'] = add_params_to_uri( diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index e7b46189..35c54402 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -107,11 +107,10 @@ def validate_token_request(self, 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.') + elif 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) diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 6110b6f3..cd3bfeb6 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -233,10 +233,7 @@ def create_token_response(self, request, token_handler): # 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 = {} + token = token_handler.create_token(request, refresh_token=False) if 'token' in request.response_type.split() else {} if request.state is not None: token['state'] = request.state diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index 4b0de5bf..55d92870 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -180,12 +180,11 @@ def validate_token_request(self, request): 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.') + elif 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 diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 0f0f423a..f7378d81 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -45,10 +45,10 @@ 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 parameter. A challenge derived from the - code_verifier that is sent in the authorization + :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 + :param code_challenge_method: PKCE parameter. A method that was used to derive the code_challenge. Defaults to "plain" if not present in the request. :param kwargs: Extra arguments to embed in the grant/authorization URL. @@ -150,9 +150,8 @@ def prepare_token_request(grant_type, body='', include_client_id=True, code_veri # pull the `client_id` out of the kwargs. client_id = kwargs.pop('client_id', None) - if include_client_id: - if client_id is not None: - params.append(('client_id', client_id)) + if include_client_id and client_id is not None: + params.append(('client_id', client_id)) # use code_verifier if code_challenge was passed in the authorization request if code_verifier is not None: @@ -280,7 +279,7 @@ def parse_authorization_code_response(uri, state=None): if 'error' in params: raise_from_error(params.get('error'), params) - if not 'code' in params: + if 'code' not in params: raise MissingCodeError("Missing code parameter in response.") return params @@ -450,12 +449,11 @@ def validate_token_parameters(params): if 'error' in params: raise_from_error(params.get('error'), params) - if not 'access_token' in params: + if 'access_token' not 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 'token_type' not in params and 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 diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 3910c0b9..6d6ebaa8 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -48,12 +48,12 @@ def authenticate_client(self, request, *args, **kwargs): Headers may be accesses through request.headers and parameters found in both body and query can be obtained by direct attribute access, i.e. request.client_id for client_id in the URL query. - + The authentication process is required to contain the identification of the client (i.e. search the database based on the client_id). In case the client doesn't exist based on the received client_id, this method has to return False and the HTTP response created by the library will contain - 'invalid_client' message. + 'invalid_client' message. After the client identification succeeds, this method needs to set the client on the request, i.e. request.client = client. A client object's diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 0757d07e..2e8c9c4a 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -123,10 +123,7 @@ def prepare_mac_header(token, uri, key, http_method, sch, net, path, par, query, fra = urlparse(uri) - if query: - request_uri = path + '?' + query - else: - request_uri = path + request_uri = path + '?' + query if query else path # Hash the body/payload if body is not None and draft == 0: @@ -305,10 +302,7 @@ def create_token(self, request, refresh_token=False, **kwargs): "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 + expires_in = self.expires_in(request) if callable(self.expires_in) else self.expires_in request.expires_in = expires_in diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py index 099b84e2..8a3e79f4 100644 --- a/oauthlib/openid/connect/core/exceptions.py +++ b/oauthlib/openid/connect/core/exceptions.py @@ -72,8 +72,8 @@ class InvalidRequestURI(OpenIDClientError): contains invalid data. """ error = 'invalid_request_uri' - description = 'The request_uri in the Authorization Request returns an ' \ - 'error or contains invalid data.' + description = ('The request_uri in the Authorization Request returns an ' + 'error or contains invalid data.') class InvalidRequestObject(OpenIDClientError): diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py index 7cb0758b..9c1fc702 100644 --- a/oauthlib/openid/connect/core/grant_types/hybrid.py +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -54,10 +54,9 @@ def openid_authorization_validator(self, request): # 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.' - ) + if request.response_type in ["code id_token", "code id_token token"] and not request.nonce: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) return request_info diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py index 936ab52e..3ab35492 100644 --- a/oauthlib/openid/connect/core/tokens.py +++ b/oauthlib/openid/connect/core/tokens.py @@ -27,10 +27,7 @@ def __init__(self, request_validator=None, token_generator=None, 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 + expires_in = self.expires_in(request) if callable(self.expires_in) else self.expires_in request.expires_in = expires_in diff --git a/oauthlib/signals.py b/oauthlib/signals.py index 8fd347a5..9538d098 100644 --- a/oauthlib/signals.py +++ b/oauthlib/signals.py @@ -7,7 +7,7 @@ try: from blinker import Namespace signals_available = True -except ImportError: # noqa +except ImportError: class Namespace: def signal(self, name, doc=None): return _FakeSignal(name, doc) @@ -26,7 +26,8 @@ def _fail(self, *args, **kwargs): raise RuntimeError('signalling support is unavailable ' 'because the blinker library is ' 'not installed.') - send = lambda *a, **kw: None + def send(*a, **kw): + return None connect = disconnect = has_receivers_for = receivers_for = \ temporarily_connected_to = connected_to = _fail del _fail diff --git a/oauthlib/uri_validate.py b/oauthlib/uri_validate.py index a6fe0fb2..69d2c950 100644 --- a/oauthlib/uri_validate.py +++ b/oauthlib/uri_validate.py @@ -174,8 +174,7 @@ 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( -) +absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals() # noqa: N816 def is_uri(uri): diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..443f123e --- /dev/null +++ b/ruff.toml @@ -0,0 +1,107 @@ +# NOTE: You have to use single-quoted strings in TOML for regular expressions. +# It's the equivalent of r-strings in Python. Multiline strings are treated as +# verbose regular expressions by Black. Use [ ] to denote a significant space +# character. + +# When switching from ruff.toml to pyproject.toml, use the section names that +# start with [tool.ruff + +# [tool.ruff] +select = [ + "A", # flake8-builtins + "AIR", # Airflow + "ASYNC", # flake8-async + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "CPY", # flake8-copyright + "DJ", # flake8-django + "E", # pycodestyle + "EXE", # flake8-executable + "F", # Pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "NPY", # NumPy-specific rules + "PD", # pandas-vet + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # Pylint + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TCH", # flake8-type-checking + "W", # pycodestyle + "YTT", # flake8-2020 + # "ANN", # flake8-annotations + # "ARG", # flake8-unused-arguments + # "B", # flake8-bugbear + # "COM", # flake8-commas + # "D", # pydocstyle + # "DTZ", # flake8-datetimez + # "EM", # flake8-errmsg + # "ERA", # eradicate + # "FBT", # flake8-boolean-trap + # "FIX", # flake8-fixme + # "G", # flake8-logging-format + # "I", # isort + # "PTH", # flake8-use-pathlib + # "Q", # flake8-quotes + # "RET", # flake8-return + # "SLF", # flake8-self + # "TD", # flake8-todos + # "TID", # flake8-tidy-imports + # "TRY", # tryceratops + # "UP", # pyupgrade +] +ignore = [ + "F401", + "F403", + "F405", + "F841", + "FLY002", + "N806", + "N818", + "PERF401", + "PLW2901", + "PT008", + "RSE102", + "RUF005", + "RUF012", + "RUF013", + "S105", + "S106", + "S107", +] +line-length = 255 +target-version = "py37" + +# [tool.ruff.mccabe] +[mccabe] +max-complexity = 24 # default is 10 + +# [tool.ruff.per-file-ignores] +[per-file-ignores] +"docs/conf.py" = ["A001", "INP001"] +"oauthlib/oauth2/rfc6749/clients/base.py" = ["E722"] +"oauthlib/oauth2/rfc6749/endpoints/base.py" = ["BLE001"] +"oauthlib/openid/connect/core/grant_types/base.py" = ["BLE001"] +"tests/*" = ["PT009", "PT027", "S101"] + +# [tool.ruff.pylint] +[pylint] +allow-magic-value-types = ["int", "str"] +max-args = 16 # default is 5 +max-branches = 24 # default is 12 +max-statements = 56 # default is 50 diff --git a/setup.py b/setup.py index 4c435f9a..ab28d801 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ +#!/usr/bin/env python3 # Hack because logging + setuptools sucks. -try: +import contextlib +with contextlib.suppress(ImportError): import multiprocessing -except ImportError: - pass + from os.path import dirname, join diff --git a/tests/oauth1/rfc5849/endpoints/test_base.py b/tests/oauth1/rfc5849/endpoints/test_base.py index e87f359b..792aaccf 100644 --- a/tests/oauth1/rfc5849/endpoints/test_base.py +++ b/tests/oauth1/rfc5849/endpoints/test_base.py @@ -304,7 +304,7 @@ def dummy_access_token(self): 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 + return (client_key, nonce, timestamp, resource_owner_key) not in self.nonces def validate_client_key(self, client_key): return client_key in self.clients diff --git a/tests/oauth1/rfc5849/test_signatures.py b/tests/oauth1/rfc5849/test_signatures.py index 2d4735ea..0dd2859a 100644 --- a/tests/oauth1/rfc5849/test_signatures.py +++ b/tests/oauth1/rfc5849/test_signatures.py @@ -82,12 +82,13 @@ class SignatureTests(TestCase): # ==== Example test vector ======================================= - eg_signature_base_string =\ - 'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q' \ - '%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_' \ - 'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m' \ - 'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk' \ + eg_signature_base_string = ( + 'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q' + '%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_' + 'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m' + 'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk' '9d7dh3k39sjv7' + ) # The _signature base string_ above is copied from the end of # RFC 5849 section 3.4.1.1. @@ -101,11 +102,11 @@ class SignatureTests(TestCase): eg_base_string_uri = 'http://example.com/request' - eg_normalized_parameters =\ - 'a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj' \ - 'dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1' \ + eg_normalized_parameters = ( + 'a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj' + 'dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1' '&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7' - + ) # The above _normalized parameters_ corresponds to the parameters below. # # The parameters below is copied from the table at the end of @@ -133,12 +134,12 @@ class SignatureTests(TestCase): eg_body = 'c2&a3=2+q' - eg_authorization_header =\ - 'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",' \ - ' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",' \ - ' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",' \ + eg_authorization_header = ( + 'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",' + ' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",' + ' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",' ' oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"' - + ) # ==== Signature base string calculating function tests ========== def test_signature_base_string(self): @@ -465,10 +466,10 @@ def test_normalize_parameters(self): expected_signature_hmac_sha256 = \ 'wdfdHUKXHbOnOGZP8WFAWMSAmWzN3EVBWWgXGlC/Eo4=' - expected_signature_hmac_sha512 = \ - 'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz' \ + expected_signature_hmac_sha512 = ( + 'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz' '1fGONffMpdZxUkUTW3k1kg==' - + ) def test_sign_hmac_sha1_with_client(self): """ Test sign and verify with HMAC-SHA1. @@ -632,21 +633,21 @@ def test_hmac_false_positives(self): # Note: the "echo -n" is needed to remove the last newline character, which # most text editors will add. - expected_signature_rsa_sha1 = \ - 'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738' \ - 'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q' \ + expected_signature_rsa_sha1 = ( + 'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738' + 'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q' 'yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E=' - - expected_signature_rsa_sha256 = \ - 'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR' \ - 'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK' \ + ) + expected_signature_rsa_sha256 = ( + 'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR' + 'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK' '/N7s9M1WMnfZZ+1dQbbwtTs1TG1+iexUcV7r3M7Heec=' - - expected_signature_rsa_sha512 = \ - 'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL' \ - 'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm' \ + ) + expected_signature_rsa_sha512 = ( + 'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL' + 'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm' 'g4JXdWCGXG8YRvRJTg+QKhXuXwS6TcMNakrgzgFIVhA=' - + ) def test_sign_rsa_sha1_with_client(self): """ Test sign and verify with RSA-SHA1. diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index 1f5b9121..bcb9c0f5 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -130,8 +130,8 @@ def test_server_metadata(self): } def sort_list(claims): - for k in claims.keys(): - claims[k] = sorted(claims[k]) + for key, value in claims.items(): + claims[key] = sorted(value) sort_list(metadata.claims) sort_list(expected_claims) diff --git a/tests/oauth2/rfc6749/test_utils.py b/tests/oauth2/rfc6749/test_utils.py index 32995919..8417fe56 100644 --- a/tests/oauth2/rfc6749/test_utils.py +++ b/tests/oauth2/rfc6749/test_utils.py @@ -78,7 +78,7 @@ def test_list_to_scope(self): for x in string_list: assert x in set_scope - self.assertRaises(ValueError, list_to_scope, object()) + self.assertRaises(ValueError, list_to_scope, object()) def test_scope_to_list(self): expected = ['foo', 'bar', 'baz'] From 4a7db54f005686128102d7f7ac5c3d783c244669 Mon Sep 17 00:00:00 2001 From: Erich Seifert Date: Thu, 27 Oct 2022 11:43:20 +0200 Subject: [PATCH 027/115] Add classifier for Python 3.11 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ab28d801..cec2bfdb 100755 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ def fread(fn): 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', From 3776064680b0d922993a00f765413ebbb57205fe Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sat, 26 Aug 2023 14:30:53 +0200 Subject: [PATCH 028/115] Changed build badge in README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index eb8c452d..be92928a 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,9 @@ OAuthLib - Python Framework for OAuth1 & OAuth2 *A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for Python 3.6+.* -.. image:: https://app.travis-ci.com/oauthlib/oauthlib.svg?branch=master - :target: https://app.travis-ci.com/oauthlib/oauthlib - :alt: Travis +.. image:: https://github.com/github/docs/actions/workflows/python-build.yml/badge.svg + :target: https://github.com/oauthlib/oauthlib/actions + :alt: GitHub Actions .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/oauthlib/oauthlib :alt: Coveralls From d6d230a5fac13b77d1fbb9763818301e70d876a7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sat, 26 Aug 2023 14:34:22 +0200 Subject: [PATCH 029/115] Fix Build Badge URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index be92928a..8e288b63 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ OAuthLib - Python Framework for OAuth1 & OAuth2 *A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for Python 3.6+.* -.. image:: https://github.com/github/docs/actions/workflows/python-build.yml/badge.svg +.. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg :target: https://github.com/oauthlib/oauthlib/actions :alt: GitHub Actions .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master From 0fc850ea33f405aaca27b9bea4b779cd38ec6904 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sun, 27 Aug 2023 12:40:31 +0600 Subject: [PATCH 030/115] Update supported python versions in classifier --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index cec2bfdb..b5c2eebc 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def fread(fn): platforms='any', license='BSD-3-Clause', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), - python_requires='>=3.6', + python_requires='>=3.8', extras_require={ 'rsa': rsa_require, 'signedtoken': signedtoken_require, @@ -52,8 +52,6 @@ def fread(fn): 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', From 5bac97eca82af06c6013b9aba7f9af952859d3a1 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sun, 27 Aug 2023 12:50:42 +0600 Subject: [PATCH 031/115] Update .github/workflows/dependency-review.yml --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index fe461b42..b0dedc42 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,4 +17,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@v3 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v3 From 3dda26bd2bd19c5c6f992812fd81995ca7feca7c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 27 Aug 2023 07:12:29 -0600 Subject: [PATCH 032/115] Add support for Python 3.12 (#859) * Update docs to match supported versions * Add support for Python 3.12 * Add colour to CI logs for readability * Also test PRs and add manual trigger --- .github/workflows/python-build.yml | 6 ++++-- docs/contributing.rst | 2 +- setup.py | 1 + tox.ini | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index c7af97ad..0f7ddf1b 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -1,19 +1,21 @@ name: Python Tests run-name: Run Tests by ${{ github.actor }} -on: [push] +on: [push, pull_request, workflow_dispatch] jobs: tests: env: + FORCE_COLOR: 1 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} strategy: matrix: - python: ["3.8", "3.9", "3.10", "3.11"] + python: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest steps: - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + allow-prereleases: true - name: Check out repository code uses: actions/checkout@v3 - name: Install prereq diff --git a/docs/contributing.rst b/docs/contributing.rst index 19ff9c9c..e7d12173 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -152,7 +152,7 @@ request that fails this test suite will be **rejected**. Testing multiple versions of Python ----------------------------------- -OAuthLib supports Python 3.5, 3.6, 3.7 and PyPy 2.7 & PyPy 3. Testing +OAuthLib supports Python 3.6+ and PyPy3. Testing all versions conveniently can be done using `Tox`_. .. sourcecode:: bash diff --git a/setup.py b/setup.py index cec2bfdb..8752eba8 100755 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def fread(fn): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', diff --git a/tox.ini b/tox.ini index 3195c771..229dd9df 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # require tox>=4 [tox] -envlist = py38,py39,py310,py311,pypy3,docs,readme,bandit,isort +envlist = py38,py39,py310,py311,py312,pypy3,docs,readme,bandit,isort [testenv] deps= From af7c6aa8abcd8e375a773c60218adca52e808ad3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 26 Aug 2023 16:11:24 +0300 Subject: [PATCH 033/115] CI: Only attempt upload for upstream --- .github/workflows/python-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 89262709..43417ee5 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -7,6 +7,7 @@ on: jobs: pypi-publish: if: | + github.repository_owner == 'oauthlib' && ${{ github.event.workflow_run.conclusion == 'success' }} && ${{ github.ref_type == 'tag' }} name: Upload release to PyPI From b216ce1add7321a88153c2263a556874cbc024ff Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 28 Aug 2023 23:48:41 +0200 Subject: [PATCH 034/115] Lint with ruff to replace bandit, flake8, isort, pyupgrade --- .github/workflows/lint_python.yml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index fe654d9f..f50e8aa9 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -8,20 +8,15 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.x + check-latest: true - run: pip install --upgrade pip setuptools wheel - - run: pip install bandit black codespell flake8 flake8-2020 flake8-bugbear - flake8-comprehensions isort mypy pytest pyupgrade safety - - run: bandit --recursive --skip B101,B105,B106,B107,B324 . + - run: pip install black codespell mypy pytest ruff safety + - run: ruff --format=github . - 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: codespell # --ignore-words-list="" --skip="*.css,*.js,*.lock" + - run: pip install -r requirements-test.txt + - run: pip install --editable . - 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: pytest - run: safety check From 00f9a212004a80df790ed071a59af53a05f5e3f2 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Fri, 11 Aug 2023 09:35:16 -0700 Subject: [PATCH 035/115] Ensure that `request.client_id` is set during Refresh Token Grant. --- .../oauth2/rfc6749/grant_types/refresh_token.py | 3 +++ .../rfc6749/grant_types/test_refresh_token.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index ce33df0e..43bf55ac 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -101,6 +101,9 @@ def validate_token_request(self, request): if not self.request_validator.authenticate_client(request): log.debug('Invalid client (%r), denying access.', request) raise errors.InvalidClientError(request=request) + # Ensure that request.client_id is set. + if request.client_id is None and request.client is not None: + request.client_id = request.client.client_id elif not self.request_validator.authenticate_client_id(request.client_id, request): log.debug('Client authentication failed, %r.', request) raise errors.InvalidClientError(request=request) diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py index 581f2a4d..0a4ddd9a 100644 --- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py +++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py @@ -130,6 +130,22 @@ def test_authentication_required(self): self.request) self.mock_validator.client_authentication_required.assert_called_once_with(self.request) + + def test_authentication_required_populate_client_id(self): + """ + Make sure that request.client_id is populated from + request.client.client_id if None. + + """ + self.mock_validator.client_authentication_required.return_value = True + self.mock_validator.authenticate_client.return_value = True + # self.mock_validator.authenticate_client_id.return_value = False + # self.request.code = 'waffles' + self.request.client_id = None + self.request.client.client_id = 'foobar' + self.auth.validate_token_request(self.request) + self.request.client_id = 'foobar' + def test_invalid_grant_type(self): self.request.grant_type = 'wrong_type' self.assertRaises(errors.UnsupportedGrantTypeError, From eddb461c1043f8ad583fd67af34749e32fdc19cc Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 3 Sep 2023 22:08:57 +0200 Subject: [PATCH 036/115] Tox use ruff (#864) * Tox: Use ruff * Tox: Use ruff --- Makefile | 10 +++++----- tox.ini | 21 ++++++--------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 2d2e1ed7..28081659 100644 --- a/Makefile +++ b/Makefile @@ -34,11 +34,11 @@ clean-build: @rm -fr dist/ @rm -fr *.egg-info -format fmt: - isort --recursive oauthlib tests +format fmt black: + black . -lint: - isort --recursive --check-only --diff oauthlib tests +lint ruff: + ruff . test: tox @@ -81,4 +81,4 @@ dance: .DEFAULT_GOAL := all .PHONY: clean test bottle dance django flask requests -all: test bottle dance django flask requests +all: lint test bottle dance django flask requests diff --git a/tox.ini b/tox.ini index 229dd9df..fb476a12 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # require tox>=4 [tox] -envlist = py38,py39,py310,py311,py312,pypy3,docs,readme,bandit,isort +envlist = py38,py39,py310,py311,py312,pypy3,docs,readme,ruff [testenv] deps= @@ -9,7 +9,6 @@ commands= pytest --cov=oauthlib tests/ # 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.11 skipsdist=True @@ -28,16 +27,8 @@ allowlist_externals=echo commands= twine check .tox/.pkg/dist/* -[testenv:bandit] -basepython=python3.11 -skipsdist=True -deps=bandit -commands=bandit -b bandit.json -r oauthlib/ -allowlist_externals=bandit - -[testenv:isort] -basepython = python3.8 -usedevelop = false -deps = isort -changedir = {toxinidir} -commands = isort --recursive --check-only --diff oauthlib tests +[testenv:ruff] +deps=ruff +allowlist_externals=ruff +skip_install=true +commands=ruff . From fe020db74199d5284c00d7735aa7d4ddc90f5d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 24 Oct 2023 15:08:10 +0200 Subject: [PATCH 037/115] Make UtilsTests.test_filter_params Python 3.13+ compatible Since Python 3.13.0a1, docstrings are automatically dedented. See https://github.com/python/cpython/issues/81283 and https://docs.python.org/3.13/whatsnew/3.13.html#other-language-changes As a result, using a docstring with leading space as a test case breaks the test assumption. The initial commit which introduced this test a decade ago (6c0c7914f3a57823834b1be492b307992f943629) does not specify why testing the spaces is important. --- tests/oauth1/rfc5849/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/oauth1/rfc5849/test_utils.py b/tests/oauth1/rfc5849/test_utils.py index 013c71a9..22128908 100644 --- a/tests/oauth1/rfc5849/test_utils.py +++ b/tests/oauth1/rfc5849/test_utils.py @@ -53,11 +53,11 @@ 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 """ + """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 ") + 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' From dcbcbeff6c1ff5775e372b4ce0b0d0223c8cae29 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 22 Dec 2023 23:15:19 +0100 Subject: [PATCH 038/115] Add dependency with CVE to trigger security check --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c3c427e3..3515dbc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pyjwt>=2.0.0,<3 blinker==1.4 cryptography>=3.0.0 +setuptools==65.5.1 From e4ffc08930c70f8379897eba497a2adbacd17f59 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 22 Dec 2023 23:18:47 +0100 Subject: [PATCH 039/115] Trigger security check failure --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3515dbc5..ac50b73b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pyjwt>=2.0.0,<3 blinker==1.4 cryptography>=3.0.0 -setuptools==65.5.1 +setuptools==65.5.0 From 7e3462ca59372c382a61d33d64c9a1ace54b2e99 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 22 Dec 2023 23:32:19 +0100 Subject: [PATCH 040/115] Revert "Trigger security check failure" This reverts commit e4ffc08930c70f8379897eba497a2adbacd17f59. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac50b73b..3515dbc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pyjwt>=2.0.0,<3 blinker==1.4 cryptography>=3.0.0 -setuptools==65.5.0 +setuptools==65.5.1 From dbda1ea43cd1cc157a1dc563b9220ddc00546694 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 22 Dec 2023 23:32:25 +0100 Subject: [PATCH 041/115] Revert "Add dependency with CVE to trigger security check" This reverts commit dcbcbeff6c1ff5775e372b4ce0b0d0223c8cae29. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3515dbc5..c3c427e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ pyjwt>=2.0.0,<3 blinker==1.4 cryptography>=3.0.0 -setuptools==65.5.1 From b80f6b5d712fb72fef70f45840efcfce8cc2cc7e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 22 Dec 2023 23:35:39 +0100 Subject: [PATCH 042/115] Bump actions/checkout Co-authored-by: Christian Clauss --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index b0dedc42..4e751977 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' uses: actions/dependency-review-action@v3 From c36e38c0fedcf097eb0368db42c3623bb2fd4bf1 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 22 Dec 2023 23:56:53 +0100 Subject: [PATCH 043/115] Update python-build.yml https://coveralls-python.readthedocs.io/en/latest/usage/configuration.html#github-actions-support --- .github/workflows/python-build.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index 0f7ddf1b..b5ba410a 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -5,25 +5,40 @@ jobs: tests: env: FORCE_COLOR: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} strategy: matrix: python: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest steps: - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install prereq run: pip install tox coveralls - name: Run python tests run: tox -e ${{ matrix.python }} - name: Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: ${{ matrix.python-version }} + COVERALLS_PARALLEL: true run: coveralls + coveralls: + name: Indicate completion to coveralls.io + needs: tests + runs-on: ubuntu-latest + container: python:3-slim + steps: + - name: Finished + run: | + pip3 install --upgrade coveralls + coveralls --service=github --finish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} docs: strategy: matrix: @@ -32,11 +47,11 @@ jobs: steps: - run: sudo apt install -y graphviz - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install prereq run: pip install tox - name: Run python tests From 1b83d10f5a8a34db9686835d0202f758f645da7e Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 23 Dec 2023 00:01:43 +0100 Subject: [PATCH 044/115] ruff --output-format=github --- .github/workflows/lint_python.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index f50e8aa9..bf962259 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -4,14 +4,14 @@ jobs: lint_python: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x check-latest: true - run: pip install --upgrade pip setuptools wheel - run: pip install black codespell mypy pytest ruff safety - - run: ruff --format=github . + - run: ruff --output-format=github . - run: black --check . || true - run: codespell # --ignore-words-list="" --skip="*.css,*.js,*.lock" - run: pip install -r requirements-test.txt From ecb888f82cb8a7eeb41301488010d982761da063 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 23 Dec 2023 00:07:45 +0100 Subject: [PATCH 045/115] Codespell --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index bf962259..abd885ed 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -13,7 +13,7 @@ jobs: - run: pip install black codespell mypy pytest ruff safety - run: ruff --output-format=github . - run: black --check . || true - - run: codespell # --ignore-words-list="" --skip="*.css,*.js,*.lock" + - run: codespell --ignore-words-list="implementor,mimiced,provicers,re-use" # --skip="*.css,*.js,*.lock" - run: pip install -r requirements-test.txt - run: pip install --editable . - run: mkdir --parents --verbose .mypy_cache From b3a7a4869cc5d89e563736b8d24ff64b34af6d6a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 23 Dec 2023 00:10:54 +0100 Subject: [PATCH 046/115] Update lint_python.yml --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index abd885ed..7cb03217 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -19,4 +19,4 @@ jobs: - run: mkdir --parents --verbose .mypy_cache - run: mypy --ignore-missing-imports --install-types --non-interactive . || true - run: pytest - - run: safety check + - run: safety check || true From 2035d692a6d14c5745ef0a432aa5cbcd2625f5d8 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sat, 6 Jan 2024 21:38:32 +0100 Subject: [PATCH 047/115] Removed obsolete versions in documentation --- README.rst | 2 +- docs/contributing.rst | 13 ++++++------- docs/oauth1/security.rst | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 8e288b63..44b243f2 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ OAuthLib - Python Framework for OAuth1 & OAuth2 =============================================== *A generic, spec-compliant, thorough implementation of the OAuth request-signing -logic for Python 3.6+.* +logic for Python 3.8+* .. image:: https://github.com/oauthlib/oauthlib/actions/workflows/python-build.yml/badge.svg :target: https://github.com/oauthlib/oauthlib/actions diff --git a/docs/contributing.rst b/docs/contributing.rst index 19ff9c9c..9da1370c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -152,8 +152,8 @@ request that fails this test suite will be **rejected**. Testing multiple versions of Python ----------------------------------- -OAuthLib supports Python 3.5, 3.6, 3.7 and PyPy 2.7 & PyPy 3. Testing -all versions conveniently can be done using `Tox`_. +OAuthLib supports Python 3.8+ & PyPy 3. Testing +all versions conveniently at once can be done using `Tox`_. .. sourcecode:: bash @@ -167,11 +167,10 @@ The versions beloew may not be up to date. .. sourcecode:: bash - $ pyenv install 3.5.7 - $ pyenv install 3.6.9 - $ pyenv install 3.7.4 - $ pyenv install pypy2.7-7.1.1 - $ pyenv install pypy3.6-7.1.1 + $ pyenv install -l # check which versions you want to use + $ pyenv install 3.8.18 + $ pyenv install 3.11.7 + $ pyenv install pypy3.10-7.3.13 .. _`Tox`: https://tox.readthedocs.io/en/latest/install.html .. _`virtualenv`: https://virtualenv.pypa.io/en/latest/installation/ diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst index d8b7d6b5..a9274c29 100644 --- a/docs/oauth1/security.rst +++ b/docs/oauth1/security.rst @@ -17,7 +17,7 @@ A few important facts regarding OAuth security * **Tokens must be random**, OAuthLib provides a method for generating secure tokens and it's packed into ``oauthlib.common.generate_token``, use it. If you decide to roll your own, use ``secrets.SystemRandom`` - for Python 3.6 and later. The ``secrets`` module is designed for + for Python 3.8 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 efficient but not truly From 3ba8fd6109ac1f9314215f3373027d86c032dffc Mon Sep 17 00:00:00 2001 From: Shawn Zivontsis Date: Tue, 13 Aug 2024 01:25:31 -0400 Subject: [PATCH 048/115] Fix CI Errors (#878) * Fix new exception type for invalid key * Fix ruff now needs check command * Fix DeprecationWarning in test * Fix/ignore linter errors * Update codespell ignore list --- .github/workflows/lint_python.yml | 4 ++-- Makefile | 2 +- oauthlib/common.py | 2 +- oauthlib/oauth2/rfc6749/parameters.py | 4 ++-- oauthlib/oauth2/rfc6749/tokens.py | 2 +- .../openid/connect/core/grant_types/dispatchers.py | 6 +++--- tests/oauth1/rfc5849/test_signatures.py | 8 +++++++- .../oauth2/rfc6749/clients/test_web_application.py | 13 ++++--------- tests/oauth2/rfc6749/test_tokens.py | 2 +- tox.ini | 2 +- 10 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 7cb03217..683a3283 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -11,9 +11,9 @@ jobs: check-latest: true - run: pip install --upgrade pip setuptools wheel - run: pip install black codespell mypy pytest ruff safety - - run: ruff --output-format=github . + - run: ruff check --output-format=github . - run: black --check . || true - - run: codespell --ignore-words-list="implementor,mimiced,provicers,re-use" # --skip="*.css,*.js,*.lock" + - run: codespell --ignore-words-list="implementor,mimiced,provicers,re-use,THIRDPARTY,assertIn" # --skip="*.css,*.js,*.lock" - run: pip install -r requirements-test.txt - run: pip install --editable . - run: mkdir --parents --verbose .mypy_cache diff --git a/Makefile b/Makefile index 28081659..550525c6 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ format fmt black: black . lint ruff: - ruff . + ruff check . test: tox diff --git a/oauthlib/common.py b/oauthlib/common.py index 9d850b9a..fd9cad09 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -316,7 +316,7 @@ def __getitem__(self, k): return super().__getitem__(key) def get(self, k, default=None): - return self[k] if k in self else default + return self[k] if k in self else default # noqa: SIM401 def __setitem__(self, k, v): super().__setitem__(k, v) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index f7378d81..6c55000c 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -273,7 +273,7 @@ def parse_authorization_code_response(uri, state=None): query = urlparse.urlparse(uri).query params = dict(urlparse.parse_qsl(query)) - if state and params.get('state', None) != state: + if state and params.get('state') != state: raise MismatchingStateError() if 'error' in params: @@ -346,7 +346,7 @@ def parse_implicit_response(uri, state=None, scope=None): if 'expires_in' in params: params['expires_at'] = round(time.time()) + int(params['expires_in']) - if state and params.get('state', None) != state: + if state and params.get('state') != state: raise ValueError("Mismatching or missing state in params.") params = OAuth2Token(params, old_scope=scope) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 2e8c9c4a..73b8c66a 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -24,7 +24,7 @@ class OAuth2Token(dict): def __init__(self, params, old_scope=None): super().__init__(params) self._new_scope = None - if 'scope' in params and params['scope']: + if params.get('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)) diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py index 5aa7d469..7e073968 100644 --- a/oauthlib/openid/connect/core/grant_types/dispatchers.py +++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py @@ -80,9 +80,9 @@ 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) + client_id = parameters.get('client_id') + code = parameters.get('code') + redirect_uri = parameters.get('redirect_uri') # If code is not present fallback to `default_grant` which will # raise an error for the missing `code` in `create_token_response` step. diff --git a/tests/oauth1/rfc5849/test_signatures.py b/tests/oauth1/rfc5849/test_signatures.py index 0dd2859a..2c4ce3df 100644 --- a/tests/oauth1/rfc5849/test_signatures.py +++ b/tests/oauth1/rfc5849/test_signatures.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from jwt import InvalidKeyError from oauthlib.oauth1.rfc5849.signature import ( base_string_uri, collect_parameters, normalize_parameters, sign_hmac_sha1_with_client, sign_hmac_sha256_with_client, @@ -765,12 +766,17 @@ def test_rsa_bad_keys(self): # Signing needs a private key - for bad_value in [None, '', 'foobar']: + for bad_value in [None, '']: self.assertRaises(ValueError, sign_rsa_sha1_with_client, self.eg_signature_base_string, MockClient(rsa_key=bad_value)) + self.assertRaises(InvalidKeyError, + sign_rsa_sha1_with_client, + self.eg_signature_base_string, + MockClient(rsa_key='foobar')) + self.assertRaises(AttributeError, sign_rsa_sha1_with_client, self.eg_signature_base_string, diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 7a711215..2a7a8ff3 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -252,18 +252,13 @@ def test_prepare_request_body(self): self.assertEqual(r4b_params['client_id'], self.client_id) # scenario Warnings - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") # catch all - - # warning1 - raise a DeprecationWarning if a `client_id` is submitted - rWarnings1 = client.prepare_request_body(client_id=self.client_id) - self.assertEqual(len(w), 1) - self.assertIsInstance(w[0].message, DeprecationWarning) - + # warning1 - raise a DeprecationWarning if a `client_id` is submitted + with self.assertWarns(DeprecationWarning): + client.prepare_request_body(client_id=self.client_id) # testing the exact warning message in Python2&Python3 is a pain # scenario Exceptions # exception1 - raise a ValueError if the a different `client_id` is submitted - with self.assertRaises(ValueError) as cm: + with self.assertWarns(DeprecationWarning), self.assertRaises(ValueError): client.prepare_request_body(client_id='different_client_id') # testing the exact exception message in Python2&Python3 is a pain diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index fa6b1c09..ec8efca4 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -76,7 +76,7 @@ class TokenTest(TestCase): bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT' def _mocked_validate_bearer_token(self, token, scopes, request): - if not token: + if not token: # noqa: SIM103 return False return True diff --git a/tox.ini b/tox.ini index fb476a12..bae86ada 100644 --- a/tox.ini +++ b/tox.ini @@ -31,4 +31,4 @@ commands= deps=ruff allowlist_externals=ruff skip_install=true -commands=ruff . +commands=ruff check . From d319c54ae0342d9a2596ef7afa1e17984c303550 Mon Sep 17 00:00:00 2001 From: Shawn Zivontsis Date: Tue, 13 Aug 2024 03:56:19 -0400 Subject: [PATCH 049/115] Update create_code_verifier to output the proper length (#876) * Add test for code verifier length * Modify create_code_verifier to use generate_token * Remove unused import * Remove extraneous character from regex * Replace length literal with variable --- oauthlib/oauth2/rfc6749/clients/base.py | 7 +++---- tests/oauth2/rfc6749/clients/test_base.py | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index cdcfa55a..a8de78ba 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -9,11 +9,10 @@ import base64 import hashlib import re -import secrets import time import warnings -from oauthlib.common import generate_token +from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_token from oauthlib.oauth2.rfc6749 import tokens from oauthlib.oauth2.rfc6749.errors import ( InsecureTransportError, TokenExpiredError, @@ -491,8 +490,8 @@ def create_code_verifier(self, length): if not length <= 128: raise ValueError("Length must be less than or equal to 128") - allowed_characters = re.compile('^[A-Zaa-z0-9-._~]') - code_verifier = secrets.token_urlsafe(length) + allowed_characters = re.compile('^[A-Za-z0-9-._~]') + code_verifier = generate_token(length, UNICODE_ASCII_CHARACTER_SET + "-._~") if not re.search(allowed_characters, code_verifier): raise ValueError("code_verifier contains invalid characters") diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index 7286b991..b0b6372b 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -340,6 +340,12 @@ def test_create_code_verifier_max_length(self): code_verifier = client.create_code_verifier(length=length) self.assertEqual(client.code_verifier, code_verifier) + def test_create_code_verifier_length(self): + client = Client(self.client_id) + length = 96 + code_verifier = client.create_code_verifier(length=length) + self.assertEqual(len(code_verifier), length) + def test_create_code_challenge_plain(self): client = Client(self.client_id) code_verifier = client.create_code_verifier(length=128) From b84647e43a10b546b10294f2941dbf5142e9dda0 Mon Sep 17 00:00:00 2001 From: david uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:03:58 +0100 Subject: [PATCH 050/115] thedrow * add create_device_authorization_response method * validate device authorization request * fix typo in comment. * fix typo in comment * fix: run fmt and lint * fix: lint issue * Update tests/oauth2/rfc8628/test_server.py * Import DeviceAuthorizationEndpoint --------- Co-authored-by: Olivier Amblet Co-authored-by: Olivier Amblet Co-authored-by: Asif Saif Uddin --- oauthlib/oauth2/__init__.py | 1 + oauthlib/oauth2/rfc8628/endpoints/__init__.py | 8 + .../rfc8628/endpoints/device_authorization.py | 223 ++++++++++++++++++ oauthlib/oauth2/rfc8628/pre_configured.py | 19 ++ oauthlib/oauth2/rfc8628/request_validator.py | 25 ++ .../connect/core/endpoints/pre_configured.py | 2 +- tests/oauth2/rfc8628/endpoints/__init__.py | 0 .../rfc8628/endpoints/test_error_responses.py | 99 ++++++++ tests/oauth2/rfc8628/test_server.py | 98 ++++++++ tests/test_uri_validate.py | 1 + 10 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 oauthlib/oauth2/rfc8628/endpoints/__init__.py create mode 100644 oauthlib/oauth2/rfc8628/endpoints/device_authorization.py create mode 100644 oauthlib/oauth2/rfc8628/pre_configured.py create mode 100644 oauthlib/oauth2/rfc8628/request_validator.py create mode 100644 tests/oauth2/rfc8628/endpoints/__init__.py create mode 100644 tests/oauth2/rfc8628/endpoints/test_error_responses.py create mode 100644 tests/oauth2/rfc8628/test_server.py diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index deefb1af..fe4ca80b 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -34,3 +34,4 @@ from .rfc6749.tokens import BearerToken, OAuth2Token from .rfc6749.utils import is_secure_transport from .rfc8628.clients import DeviceClient +from .rfc8628.endpoints import DeviceAuthorizationEndpoint diff --git a/oauthlib/oauth2/rfc8628/endpoints/__init__.py b/oauthlib/oauth2/rfc8628/endpoints/__init__.py new file mode 100644 index 00000000..75eef2b7 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/endpoints/__init__.py @@ -0,0 +1,8 @@ +""" +oauthlib.oauth2.rfc8628 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 Device Authorization RFC8628. +""" +from .device_authorization import DeviceAuthorizationEndpoint diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py new file mode 100644 index 00000000..83c0dd74 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -0,0 +1,223 @@ +""" +oauthlib.oauth2.rfc8628 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC8628. +""" +import json +import logging + +from oauthlib.common import Request, generate_token +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.endpoints.base import ( + BaseEndpoint, catch_errors_and_unavailability, +) + +log = logging.getLogger(__name__) + + +class DeviceAuthorizationEndpoint(BaseEndpoint): + + """DeviceAuthorization endpoint - used by the client to initiate + the authorization flow by requesting a set of verification codes + from the authorization server by making an HTTP "POST" request to + the device authorization endpoint. + + The client authentication requirements of Section 3.2.1 of [RFC6749] + apply to requests on this endpoint, which means that confidential + clients (those that have established client credentials) authenticate + in the same manner as when making requests to the token endpoint, and + public clients provide the "client_id" parameter to identify + themselves. + """ + + def __init__( + self, + request_validator, + verification_uri, + expires_in=1800, + interval=None, + verification_uri_complete=None, + ): + """ + :param request_validator: An instance of RequestValidator. + :type request_validator: oauthlib.oauth2.rfc6749.RequestValidator. + :param verification_uri: a string containing the URL that can be polled by the client application + :param expires_in: a number that represents the lifetime of the `user_code` and `device_code` + :param interval: an option number that represents the number of seconds between each poll requests + :param verification_uri_complete: a string of a function that can be called with `user_data` as parameter + """ + self.request_validator = request_validator + self._expires_in = expires_in + self._interval = interval + self._verification_uri = verification_uri + self._verification_uri_complete = verification_uri_complete + self._interval = interval + + BaseEndpoint.__init__(self) + + @property + def interval(self): + """The minimum amount of time in seconds that the client + SHOULD wait between polling requests to the token endpoint. If no + value is provided, clients MUST use 5 as the default. + """ + return self._interval + + @property + def expires_in(self): + """The lifetime in seconds of the "device_code" and "user_code".""" + return self._expires_in + + @property + def verification_uri(self): + """The end-user verification URI on the authorization + server. The URI should be short and easy to remember as end users + will be asked to manually type it into their user agent. + """ + return self._verification_uri + + def verification_uri_complete(self, user_code): + if not self._verification_uri_complete: + return None + if isinstance(self._verification_uri_complete, str): + return self._verification_uri_complete.format(user_code=user_code) + if callable(self._verification_uri_complete): + return self._verification_uri_complete(user_code) + return None + + @catch_errors_and_unavailability + def validate_device_authorization_request(self, request): + """Validate the device authorization request. + + The client_id is required if the client is not authenticating with the + authorization server as described in `Section 3.2.1. of [RFC6749]`_. + The client identifier as described in `Section 2.2 of [RFC6749]`_. + + .. _`Section 3.2.1. of [RFC6749]`: https://www.rfc-editor.org/rfc/rfc6749#section-3.2.1 + .. _`Section 2.2 of [RFC6749]`: https://www.rfc-editor.org/rfc/rfc6749#section-2.2 + """ + + # First check duplicate parameters + for param in ("client_id", "scope"): + 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 + ) + + # the "application/x-www-form-urlencoded" format, per Appendix B of [RFC6749] + # https://www.rfc-editor.org/rfc/rfc6749#appendix-B + if request.headers["Content-Type"] != "application/x-www-form-urlencoded": + raise errors.InvalidRequestError( + "Content-Type must be application/x-www-form-urlencoded", + request=request, + ) + + # REQUIRED. The client identifier as described in Section 2.2. + # https://tools.ietf.org/html/rfc6749#section-2.2 + # TODO: extract client_id an helper validation function. + 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) + + # The client authentication requirements of Section 3.2.1 of [RFC6749] + # apply to requests on this endpoint, which means that confidential + # clients (those that have established client credentials) authenticate + # in the same manner as when making requests to the token endpoint, and + # public clients provide the "client_id" parameter to identify + # themselves. + self._raise_on_invalid_client(request) + + @catch_errors_and_unavailability + def create_device_authorization_response( + self, uri, http_method="POST", body=None, headers=None + ): + """create_device_authorization_response - generates a unique device + verification code and an end-user code that are valid for a limited + time and includes them in the HTTP response body using the + "application/json" format [RFC8259] with a 200 (OK) status code, as + described in `Section-3.2`_. + + :param uri: The full URI of the token request. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :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. + + The response contains the following parameters: + + device_code + REQUIRED. The device verification code. + + user_code + REQUIRED. The end-user verification code. + + verification_uri + REQUIRED. The end-user verification URI on the authorization + server. The URI should be short and easy to remember as end users + will be asked to manually type it into their user agent. + + verification_uri_complete + OPTIONAL. A verification URI that includes the "user_code" (or + other information with the same function as the "user_code"), + which is designed for non-textual transmission. + + expires_in + REQUIRED. The lifetime in seconds of the "device_code" and + "user_code". + + interval + OPTIONAL. The minimum amount of time in seconds that the client + SHOULD wait between polling requests to the token endpoint. If no + value is provided, clients MUST use 5 as the default. + + For example: + + HTTP/1.1 200 OK + Content-Type: application/json + Cache-Control: no-store + + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://example.com/device", + "verification_uri_complete": + "https://example.com/device?user_code=WDJB-MJHT", + "expires_in": 1800, + "interval": 5 + } + + .. _`Section-3.2`: https://www.rfc-editor.org/rfc/rfc8628#section-3.2 + """ + request = Request(uri, http_method, body, headers) + self.validate_device_authorization_request(request) + log.debug("Pre resource owner authorization validation ok for %r.", request) + + headers = {} + user_code = generate_token() + data = { + "verification_uri": self.verification_uri, + "expires_in": self.expires_in, + "user_code": user_code, + "device_code": generate_token(), + } + if self.interval is not None: + data["interval"] = self.interval + + verification_uri_complete = self.verification_uri_complete(user_code) + if verification_uri_complete: + data["verification_uri_complete"] = verification_uri_complete + + body = json.dumps(data) + return headers, body, 200 diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/pre_configured.py new file mode 100644 index 00000000..3123d57f --- /dev/null +++ b/oauthlib/oauth2/rfc8628/pre_configured.py @@ -0,0 +1,19 @@ +from oauthlib.oauth2.rfc8628.endpoints.device_authorization import ( + DeviceAuthorizationEndpoint, +) + + +class DeviceApplicationServer(DeviceAuthorizationEndpoint): + + """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" + + def __init__(self, request_validator, verification_uri, **kwargs): + """Construct a new web application server. + + :param request_validator: An implementation of + oauthlib.oauth2.rfc8626.RequestValidator. + :param verification_uri: the verification_uri to be send back. + """ + DeviceAuthorizationEndpoint.__init__( + self, request_validator, verification_uri=verification_uri + ) diff --git a/oauthlib/oauth2/rfc8628/request_validator.py b/oauthlib/oauth2/rfc8628/request_validator.py new file mode 100644 index 00000000..70ee7824 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/request_validator.py @@ -0,0 +1,25 @@ +from oauthlib.oauth2 import RequestValidator as OAuth2RequestValidator + + +class RequestValidator(OAuth2RequestValidator): + def client_authentication_required(self, request, *args, **kwargs): + """Determine if client authentication is required for current request. + + According to the rfc8628, client authentication is required in the following cases: + - Device Authorization Request follows the, the client authentication requirements + of Section 3.2.1 of [RFC6749] apply to requests on this endpoint, which means that + confidential clients (those that have established client credentials) authenticate + in the same manner as when making requests to the token endpoint, and + public clients provide the "client_id" parameter to identify themselves, + see `Section 3.1`_. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: True or False + + Method is used by: + - Device Authorization Request + + .. _`Section 3.1`: https://www.rfc-editor.org/rfc/rfc8628#section-3.1 + """ + return True diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 1f4370ad..8cbc37df 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -17,7 +17,7 @@ from oauthlib.oauth2.rfc6749.tokens import BearerToken from ..grant_types import ( - AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant + AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant, ) from ..grant_types.dispatchers import ( AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher, diff --git a/tests/oauth2/rfc8628/endpoints/__init__.py b/tests/oauth2/rfc8628/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/oauth2/rfc8628/endpoints/test_error_responses.py b/tests/oauth2/rfc8628/endpoints/test_error_responses.py new file mode 100644 index 00000000..16f05961 --- /dev/null +++ b/tests/oauth2/rfc8628/endpoints/test_error_responses.py @@ -0,0 +1,99 @@ +import json +from unittest import TestCase, mock + +from oauthlib.common import Request, urlencode +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc8628.pre_configured import DeviceApplicationServer +from oauthlib.oauth2.rfc8628.request_validator import RequestValidator + + +class ErrorResponseTest(TestCase): + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = "mocked" + return True + + def build_request( + self, uri="https://example.com/device_authorize", client_id="foo" + ): + body = "" + if client_id: + body = f"client_id={client_id}" + return Request( + uri, + http_method="POST", + body=body, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + def assert_request_raises(self, error, request): + """Test that the request fails similarly on the validation and response endpoint.""" + self.assertRaises( + error, + self.device.validate_device_authorization_request, + request, + ) + self.assertRaises( + error, + self.device.create_device_authorization_response, + uri=request.uri, + http_method=request.http_method, + body=request.body, + headers=request.headers, + ) + + def setUp(self): + self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.get_default_redirect_uri.return_value = None + self.validator.get_code_challenge.return_value = None + self.device = DeviceApplicationServer( + self.validator, "https://example.com/verify" + ) + + def test_missing_client_id(self): + # Device code grant + request = self.build_request(client_id=None) + self.assert_request_raises(errors.MissingClientIdError, request) + + def test_empty_client_id(self): + # Device code grant + self.assertRaises( + errors.MissingClientIdError, + self.device.create_device_authorization_response, + "https://i.l/", + "POST", + "client_id=", + {"Content-Type": "application/x-www-form-urlencoded"}, + ) + + def test_invalid_client_id(self): + request = self.build_request(client_id="foo") + # Device code grant + self.validator.validate_client_id.return_value = False + self.assert_request_raises(errors.InvalidClientIdError, request) + + def test_duplicate_client_id(self): + request = self.build_request() + request.body = "client_id=foo&client_id=bar" + # Device code grant + self.validator.validate_client_id.return_value = False + self.assert_request_raises(errors.InvalidRequestFatalError, request) + + def test_unauthenticated_confidential_client(self): + self.validator.client_authentication_required.return_value = True + self.validator.authenticate_client.return_value = False + request = self.build_request() + self.assert_request_raises(errors.InvalidClientError, request) + + def test_unauthenticated_public_client(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = False + request = self.build_request() + self.assert_request_raises(errors.InvalidClientError, request) + + def test_duplicate_scope_parameter(self): + request = self.build_request() + request.body = "client_id=foo&scope=foo&scope=bar" + # Device code grant + self.validator.validate_client_id.return_value = False + self.assert_request_raises(errors.InvalidRequestFatalError, request) diff --git a/tests/oauth2/rfc8628/test_server.py b/tests/oauth2/rfc8628/test_server.py new file mode 100644 index 00000000..7fc1d5a4 --- /dev/null +++ b/tests/oauth2/rfc8628/test_server.py @@ -0,0 +1,98 @@ +import json +from unittest import mock + +from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint +from oauthlib.oauth2.rfc8628.request_validator import RequestValidator + +from tests.unittest import TestCase + + +class DeviceAuthorizationEndpointTest(TestCase): + def _configure_endpoint(self, interval=None, verification_uri_complete=None): + self.endpoint = DeviceAuthorizationEndpoint( + request_validator=mock.MagicMock(spec=RequestValidator), + verification_uri=self.verification_uri, + interval=interval, + verification_uri_complete=verification_uri_complete, + ) + + def setUp(self): + self.request_validator = mock.MagicMock(spec=RequestValidator) + self.verification_uri = "http://i.b/l/verify" + self.uri = "http://i.b/l" + self.http_method = "POST" + self.body = "client_id=abc" + self.headers = {"Content-Type": "application/x-www-form-urlencoded"} + self._configure_endpoint() + + def response_payload(self): + return self.uri, self.http_method, self.body, self.headers + + @mock.patch("oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token") + def test_device_authorization_grant(self, generate_token): + generate_token.side_effect = ["abc", "def"] + _, body, status_code = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) + expected_payload = { + "verification_uri": "http://i.b/l/verify", + "user_code": "abc", + "device_code": "def", + "expires_in": 1800, + } + self.assertEqual(200, status_code) + self.assertEqual(json.loads(body), expected_payload) + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_authorization_grant_interval(self): + self._configure_endpoint(interval=5) + _, body, _ = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) + self.assertEqual(5, json.loads(body)["interval"]) + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_authorization_grant_interval_with_zero(self): + self._configure_endpoint(interval=0) + _, body, _ = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) + self.assertEqual(0, json.loads(body)["interval"]) + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_authorization_grant_verify_url_complete_string(self): + self._configure_endpoint( + verification_uri_complete="http://i.l/v?user_code={user_code}" + ) + _, body, _ = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) + self.assertEqual( + "http://i.l/v?user_code=abc", + json.loads(body)["verification_uri_complete"], + ) + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_authorization_grant_verify_url_complete_callable(self): + self._configure_endpoint( + verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}" + ) + _, body, _ = self.endpoint.create_device_authorization_response( + *self.response_payload() + ) + self.assertEqual( + "http://i.l/v?user_code=abc", + json.loads(body)["verification_uri_complete"], + ) diff --git a/tests/test_uri_validate.py b/tests/test_uri_validate.py index 6a9f8ea6..04138d60 100644 --- a/tests/test_uri_validate.py +++ b/tests/test_uri_validate.py @@ -1,4 +1,5 @@ import unittest + from oauthlib.uri_validate import is_absolute_uri from tests.unittest import TestCase From e1d9525f9a29cbc076bce9d48134ee09209957e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:57:42 +0300 Subject: [PATCH 051/115] thedrow --- .github/workflows/python-build.yml | 2 +- setup.py | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index b5ba410a..8108b6dc 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -7,7 +7,7 @@ jobs: FORCE_COLOR: 1 strategy: matrix: - python: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest steps: - name: Set up Python ${{ matrix.python }} diff --git a/setup.py b/setup.py index 4043db0f..b80ff3a3 100755 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ def fread(fn): 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', diff --git a/tox.ini b/tox.ini index bae86ada..ae49af48 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # require tox>=4 [tox] -envlist = py38,py39,py310,py311,py312,pypy3,docs,readme,ruff +envlist = py38,py39,py310,py311,py312,py313,pypy3,docs,readme,ruff [testenv] deps= From bf75322aad61785501405308f3c4cd020983d17d Mon Sep 17 00:00:00 2001 From: david uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:59:03 +0100 Subject: [PATCH 052/115] Allow user_code to be configured for device auth flow (Device Authorization Grant) (#885) * Allow custom user_code * Add convience import * Allow user code to be configured in device auth flow Whilst oauth2 doesn't specify how the format should be the current behaviour generates a code that is not human and device friendly. e.g 6Pp9vPKaanbFydF9omtlNLLdJA4HG7 This commit makes it so that's the default behvaiour but allows the caller of DeviceApplicationServer to pass in a user code in a format they prefer in the form of a callable to be called that returns the code as a string * Return a python dict in create_device_authorization_response The json serialisation should occur at the interface level (e.g a view) that will use this method not the method itself as it can lead to "double" json serialisation or the need to use json.loads() to deserialize and serialise it again before the httpResponse is made back to the client * Update tests * Add typing * Apply ruff format * Add device authorization doc * Apply 99 line length ruff format --- docs/oauth2/endpoints/device.rst | 55 +++++++ docs/oauth2/endpoints/endpoints.rst | 1 + docs/oauth2/preconfigured_servers.rst | 4 + oauthlib/oauth2/__init__.py | 66 ++++++--- oauthlib/oauth2/rfc8628/__init__.py | 1 + oauthlib/oauth2/rfc8628/endpoints/__init__.py | 2 + .../rfc8628/endpoints/device_authorization.py | 138 +++++++++--------- .../rfc8628/{ => endpoints}/pre_configured.py | 16 +- .../connect/core/endpoints/pre_configured.py | 119 +++++++++------ .../rfc8628/endpoints/test_error_responses.py | 10 +- tests/oauth2/rfc8628/test_server.py | 61 +++++--- 11 files changed, 316 insertions(+), 157 deletions(-) create mode 100644 docs/oauth2/endpoints/device.rst rename oauthlib/oauth2/rfc8628/{ => endpoints}/pre_configured.py (56%) diff --git a/docs/oauth2/endpoints/device.rst b/docs/oauth2/endpoints/device.rst new file mode 100644 index 00000000..728fd3c2 --- /dev/null +++ b/docs/oauth2/endpoints/device.rst @@ -0,0 +1,55 @@ +============= +Device +============= + +The device endpoint is used to initiate the authorization flow by requesting a set of +verification codes from the authorization server by making an HTTP "POST" request to +the device authorization endpoint. + +** Device Authorization Request ** + The client makes a device authorization request to the device + authorization endpoint by including the following parameters using + the "application/x-www-form-urlencoded" format: + + POST /device_authorization HTTP/1.1 + Host: server.example.com + Content-Type: application/x-www-form-urlencoded + client_id=123456&scope=example_scope + +.. code-block:: python + + # Initial setup + from your_validator import your_validator + verification_uri = "https://example.com/device" + + def user_code(): + # some logic to generate a random string... + return "123-456" + + # user code is optional + server = DeviceApplicationServer(your_validator, verification_uri, user_code) + + headers, data, status = server.create_device_authorization_response(request) + + # response from /device_authorization endpoint on your server + from your_framework import http_response + http_response(data, status=status, headers=headers) + + + +.. code-block:: python + + # example response + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "123-456", + "verification_uri": "https://example.com/device", + "verification_uri_complete": + "https://example.com/device?user_code=WDJB-MJHT", + "expires_in": 1800, + "interval": 5 + } + + +.. autoclass:: oauthlib.oauth2.DeviceAuthorizationEndpoint + :members: diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index f05c44b6..d2a4a07a 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -15,6 +15,7 @@ client attempts to access the user resources on their behalf. :maxdepth: 2 authorization + device introspect token metadata diff --git a/docs/oauth2/preconfigured_servers.rst b/docs/oauth2/preconfigured_servers.rst index e1f629c2..a32cf8a1 100644 --- a/docs/oauth2/preconfigured_servers.rst +++ b/docs/oauth2/preconfigured_servers.rst @@ -36,3 +36,7 @@ This function is passed the request object and a boolean indicating whether to g .. autoclass:: oauthlib.oauth2.BackendApplicationServer :members: + + +.. autoclass:: oauthlib.oauth2.DeviceApplicationServer + :members: diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index fe4ca80b..da9ccf3d 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -5,33 +5,65 @@ This module is a wrapper for the most recent implementation of OAuth 2.0 Client and Server classes. """ + from .rfc6749.clients import ( - BackendApplicationClient, Client, LegacyApplicationClient, - MobileApplicationClient, ServiceApplicationClient, WebApplicationClient, + BackendApplicationClient, + Client, + LegacyApplicationClient, + MobileApplicationClient, + ServiceApplicationClient, + WebApplicationClient, ) from .rfc6749.endpoints import ( - AuthorizationEndpoint, BackendApplicationServer, IntrospectEndpoint, - LegacyApplicationServer, MetadataEndpoint, MobileApplicationServer, - ResourceEndpoint, RevocationEndpoint, Server, TokenEndpoint, + AuthorizationEndpoint, + BackendApplicationServer, + IntrospectEndpoint, + LegacyApplicationServer, + MetadataEndpoint, + MobileApplicationServer, + ResourceEndpoint, + RevocationEndpoint, + Server, + TokenEndpoint, WebApplicationServer, ) from .rfc6749.errors import ( - AccessDeniedError, FatalClientError, InsecureTransportError, - InvalidClientError, InvalidClientIdError, InvalidGrantError, - InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, - InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, - MissingClientIdError, MissingCodeError, MissingRedirectURIError, - MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, - OAuth2Error, ServerError, TemporarilyUnavailableError, TokenExpiredError, - UnauthorizedClientError, UnsupportedGrantTypeError, - UnsupportedResponseTypeError, UnsupportedTokenTypeError, + AccessDeniedError, + FatalClientError, + InsecureTransportError, + InvalidClientError, + InvalidClientIdError, + InvalidGrantError, + InvalidRedirectURIError, + InvalidRequestError, + InvalidRequestFatalError, + InvalidScopeError, + MismatchingRedirectURIError, + MismatchingStateError, + MissingClientIdError, + MissingCodeError, + MissingRedirectURIError, + MissingResponseTypeError, + MissingTokenError, + MissingTokenTypeError, + OAuth2Error, + ServerError, + TemporarilyUnavailableError, + TokenExpiredError, + UnauthorizedClientError, + UnsupportedGrantTypeError, + UnsupportedResponseTypeError, + UnsupportedTokenTypeError, ) from .rfc6749.grant_types import ( - AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, + AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant, ) from .rfc6749.request_validator import RequestValidator from .rfc6749.tokens import BearerToken, OAuth2Token from .rfc6749.utils import is_secure_transport from .rfc8628.clients import DeviceClient -from .rfc8628.endpoints import DeviceAuthorizationEndpoint +from .rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index 531929dc..6c3d14af 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -5,6 +5,7 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ + import logging log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc8628/endpoints/__init__.py b/oauthlib/oauth2/rfc8628/endpoints/__init__.py index 75eef2b7..dc834797 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc8628/endpoints/__init__.py @@ -5,4 +5,6 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ + from .device_authorization import DeviceAuthorizationEndpoint +from .pre_configured import DeviceApplicationServer diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 83c0dd74..7cda5ee1 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -5,20 +5,21 @@ This module is an implementation of various logic needed for consuming and providing OAuth 2.0 RFC8628. """ -import json + import logging +from typing import Callable from oauthlib.common import Request, generate_token from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.endpoints.base import ( - BaseEndpoint, catch_errors_and_unavailability, + BaseEndpoint, + catch_errors_and_unavailability, ) log = logging.getLogger(__name__) class DeviceAuthorizationEndpoint(BaseEndpoint): - """DeviceAuthorization endpoint - used by the client to initiate the authorization flow by requesting a set of verification codes from the authorization server by making an HTTP "POST" request to @@ -39,6 +40,7 @@ def __init__( expires_in=1800, interval=None, verification_uri_complete=None, + user_code_generator: Callable[[None], str] = None, ): """ :param request_validator: An instance of RequestValidator. @@ -47,13 +49,14 @@ def __init__( :param expires_in: a number that represents the lifetime of the `user_code` and `device_code` :param interval: an option number that represents the number of seconds between each poll requests :param verification_uri_complete: a string of a function that can be called with `user_data` as parameter + :param user_code_generator: a callable that returns a configurable user code """ self.request_validator = request_validator self._expires_in = expires_in self._interval = interval self._verification_uri = verification_uri self._verification_uri_complete = verification_uri_complete - self._interval = interval + self.user_code_generator = user_code_generator BaseEndpoint.__init__(self) @@ -141,71 +144,77 @@ def validate_device_authorization_request(self, request): def create_device_authorization_response( self, uri, http_method="POST", body=None, headers=None ): - """create_device_authorization_response - generates a unique device - verification code and an end-user code that are valid for a limited - time and includes them in the HTTP response body using the - "application/json" format [RFC8259] with a 200 (OK) status code, as - described in `Section-3.2`_. - - :param uri: The full URI of the token request. - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :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. - - The response contains the following parameters: - - device_code - REQUIRED. The device verification code. - - user_code - REQUIRED. The end-user verification code. - - verification_uri - REQUIRED. The end-user verification URI on the authorization - server. The URI should be short and easy to remember as end users - will be asked to manually type it into their user agent. - - verification_uri_complete - OPTIONAL. A verification URI that includes the "user_code" (or - other information with the same function as the "user_code"), - which is designed for non-textual transmission. - - expires_in - REQUIRED. The lifetime in seconds of the "device_code" and - "user_code". - - interval - OPTIONAL. The minimum amount of time in seconds that the client - SHOULD wait between polling requests to the token endpoint. If no - value is provided, clients MUST use 5 as the default. - - For example: - - HTTP/1.1 200 OK - Content-Type: application/json - Cache-Control: no-store - - { - "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", - "user_code": "WDJB-MJHT", - "verification_uri": "https://example.com/device", - "verification_uri_complete": - "https://example.com/device?user_code=WDJB-MJHT", - "expires_in": 1800, - "interval": 5 - } - - .. _`Section-3.2`: https://www.rfc-editor.org/rfc/rfc8628#section-3.2 """ + Generate a unique device verification code and an end-user code that are valid for a limited time. + Include them in the HTTP response body using the "application/json" format [RFC8259] with a + 200 (OK) status code, as described in `Section-3.2`_. + + :param uri: The full URI of the token request. + :type uri: str + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param user_code_generator: + A callable that returns a string for the user code. + This allows the caller to decide how the `user_code` should be formatted. + :type user_code_generator: Callable[[], str] + :return: A tuple of three 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. + :rtype: tuple + + The response contains the following parameters: + + device_code + **REQUIRED.** The device verification code. + + user_code + **REQUIRED.** The end-user verification code. + + verification_uri + **REQUIRED.** The end-user verification URI on the authorization server. + The URI should be short and easy to remember as end users will be asked + to manually type it into their user agent. + + verification_uri_complete + **OPTIONAL.** A verification URI that includes the `user_code` (or + other information with the same function as the `user_code`), which is + designed for non-textual transmission. + + expires_in + **REQUIRED.** The lifetime in seconds of the `device_code` and `user_code`. + + interval + **OPTIONAL.** The minimum amount of time in seconds that the client + SHOULD wait between polling requests to the token endpoint. If no + value is provided, clients MUST use 5 as the default. + + **For example:** + + .. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json + Cache-Control: no-store + + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://example.com/device", + "verification_uri_complete": + "https://example.com/device?user_code=WDJB-MJHT", + "expires_in": 1800, + "interval": 5 + } + + .. _`Section-3.2`: https://www.rfc-editor.org/rfc/rfc8628#section-3.2 + """ request = Request(uri, http_method, body, headers) self.validate_device_authorization_request(request) log.debug("Pre resource owner authorization validation ok for %r.", request) headers = {} - user_code = generate_token() + user_code = self.user_code_generator() if self.user_code_generator else generate_token() data = { "verification_uri": self.verification_uri, "expires_in": self.expires_in, @@ -219,5 +228,4 @@ def create_device_authorization_response( if verification_uri_complete: data["verification_uri_complete"] = verification_uri_complete - body = json.dumps(data) - return headers, body, 200 + return headers, data, 200 diff --git a/oauthlib/oauth2/rfc8628/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py similarity index 56% rename from oauthlib/oauth2/rfc8628/pre_configured.py rename to oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index 3123d57f..cdb6b313 100644 --- a/oauthlib/oauth2/rfc8628/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -1,19 +1,29 @@ from oauthlib.oauth2.rfc8628.endpoints.device_authorization import ( DeviceAuthorizationEndpoint, ) +from typing import Callable class DeviceApplicationServer(DeviceAuthorizationEndpoint): - """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" - def __init__(self, request_validator, verification_uri, **kwargs): + def __init__( + self, + request_validator, + verification_uri, + user_code_generator: Callable[[None], str] = None, + **kwargs, + ): """Construct a new web application server. :param request_validator: An implementation of oauthlib.oauth2.rfc8626.RequestValidator. :param verification_uri: the verification_uri to be send back. + :param user_code_generator: a callable that allows the user code to be configured. """ DeviceAuthorizationEndpoint.__init__( - self, request_validator, verification_uri=verification_uri + self, + request_validator, + verification_uri=verification_uri, + user_code_generator=user_code_generator, ) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 8cbc37df..7c9393e6 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -5,36 +5,57 @@ This module is an implementation of various endpoints needed for providing OpenID Connect servers. """ + from oauthlib.oauth2.rfc6749.endpoints import ( - AuthorizationEndpoint, IntrospectEndpoint, ResourceEndpoint, - RevocationEndpoint, TokenEndpoint, + AuthorizationEndpoint, + IntrospectEndpoint, + ResourceEndpoint, + RevocationEndpoint, + TokenEndpoint, ) from oauthlib.oauth2.rfc6749.grant_types import ( AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, - ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant, + ClientCredentialsGrant, + ImplicitGrant as OAuth2ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ) from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint from ..grant_types import ( - AuthorizationCodeGrant, HybridGrant, ImplicitGrant, RefreshTokenGrant, + AuthorizationCodeGrant, + HybridGrant, + ImplicitGrant, + RefreshTokenGrant, ) from ..grant_types.dispatchers import ( - AuthorizationCodeGrantDispatcher, AuthorizationTokenGrantDispatcher, + AuthorizationCodeGrantDispatcher, + AuthorizationTokenGrantDispatcher, ImplicitTokenGrantDispatcher, ) from ..tokens import JWTToken from .userinfo import UserInfoEndpoint -class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint, 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): + 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 @@ -50,50 +71,64 @@ def __init__(self, request_validator, token_expires_in=None, """ self.auth_grant = OAuth2AuthorizationCodeGrant(request_validator) self.implicit_grant = OAuth2ImplicitGrant(request_validator) - self.password_grant = ResourceOwnerPasswordCredentialsGrant( - request_validator) + self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) self.openid_connect_auth = AuthorizationCodeGrant(request_validator) self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) - self.jwt = JWTToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) + self.jwt = JWTToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) - self.auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) - self.implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit) + self.auth_grant_choice = AuthorizationCodeGrantDispatcher( + default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth + ) + self.implicit_grant_choice = ImplicitTokenGrantDispatcher( + default_grant=self.implicit_grant, oidc_grant=self.openid_connect_implicit + ) # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={ - 'code': self.auth_grant_choice, - 'token': self.implicit_grant_choice, - 'id_token': self.openid_connect_implicit, - 'id_token token': self.openid_connect_implicit, - 'code token': self.openid_connect_hybrid, - 'code id_token': self.openid_connect_hybrid, - 'code id_token token': self.openid_connect_hybrid, - 'none': self.auth_grant - }, - default_token_type=self.bearer) + AuthorizationEndpoint.__init__( + self, + default_response_type="code", + response_types={ + "code": self.auth_grant_choice, + "token": self.implicit_grant_choice, + "id_token": self.openid_connect_implicit, + "id_token token": self.openid_connect_implicit, + "code token": self.openid_connect_hybrid, + "code id_token": self.openid_connect_hybrid, + "code id_token token": self.openid_connect_hybrid, + "none": self.auth_grant, + }, + default_token_type=self.bearer, + ) - self.token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth) + self.token_grant_choice = AuthorizationTokenGrantDispatcher( + request_validator, default_grant=self.auth_grant, oidc_grant=self.openid_connect_auth + ) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': self.token_grant_choice, - 'password': self.password_grant, - 'client_credentials': self.credentials_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer, 'JWT': self.jwt}) + TokenEndpoint.__init__( + self, + default_grant_type="authorization_code", + grant_types={ + "authorization_code": self.token_grant_choice, + "password": self.password_grant, + "client_credentials": self.credentials_grant, + "refresh_token": self.refresh_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer, "JWT": self.jwt} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) UserInfoEndpoint.__init__(self, request_validator) diff --git a/tests/oauth2/rfc8628/endpoints/test_error_responses.py b/tests/oauth2/rfc8628/endpoints/test_error_responses.py index 16f05961..c799cc81 100644 --- a/tests/oauth2/rfc8628/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc8628/endpoints/test_error_responses.py @@ -3,7 +3,7 @@ from oauthlib.common import Request, urlencode from oauthlib.oauth2.rfc6749 import errors -from oauthlib.oauth2.rfc8628.pre_configured import DeviceApplicationServer +from oauthlib.oauth2.rfc8628.endpoints.pre_configured import DeviceApplicationServer from oauthlib.oauth2.rfc8628.request_validator import RequestValidator @@ -13,9 +13,7 @@ def set_client(self, request): request.client.client_id = "mocked" return True - def build_request( - self, uri="https://example.com/device_authorize", client_id="foo" - ): + def build_request(self, uri="https://example.com/device_authorize", client_id="foo"): body = "" if client_id: body = f"client_id={client_id}" @@ -46,9 +44,7 @@ def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) self.validator.get_default_redirect_uri.return_value = None self.validator.get_code_challenge.return_value = None - self.device = DeviceApplicationServer( - self.validator, "https://example.com/verify" - ) + self.device = DeviceApplicationServer(self.validator, "https://example.com/verify") def test_missing_client_id(self): # Device code grant diff --git a/tests/oauth2/rfc8628/test_server.py b/tests/oauth2/rfc8628/test_server.py index 7fc1d5a4..52025032 100644 --- a/tests/oauth2/rfc8628/test_server.py +++ b/tests/oauth2/rfc8628/test_server.py @@ -8,12 +8,15 @@ class DeviceAuthorizationEndpointTest(TestCase): - def _configure_endpoint(self, interval=None, verification_uri_complete=None): + def _configure_endpoint( + self, interval=None, verification_uri_complete=None, user_code_generator=None + ): self.endpoint = DeviceAuthorizationEndpoint( request_validator=mock.MagicMock(spec=RequestValidator), verification_uri=self.verification_uri, interval=interval, verification_uri_complete=verification_uri_complete, + user_code_generator=user_code_generator, ) def setUp(self): @@ -41,7 +44,7 @@ def test_device_authorization_grant(self, generate_token): "expires_in": 1800, } self.assertEqual(200, status_code) - self.assertEqual(json.loads(body), expected_payload) + self.assertEqual(body, expected_payload) @mock.patch( "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", @@ -49,10 +52,8 @@ def test_device_authorization_grant(self, generate_token): ) def test_device_authorization_grant_interval(self): self._configure_endpoint(interval=5) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) - self.assertEqual(5, json.loads(body)["interval"]) + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) + self.assertEqual(5, body["interval"]) @mock.patch( "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", @@ -60,25 +61,19 @@ def test_device_authorization_grant_interval(self): ) def test_device_authorization_grant_interval_with_zero(self): self._configure_endpoint(interval=0) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) - self.assertEqual(0, json.loads(body)["interval"]) + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) + self.assertEqual(0, body["interval"]) @mock.patch( "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", lambda: "abc", ) def test_device_authorization_grant_verify_url_complete_string(self): - self._configure_endpoint( - verification_uri_complete="http://i.l/v?user_code={user_code}" - ) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() - ) + self._configure_endpoint(verification_uri_complete="http://i.l/v?user_code={user_code}") + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) self.assertEqual( "http://i.l/v?user_code=abc", - json.loads(body)["verification_uri_complete"], + body["verification_uri_complete"], ) @mock.patch( @@ -86,13 +81,33 @@ def test_device_authorization_grant_verify_url_complete_string(self): lambda: "abc", ) def test_device_authorization_grant_verify_url_complete_callable(self): - self._configure_endpoint( - verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}" + self._configure_endpoint(verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}") + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) + self.assertEqual( + "http://i.l/v?user_code=abc", + body["verification_uri_complete"], ) - _, body, _ = self.endpoint.create_device_authorization_response( - *self.response_payload() + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_authorization_grant_user_gode_generator(self): + def user_code(): + """ + A friendly user code the device can display and the user + can type in. It's up to the device how + this code should be displayed. e.g 123-456 + """ + return "123456" + + self._configure_endpoint( + verification_uri_complete=lambda u: f"http://i.l/v?user_code={u}", + user_code_generator=user_code, ) + + _, body, _ = self.endpoint.create_device_authorization_response(*self.response_payload()) self.assertEqual( - "http://i.l/v?user_code=abc", - json.loads(body)["verification_uri_complete"], + "http://i.l/v?user_code=123456", + body["verification_uri_complete"], ) From 1fd5253630c03e3f12719dd8c13d43111f66a8d2 Mon Sep 17 00:00:00 2001 From: Jaap Roes Date: Mon, 21 Oct 2024 11:56:47 +0200 Subject: [PATCH 053/115] Guard ui_locales.split() (#879) `request.ui_locales` might already be a list, only split the value if the `split` method is available. --- oauthlib/openid/connect/core/grant_types/base.py | 6 +++++- .../core/endpoints/test_openid_connect_params_handling.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 33411dad..29d583eb 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -310,11 +310,15 @@ def openid_authorization_validator(self, request): msg = "Session user does not match client supplied user." raise LoginRequired(request=request, description=msg) + ui_locales = request.ui_locales if request.ui_locales else [] + if hasattr(ui_locales, 'split'): + ui_locales = ui_locales.strip().split() + request_info = { 'display': request.display, 'nonce': request.nonce, 'prompt': prompt, - 'ui_locales': request.ui_locales.split() if request.ui_locales else [], + 'ui_locales': ui_locales, 'id_token_hint': request.id_token_hint, 'login_hint': request.login_hint, 'claims': request.claims diff --git a/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py index c55136fb..5b04edff 100644 --- a/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py +++ b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py @@ -28,7 +28,8 @@ def setUp(self): 'redirect_uri': 'https://a.b/cb', 'response_type': 'code', 'client_id': 'abcdef', - 'scope': 'hello openid' + 'scope': 'hello openid', + 'ui_locales': 'en-US' } self.url = 'http://a.b/path?' + urlencode(params) @@ -76,3 +77,4 @@ def test_oidc_params_preservation(self): self.assertEqual(creds['prompt'], {'consent'}) self.assertEqual(creds['nonce'], 'abcd') self.assertEqual(creds['display'], 'touch') + self.assertEqual(creds['ui_locales'], ['en-US']) From 028c563eb6af65426df45ba4bbfc771974e9594e Mon Sep 17 00:00:00 2001 From: david uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Sun, 8 Dec 2024 07:28:28 +0000 Subject: [PATCH 054/115] Add DeviceCodeGrant type for device code flow(rfc8628) section 3.4 & 3.5 (#889) * Add device grant specific errors These are from section 3.5 of the rfc https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 * Add device code grant handler In the device flow, the device must poll the token endpoint to check if the user has submitted the user_code. This commit adds that handler to validate the request and return the token. * Allow interval to be controlled by caller Any caller of DeviceApplicationServer() needs control of the inerval polling time. This is part of the flow where the device polls the auth server to check in the user has submitted the user_code * Use absolute imports Easier to read, easier to derive where the module lives. * Add device grant to oauth2 based server This grant type handler exists in both the openid conenct server and the oauth 2.0 server. Why? It sets up for a potentitial open id over device flow implementation. It also alo ensures the access_token and refresh_token still get returned whether openid is enabled or not. device flow isn't a spec that's part of open id but some IDps like microsoft, auth0, okta implement an off-spec version. However, that's not the concern of the pr this commit is apart of but if returning an id_token(openid) is needed down the line that will have to be a seperate issue rasied. * Add interval default to align with rfc * Add device code grant tests * Add device code grant docs * Apply ruff format using line-length 99 The 255 is too high, leads to lines that are too long. I'll get a pr to format the entire repo after this one * Add device code grant to support grant claiims * Add pseudocode example of how to implement the device flow * Update tests/oauth2/rfc8628/grant_types/test_device_code.py --------- Co-authored-by: Asif Saif Uddin --- docs/oauth2/grants/device_code.rst | 12 + docs/oauth2/grants/grants.rst | 8 + examples/device_code_flow.py | 260 +++++++++++++++++ oauthlib/oauth2/__init__.py | 3 +- .../rfc6749/endpoints/pre_configured.py | 275 +++++++++++------- oauthlib/oauth2/rfc8628/__init__.py | 5 + .../rfc8628/endpoints/pre_configured.py | 8 +- oauthlib/oauth2/rfc8628/errors.py | 55 ++++ .../oauth2/rfc8628/grant_types/__init__.py | 1 + .../oauth2/rfc8628/grant_types/device_code.py | 111 +++++++ .../connect/core/endpoints/pre_configured.py | 9 +- .../oauth2/rfc6749/endpoints/test_metadata.py | 1 + tests/oauth2/rfc8628/grant_types/__init__.py | 0 .../rfc8628/grant_types/test_device_code.py | 172 +++++++++++ 14 files changed, 813 insertions(+), 107 deletions(-) create mode 100644 docs/oauth2/grants/device_code.rst create mode 100644 examples/device_code_flow.py create mode 100644 oauthlib/oauth2/rfc8628/errors.py create mode 100644 oauthlib/oauth2/rfc8628/grant_types/__init__.py create mode 100644 oauthlib/oauth2/rfc8628/grant_types/device_code.py create mode 100644 tests/oauth2/rfc8628/grant_types/__init__.py create mode 100644 tests/oauth2/rfc8628/grant_types/test_device_code.py diff --git a/docs/oauth2/grants/device_code.rst b/docs/oauth2/grants/device_code.rst new file mode 100644 index 00000000..ff372b5c --- /dev/null +++ b/docs/oauth2/grants/device_code.rst @@ -0,0 +1,12 @@ +Device code Grant +----------------- + +.. autoclass:: oauthlib.oauth2.DeviceCodeGrant + :members: + :inherited-members: + + +An pseudocode/skeleton example of how the device flow can be implemented is +available in the `examples`_ folder on GitHub. + +.. _`examples`: https://github.com/oauthlib/oauthlib/blob/master/examples/device_code_flow.py diff --git a/docs/oauth2/grants/grants.rst b/docs/oauth2/grants/grants.rst index e1837617..d877bac6 100644 --- a/docs/oauth2/grants/grants.rst +++ b/docs/oauth2/grants/grants.rst @@ -10,6 +10,7 @@ Grant types password credentials refresh + device_code jwt custom_validators custom_grant @@ -26,6 +27,13 @@ degree of trust between the resource owner and the client, and when other authorization grant types are not available. This is also often used for legacy applications to incrementally transition to OAuth 2. +The device code grant(officially referred to as 'urn:ietf:params:oauth:grant-type:device_code') +is used when trying to authenticate device with limited or no input capabilities by getting +the user to approve the login on an external device (like a mobile phone or laptop) in their +possession that they're already logged into. Unlike the previously mentioned grants it is an extension grant, which is a type of grant +to address specific authorization scenarios. +:doc:`Device code grant ` + The main purpose of the grant types is to authorize access to protected resources in various ways with different security credentials. diff --git a/examples/device_code_flow.py b/examples/device_code_flow.py new file mode 100644 index 00000000..d281491f --- /dev/null +++ b/examples/device_code_flow.py @@ -0,0 +1,260 @@ +import enum +import json +import datetime +from datetime import timedelta + +from oauthlib.oauth2 import RequestValidator, Server, DeviceApplicationServer +from oauthlib.oauth2.rfc8628 import errors as device_flow_errors +from oauthlib.oauth2.rfc8628.errors import AccessDenied, AuthorizationPendingError, ExpiredTokenError, SlowDownError + + +""" +A pseudocode implementation of the device flow code under an Oauth2 provider. + +This example is not concerned with openid in any way. + +This example is also not a 1:1 pseudocode implementation. Please refer to the rfc +for the full details. +https://datatracker.ietf.org/doc/html/rfc8628 + +This module is just acting as a way to demonstrate the main pieces +needed in oauthlib to implement the flow + + +We also assume you already have the /token & /login endpoint in your provider. + +Your provider will also need the following endpoints(which will be discussed +in the example below): + - /device_authorization (part of rfc) + - /device (part of rfc) + - /approve-deny (up to your implementation, this is an example) +""" + + +""" +Device flow pseudocode implementation step by step: + 0. Providing some way to represent the device flow session + + Some python object to represent the current state of the device during + the device flow. This, for example, could be an object that persists + and represents the device in a database +""" + + +class Device: + class DeviceFlowStatus(enum.Enum): + AUTHORIZED = "Authorized" + AUTHORIZATION_PENDING = "Authorization_pending" + EXPIRED = "Expired" + DENIED = "Denied" + + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 + id = ... # if Device is representing a database object, this will be the id of that row + device_code = ... + user_code = ... + scope = ... + interval = ... # in seconds, default is 5 + expires = ... # seconds + status = ... # DeviceFlowStatus with AUTHORIZATION_PENDING as the default + + client_id = ... + last_checked = ... # datetime + + +""" + 1. User goes on their device(client) and the client sends a request to /device_authorization + against the provider: + https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 + https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + + + POST /device_authorization HTTP/1.1 + Host: server.example.com + Content-Type: application/x-www-form-urlencoded + + client_id=1406020730&scope=example_scope + + Response: + HTTP/1.1 200 OK + Content-Type: application/json + Cache-Control: no-store + + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://example.com/device", + "verification_uri_complete": + "https://example.com/device?user_code=WDJB-MJHT", + "expires_in": 1800, + "interval": 5 + } +""" + + +class DeviceAuthorizationEndpoint: + @staticmethod + def create_device_authorization_response(request): + server = DeviceApplicationServer(interval=5, verification_uri="https://example.com/device") + return server.create_device_authorization_response(request) + + def post(self, request): + headers, data, status = self.create_device_authorization_response(request) + device_response = ... + + # Create an instance of examples.device_flow.Device` using `request` and `data`that encapsulates + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 & + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + + return device_response + + +""" + 2. Client presents the information to the user + (There's a section on non visual capable devices as well + https://datatracker.ietf.org/doc/html/rfc8628#section-5.7) + +-------------------------------------------------+ + | | + | Scan the QR code or, using +------------+ | + | a browser on another device, |[_].. . [_]| | + | visit: | . .. . .| | + | https://example.com/device | . . . ....| | + | |. . . . | | + | And enter the code: |[_]. ... . | | + | WDJB-MJHT +------------+ | + | | + +-------------------------------------------------+ +""" +# The implementation for step 2 is up to the owner of device. + + +"""" + 3 (The browser flow). User goes to https://example.com/device where they're presented with a + form to fill in the user code. + + Implement that endpoint on your provider and follow the logic in the rfc. + + Making use of the errors in `oauthlib.oauth2.rfc8628.errors` + + raise AccessDenied/AuthorizationPendingError/ExpiredTokenError where appropriate making use of + `examples.device_flow.Device` to get and update current state of the device during the session + + If the user isn't logged in(after inputting the user-code), they should be redirected to the provider's /login + endpoint and redirected back to an /approve-deny endpoint(The name and implementation of /approve-deny is up + to the owner of the provider, this is just an example). + They should then see an "approve" or "deny" button to authorize the device. + + Again, using `examples.device_flow.Device` to update the status appropriately during the session. +""" +# /device and /approve-deny is up to the owner of the provider to implement. Again, make sure to +# keep referring to the rfc when implementing. + + +""" +4 (The polling flow) + https://datatracker.ietf.org/doc/html/rfc8628#section-3.4 + https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 + + + Right after step 2, the device polls the /token endpoint every "interval" amount of seconds + to check if user has approved or denied the request. + + When grant type is `urn:ietf:params:oauth:grant-type:device_code`, + `oauthlib.oauth2.rfc8628.grant_types.device_code.DeviceCodeGrant` will be the handler + that handles token generation. +""" + + +# This is purely for illustrative purposes +# to demonstrate rate limiting on the token endpoint for the device flow. +# It is up to as the provider to decide how you want +# to rate limit the device during polling. +def rate_limit(func, rate="1/5s"): + def wrapper(): + # some logic to ensure client device is rate limited by a minimum + # of 1 request every 5 seconds during device polling + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 + + # use device_code to retrieve device + device = Device + + # get the time in seconds since the device polled the /token endpoint + now = datetime.datetime.now(tz=datetime.UTC) + diff = now - timedelta(device.last_checked) + total_seconds_since_last_device_poll = diff.total_seconds() + + device.last_checked = now + + # for illustrative purposes. 1/5s means 1 request every 5 seconds. + # so if `total_seconds_since_last_device_poll` is 4 seconds, this will + # raise an error + if total_seconds_since_last_device_poll < rate: + raise device_flow_errors.SlowDownError() + + result = func() + return result + + return wrapper + + +class ExampleRequestValidator(RequestValidator): + # All the other methods that need to be implemented... + # see examples.skeleton_oauth2_web_application_server.SkeletonValidator + # for a more complete example. + + # Here our main concern is this method: + def create_token_response(self): ... + + +class ServerSetupForTokenEndpoint: + def __init__(self): + validator = ExampleRequestValidator + self.server = Server(validator) + + +# You should already have the /token endpoint implemented in your provider. +class TokenEndpoint(ServerSetupForTokenEndpoint): + def default_flow_token_response(self, request): + url, headers, body, status = self.server.create_token_response(request) + access_token = json.loads(body).get("access_token") + + # return access_token in a http response + return access_token + + @rate_limit # this will raise the SlowDownError + def device_flow_token_response(self, request, device_code): + """ + Following the rfc, this will route the device request accordingly and raise + required errors. + + Remember that unlike other auth flows, the device if polling this endpoint once + every "interval" amount of seconds. + """ + # using device_code arg to retrieve the correct device object instance + device = Device + + if device.status == device.DeviceFlowStatus.AUTHORIZATION_PENDING: + raise AuthorizationPendingError() + + # If user clicked "deny" in the /approve-deny page endpoint. + # the device gets set to 'authorized' in /approve-deny and /device checks + # if someone tries to input a code for a user code that's already been authorized + if device.status == device.DeviceFlowStatus.DENIED: + raise AccessDenied() + + url, headers, body, status = self.server.create_token_response(request) + + access_token = json.loads(body).get("access_token") + + device.status = device.EXPIRED + + # return access_token in a http response + return access_token + + # Example of how token endpoint could handle the token creation depending on + # the grant type during a POST to /token. + def post(self, request): + params = request.POST + if params.get("grant_type") == "urn:ietf:params:oauth:grant-type:device_code": + return self.device_flow_token_response(request, params["device_code"]) + return self.default_flow_token_response(request) diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index da9ccf3d..3bb51021 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -66,4 +66,5 @@ from .rfc6749.tokens import BearerToken, OAuth2Token from .rfc6749.utils import is_secure_transport from .rfc8628.clients import DeviceClient -from .rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer +from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint, DeviceApplicationServer +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index d64a1663..ef7db0c9 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -5,9 +5,13 @@ This module is an implementation of various endpoints needed for providing OAuth 2.0 RFC6749 servers. """ + from ..grant_types import ( - AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant, + AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant, ) from ..tokens import BearerToken from .authorization import AuthorizationEndpoint @@ -15,16 +19,26 @@ from .resource import ResourceEndpoint from .revocation import RevocationEndpoint from .token import TokenEndpoint +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant -class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint): - - """An all-in-one endpoint featuring all four major grant types.""" +class Server( + AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, ResourceEndpoint, RevocationEndpoint +): + """ + An all-in-one endpoint featuring all four major grant types + and extension grants. + """ - def __init__(self, request_validator, token_expires_in=None, - token_generator=None, refresh_token_generator=None, - *args, **kwargs): + 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 @@ -40,43 +54,58 @@ def __init__(self, request_validator, token_expires_in=None, """ self.auth_grant = AuthorizationCodeGrant(request_validator) self.implicit_grant = ImplicitGrant(request_validator) - self.password_grant = ResourceOwnerPasswordCredentialsGrant( - request_validator) + self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) + self.device_code_grant = DeviceCodeGrant(request_validator) + + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + + AuthorizationEndpoint.__init__( + self, + default_response_type="code", + response_types={ + "code": self.auth_grant, + "token": self.implicit_grant, + "none": self.auth_grant, + }, + default_token_type=self.bearer, + ) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={ - 'code': self.auth_grant, - 'token': self.implicit_grant, - 'none': self.auth_grant - }, - default_token_type=self.bearer) - - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': self.auth_grant, - 'password': self.password_grant, - 'client_credentials': self.credentials_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) + TokenEndpoint.__init__( + self, + default_grant_type="authorization_code", + grant_types={ + "authorization_code": self.auth_grant, + "password": self.password_grant, + "client_credentials": self.credentials_grant, + "refresh_token": self.refresh_grant, + "urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) -class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint): - +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): + 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 @@ -92,30 +121,44 @@ def __init__(self, request_validator, token_generator=None, """ self.auth_grant = AuthorizationCodeGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - AuthorizationEndpoint.__init__(self, default_response_type='code', - response_types={'code': self.auth_grant}, - default_token_type=self.bearer) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', - grant_types={ - 'authorization_code': self.auth_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + AuthorizationEndpoint.__init__( + self, + default_response_type="code", + response_types={"code": self.auth_grant}, + default_token_type=self.bearer, + ) + TokenEndpoint.__init__( + self, + default_grant_type="authorization_code", + grant_types={ + "authorization_code": self.auth_grant, + "refresh_token": self.refresh_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) -class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, - ResourceEndpoint, RevocationEndpoint): - +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): + 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 @@ -130,27 +173,39 @@ def __init__(self, request_validator, token_generator=None, token-, resource-, and revocation-endpoint constructors. """ self.implicit_grant = ImplicitGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - AuthorizationEndpoint.__init__(self, default_response_type='token', - response_types={ - 'token': self.implicit_grant}, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) - RevocationEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) - - -class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, - ResourceEndpoint, RevocationEndpoint): + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + AuthorizationEndpoint.__init__( + self, + default_response_type="token", + response_types={"token": self.implicit_grant}, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) + RevocationEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) + IntrospectEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) + +class LegacyApplicationServer( + TokenEndpoint, IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint +): """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" - def __init__(self, request_validator, token_generator=None, - token_expires_in=None, refresh_token_generator=None, **kwargs): + 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 @@ -164,30 +219,40 @@ def __init__(self, request_validator, token_generator=None, :param kwargs: Extra parameters to pass to authorization-, token-, resource-, and revocation-endpoint constructors. """ - self.password_grant = ResourceOwnerPasswordCredentialsGrant( - request_validator) + self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - TokenEndpoint.__init__(self, default_grant_type='password', - grant_types={ - 'password': self.password_grant, - 'refresh_token': self.refresh_grant, - }, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + TokenEndpoint.__init__( + self, + default_grant_type="password", + grant_types={ + "password": self.password_grant, + "refresh_token": self.refresh_grant, + }, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) -class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, - ResourceEndpoint, RevocationEndpoint): - +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): + 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 @@ -202,15 +267,21 @@ def __init__(self, request_validator, token_generator=None, token-, resource-, and revocation-endpoint constructors. """ self.credentials_grant = ClientCredentialsGrant(request_validator) - self.bearer = BearerToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - TokenEndpoint.__init__(self, default_grant_type='client_credentials', - grant_types={ - 'client_credentials': self.credentials_grant}, - default_token_type=self.bearer) - ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': self.bearer}) - RevocationEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator, - supported_token_types=['access_token']) + self.bearer = BearerToken( + request_validator, token_generator, token_expires_in, refresh_token_generator + ) + TokenEndpoint.__init__( + self, + default_grant_type="client_credentials", + grant_types={"client_credentials": self.credentials_grant}, + default_token_type=self.bearer, + ) + ResourceEndpoint.__init__( + self, default_token="Bearer", token_types={"Bearer": self.bearer} + ) + RevocationEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) + IntrospectEndpoint.__init__( + self, request_validator, supported_token_types=["access_token"] + ) diff --git a/oauthlib/oauth2/rfc8628/__init__.py b/oauthlib/oauth2/rfc8628/__init__.py index 6c3d14af..65891445 100644 --- a/oauthlib/oauth2/rfc8628/__init__.py +++ b/oauthlib/oauth2/rfc8628/__init__.py @@ -6,6 +6,11 @@ for consuming and providing OAuth 2.0 Device Authorization RFC8628. """ +from oauthlib.oauth2.rfc8628.errors import ( + SlowDownError, + AuthorizationPendingError, + ExpiredTokenError, +) import logging log = logging.getLogger(__name__) diff --git a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index cdb6b313..bfc6c404 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -2,6 +2,7 @@ DeviceAuthorizationEndpoint, ) from typing import Callable +from oauthlib.openid.connect.core.request_validator import RequestValidator class DeviceApplicationServer(DeviceAuthorizationEndpoint): @@ -9,8 +10,9 @@ class DeviceApplicationServer(DeviceAuthorizationEndpoint): def __init__( self, - request_validator, - verification_uri, + request_validator: RequestValidator, + verification_uri: str, + interval: int = 5, user_code_generator: Callable[[None], str] = None, **kwargs, ): @@ -18,12 +20,14 @@ def __init__( :param request_validator: An implementation of oauthlib.oauth2.rfc8626.RequestValidator. + :param interval: How long the device needs to wait before polling the server :param verification_uri: the verification_uri to be send back. :param user_code_generator: a callable that allows the user code to be configured. """ DeviceAuthorizationEndpoint.__init__( self, request_validator, + interval=interval, verification_uri=verification_uri, user_code_generator=user_code_generator, ) diff --git a/oauthlib/oauth2/rfc8628/errors.py b/oauthlib/oauth2/rfc8628/errors.py new file mode 100644 index 00000000..a4359389 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/errors.py @@ -0,0 +1,55 @@ +from oauthlib.oauth2.rfc6749.errors import OAuth2Error + +""" +oauthlib.oauth2.rfc8628.errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Error used both by OAuth2 clients and providers to represent the spec +defined error responses specific to the the device grant +""" + + +class AuthorizationPendingError(OAuth2Error): + """ + For the device authorization grant; + The authorization request is still pending as the end user hasn't + yet completed the user-interaction steps (Section 3.3). The + client SHOULD repeat the access token request to the token + endpoint (a process known as polling). Before each new request, + the client MUST wait at least the number of seconds specified by + the "interval" parameter of the device authorization response, + or 5 seconds if none was provided, and respect any + increase in the polling interval required by the "slow_down" + error. + """ + + error = "authorization_pending" + + +class SlowDownError(OAuth2Error): + """ + A variant of "authorization_pending", the authorization request is + still pending and polling should continue, but the interval MUST + be increased by 5 seconds for this and all subsequent requests. + """ + + error = "slow_down" + + +class ExpiredTokenError(OAuth2Error): + """ + The "device_code" has expired, and the device authorization + session has concluded. The client MAY commence a new device + authorization request but SHOULD wait for user interaction before + restarting to avoid unnecessary polling. + """ + + error = "expired_token" + + +class AccessDenied(OAuth2Error): + """ + The authorization request was denied. + """ + + error = "access_denied" diff --git a/oauthlib/oauth2/rfc8628/grant_types/__init__.py b/oauthlib/oauth2/rfc8628/grant_types/__init__.py new file mode 100644 index 00000000..418dba77 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/grant_types/__init__.py @@ -0,0 +1 @@ +from oauthlib.oauth2.rfc8628.grant_types.device_code import DeviceCodeGrant diff --git a/oauthlib/oauth2/rfc8628/grant_types/device_code.py b/oauthlib/oauth2/rfc8628/grant_types/device_code.py new file mode 100644 index 00000000..8e4393d3 --- /dev/null +++ b/oauthlib/oauth2/rfc8628/grant_types/device_code.py @@ -0,0 +1,111 @@ +from __future__ import annotations +import json + +from typing import Callable + +from oauthlib import common # noqa: TCH001 + +from oauthlib.oauth2.rfc6749 import errors as rfc6749_errors +from oauthlib.oauth2.rfc6749.grant_types.base import GrantTypeBase + + +class DeviceCodeGrant(GrantTypeBase): + def create_authorization_response( + self, request: common.Request, token_handler: Callable + ) -> tuple[dict, str, int]: + """ + Validate the device flow request -> create the access token + -> persist the token -> return the token. + """ + headers = self._get_default_headers() + try: + self.validate_token_request(request) + except rfc6749_errors.OAuth2Error as 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) + + return self.create_token_response(request, token_handler) + + def validate_token_request(self, request: common.Request) -> None: + """ + Performs the necessary check against the request to ensure + it's allowed to retrieve a token. + """ + for validator in self.custom_validators.pre_token: + validator(request) + + if not getattr(request, "grant_type", None): + raise rfc6749_errors.InvalidRequestError( + "Request is missing grant type.", request=request + ) + + if request.grant_type != "urn:ietf:params:oauth:grant-type:device_code": + raise rfc6749_errors.UnsupportedGrantTypeError(request=request) + + for param in ("grant_type", "scope"): + if param in request.duplicate_params: + raise rfc6749_errors.InvalidRequestError( + description=f"Duplicate {param} parameter.", request=request + ) + + if not self.request_validator.authenticate_client(request): + raise rfc6749_errors.InvalidClientError(request=request) + elif not hasattr(request.client, "client_id"): + raise NotImplementedError( + "Authenticate client must set the " + "request.client.client_id attribute " + "in authenticate_client." + ) + + # Ensure client is authorized use of this grant type + self.validate_grant_type(request) + + request.client_id = request.client_id or request.client.client_id + self.validate_scopes(request) + + for validator in self.custom_validators.post_token: + validator(request) + + def create_token_response( + self, request: common.Request, token_handler: Callable + ) -> tuple[dict, str, int]: + """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 + ) and not self.request_validator.authenticate_client(request): + raise rfc6749_errors.InvalidClientError(request=request) + + self.validate_token_request(request) + + except rfc6749_errors.OAuth2Error as e: + headers.update(e.headers) + return headers, e.json, e.status_code + + token = token_handler.create_token(request, self.refresh_token) + + self.request_validator.save_token(token, request) + + return headers, json.dumps(token), 200 diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 7c9393e6..62983014 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -19,8 +19,8 @@ ImplicitGrant as OAuth2ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ) +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant from oauthlib.oauth2.rfc6749.tokens import BearerToken -from oauthlib.oauth2.rfc8628.endpoints import DeviceAuthorizationEndpoint from ..grant_types import ( AuthorizationCodeGrant, @@ -45,7 +45,10 @@ class Server( RevocationEndpoint, UserInfoEndpoint, ): - """An all-in-one endpoint featuring all four major grant types.""" + """ + An all-in-one endpoint featuring all four major grant types + and extension grants. + """ def __init__( self, @@ -77,6 +80,7 @@ def __init__( self.openid_connect_auth = AuthorizationCodeGrant(request_validator) self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator) + self.device_code_grant = DeviceCodeGrant(request_validator) self.bearer = BearerToken( request_validator, token_generator, token_expires_in, refresh_token_generator @@ -123,6 +127,7 @@ def __init__( "password": self.password_grant, "client_credentials": self.credentials_grant, "refresh_token": self.refresh_grant, + "urn:ietf:params:oauth:grant-type:device_code": self.device_code_grant, }, default_token_type=self.bearer, ) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index bcb9c0f5..facf69d0 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -98,6 +98,7 @@ def test_server_metadata(self): "scopes_supported": ["email", "profile"], "grant_types_supported": [ "authorization_code", + "urn:ietf:params:oauth:grant-type:device_code", "password", "client_credentials", "refresh_token", diff --git a/tests/oauth2/rfc8628/grant_types/__init__.py b/tests/oauth2/rfc8628/grant_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/oauth2/rfc8628/grant_types/test_device_code.py b/tests/oauth2/rfc8628/grant_types/test_device_code.py new file mode 100644 index 00000000..da0592f7 --- /dev/null +++ b/tests/oauth2/rfc8628/grant_types/test_device_code.py @@ -0,0 +1,172 @@ +import json +from unittest import mock +import pytest + +from oauthlib import common + +from oauthlib.oauth2.rfc8628.grant_types import DeviceCodeGrant +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +def create_request(body: str = "") -> common.Request: + request = common.Request("http://a.b/path", body=body or None) + request.scopes = ("hello", "world") + request.expires_in = 1800 + request.client = "batman" + request.client_id = "abcdef" + request.code = "1234" + request.response_type = "code" + request.grant_type = "urn:ietf:params:oauth:grant-type:device_code" + request.redirect_uri = "https://a.b/" + return request + + +def create_device_code_grant(mock_validator: mock.MagicMock) -> DeviceCodeGrant: + return DeviceCodeGrant(request_validator=mock_validator) + + +def test_custom_auth_validators_unsupported(): + custom_validator = mock.Mock() + validator = mock.MagicMock() + + expected = ( + "DeviceCodeGrant does not " + "support authorization validators. Use token validators instead." + ) + with pytest.raises(ValueError, match=expected): + DeviceCodeGrant(validator, pre_auth=[custom_validator]) + + with pytest.raises(ValueError, match=expected): + DeviceCodeGrant(validator, post_auth=[custom_validator]) + + expected = "'tuple' object has no attribute 'append'" + auth = DeviceCodeGrant(validator) + with pytest.raises(AttributeError, match=expected): + auth.custom_validators.pre_auth.append(custom_validator) + + +def test_custom_pre_and_post_token_validators(): + client = mock.MagicMock() + + validator = mock.MagicMock() + pre_token_validator = mock.Mock() + post_token_validator = mock.Mock() + + request: common.Request = create_request() + request.client = client + + auth = DeviceCodeGrant(validator) + + auth.custom_validators.pre_token.append(pre_token_validator) + auth.custom_validators.post_token.append(post_token_validator) + + bearer = BearerToken(validator) + auth.create_token_response(request, bearer) + + pre_token_validator.assert_called() + post_token_validator.assert_called() + + +def test_create_token_response(): + validator = mock.MagicMock() + request: common.Request = create_request() + request.client = mock.Mock() + + auth = DeviceCodeGrant(validator) + + bearer = BearerToken(validator) + + headers, body, status_code = auth.create_token_response(request, bearer) + token = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + } + + # when a custom token generator callable isn't used + # the random generator is used as default for the access token + assert token == { + "access_token": mock.ANY, + "expires_in": 3600, + "token_type": "Bearer", + "scope": "hello world", + "refresh_token": mock.ANY, + } + + assert status_code == 200 + + validator.save_token.assert_called_once() + + +def test_invalid_client_error(): + validator = mock.MagicMock() + request: common.Request = create_request() + request.client = mock.Mock() + + auth = DeviceCodeGrant(validator) + bearer = BearerToken(validator) + + validator.authenticate_client.return_value = False + + headers, body, status_code = auth.create_token_response(request, bearer) + body = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + "WWW-Authenticate": 'Bearer error="invalid_client"', + } + assert body == {"error": "invalid_client"} + assert status_code == 401 + + validator.save_token.assert_not_called() + + +def test_invalid_grant_type_error(): + validator = mock.MagicMock() + request: common.Request = create_request() + request.client = mock.Mock() + + request.grant_type = "not_device_code" + + auth = DeviceCodeGrant(validator) + bearer = BearerToken(validator) + + headers, body, status_code = auth.create_token_response(request, bearer) + body = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + } + assert body == {"error": "unsupported_grant_type"} + assert status_code == 400 + + validator.save_token.assert_not_called() + + +def test_duplicate_params_error(): + validator = mock.MagicMock() + request: common.Request = create_request( + "client_id=123&scope=openid&scope=openid" + ) + request.client = mock.Mock() + + auth = DeviceCodeGrant(validator) + bearer = BearerToken(validator) + + headers, body, status_code = auth.create_token_response(request, bearer) + body = json.loads(body) + + assert headers == { + "Content-Type": "application/json", + "Cache-Control": "no-store", + "Pragma": "no-cache", + } + assert body == {"error": "invalid_request", "error_description": "Duplicate scope parameter."} + assert status_code == 400 + + validator.save_token.assert_not_called() From 028a8f18808bebf3659388f914fc6eb89f9d2bda Mon Sep 17 00:00:00 2001 From: david uzumaki <56260075+duzumaki@users.noreply.github.com> Date: Tue, 28 Jan 2025 08:57:44 +0000 Subject: [PATCH 055/115] Device flow: Pass verification_uri_complete to endpoint + pass Server kwargs to DeviceCodeGrant to allow validators to be setup with more flexibility (#891) * Ensure verification_uri_complete make it to DeviceAuthorizationEndpoint Needed as a caller is unable to tell the device endpoint to return the verification_uri_complete value * Ensure the device grant gets the kwargs In order for the device grant's pre token custom valisator to run as the parent class "GrantTypeBase" sets it up like 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', []) The need for this was discovered because a pre_token callable needs to be passed in order to set oauthlib's request.user attribute accordingly before the token gets saved * Add test * Ruff Don't know why it's complaining on modules I didn't touch A005 Module `resource` shadows a Python standard-library module PLC0206 Extracting value from dictionary without calling `.items()` RUF023 `BearerToken.__slots__` is not sorted ignoring as this isn't realted to this pr * Update device docs --- docs/oauth2/endpoints/device.rst | 11 +++++++- .../rfc6749/endpoints/pre_configured.py | 2 +- oauthlib/oauth2/rfc8628/clients/device.py | 6 ++--- .../rfc8628/endpoints/device_authorization.py | 1 + .../rfc8628/endpoints/pre_configured.py | 5 +++- .../oauth2/rfc8628/grant_types/device_code.py | 2 +- .../connect/core/endpoints/pre_configured.py | 2 +- ruff.toml | 6 +++++ .../test_device_application_server.py | 26 +++++++++++++++++++ 9 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 tests/oauth2/rfc8628/endpoints/test_device_application_server.py diff --git a/docs/oauth2/endpoints/device.rst b/docs/oauth2/endpoints/device.rst index 728fd3c2..c88437a1 100644 --- a/docs/oauth2/endpoints/device.rst +++ b/docs/oauth2/endpoints/device.rst @@ -22,12 +22,21 @@ the device authorization endpoint. from your_validator import your_validator verification_uri = "https://example.com/device" + # verification_uri_complete can either be a callable that receives user code as an arg + # or a string (e.g verification_uri_complete = "https://example.com/device=1234") + verification_uri_complete = lambda user_code: f"https://example.com/device={user_code}" + def user_code(): # some logic to generate a random string... return "123-456" # user code is optional - server = DeviceApplicationServer(your_validator, verification_uri, user_code) + server = DeviceApplicationServer( + request_validator=your_validator, + verification_uri=verification_uri, + verification_uri_complete=verification_uri_complete, + user_code=user_code + ) headers, data, status = server.create_device_authorization_response(request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index ef7db0c9..8f6aa32b 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -57,7 +57,7 @@ def __init__( self.password_grant = ResourceOwnerPasswordCredentialsGrant(request_validator) self.credentials_grant = ClientCredentialsGrant(request_validator) self.refresh_grant = RefreshTokenGrant(request_validator) - self.device_code_grant = DeviceCodeGrant(request_validator) + self.device_code_grant = DeviceCodeGrant(request_validator, **kwargs) self.bearer = BearerToken( request_validator, token_generator, token_expires_in, refresh_token_generator diff --git a/oauthlib/oauth2/rfc8628/clients/device.py b/oauthlib/oauth2/rfc8628/clients/device.py index b9ba2150..ee0ccf8d 100644 --- a/oauthlib/oauth2/rfc8628/clients/device.py +++ b/oauthlib/oauth2/rfc8628/clients/device.py @@ -45,9 +45,9 @@ def prepare_request_uri(self, uri, scope=None, **kwargs): if scope: params.append(('scope', list_to_scope(scope))) - for k in kwargs: - if kwargs[k]: - params.append((str(k), kwargs[k])) + for k,v in kwargs.items(): + if v: + params.append((str(k), v)) return add_params_to_uri(uri, params) diff --git a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py index 7cda5ee1..3f38a540 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py +++ b/oauthlib/oauth2/rfc8628/endpoints/device_authorization.py @@ -224,6 +224,7 @@ def create_device_authorization_response( if self.interval is not None: data["interval"] = self.interval + verification_uri_complete = self.verification_uri_complete(user_code) if verification_uri_complete: data["verification_uri_complete"] = verification_uri_complete diff --git a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py index bfc6c404..6cce6833 100644 --- a/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc8628/endpoints/pre_configured.py @@ -1,7 +1,8 @@ from oauthlib.oauth2.rfc8628.endpoints.device_authorization import ( DeviceAuthorizationEndpoint, ) -from typing import Callable + +from typing import Callable, Optional from oauthlib.openid.connect.core.request_validator import RequestValidator @@ -13,6 +14,7 @@ def __init__( request_validator: RequestValidator, verification_uri: str, interval: int = 5, + verification_uri_complete: Optional[str] = None, # noqa: FA100 user_code_generator: Callable[[None], str] = None, **kwargs, ): @@ -30,4 +32,5 @@ def __init__( interval=interval, verification_uri=verification_uri, user_code_generator=user_code_generator, + verification_uri_complete=verification_uri_complete, ) diff --git a/oauthlib/oauth2/rfc8628/grant_types/device_code.py b/oauthlib/oauth2/rfc8628/grant_types/device_code.py index 8e4393d3..082daf0f 100644 --- a/oauthlib/oauth2/rfc8628/grant_types/device_code.py +++ b/oauthlib/oauth2/rfc8628/grant_types/device_code.py @@ -3,7 +3,7 @@ from typing import Callable -from oauthlib import common # noqa: TCH001 +from oauthlib import common # noqa: TC001 from oauthlib.oauth2.rfc6749 import errors as rfc6749_errors from oauthlib.oauth2.rfc6749.grant_types.base import GrantTypeBase diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 62983014..17df516d 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -80,7 +80,7 @@ def __init__( self.openid_connect_auth = AuthorizationCodeGrant(request_validator) self.openid_connect_implicit = ImplicitGrant(request_validator) self.openid_connect_hybrid = HybridGrant(request_validator) - self.device_code_grant = DeviceCodeGrant(request_validator) + self.device_code_grant = DeviceCodeGrant(request_validator, **kwargs) self.bearer = BearerToken( request_validator, token_generator, token_expires_in, refresh_token_generator diff --git a/ruff.toml b/ruff.toml index 443f123e..0546d652 100644 --- a/ruff.toml +++ b/ruff.toml @@ -98,6 +98,12 @@ max-complexity = 24 # default is 10 "oauthlib/oauth2/rfc6749/endpoints/base.py" = ["BLE001"] "oauthlib/openid/connect/core/grant_types/base.py" = ["BLE001"] "tests/*" = ["PT009", "PT027", "S101"] +"oauthlib/oauth1/rfc5849/endpoints/resource.py" = ["A005"] +"oauthlib/oauth2/rfc6749/endpoints/resource.py" = ["A005"] +"oauthlib/oauth2/rfc6749/endpoints/token.py" = ["A005"] +"oauthlib/oauth2/rfc6749/parameters.py" = ["PLC0206"] +"oauthlib/oauth2/rfc6749/tokens.py" = ["RUF023"] +"oauthlib/openid/connect/core/tokens.py" = ["RUF023"] # [tool.ruff.pylint] [pylint] diff --git a/tests/oauth2/rfc8628/endpoints/test_device_application_server.py b/tests/oauth2/rfc8628/endpoints/test_device_application_server.py new file mode 100644 index 00000000..f0436754 --- /dev/null +++ b/tests/oauth2/rfc8628/endpoints/test_device_application_server.py @@ -0,0 +1,26 @@ +import json +from unittest import TestCase, mock + +from oauthlib.common import Request, urlencode +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc8628.endpoints.pre_configured import DeviceApplicationServer +from oauthlib.oauth2.rfc8628.request_validator import RequestValidator + + +def test_server_set_up_device_endpoint_instance_attributes_correctly(): + """ + Simple test that just instantiates DeviceApplicationServer + and asserts the important attributes are present + """ + validator = mock.MagicMock(spec=RequestValidator) + validator.get_default_redirect_uri.return_value = None + validator.get_code_challenge.return_value = None + + verification_uri = "test.com/device" + verification_uri_complete = "test.com/device?user_code=123" + device = DeviceApplicationServer(validator, verification_uri=verification_uri, verification_uri_complete=verification_uri_complete) + device_vars = vars(device) + assert device_vars["_verification_uri_complete"] == "test.com/device?user_code=123" + assert device_vars["_verification_uri"] == "test.com/device" + assert device_vars["_expires_in"] == 1800 + assert device_vars["_interval"] == 5 From c0844e779d9cadc3e4e44a8bf8c9219ed58a4425 Mon Sep 17 00:00:00 2001 From: Shawn Zivontsis Date: Thu, 6 Feb 2025 01:35:56 -0500 Subject: [PATCH 056/115] Remove code verifier regex (#893) * Remove code verifier regex Fixes #884 This removes a check which is not effective due to an incorrect regex syntax and theoretically shouldn't be necessary anyway. * Remove unused import --- oauthlib/oauth2/rfc6749/clients/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index a8de78ba..a94e73c4 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -8,7 +8,6 @@ """ import base64 import hashlib -import re import time import warnings @@ -490,12 +489,8 @@ def create_code_verifier(self, length): if not length <= 128: raise ValueError("Length must be less than or equal to 128") - allowed_characters = re.compile('^[A-Za-z0-9-._~]') code_verifier = generate_token(length, UNICODE_ASCII_CHARACTER_SET + "-._~") - if not re.search(allowed_characters, code_verifier): - raise ValueError("code_verifier contains invalid characters") - self.code_verifier = code_verifier return code_verifier From dab6a5ae1830ddd8a79c1e9687f63508eae60b57 Mon Sep 17 00:00:00 2001 From: Everton Seiei Arakaki Date: Sat, 8 Mar 2025 07:23:18 +0100 Subject: [PATCH 057/115] license: remove generic classifier (#895) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index b80ff3a3..1a100bac 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ def fread(fn): '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', From 8b6749d84f7ba104b7d9aa817a2974f3a700c8b3 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 08:11:05 +0200 Subject: [PATCH 058/115] Fix latest ruff findings Replaced deprecated ruff config --- oauthlib/oauth2/rfc6749/endpoints/resource.py | 2 +- ruff.toml | 10 +++++----- tests/oauth2/rfc6749/grant_types/test_refresh_token.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py index f7562255..d1ff5049 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/resource.py +++ b/oauthlib/oauth2/rfc6749/endpoints/resource.py @@ -81,4 +81,4 @@ def find_token_type(self, 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 + return estimates[0][1] if estimates else None diff --git a/ruff.toml b/ruff.toml index 0546d652..ec45dd4b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,7 +7,7 @@ # start with [tool.ruff # [tool.ruff] -select = [ +lint.select = [ "A", # flake8-builtins "AIR", # Airflow "ASYNC", # flake8-async @@ -65,7 +65,7 @@ select = [ # "TRY", # tryceratops # "UP", # pyupgrade ] -ignore = [ +lint.ignore = [ "F401", "F403", "F405", @@ -88,11 +88,11 @@ line-length = 255 target-version = "py37" # [tool.ruff.mccabe] -[mccabe] +[lint.mccabe] max-complexity = 24 # default is 10 # [tool.ruff.per-file-ignores] -[per-file-ignores] +[lint.per-file-ignores] "docs/conf.py" = ["A001", "INP001"] "oauthlib/oauth2/rfc6749/clients/base.py" = ["E722"] "oauthlib/oauth2/rfc6749/endpoints/base.py" = ["BLE001"] @@ -106,7 +106,7 @@ max-complexity = 24 # default is 10 "oauthlib/openid/connect/core/tokens.py" = ["RUF023"] # [tool.ruff.pylint] -[pylint] +[lint.pylint] allow-magic-value-types = ["int", "str"] max-args = 16 # default is 5 max-branches = 24 # default is 12 diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py index 0a4ddd9a..f963444a 100644 --- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py +++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py @@ -184,7 +184,7 @@ def test_valid_token_request(self): # all ok but without request.scope del self.request.scope self.auth.validate_token_request(self.request) - self.assertEqual(self.request.scopes, 'foo bar baz'.split()) + self.assertEqual(self.request.scopes, ['foo', 'bar', 'baz']) # CORS From 1137a2fbedb586f105c6512dabe3da17d0f8f88a Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 08:21:45 +0200 Subject: [PATCH 059/115] Removed year from license Removing date from copyrights to ease maintenance is the practice nowadays, see https://aboutcode.org/2023/update-copyright-each-new-year/ --- LICENSE | 2 +- docs/conf.py | 2 +- oauthlib/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index d5a9e9ac..e75118ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 The OAuthlib Community +Copyright (c) The OAuthlib Community All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/conf.py b/docs/conf.py index 05e93ee0..e609c541 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,7 @@ # General information about the project. project = 'OAuthLib' -copyright = '2019, The OAuthlib Community' +copyright = 'The OAuthlib Community' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 4f56ef14..9a5e0786 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -5,7 +5,7 @@ A generic, spec-compliant, thorough implementation of the OAuth request-signing logic. - :copyright: (c) 2019 by The OAuthlib Community + :copyright: (c) The OAuthlib Community :license: BSD, see LICENSE for details. """ import logging From f9cac23b8325c9b3461a61bad5e9ede2b75fd482 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 08:32:05 +0200 Subject: [PATCH 060/115] Removed unused bandit, since it has been replaced with ruff --- bandit.json | 1222 --------------------------------------------------- 1 file changed, 1222 deletions(-) delete mode 100644 bandit.json diff --git a/bandit.json b/bandit.json deleted file mode 100644 index 7161f005..00000000 --- a/bandit.json +++ /dev/null @@ -1,1222 +0,0 @@ -{ - "errors": [], - "generated_at": "2019-05-13T12:51:49Z", - "metrics": { - "_totals": { - "CONFIDENCE.HIGH": 3.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 10.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 12.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8338, - "nosec": 0 - }, - "oauthlib/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 25, - "nosec": 0 - }, - "oauthlib/common.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 337, - "nosec": 0 - }, - "oauthlib/oauth1/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 16, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/__init__.py": { - "CONFIDENCE.HIGH": 1.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 230, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/access_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 152, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/authorization.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 135, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 142, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/pre_configured.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 10, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/request_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 141, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/resource.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 97, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/signature_only.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 53, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/errors.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 58, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/parameters.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 75, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/request_validator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 630, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/signature.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 379, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/utils.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 61, - "nosec": 0 - }, - "oauthlib/oauth2/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 33, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 14, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 13, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/backend_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 56, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 3.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 3.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 384, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/legacy_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 67, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/mobile_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 140, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/service_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 144, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/web_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 165, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 18, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/authorization.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 85, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 71, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/introspect.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 98, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/metadata.py": { - "CONFIDENCE.HIGH": 2.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 2.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 182, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 5.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 5.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 189, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/resource.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 65, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/revocation.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 96, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 76, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/errors.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 311, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 10, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/authorization_code.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 389, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 199, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/client_credentials.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 96, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/implicit.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 259, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/refresh_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 102, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 156, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/parameters.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 335, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/request_validator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 504, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/tokens.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 277, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/utils.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 70, - "nosec": 0 - }, - "oauthlib/openid/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8, - "nosec": 0 - }, - "oauthlib/openid/connect/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 0, - "nosec": 0 - }, - "oauthlib/openid/connect/core/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 0, - "nosec": 0 - }, - "oauthlib/openid/connect/core/endpoints/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 9, - "nosec": 0 - }, - "oauthlib/openid/connect/core/endpoints/pre_configured.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 93, - "nosec": 0 - }, - "oauthlib/openid/connect/core/endpoints/userinfo.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 83, - "nosec": 0 - }, - "oauthlib/openid/connect/core/exceptions.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 117, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 15, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/authorization_code.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 32, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 234, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/dispatchers.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 66, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/exceptions.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 26, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/hybrid.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 38, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/implicit.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 35, - "nosec": 0 - }, - "oauthlib/openid/connect/core/request_validator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 235, - "nosec": 0 - }, - "oauthlib/openid/connect/core/tokens.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 42, - "nosec": 0 - }, - "oauthlib/signals.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 32, - "nosec": 0 - }, - "oauthlib/tokens/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 0, - "nosec": 0 - }, - "oauthlib/tokens/access_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 13, - "nosec": 0 - }, - "oauthlib/tokens/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8, - "nosec": 0 - }, - "oauthlib/tokens/id_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 6, - "nosec": 0 - }, - "oauthlib/uri_validate.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 93, - "nosec": 0 - } - }, - "results": [ - { - "code": "183 if request.body is not None and content_type_eligible:\n184 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n185 \n", - "filename": "oauthlib/oauth1/rfc5849/__init__.py", - "issue_confidence": "HIGH", - "issue_severity": "MEDIUM", - "issue_text": "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", - "line_number": 184, - "line_range": [ - 184 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b303-md5", - "test_id": "B303", - "test_name": "blacklist" - }, - { - "code": "49 \"\"\"\n50 refresh_token_key = 'refresh_token'\n51 \n52 def __init__(self, client_id,\n", - "filename": "oauthlib/oauth2/rfc6749/clients/base.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'refresh_token'", - "line_number": 50, - "line_range": [ - 50, - 51 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", - "test_id": "B105", - "test_name": "hardcoded_password_string" - }, - { - "code": "51 \n52 def __init__(self, client_id,\n53 default_token_placement=AUTH_HEADER,\n54 token_type='Bearer',\n55 access_token=None,\n56 refresh_token=None,\n57 mac_key=None,\n58 mac_algorithm=None,\n59 token=None,\n60 scope=None,\n61 state=None,\n62 redirect_url=None,\n63 state_generator=generate_token,\n64 **kwargs):\n65 \"\"\"Initialize a client with commonly used attributes.\n66 \n67 :param client_id: Client identifier given by the OAuth provider upon\n68 registration.\n69 \n70 :param default_token_placement: Tokens can be supplied in the Authorization\n71 header (default), the URL query component (``query``) or the request\n72 body (``body``).\n73 \n74 :param token_type: OAuth 2 token type. Defaults to Bearer. Change this\n75 if you specify the ``access_token`` parameter and know it is of a\n76 different token type, such as a MAC, JWT or SAML token. Can\n77 also be supplied as ``token_type`` inside the ``token`` dict parameter.\n78 \n79 :param access_token: An access token (string) used to authenticate\n80 requests to protected resources. Can also be supplied inside the\n81 ``token`` dict parameter.\n82 \n83 :param refresh_token: A refresh token (string) used to refresh expired\n84 tokens. Can also be supplied inside the ``token`` dict parameter.\n85 \n86 :param mac_key: Encryption key used with MAC tokens.\n87 \n88 :param mac_algorithm: Hashing algorithm for MAC tokens.\n89 \n90 :param token: A dict of token attributes such as ``access_token``,\n91 ``token_type`` and ``expires_at``.\n92 \n93 :param scope: A list of default scopes to request authorization for.\n94 \n95 :param state: A CSRF protection string used during authorization.\n96 \n97 :param redirect_url: The redirection endpoint on the client side to which\n98 the user returns after authorization.\n99 \n100 :param state_generator: A no argument state generation callable. Defaults\n101 to :py:meth:`oauthlib.common.generate_token`.\n102 \"\"\"\n103 \n104 self.client_id = client_id\n105 self.default_token_placement = default_token_placement\n106 self.token_type = token_type\n107 self.access_token = access_token\n108 self.refresh_token = refresh_token\n109 self.mac_key = mac_key\n110 self.mac_algorithm = mac_algorithm\n111 self.token = token or {}\n112 self.scope = scope\n113 self.state_generator = state_generator\n114 self.state = state\n115 self.redirect_url = redirect_url\n116 self.code = None\n117 self.expires_in = None\n118 self._expires_at = None\n119 self.populate_token_attributes(self.token)\n120 \n121 @property\n", - "filename": "oauthlib/oauth2/rfc6749/clients/base.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 52, - "line_range": [ - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - 91, - 92, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 102, - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117, - 118, - 119, - 120 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", - "test_id": "B107", - "test_name": "hardcoded_password_default" - }, - { - "code": "313 \n314 def prepare_token_revocation_request(self, revocation_url, token,\n315 token_type_hint=\"access_token\", body='', callback=None, **kwargs):\n316 \"\"\"Prepare a token revocation request.\n317 \n318 :param revocation_url: Provider token revocation endpoint URL.\n319 \n320 :param token: The access or refresh token to be revoked (string).\n321 \n322 :param token_type_hint: ``\"access_token\"`` (default) or\n323 ``\"refresh_token\"``. This is optional and if you wish to not pass it you\n324 must provide ``token_type_hint=None``.\n325 \n326 :param body:\n327 \n328 :param callback: A jsonp callback such as ``package.callback`` to be invoked\n329 upon receiving the response. Not that it should not include a () suffix.\n330 \n331 :param kwargs: Additional parameters to included in the request.\n332 \n333 :returns: The prepared request tuple with (url, headers, body).\n334 \n335 Note that JSONP request may use GET requests as the parameters will\n336 be added to the request URL query as opposed to the request body.\n337 \n338 An example of a revocation request\n339 \n340 .. code-block: http\n341 \n342 POST /revoke HTTP/1.1\n343 Host: server.example.com\n344 Content-Type: application/x-www-form-urlencoded\n345 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n346 \n347 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token\n348 \n349 An example of a jsonp revocation request\n350 \n351 .. code-block: http\n352 \n353 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1\n354 Host: server.example.com\n355 Content-Type: application/x-www-form-urlencoded\n356 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n357 \n358 and an error response\n359 \n360 .. code-block: http\n361 \n362 package.myCallback({\"error\":\"unsupported_token_type\"});\n363 \n364 Note that these requests usually require client credentials, client_id in\n365 the case for public clients and provider specific authentication\n366 credentials for confidential clients.\n367 \"\"\"\n368 if not is_secure_transport(revocation_url):\n369 raise InsecureTransportError()\n370 \n371 return prepare_token_revocation_request(revocation_url, token,\n372 token_type_hint=token_type_hint, body=body, callback=callback,\n373 **kwargs)\n374 \n375 def parse_request_body_response(self, body, scope=None, **kwargs):\n", - "filename": "oauthlib/oauth2/rfc6749/clients/base.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'access_token'", - "line_number": 314, - "line_range": [ - 314, - 315, - 316, - 317, - 318, - 319, - 320, - 321, - 322, - 323, - 324, - 325, - 326, - 327, - 328, - 329, - 330, - 331, - 332, - 333, - 334, - 335, - 336, - 337, - 338, - 339, - 340, - 341, - 342, - 343, - 344, - 345, - 346, - 347, - 348, - 349, - 350, - 351, - 352, - 353, - 354, - 355, - 356, - 357, - 358, - 359, - 360, - 361, - 362, - 363, - 364, - 365, - 366, - 367, - 368, - 369, - 370, - 371, - 372, - 373, - 374 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", - "test_id": "B107", - "test_name": "hardcoded_password_default" - }, - { - "code": "45 def __init__(self, endpoints, claims={}, raise_errors=True):\n46 assert isinstance(claims, dict)\n47 for endpoint in endpoints:\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", - "issue_confidence": "HIGH", - "issue_severity": "LOW", - "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.", - "line_number": 46, - "line_range": [ - 46 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", - "test_id": "B101", - "test_name": "assert_used" - }, - { - "code": "47 for endpoint in endpoints:\n48 assert isinstance(endpoint, BaseEndpoint)\n49 \n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", - "issue_confidence": "HIGH", - "issue_severity": "LOW", - "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.", - "line_number": 48, - "line_range": [ - 48 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", - "test_id": "B101", - "test_name": "assert_used" - }, - { - "code": "70 default_token_type=bearer)\n71 ResourceEndpoint.__init__(self, default_token='Bearer',\n72 token_types={'Bearer': bearer})\n73 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 71, - "line_range": [ - 71, - 72 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "109 default_token_type=bearer)\n110 ResourceEndpoint.__init__(self, default_token='Bearer',\n111 token_types={'Bearer': bearer})\n112 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 110, - "line_range": [ - 110, - 111 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "142 default_token_type=bearer)\n143 ResourceEndpoint.__init__(self, default_token='Bearer',\n144 token_types={'Bearer': bearer})\n145 RevocationEndpoint.__init__(self, request_validator,\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 143, - "line_range": [ - 143, - 144 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "181 default_token_type=bearer)\n182 ResourceEndpoint.__init__(self, default_token='Bearer',\n183 token_types={'Bearer': bearer})\n184 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 182, - "line_range": [ - 182, - 183 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "214 default_token_type=bearer)\n215 ResourceEndpoint.__init__(self, default_token='Bearer',\n216 token_types={'Bearer': bearer})\n217 RevocationEndpoint.__init__(self, request_validator,\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 215, - "line_range": [ - 215, - 216 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "164 \n165 def prepare_token_revocation_request(url, token, token_type_hint=\"access_token\",\n166 callback=None, body='', **kwargs):\n167 \"\"\"Prepare a token revocation request.\n168 \n169 The client constructs the request by including the following parameters\n170 using the \"application/x-www-form-urlencoded\" format in the HTTP request\n171 entity-body:\n172 \n173 :param token: REQUIRED. The token that the client wants to get revoked.\n174 \n175 :param token_type_hint: OPTIONAL. A hint about the type of the token\n176 submitted for revocation. Clients MAY pass this\n177 parameter in order to help the authorization server\n178 to optimize the token lookup. If the server is\n179 unable to locate the token using the given hint, it\n180 MUST extend its search across all of its supported\n181 token types. An authorization server MAY ignore\n182 this parameter, particularly if it is able to detect\n183 the token type automatically.\n184 \n185 This specification defines two values for `token_type_hint`:\n186 \n187 * access_token: An access token as defined in [RFC6749],\n188 `Section 1.4`_\n189 \n190 * refresh_token: A refresh token as defined in [RFC6749],\n191 `Section 1.5`_\n192 \n193 Specific implementations, profiles, and extensions of this\n194 specification MAY define other values for this parameter using the\n195 registry defined in `Section 4.1.2`_.\n196 \n197 .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4\n198 .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5\n199 .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2\n200 \n201 \"\"\"\n202 if not is_secure_transport(url):\n203 raise InsecureTransportError()\n204 \n205 params = [('token', token)]\n206 \n207 if token_type_hint:\n208 params.append(('token_type_hint', token_type_hint))\n209 \n210 for k in kwargs:\n211 if kwargs[k]:\n212 params.append((str(k), kwargs[k]))\n213 \n214 headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n215 \n216 if callback:\n217 params.append(('callback', callback))\n218 return add_params_to_uri(url, params), headers, body\n219 else:\n220 return url, headers, add_params_to_qs(body, params)\n221 \n222 \n223 def parse_authorization_code_response(uri, state=None):\n", - "filename": "oauthlib/oauth2/rfc6749/parameters.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'access_token'", - "line_number": 165, - "line_range": [ - 165, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - 178, - 179, - 180, - 181, - 182, - 183, - 184, - 185, - 186, - 187, - 188, - 189, - 190, - 191, - 192, - 193, - 194, - 195, - 196, - 197, - 198, - 199, - 200, - 201, - 202, - 203, - 204, - 205, - 206, - 207, - 208, - 209, - 210, - 211, - 212, - 213, - 214, - 215, - 216, - 217, - 218, - 219, - 220, - 221, - 222 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", - "test_id": "B107", - "test_name": "hardcoded_password_default" - }, - { - "code": "104 default_token_type=bearer)\n105 ResourceEndpoint.__init__(self, default_token='Bearer',\n106 token_types={'Bearer': bearer, 'JWT': jwt})\n107 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/openid/connect/core/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 105, - "line_range": [ - 105, - 106 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - } - ] -} \ No newline at end of file From 20e51c96ea297cd564fdbabaf52c4493760a0b2c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 09:22:11 +0200 Subject: [PATCH 061/115] Removed inactive users while keeping them in AUTHORS file. --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 1a100bac..3b75880a 100755 --- a/setup.py +++ b/setup.py @@ -28,9 +28,8 @@ def fread(fn): long_description=fread('README.rst'), long_description_content_type='text/x-rst', author='The OAuthlib Community', - author_email='idan@gazit.me', - maintainer='Ib Lundgren', - maintainer_email='ib.lundgren@gmail.com', + maintainer='Jonathan Huot', + maintainer_email='jonathan.huot@gmail.com', url='https://github.com/oauthlib/oauthlib', platforms='any', license='BSD-3-Clause', From 059875a888d2200e43e18b818eda8442b2c3d0cc Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 09:29:10 +0200 Subject: [PATCH 062/115] Refresh wording of license to not confuse tools between BSD-3 & BSD Based on latest https://opensource.org/license/BSD-3-Clause, suggested removal of ambiguity from https://peps.python.org/pep-0639/appendix-mapping-classifiers/ and https://peps.python.org/pep-0639/#deprecate-license-classifiers Fixed #896 --- LICENSE | 8 ++++---- README.rst | 2 +- oauthlib/__init__.py | 2 +- setup.py | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index e75118ad..ffab1267 100644 --- a/LICENSE +++ b/LICENSE @@ -11,14 +11,14 @@ modification, are permitted provided that the following conditions are met: 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. + 3. Neither the name of the copyright holder 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 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 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 diff --git a/README.rst b/README.rst index 44b243f2..529bbb09 100644 --- a/README.rst +++ b/README.rst @@ -110,7 +110,7 @@ have the pleasure to run into each other, please send a docs pull request =) License ------- -OAuthLib is yours to use and abuse according to the terms of the BSD license. +OAuthLib is yours to use and abuse according to the terms of the BSD-3-Clause license. Check the LICENSE file for full details. Credits diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 9a5e0786..98b123e8 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -6,7 +6,7 @@ request-signing logic. :copyright: (c) The OAuthlib Community - :license: BSD, see LICENSE for details. + :license: BSD-3-Clause, see LICENSE for details. """ import logging from logging import NullHandler diff --git a/setup.py b/setup.py index 3b75880a..70162daa 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ def fread(fn): 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', From 2660fbd73139d3673abb78c83b0aa8f143858e30 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 09:49:50 +0200 Subject: [PATCH 063/115] Add 3.3.0 changelog and bump version --- CHANGELOG.rst | 23 +++++++++++++++++++++++ oauthlib/__init__.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 82dbd75a..4ec62bdf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,29 @@ Changelog ========= +3.3.0 (2025-05-11): +------------------ +OAuth2.0 Provider: +* OIDC: #879 Changed in how ui_locales is parsed +* RFC8628: Added OAuth2.0 Device Authorization Grant support +* PKCE: #876, #893 Fixed `create_code_verifier` length +* OIDC: Pre-configured OIDC server to use Refresh Token by default + +OAuth2.0 Common: +* OAuth2Error: Allow 0 to be a valid state + +OAuth2.0 Client: +* #745: expires_at is forced to be an int + +General: +* Removed Python 3.5, 3.6, 3.7 support +* #859, #883: Added Python 3.12, 3.13 Support +* Added dependency-review GitHub Action +* Updated various references of license (SPDX identifier..) +* Added GitHub Action for lint, replaced bandy with ruff, removed isort... +* Migrated to GitHub Actions from Travis +* Added Security Policy + 3.2.2 (2022-10-17) ------------------ OAuth2.0 Provider: diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 98b123e8..2920cf44 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.2.2' +__version__ = '3.3.0' logging.getLogger('oauthlib').addHandler(NullHandler()) From 4f9ce52cb589a779c9f5628f783f55b46fb02bf6 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 13 May 2025 10:45:32 +0200 Subject: [PATCH 064/115] Fix multiple if statements --- .github/workflows/python-publish.yml | 9 ++++----- Makefile | 15 ++++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 43417ee5..110e5f60 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,4 +1,4 @@ -name: Production deploy +name: Publish release on: workflow_run: workflows: ["Python Tests"] @@ -6,10 +6,9 @@ on: - completed jobs: pypi-publish: - if: | - github.repository_owner == 'oauthlib' && - ${{ github.event.workflow_run.conclusion == 'success' }} && - ${{ github.ref_type == 'tag' }} + if: ${{ github.repository_owner == 'oauthlib' && + github.event.workflow_run.conclusion == 'success' && + github.ref_type == 'tag' }} name: Upload release to PyPI runs-on: ubuntu-latest environment: diff --git a/Makefile b/Makefile index 550525c6..9622f70d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ # # Please specify your library as well as primary contacts. # Since these contacts will be addressed with Github mentions they -# need to be Github users (for now)(sorry Bitbucket). +# need to be Github users. # clean: clean-eggs clean-build @find . -iname '*.pyc' -delete @@ -45,25 +45,18 @@ test: bottle: #--------------------------- - # Library thomsonreuters/bottle-oauthlib + # Library refinitiv/bottle-oauthlib # Contacts: Jonathan.Huot - cd bottle-oauthlib 2>/dev/null || git clone https://github.com/thomsonreuters/bottle-oauthlib.git + cd bottle-oauthlib 2>/dev/null || git clone https://github.com/refinitiv/bottle-oauthlib.git cd bottle-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox -flask: - #--------------------------- - # Library: lepture/flask-oauthLib - # Contacts: lepture,widnyana - cd flask-oauthlib 2>/dev/null || git clone https://github.com/lepture/flask-oauthlib.git - cd flask-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox - django: #--------------------------- # Library: evonove/django-oauth-toolkit # Contacts: evonove,masci # (note: has tox.ini already) cd django-oauth-toolkit 2>/dev/null || git clone https://github.com/evonove/django-oauth-toolkit.git - cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox -e py27,py35,py36 + cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox requests: #--------------------------- From 0f00aba6f2bcc217622255a26a433e180aa07d0b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 14 May 2025 21:58:38 +0200 Subject: [PATCH 065/115] Handle expires_at with best effort basis --- oauthlib/oauth2/rfc6749/clients/base.py | 28 ++++++++-- oauthlib/oauth2/rfc6749/parameters.py | 4 +- requirements-test.txt | 1 + tests/oauth2/rfc6749/clients/test_base.py | 65 +++++++++-------------- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index a94e73c4..66ebc3e8 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -586,10 +586,30 @@ def populate_token_attributes(self, response): self._expires_at = round(time.time()) + int(self.expires_in) if 'expires_at' in response: - try: - self._expires_at = round(float(response.get('expires_at'))) - except: - self._expires_at = None + # expires_at is not in specification + # so it does its best to : + # convert into a float, or + # convert into an integer, or + # reuse the type as-is + + if isinstance(response.get('expires_at'), str): + try: + self.expires_at = int(response.get('expires_at')) + except ValueError: + self.expires_at = response.get('expires_at') + try: + self.expires_at = float(response.get('expires_at')) + except ValueError: + self.expires_at = response.get('expires_at') + else: + self.expires_at = response.get('expires_at') + + # we preserve internal capability to raise TokenExpiredError + # for valid types only + if isinstance(self.expires_at, float): + self._expires_at = round(self.expires_at) + elif isinstance(self.expires_at, int): + self._expires_at = self.expires_at if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 6c55000c..f6e55a16 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -434,10 +434,8 @@ def parse_token_response(body, scope=None): if params['expires_in'] is None: params.pop('expires_in') else: - params['expires_at'] = time.time() + int(params['expires_in']) + params['expires_at'] = round(time.time()) + int(params['expires_in']) - if isinstance(params.get('expires_at'), float): - params['expires_at'] = round(params['expires_at']) params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) diff --git a/requirements-test.txt b/requirements-test.txt index 6d8d6e9d..2761eca0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -r requirements.txt pytest>=4.0 pytest-cov>=2.6 +pytest-subtests==0.14.1 diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index b0b6372b..cddd1a14 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import datetime +import json from unittest.mock import patch from oauthlib import common @@ -303,31 +304,6 @@ def test_prepare_refresh_token_request(self): self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'}) self.assertFormBodyEqual(b, 'grant_type=refresh_token&scope={}&refresh_token={}'.format(scope, token)) - def test_parse_token_response_invalid_expires_at(self): - token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' - ' "token_type":"example",' - ' "expires_at":"2006-01-02T15:04:05Z",' - ' "scope":"/profile",' - ' "example_parameter":"example_value"}') - token = { - "access_token": "2YotnFZFEjr1zCsicMWpAA", - "token_type": "example", - "expires_at": "2006-01-02T15:04:05Z", - "scope": ["/profile"], - "example_parameter": "example_value" - } - - client = Client(self.client_id) - - # Parse code and state - response = client.parse_request_body_response(token_json, scope=["/profile"]) - self.assertEqual(response, token) - self.assertEqual(None, client._expires_at) - self.assertEqual(client.access_token, response.get("access_token")) - self.assertEqual(client.refresh_token, response.get("refresh_token")) - self.assertEqual(client.token_type, response.get("token_type")) - - def test_create_code_verifier_min_length(self): client = Client(self.client_id) length = 43 @@ -361,20 +337,31 @@ def test_create_code_challenge_s256(self): code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256') self.assertEqual(code_challenge_s256, client.code_challenge) - def test_parse_token_response_expires_at_is_int(self): - expected_expires_at = 1661185149 - token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' - ' "token_type":"example",' - ' "expires_at":1661185148.6437678,' - ' "scope":"/profile",' - ' "example_parameter":"example_value"}') - - client = Client(self.client_id) - - response = client.parse_request_body_response(token_json, scope=["/profile"]) - - self.assertEqual(response['expires_at'], expected_expires_at) - self.assertEqual(client._expires_at, expected_expires_at) + def test_parse_token_response_expires_at_types(self): + for data in [ # title, expected, expected_valid, fieldjson + ('int', 1661185148, 1661185148, 1661185148), + ('float', 1661185148.6437678, 1661185149, 1661185148.6437678), + ('str', "2006-01-02T15:04:05Z", None, "\"2006-01-02T15:04:05Z\""), + ('str-as-int', 1661185148, 1661185148, "\"1661185148\""), + ('str-as-float', 1661185148.42, 1661185148, "\"1661185148.42\""), + ]: + with self.subTest(msg=data[0]): + expected_expires_at = data[1] + expected_valid_expires_at = data[2] + token_json = ('{{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_at":{expires_at},' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}}'.format(expires_at=data[3])) + + client = Client(self.client_id) + + response = client.parse_request_body_response(token_json, scope=["/profile"]) + + self.assertEqual(response['expires_at'], json.loads('{{"foo":{}}}'.format(data[3]))["foo"], "response attribute wrong") + self.assertEqual(client.expires_at, expected_expires_at, "client attribute wrong") + if expected_valid_expires_at: + self.assertEqual(client._expires_at, expected_valid_expires_at, "internal expiration wrong") @patch('time.time') def test_parse_token_response_generated_expires_at_is_int(self, t): From 6bdec303efded1b1a2d897cce273e04b5977b8ed Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 14 May 2025 22:02:27 +0200 Subject: [PATCH 066/115] Removed version ping to work with all py versions --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2761eca0..521036a4 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -r requirements.txt pytest>=4.0 pytest-cov>=2.6 -pytest-subtests==0.14.1 +pytest-subtests From 7d4b288dbde1b5be2f18791c59114b2e21ef5ae8 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 15 May 2025 23:26:01 +0200 Subject: [PATCH 067/115] Simplified str convertion of expires_at Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- oauthlib/oauth2/rfc6749/clients/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 66ebc3e8..ad273a84 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -594,13 +594,13 @@ def populate_token_attributes(self, response): if isinstance(response.get('expires_at'), str): try: + # Attempt to convert to int first, then float if int fails self.expires_at = int(response.get('expires_at')) except ValueError: - self.expires_at = response.get('expires_at') - try: - self.expires_at = float(response.get('expires_at')) - except ValueError: - self.expires_at = response.get('expires_at') + try: + self.expires_at = float(response.get('expires_at')) + except ValueError: + self.expires_at = response.get('expires_at') else: self.expires_at = response.get('expires_at') From 6588f80bf68c7259c484b4c28d3e7ef5cb494e2f Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 28 May 2025 21:40:36 +0200 Subject: [PATCH 068/115] Does not round if float is provided --- oauthlib/oauth2/rfc6749/clients/base.py | 2 +- tests/oauth2/rfc6749/clients/test_base.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index ad273a84..e104cb3a 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -607,7 +607,7 @@ def populate_token_attributes(self, response): # we preserve internal capability to raise TokenExpiredError # for valid types only if isinstance(self.expires_at, float): - self._expires_at = round(self.expires_at) + self._expires_at = self.expires_at elif isinstance(self.expires_at, int): self._expires_at = self.expires_at diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index cddd1a14..f059db9c 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -340,10 +340,10 @@ def test_create_code_challenge_s256(self): def test_parse_token_response_expires_at_types(self): for data in [ # title, expected, expected_valid, fieldjson ('int', 1661185148, 1661185148, 1661185148), - ('float', 1661185148.6437678, 1661185149, 1661185148.6437678), + ('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678), ('str', "2006-01-02T15:04:05Z", None, "\"2006-01-02T15:04:05Z\""), ('str-as-int', 1661185148, 1661185148, "\"1661185148\""), - ('str-as-float', 1661185148.42, 1661185148, "\"1661185148.42\""), + ('str-as-float', 1661185148.42, 1661185148.42, "\"1661185148.42\""), ]: with self.subTest(msg=data[0]): expected_expires_at = data[1] From 2e203f17572d7a4a49de738b1d6c143fab919892 Mon Sep 17 00:00:00 2001 From: Raymond Penners Date: Wed, 21 May 2025 23:47:08 +0200 Subject: [PATCH 069/115] docs: add django-allauth to available options --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 44b243f2..ef0b5ad7 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,9 @@ 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 Django there is: + - `django-oauth-toolkit`_, which includes `Django REST framework`_ support. + - `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Pyramid there is `pyramid-oauthlib`_. - For Bottle there is `bottle-oauthlib`_. @@ -89,6 +91,8 @@ please open a Pull Request, updating the documentation. .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib +.. _`django-allauth`: https://allauth.org/ +.. _`Django Ninja`: https://django-ninja.dev/ Using OAuthLib? Please get in touch! ------------------------------------ From c12d0ebe5122aac2ea96c403fb3a084f09c1a9c3 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 11 Jun 2025 07:46:09 +0200 Subject: [PATCH 070/115] Update SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index ddb8632d..e5b7db85 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,4 +11,4 @@ following versions are currently being supported with security updates. | < 3.2.0 | :x: | ## Reporting a Vulnerability -Contact auvipy@gmail.com for reporting any vulnerability. +Please raise a draft advisory to start discussing about the vulnerability in a private channel with OAuthlib Admin: https://github.com/oauthlib/oauthlib/security/advisories/new From d9531884c31cb9d3d99fd43adcc57ab2e5b28d40 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:04:24 +0200 Subject: [PATCH 071/115] Factorized parsing of expires_at Added similar behaviors to all interfaces where expires_at is parsed, this will facilitate the implementations. Note this is breaking change for those which are expecting the "default" `expires_at` (as in, not provided) to be a float. This will now default to a int. --- oauthlib/oauth2/rfc6749/clients/base.py | 37 ++------ .../rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/parameters.py | 87 +++++++++++++++---- tests/oauth2/rfc6749/clients/test_base.py | 23 +++-- tests/oauth2/rfc6749/test_parameters.py | 22 +++++ 5 files changed, 113 insertions(+), 58 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index e104cb3a..17f833d2 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -17,6 +17,7 @@ InsecureTransportError, TokenExpiredError, ) from oauthlib.oauth2.rfc6749.parameters import ( + parse_expires, parse_token_response, prepare_token_request, prepare_token_revocation_request, ) @@ -581,35 +582,13 @@ def populate_token_attributes(self, response): 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 = round(time.time()) + int(self.expires_in) - - if 'expires_at' in response: - # expires_at is not in specification - # so it does its best to : - # convert into a float, or - # convert into an integer, or - # reuse the type as-is - - if isinstance(response.get('expires_at'), str): - try: - # Attempt to convert to int first, then float if int fails - self.expires_at = int(response.get('expires_at')) - except ValueError: - try: - self.expires_at = float(response.get('expires_at')) - except ValueError: - self.expires_at = response.get('expires_at') - else: - self.expires_at = response.get('expires_at') - - # we preserve internal capability to raise TokenExpiredError - # for valid types only - if isinstance(self.expires_at, float): - self._expires_at = self.expires_at - elif isinstance(self.expires_at, int): - self._expires_at = self.expires_at + vin, vat, v_at = parse_expires(response) + if vin: + self.expires_in = vin + if vat: + self.expires_at = vat + if v_at: + self._expires_at = v_at if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 8fb17377..9c223840 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -91,7 +91,7 @@ def prepare_request_body(self, ``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``. + to an hour from now, i.e. ``round(time.time()) + 3600``. :param issued_at: A unix timestamp of when the JWT was created. Defaults to now, i.e. ``time.time()``. diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index f6e55a16..7a0b2d3e 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -336,15 +336,18 @@ def parse_implicit_response(uri, state=None, scope=None): 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'] = round(time.time()) + int(params['expires_in']) + vin, vat, v_at = parse_expires(params) + if vin: + params['expires_in'] = vin + elif 'expires_in' in params: + params.pop('expires_in') + if vat: + params['expires_at'] = vat + elif 'expires_at' in params: + params.pop('expires_at') if state and params.get('state') != state: raise ValueError("Mismatching or missing state in params.") @@ -423,19 +426,19 @@ def parse_token_response(body, scope=None): # 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'] = round(time.time()) + int(params['expires_in']) - + vin, vat, v_at = parse_expires(params) + if vin: + params['expires_in'] = vin + elif 'expires_in' in params: + params.pop('expires_in') + if vat: + params['expires_at'] = vat + elif 'expires_at' in params: + params.pop('expires_at') params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) @@ -468,3 +471,57 @@ def validate_token_parameters(params): w.old_scope = params.old_scopes w.new_scope = params.scopes raise w + +def parse_expires(params): + """Parse `expires_in`, `expires_at` fields from params + + Parse following these rules: + - `expires_in` must be either blank or a valid integer. + - `expires_at` is not in specification so it does its best to: + - convert into a int, else + - convert into a float, else + - reuse the same type as-is (usually string) + - `_expires_at` is a special internal value returned to be always an `int`, based + either on the presence of `expires_at`, or reuse the current time plus + `expires_in`. This is typically used to validate token expiry. + + :param params: Dict with expires_in and expires_at optionally set + :return: Tuple of `expires_in`, `expires_at`, and `_expires_at`. None if not set. + """ + expires_in = None + expires_at = None + _expires_at = None + + if 'expires_in' in params: + if isinstance(params.get('expires_in'), int): + expires_in = params.get('expires_in') + elif isinstance(params.get('expires_in'), str): + try: + # Attempt to convert to int + expires_in = int(params.get('expires_in')) + except ValueError: + raise ValueError("expires_int must be an int") + elif params.get('expires_in') is not None: + raise ValueError("expires_int must be an int") + + if 'expires_at' in params: + if isinstance(params.get('expires_at'), float) or \ + isinstance(params.get('expires_at'), int): + expires_at = params.get('expires_at') + _expires_at = expires_at + elif isinstance(params.get('expires_at'), str): + try: + # Attempt to convert to int first, then float if int fails + expires_at = int(params.get('expires_at')) + _expires_at = expires_at + except ValueError: + try: + expires_at = float(params.get('expires_at')) + _expires_at = expires_at + except ValueError: + # no change from str + expires_at = params.get('expires_at') + if _expires_at is None and expires_in: + expires_at = round(time.time()) + expires_in + _expires_at = expires_at + return expires_in, expires_at, _expires_at diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index f059db9c..0e68d3bb 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -338,30 +338,27 @@ def test_create_code_challenge_s256(self): self.assertEqual(code_challenge_s256, client.code_challenge) def test_parse_token_response_expires_at_types(self): - for data in [ # title, expected, expected_valid, fieldjson + for title, fieldjson, expected, generated in [ # title, fieldjson, expected, expected_valid ('int', 1661185148, 1661185148, 1661185148), ('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678), - ('str', "2006-01-02T15:04:05Z", None, "\"2006-01-02T15:04:05Z\""), - ('str-as-int', 1661185148, 1661185148, "\"1661185148\""), - ('str-as-float', 1661185148.42, 1661185148.42, "\"1661185148.42\""), + ('str', "\"2006-01-02T15:04:05Z\"", "2006-01-02T15:04:05Z", None), + ('str-as-int', "\"1661185148\"", 1661185148, 1661185148), + ('str-as-float', "\"1661185148.42\"", 1661185148.42, 1661185148.42), ]: - with self.subTest(msg=data[0]): - expected_expires_at = data[1] - expected_valid_expires_at = data[2] + with self.subTest(msg=title): token_json = ('{{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' ' "token_type":"example",' ' "expires_at":{expires_at},' ' "scope":"/profile",' - ' "example_parameter":"example_value"}}'.format(expires_at=data[3])) + ' "example_parameter":"example_value"}}'.format(expires_at=fieldjson)) client = Client(self.client_id) - response = client.parse_request_body_response(token_json, scope=["/profile"]) - self.assertEqual(response['expires_at'], json.loads('{{"foo":{}}}'.format(data[3]))["foo"], "response attribute wrong") - self.assertEqual(client.expires_at, expected_expires_at, "client attribute wrong") - if expected_valid_expires_at: - self.assertEqual(client._expires_at, expected_valid_expires_at, "internal expiration wrong") + self.assertEqual(response['expires_at'], expected, "response attribute wrong") + self.assertEqual(client.expires_at, expected, "client attribute wrong") + if generated: + self.assertEqual(client._expires_at, generated, "internal expiration wrong") @patch('time.time') def test_parse_token_response_generated_expires_at_is_int(self, t): diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index cd8c9e95..cdc7164b 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -302,3 +302,25 @@ def record_scope_change(sender, message, old, new): finally: signals.scope_changed.disconnect(record_scope_change) del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] + + + def test_parse_expires(self): + for title, arg, expected in [ # title, arg_tuple, returned_expected_tuple, + ('none', (None, None), (None, None, None)), + ('expires_in only', (3600, None), (3600, 4600, 4600)), + ('expires_in and expires_at', (3600, 200), (3600, 200, 200)), + ('expires_in and expires_at float', (3600, 200.42), (3600, 200.42, 200.42)), + ('expires_in and expires_at str-int', (3600, "200"), (3600, 200, 200)), + ('expires_in and expires_at str-float', (3600, "200.42"), (3600, 200.42, 200.42)), + ('expires_in str only', ("3600", None), (3600, 4600, 4600)), + ('expires_in str and expires_at', ("3600", 200), (3600, 200, 200)), + ('expires_in str and expires_at float', ("3600", 200.42), (3600, 200.42, 200.42)), + ('expires_in str and expires_at str-int', ("3600", "200"), (3600, 200, 200)), + ('expires_in str and expires_at str-float', ("3600", "200.42"), (3600, 200.42, 200.42)), + ]: + with self.subTest(msg=title): + params = { + "expires_in": arg[0], + "expires_at": arg[1] + } + self.assertEqual(expected, parse_expires(params)) From 753619bfb3f9b4694a53cf1ff3395cfceb625f31 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:15:35 +0200 Subject: [PATCH 072/115] Added expires_at changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ec62bdf..c0f02515 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,7 @@ OAuth2.0 Common: OAuth2.0 Client: * #745: expires_at is forced to be an int +* #899: expires_at clarification General: * Removed Python 3.5, 3.6, 3.7 support From 102a3aa7e595abe57b819055c6f563f265b9c2e7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:15:52 +0200 Subject: [PATCH 073/115] Removed unnecessary comments --- tests/oauth2/rfc6749/clients/test_base.py | 2 +- tests/oauth2/rfc6749/test_parameters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index 0e68d3bb..b0970f2d 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -338,7 +338,7 @@ def test_create_code_challenge_s256(self): self.assertEqual(code_challenge_s256, client.code_challenge) def test_parse_token_response_expires_at_types(self): - for title, fieldjson, expected, generated in [ # title, fieldjson, expected, expected_valid + for title, fieldjson, expected, generated in [ ('int', 1661185148, 1661185148, 1661185148), ('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678), ('str', "\"2006-01-02T15:04:05Z\"", "2006-01-02T15:04:05Z", None), diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index cdc7164b..63b74c37 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -305,7 +305,7 @@ def record_scope_change(sender, message, old, new): def test_parse_expires(self): - for title, arg, expected in [ # title, arg_tuple, returned_expected_tuple, + for title, arg, expected in [ ('none', (None, None), (None, None, None)), ('expires_in only', (3600, None), (3600, 4600, 4600)), ('expires_in and expires_at', (3600, 200), (3600, 200, 200)), From a74f72b3ab9ab566a2c2cebdce28d06ad06d46d0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:16:23 +0200 Subject: [PATCH 074/115] Simplify isinstance calls to one/Ruff/SIM101 --- oauthlib/oauth2/rfc6749/parameters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 7a0b2d3e..4675a31f 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -505,8 +505,7 @@ def parse_expires(params): raise ValueError("expires_int must be an int") if 'expires_at' in params: - if isinstance(params.get('expires_at'), float) or \ - isinstance(params.get('expires_at'), int): + if isinstance(params.get('expires_at'), (float, int)): expires_at = params.get('expires_at') _expires_at = expires_at elif isinstance(params.get('expires_at'), str): From 5a1c149d3bd530f2763ae77633d610d6717f1337 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 19:22:20 +0200 Subject: [PATCH 075/115] Updated release date --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c0f02515..8064cd0d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -3.3.0 (2025-05-11): +3.3.0 (2025-06-17): ------------------ OAuth2.0 Provider: * OIDC: #879 Changed in how ui_locales is parsed From 46bc4fd3751802563e1eabb06de1f137ca8f39a1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 19:22:26 +0200 Subject: [PATCH 076/115] Bumped release --- SECURITY.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index ddb8632d..7d0f5250 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,13 +2,14 @@ ## Supported Versions -following versions are currently being supported with security updates. +The following versions are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | +| 3.3.x | :white_check_mark: | | 3.2.x | :white_check_mark: | | 3.1.x | :x: | -| < 3.2.0 | :x: | +| < 3.1 | :x: | ## Reporting a Vulnerability Contact auvipy@gmail.com for reporting any vulnerability. From 4b95bce396c256d20177a8bf87c994d415e833ff Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:09:57 +0200 Subject: [PATCH 077/115] Updated GH actions --- .github/workflows/python-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 110e5f60..dd4630bd 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -18,8 +18,8 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - name: Check out repository code - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install prereq From 8d1035156e0bc76358f55736e0856a6a23438080 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:10:14 +0200 Subject: [PATCH 078/115] Added maintainer instructions --- docs/release_process.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/release_process.rst b/docs/release_process.rst index 2796f29c..e915f938 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -51,3 +51,26 @@ Minor point (1.1.0) releases will introduce non API breaking new features and changes. Bug releases (1.0.1) will include minor fixes that needs to be released quickly (e.g. after a bigger release unintentionally introduced a bug). + +For maintainer - Publishing a newer version +-------------------------------------------- + +List of tasks to do a release from a maintainer point of view: + + - Create a Branch ``xyz-release`` + - Update ``oauthlib/__init__.py`` version + - Update ``CHANGELOG.rst`` accordingly + - Review Github Issues and PR, and associate the milestone of the version + - Run ``make`` to cover the release readiness + - Create a PR to let downstreams developers test their apps and comments + - Create a tag and push tag, it will automatically publish the release to pypi + - Create a release with GitHub Releases + - Merge PR, close Github milestone + + +Initial setup: + - Because we currently use "trusted publisher", it does not require to setup + token. However, OIDC Authorization flow has to be configured in `pypi publishing`. + - During setup, refer to the environment and name of the workflow directly in the code. + +.. _`pypi publishing`: https://pypi.org/manage/project/oauthlib/settings/publishing/ From 0c61345f8d658ad9848a58bf5e503bfd15aa41d0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:10:33 +0200 Subject: [PATCH 079/115] Fixed ruff findings about function import --- oauthlib/common.py | 4 ++-- oauthlib/oauth1/rfc5849/signature.py | 2 +- oauthlib/oauth2/rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/errors.py | 4 ++-- oauthlib/openid/connect/core/exceptions.py | 5 +++-- tests/oauth2/rfc6749/endpoints/test_metadata.py | 4 ++-- tests/test_uri_validate.py | 2 +- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/oauthlib/common.py b/oauthlib/common.py index fd9cad09..dfa85179 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -198,7 +198,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): def generate_signed_token(private_pem, request): - import jwt + import jwt # noqa: PLC0415 now = datetime.datetime.utcnow() @@ -216,7 +216,7 @@ def generate_signed_token(private_pem, request): def verify_signed_token(public_pem, token): - import jwt + import jwt # noqa: PLC0415 return jwt.decode(token, public_pem, algorithms=['RS256']) diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 8916782b..a27cb2e7 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -568,7 +568,7 @@ def _get_jwt_rsa_algorithm(hash_algorithm_name: str): # Not in cache: instantiate a new RSAAlgorithm # PyJWT has some nice pycrypto/cryptography abstractions - import jwt.algorithms as jwt_algorithms + import jwt.algorithms as jwt_algorithms # noqa: PLC0415 m = { 'SHA-1': jwt_algorithms.hashes.SHA1, 'SHA-256': jwt_algorithms.hashes.SHA256, diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 9c223840..abf22d2d 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -149,7 +149,7 @@ def prepare_request_body(self, .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ - import jwt + import jwt # noqa: PLC0415 key = private_key or self.private_key if not key: diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 3b415748..be8e7a1e 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -6,6 +6,8 @@ defined error responses for all four core grant types. """ import json +import inspect +import sys from oauthlib.common import add_params_to_uri, urlencode @@ -386,8 +388,6 @@ def __init__(self, error, *args, **kwargs): def raise_from_error(error, params=None): - import inspect - import sys kwargs = { 'description': params.get('error_description'), 'uri': params.get('error_uri'), diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py index 8a3e79f4..291cf137 100644 --- a/oauthlib/openid/connect/core/exceptions.py +++ b/oauthlib/openid/connect/core/exceptions.py @@ -5,6 +5,9 @@ Error used both by OAuth 2 clients and providers to represent the spec defined error responses for all four core grant types. """ +import inspect +import sys + from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error @@ -137,8 +140,6 @@ class InsufficientScopeError(OAuth2Error): def raise_from_error(error, params=None): - import inspect - import sys kwargs = { 'description': params.get('error_description'), 'uri': params.get('error_uri'), diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index facf69d0..c36d94bf 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -20,8 +20,8 @@ def test_openid_oauth2_preconfigured(self): "introspection_endpoint": "https://foo.bar/introspect", "token_endpoint": "https://foo.bar/token" } - from oauthlib.oauth2 import Server as OAuth2Server - from oauthlib.openid import Server as OpenIDServer + from oauthlib.oauth2 import Server as OAuth2Server # noqa: PLC0415 + from oauthlib.openid import Server as OpenIDServer # noqa: PLC0415 endpoint = OAuth2Server(None) metadata = MetadataEndpoint([endpoint], default_claims) diff --git a/tests/test_uri_validate.py b/tests/test_uri_validate.py index 04138d60..f1ac404e 100644 --- a/tests/test_uri_validate.py +++ b/tests/test_uri_validate.py @@ -1,3 +1,4 @@ +from datetime import datetime import unittest from oauthlib.uri_validate import is_absolute_uri @@ -77,7 +78,6 @@ def test_failures(self): self.assertIsNone(is_absolute_uri('http://[abcd:efgh::1]/')) def test_recursive_regex(self): - from datetime import datetime t0 = datetime.now() is_absolute_uri('http://[::::::::::::::::::::::::::]/path') t1 = datetime.now() From 2a3e5bceb092574533addaf65b0bc873e3567d26 Mon Sep 17 00:00:00 2001 From: Raymond Penners Date: Wed, 21 May 2025 23:47:08 +0200 Subject: [PATCH 080/115] docs: add django-allauth to available options --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 529bbb09..c4e38b66 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,9 @@ 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 Django there is: + - `django-oauth-toolkit`_, which includes `Django REST framework`_ support. + - `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Pyramid there is `pyramid-oauthlib`_. - For Bottle there is `bottle-oauthlib`_. @@ -89,6 +91,8 @@ please open a Pull Request, updating the documentation. .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib +.. _`django-allauth`: https://allauth.org/ +.. _`Django Ninja`: https://django-ninja.dev/ Using OAuthLib? Please get in touch! ------------------------------------ From 92a785b389e01780501ba89f88b181c0da134ea4 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:24:22 +0200 Subject: [PATCH 081/115] Fix linter after rebase --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c4e38b66..840c33d5 100644 --- a/README.rst +++ b/README.rst @@ -76,8 +76,8 @@ 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. - - `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support. + - `django-oauth-toolkit`_, which includes `Django REST framework`_ support. + - `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Pyramid there is `pyramid-oauthlib`_. - For Bottle there is `bottle-oauthlib`_. From 363d2de954e63393f0d09e8cba3769ce37441bb0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:34:34 +0200 Subject: [PATCH 082/115] Updated recommended filter for publish --- .github/workflows/python-publish.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index dd4630bd..fc9181d4 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -6,9 +6,7 @@ on: - completed jobs: pypi-publish: - if: ${{ github.repository_owner == 'oauthlib' && - github.event.workflow_run.conclusion == 'success' && - github.ref_type == 'tag' }} + if: github.repository_owner == 'oauthlib' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') name: Upload release to PyPI runs-on: ubuntu-latest environment: From 80ffd03c15f8a7d254239c8ce71cc688f3403e53 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 18 Jun 2025 00:12:41 +0200 Subject: [PATCH 083/115] Execute publish workflow of the tagged source. Not of master branch. --- .github/workflows/python-publish.yml | 2 ++ docs/release_process.rst | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index fc9181d4..b36e018a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v4 + with: # by default, this event will trigger a workflow on the default branch. + ref: ${{ github.event.workflow_run.head_ref }} # set source branch - uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/docs/release_process.rst b/docs/release_process.rst index e915f938..70cc1a5f 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -71,6 +71,9 @@ List of tasks to do a release from a maintainer point of view: Initial setup: - Because we currently use "trusted publisher", it does not require to setup token. However, OIDC Authorization flow has to be configured in `pypi publishing`. - - During setup, refer to the environment and name of the workflow directly in the code. + + - During setup, refer to the environment and name of the workflow directly in the code. + - GitHub Restrictions: tag protection must be enabled + .. _`pypi publishing`: https://pypi.org/manage/project/oauthlib/settings/publishing/ From 4412df731bf3a3b6be417e03b053029666d86cb3 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 18 Jun 2025 08:50:17 +0200 Subject: [PATCH 084/115] Add Incident Response Plan --- SECURITY.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index e5b7db85..3ddddebc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,4 +11,16 @@ following versions are currently being supported with security updates. | < 3.2.0 | :x: | ## Reporting a Vulnerability + Please raise a draft advisory to start discussing about the vulnerability in a private channel with OAuthlib Admin: https://github.com/oauthlib/oauthlib/security/advisories/new + +## Incident Response Plan + +The Incident Response Plan for oauthlib is composed of four steps: + +- Triage: discussion about the validity of the vulnerability with the reporter + in the private channel. +- Mitigate: work on a fix and release a newer version. +- Disclose: let downstream applications some time to update to the latest + release, then make the CVE public. +- Learn: discuss about any potential actions that could have prevented the vulnerability. From 694134d439a071a3dafb131ee8873fb473393ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 18 Jun 2025 07:35:40 +0200 Subject: [PATCH 085/115] Stop installing `examples` into `site-packages` Fix `setup.py` not to install `examples` as a top-level package into `site-packages`. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 70162daa..a35de99b 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def fread(fn): url='https://github.com/oauthlib/oauthlib', platforms='any', license='BSD-3-Clause', - packages=find_packages(exclude=('docs', 'tests', 'tests.*')), + packages=find_packages(exclude=('docs', 'examples', 'tests', 'tests.*')), python_requires='>=3.8', extras_require={ 'rsa': rsa_require, From 28aab3a42ad830433821b30e155865f885301718 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 08:11:05 +0200 Subject: [PATCH 086/115] Fix latest ruff findings Replaced deprecated ruff config --- oauthlib/oauth2/rfc6749/endpoints/resource.py | 2 +- ruff.toml | 10 +++++----- tests/oauth2/rfc6749/grant_types/test_refresh_token.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py index f7562255..d1ff5049 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/resource.py +++ b/oauthlib/oauth2/rfc6749/endpoints/resource.py @@ -81,4 +81,4 @@ def find_token_type(self, 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 + return estimates[0][1] if estimates else None diff --git a/ruff.toml b/ruff.toml index 0546d652..ec45dd4b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,7 +7,7 @@ # start with [tool.ruff # [tool.ruff] -select = [ +lint.select = [ "A", # flake8-builtins "AIR", # Airflow "ASYNC", # flake8-async @@ -65,7 +65,7 @@ select = [ # "TRY", # tryceratops # "UP", # pyupgrade ] -ignore = [ +lint.ignore = [ "F401", "F403", "F405", @@ -88,11 +88,11 @@ line-length = 255 target-version = "py37" # [tool.ruff.mccabe] -[mccabe] +[lint.mccabe] max-complexity = 24 # default is 10 # [tool.ruff.per-file-ignores] -[per-file-ignores] +[lint.per-file-ignores] "docs/conf.py" = ["A001", "INP001"] "oauthlib/oauth2/rfc6749/clients/base.py" = ["E722"] "oauthlib/oauth2/rfc6749/endpoints/base.py" = ["BLE001"] @@ -106,7 +106,7 @@ max-complexity = 24 # default is 10 "oauthlib/openid/connect/core/tokens.py" = ["RUF023"] # [tool.ruff.pylint] -[pylint] +[lint.pylint] allow-magic-value-types = ["int", "str"] max-args = 16 # default is 5 max-branches = 24 # default is 12 diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py index 0a4ddd9a..f963444a 100644 --- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py +++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py @@ -184,7 +184,7 @@ def test_valid_token_request(self): # all ok but without request.scope del self.request.scope self.auth.validate_token_request(self.request) - self.assertEqual(self.request.scopes, 'foo bar baz'.split()) + self.assertEqual(self.request.scopes, ['foo', 'bar', 'baz']) # CORS From 034c8eacc9971be5ce3d98b292cc34f858c8fb50 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 08:21:45 +0200 Subject: [PATCH 087/115] Removed year from license Removing date from copyrights to ease maintenance is the practice nowadays, see https://aboutcode.org/2023/update-copyright-each-new-year/ --- LICENSE | 2 +- docs/conf.py | 2 +- oauthlib/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index d5a9e9ac..e75118ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 The OAuthlib Community +Copyright (c) The OAuthlib Community All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/conf.py b/docs/conf.py index 05e93ee0..e609c541 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,7 @@ # General information about the project. project = 'OAuthLib' -copyright = '2019, The OAuthlib Community' +copyright = 'The OAuthlib Community' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 4f56ef14..9a5e0786 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -5,7 +5,7 @@ A generic, spec-compliant, thorough implementation of the OAuth request-signing logic. - :copyright: (c) 2019 by The OAuthlib Community + :copyright: (c) The OAuthlib Community :license: BSD, see LICENSE for details. """ import logging From c3550a0072dca48304634fafd7affb1cf1a0157d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 08:32:05 +0200 Subject: [PATCH 088/115] Removed unused bandit, since it has been replaced with ruff --- bandit.json | 1222 --------------------------------------------------- 1 file changed, 1222 deletions(-) delete mode 100644 bandit.json diff --git a/bandit.json b/bandit.json deleted file mode 100644 index 7161f005..00000000 --- a/bandit.json +++ /dev/null @@ -1,1222 +0,0 @@ -{ - "errors": [], - "generated_at": "2019-05-13T12:51:49Z", - "metrics": { - "_totals": { - "CONFIDENCE.HIGH": 3.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 10.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 12.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8338, - "nosec": 0 - }, - "oauthlib/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 25, - "nosec": 0 - }, - "oauthlib/common.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 337, - "nosec": 0 - }, - "oauthlib/oauth1/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 16, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/__init__.py": { - "CONFIDENCE.HIGH": 1.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 230, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/access_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 152, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/authorization.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 135, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 142, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/pre_configured.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 10, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/request_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 141, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/resource.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 97, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/endpoints/signature_only.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 53, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/errors.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 58, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/parameters.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 75, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/request_validator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 630, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/signature.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 379, - "nosec": 0 - }, - "oauthlib/oauth1/rfc5849/utils.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 61, - "nosec": 0 - }, - "oauthlib/oauth2/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 33, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 14, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 13, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/backend_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 56, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 3.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 3.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 384, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/legacy_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 67, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/mobile_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 140, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/service_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 144, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/clients/web_application.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 165, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 18, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/authorization.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 85, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 71, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/introspect.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 98, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/metadata.py": { - "CONFIDENCE.HIGH": 2.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 2.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 182, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 5.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 5.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 189, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/resource.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 65, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/revocation.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 96, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/endpoints/token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 76, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/errors.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 311, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 10, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/authorization_code.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 389, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 199, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/client_credentials.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 96, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/implicit.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 259, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/refresh_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 102, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 156, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/parameters.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 335, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/request_validator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 504, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/tokens.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 277, - "nosec": 0 - }, - "oauthlib/oauth2/rfc6749/utils.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 70, - "nosec": 0 - }, - "oauthlib/openid/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8, - "nosec": 0 - }, - "oauthlib/openid/connect/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 0, - "nosec": 0 - }, - "oauthlib/openid/connect/core/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 0, - "nosec": 0 - }, - "oauthlib/openid/connect/core/endpoints/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 9, - "nosec": 0 - }, - "oauthlib/openid/connect/core/endpoints/pre_configured.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 93, - "nosec": 0 - }, - "oauthlib/openid/connect/core/endpoints/userinfo.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 83, - "nosec": 0 - }, - "oauthlib/openid/connect/core/exceptions.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 117, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 15, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/authorization_code.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 32, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 234, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/dispatchers.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 66, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/exceptions.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 26, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/hybrid.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 38, - "nosec": 0 - }, - "oauthlib/openid/connect/core/grant_types/implicit.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 35, - "nosec": 0 - }, - "oauthlib/openid/connect/core/request_validator.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 235, - "nosec": 0 - }, - "oauthlib/openid/connect/core/tokens.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 42, - "nosec": 0 - }, - "oauthlib/signals.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 32, - "nosec": 0 - }, - "oauthlib/tokens/__init__.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 0, - "nosec": 0 - }, - "oauthlib/tokens/access_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 13, - "nosec": 0 - }, - "oauthlib/tokens/base.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 8, - "nosec": 0 - }, - "oauthlib/tokens/id_token.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 6, - "nosec": 0 - }, - "oauthlib/uri_validate.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 93, - "nosec": 0 - } - }, - "results": [ - { - "code": "183 if request.body is not None and content_type_eligible:\n184 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n185 \n", - "filename": "oauthlib/oauth1/rfc5849/__init__.py", - "issue_confidence": "HIGH", - "issue_severity": "MEDIUM", - "issue_text": "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", - "line_number": 184, - "line_range": [ - 184 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b303-md5", - "test_id": "B303", - "test_name": "blacklist" - }, - { - "code": "49 \"\"\"\n50 refresh_token_key = 'refresh_token'\n51 \n52 def __init__(self, client_id,\n", - "filename": "oauthlib/oauth2/rfc6749/clients/base.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'refresh_token'", - "line_number": 50, - "line_range": [ - 50, - 51 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", - "test_id": "B105", - "test_name": "hardcoded_password_string" - }, - { - "code": "51 \n52 def __init__(self, client_id,\n53 default_token_placement=AUTH_HEADER,\n54 token_type='Bearer',\n55 access_token=None,\n56 refresh_token=None,\n57 mac_key=None,\n58 mac_algorithm=None,\n59 token=None,\n60 scope=None,\n61 state=None,\n62 redirect_url=None,\n63 state_generator=generate_token,\n64 **kwargs):\n65 \"\"\"Initialize a client with commonly used attributes.\n66 \n67 :param client_id: Client identifier given by the OAuth provider upon\n68 registration.\n69 \n70 :param default_token_placement: Tokens can be supplied in the Authorization\n71 header (default), the URL query component (``query``) or the request\n72 body (``body``).\n73 \n74 :param token_type: OAuth 2 token type. Defaults to Bearer. Change this\n75 if you specify the ``access_token`` parameter and know it is of a\n76 different token type, such as a MAC, JWT or SAML token. Can\n77 also be supplied as ``token_type`` inside the ``token`` dict parameter.\n78 \n79 :param access_token: An access token (string) used to authenticate\n80 requests to protected resources. Can also be supplied inside the\n81 ``token`` dict parameter.\n82 \n83 :param refresh_token: A refresh token (string) used to refresh expired\n84 tokens. Can also be supplied inside the ``token`` dict parameter.\n85 \n86 :param mac_key: Encryption key used with MAC tokens.\n87 \n88 :param mac_algorithm: Hashing algorithm for MAC tokens.\n89 \n90 :param token: A dict of token attributes such as ``access_token``,\n91 ``token_type`` and ``expires_at``.\n92 \n93 :param scope: A list of default scopes to request authorization for.\n94 \n95 :param state: A CSRF protection string used during authorization.\n96 \n97 :param redirect_url: The redirection endpoint on the client side to which\n98 the user returns after authorization.\n99 \n100 :param state_generator: A no argument state generation callable. Defaults\n101 to :py:meth:`oauthlib.common.generate_token`.\n102 \"\"\"\n103 \n104 self.client_id = client_id\n105 self.default_token_placement = default_token_placement\n106 self.token_type = token_type\n107 self.access_token = access_token\n108 self.refresh_token = refresh_token\n109 self.mac_key = mac_key\n110 self.mac_algorithm = mac_algorithm\n111 self.token = token or {}\n112 self.scope = scope\n113 self.state_generator = state_generator\n114 self.state = state\n115 self.redirect_url = redirect_url\n116 self.code = None\n117 self.expires_in = None\n118 self._expires_at = None\n119 self.populate_token_attributes(self.token)\n120 \n121 @property\n", - "filename": "oauthlib/oauth2/rfc6749/clients/base.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 52, - "line_range": [ - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - 91, - 92, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 102, - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117, - 118, - 119, - 120 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", - "test_id": "B107", - "test_name": "hardcoded_password_default" - }, - { - "code": "313 \n314 def prepare_token_revocation_request(self, revocation_url, token,\n315 token_type_hint=\"access_token\", body='', callback=None, **kwargs):\n316 \"\"\"Prepare a token revocation request.\n317 \n318 :param revocation_url: Provider token revocation endpoint URL.\n319 \n320 :param token: The access or refresh token to be revoked (string).\n321 \n322 :param token_type_hint: ``\"access_token\"`` (default) or\n323 ``\"refresh_token\"``. This is optional and if you wish to not pass it you\n324 must provide ``token_type_hint=None``.\n325 \n326 :param body:\n327 \n328 :param callback: A jsonp callback such as ``package.callback`` to be invoked\n329 upon receiving the response. Not that it should not include a () suffix.\n330 \n331 :param kwargs: Additional parameters to included in the request.\n332 \n333 :returns: The prepared request tuple with (url, headers, body).\n334 \n335 Note that JSONP request may use GET requests as the parameters will\n336 be added to the request URL query as opposed to the request body.\n337 \n338 An example of a revocation request\n339 \n340 .. code-block: http\n341 \n342 POST /revoke HTTP/1.1\n343 Host: server.example.com\n344 Content-Type: application/x-www-form-urlencoded\n345 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n346 \n347 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token\n348 \n349 An example of a jsonp revocation request\n350 \n351 .. code-block: http\n352 \n353 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1\n354 Host: server.example.com\n355 Content-Type: application/x-www-form-urlencoded\n356 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n357 \n358 and an error response\n359 \n360 .. code-block: http\n361 \n362 package.myCallback({\"error\":\"unsupported_token_type\"});\n363 \n364 Note that these requests usually require client credentials, client_id in\n365 the case for public clients and provider specific authentication\n366 credentials for confidential clients.\n367 \"\"\"\n368 if not is_secure_transport(revocation_url):\n369 raise InsecureTransportError()\n370 \n371 return prepare_token_revocation_request(revocation_url, token,\n372 token_type_hint=token_type_hint, body=body, callback=callback,\n373 **kwargs)\n374 \n375 def parse_request_body_response(self, body, scope=None, **kwargs):\n", - "filename": "oauthlib/oauth2/rfc6749/clients/base.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'access_token'", - "line_number": 314, - "line_range": [ - 314, - 315, - 316, - 317, - 318, - 319, - 320, - 321, - 322, - 323, - 324, - 325, - 326, - 327, - 328, - 329, - 330, - 331, - 332, - 333, - 334, - 335, - 336, - 337, - 338, - 339, - 340, - 341, - 342, - 343, - 344, - 345, - 346, - 347, - 348, - 349, - 350, - 351, - 352, - 353, - 354, - 355, - 356, - 357, - 358, - 359, - 360, - 361, - 362, - 363, - 364, - 365, - 366, - 367, - 368, - 369, - 370, - 371, - 372, - 373, - 374 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", - "test_id": "B107", - "test_name": "hardcoded_password_default" - }, - { - "code": "45 def __init__(self, endpoints, claims={}, raise_errors=True):\n46 assert isinstance(claims, dict)\n47 for endpoint in endpoints:\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", - "issue_confidence": "HIGH", - "issue_severity": "LOW", - "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.", - "line_number": 46, - "line_range": [ - 46 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", - "test_id": "B101", - "test_name": "assert_used" - }, - { - "code": "47 for endpoint in endpoints:\n48 assert isinstance(endpoint, BaseEndpoint)\n49 \n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", - "issue_confidence": "HIGH", - "issue_severity": "LOW", - "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.", - "line_number": 48, - "line_range": [ - 48 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", - "test_id": "B101", - "test_name": "assert_used" - }, - { - "code": "70 default_token_type=bearer)\n71 ResourceEndpoint.__init__(self, default_token='Bearer',\n72 token_types={'Bearer': bearer})\n73 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 71, - "line_range": [ - 71, - 72 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "109 default_token_type=bearer)\n110 ResourceEndpoint.__init__(self, default_token='Bearer',\n111 token_types={'Bearer': bearer})\n112 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 110, - "line_range": [ - 110, - 111 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "142 default_token_type=bearer)\n143 ResourceEndpoint.__init__(self, default_token='Bearer',\n144 token_types={'Bearer': bearer})\n145 RevocationEndpoint.__init__(self, request_validator,\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 143, - "line_range": [ - 143, - 144 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "181 default_token_type=bearer)\n182 ResourceEndpoint.__init__(self, default_token='Bearer',\n183 token_types={'Bearer': bearer})\n184 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 182, - "line_range": [ - 182, - 183 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "214 default_token_type=bearer)\n215 ResourceEndpoint.__init__(self, default_token='Bearer',\n216 token_types={'Bearer': bearer})\n217 RevocationEndpoint.__init__(self, request_validator,\n", - "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 215, - "line_range": [ - 215, - 216 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - }, - { - "code": "164 \n165 def prepare_token_revocation_request(url, token, token_type_hint=\"access_token\",\n166 callback=None, body='', **kwargs):\n167 \"\"\"Prepare a token revocation request.\n168 \n169 The client constructs the request by including the following parameters\n170 using the \"application/x-www-form-urlencoded\" format in the HTTP request\n171 entity-body:\n172 \n173 :param token: REQUIRED. The token that the client wants to get revoked.\n174 \n175 :param token_type_hint: OPTIONAL. A hint about the type of the token\n176 submitted for revocation. Clients MAY pass this\n177 parameter in order to help the authorization server\n178 to optimize the token lookup. If the server is\n179 unable to locate the token using the given hint, it\n180 MUST extend its search across all of its supported\n181 token types. An authorization server MAY ignore\n182 this parameter, particularly if it is able to detect\n183 the token type automatically.\n184 \n185 This specification defines two values for `token_type_hint`:\n186 \n187 * access_token: An access token as defined in [RFC6749],\n188 `Section 1.4`_\n189 \n190 * refresh_token: A refresh token as defined in [RFC6749],\n191 `Section 1.5`_\n192 \n193 Specific implementations, profiles, and extensions of this\n194 specification MAY define other values for this parameter using the\n195 registry defined in `Section 4.1.2`_.\n196 \n197 .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4\n198 .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5\n199 .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2\n200 \n201 \"\"\"\n202 if not is_secure_transport(url):\n203 raise InsecureTransportError()\n204 \n205 params = [('token', token)]\n206 \n207 if token_type_hint:\n208 params.append(('token_type_hint', token_type_hint))\n209 \n210 for k in kwargs:\n211 if kwargs[k]:\n212 params.append((str(k), kwargs[k]))\n213 \n214 headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n215 \n216 if callback:\n217 params.append(('callback', callback))\n218 return add_params_to_uri(url, params), headers, body\n219 else:\n220 return url, headers, add_params_to_qs(body, params)\n221 \n222 \n223 def parse_authorization_code_response(uri, state=None):\n", - "filename": "oauthlib/oauth2/rfc6749/parameters.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'access_token'", - "line_number": 165, - "line_range": [ - 165, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - 178, - 179, - 180, - 181, - 182, - 183, - 184, - 185, - 186, - 187, - 188, - 189, - 190, - 191, - 192, - 193, - 194, - 195, - 196, - 197, - 198, - 199, - 200, - 201, - 202, - 203, - 204, - 205, - 206, - 207, - 208, - 209, - 210, - 211, - 212, - 213, - 214, - 215, - 216, - 217, - 218, - 219, - 220, - 221, - 222 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", - "test_id": "B107", - "test_name": "hardcoded_password_default" - }, - { - "code": "104 default_token_type=bearer)\n105 ResourceEndpoint.__init__(self, default_token='Bearer',\n106 token_types={'Bearer': bearer, 'JWT': jwt})\n107 RevocationEndpoint.__init__(self, request_validator)\n", - "filename": "oauthlib/openid/connect/core/endpoints/pre_configured.py", - "issue_confidence": "MEDIUM", - "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: 'Bearer'", - "line_number": 105, - "line_range": [ - 105, - 106 - ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", - "test_id": "B106", - "test_name": "hardcoded_password_funcarg" - } - ] -} \ No newline at end of file From 7760637843935b6e73d63f3c99b3d85ca5f4e031 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 09:22:11 +0200 Subject: [PATCH 089/115] Removed inactive users while keeping them in AUTHORS file. --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 1a100bac..3b75880a 100755 --- a/setup.py +++ b/setup.py @@ -28,9 +28,8 @@ def fread(fn): long_description=fread('README.rst'), long_description_content_type='text/x-rst', author='The OAuthlib Community', - author_email='idan@gazit.me', - maintainer='Ib Lundgren', - maintainer_email='ib.lundgren@gmail.com', + maintainer='Jonathan Huot', + maintainer_email='jonathan.huot@gmail.com', url='https://github.com/oauthlib/oauthlib', platforms='any', license='BSD-3-Clause', From e9a575721046329e9dd54a3127f0e8d85d9837f5 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 09:29:10 +0200 Subject: [PATCH 090/115] Refresh wording of license to not confuse tools between BSD-3 & BSD Based on latest https://opensource.org/license/BSD-3-Clause, suggested removal of ambiguity from https://peps.python.org/pep-0639/appendix-mapping-classifiers/ and https://peps.python.org/pep-0639/#deprecate-license-classifiers Fixed #896 --- LICENSE | 8 ++++---- README.rst | 2 +- oauthlib/__init__.py | 2 +- setup.py | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index e75118ad..ffab1267 100644 --- a/LICENSE +++ b/LICENSE @@ -11,14 +11,14 @@ modification, are permitted provided that the following conditions are met: 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. + 3. Neither the name of the copyright holder 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 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 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 diff --git a/README.rst b/README.rst index ef0b5ad7..c4e38b66 100644 --- a/README.rst +++ b/README.rst @@ -114,7 +114,7 @@ have the pleasure to run into each other, please send a docs pull request =) License ------- -OAuthLib is yours to use and abuse according to the terms of the BSD license. +OAuthLib is yours to use and abuse according to the terms of the BSD-3-Clause license. Check the LICENSE file for full details. Credits diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 9a5e0786..98b123e8 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -6,7 +6,7 @@ request-signing logic. :copyright: (c) The OAuthlib Community - :license: BSD, see LICENSE for details. + :license: BSD-3-Clause, see LICENSE for details. """ import logging from logging import NullHandler diff --git a/setup.py b/setup.py index 3b75880a..70162daa 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ def fread(fn): 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', From e57283b4209a2a4ca86adbbe6898b59fded0426b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 11 May 2025 09:49:50 +0200 Subject: [PATCH 091/115] Add 3.3.0 changelog and bump version --- CHANGELOG.rst | 23 +++++++++++++++++++++++ oauthlib/__init__.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 82dbd75a..4ec62bdf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,29 @@ Changelog ========= +3.3.0 (2025-05-11): +------------------ +OAuth2.0 Provider: +* OIDC: #879 Changed in how ui_locales is parsed +* RFC8628: Added OAuth2.0 Device Authorization Grant support +* PKCE: #876, #893 Fixed `create_code_verifier` length +* OIDC: Pre-configured OIDC server to use Refresh Token by default + +OAuth2.0 Common: +* OAuth2Error: Allow 0 to be a valid state + +OAuth2.0 Client: +* #745: expires_at is forced to be an int + +General: +* Removed Python 3.5, 3.6, 3.7 support +* #859, #883: Added Python 3.12, 3.13 Support +* Added dependency-review GitHub Action +* Updated various references of license (SPDX identifier..) +* Added GitHub Action for lint, replaced bandy with ruff, removed isort... +* Migrated to GitHub Actions from Travis +* Added Security Policy + 3.2.2 (2022-10-17) ------------------ OAuth2.0 Provider: diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 98b123e8..2920cf44 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.2.2' +__version__ = '3.3.0' logging.getLogger('oauthlib').addHandler(NullHandler()) From c7a3c8c577affe23b27a1b4a2d6300fa07ec8f4c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 13 May 2025 10:45:32 +0200 Subject: [PATCH 092/115] Fix multiple if statements --- .github/workflows/python-publish.yml | 9 ++++----- Makefile | 15 ++++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 43417ee5..110e5f60 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,4 +1,4 @@ -name: Production deploy +name: Publish release on: workflow_run: workflows: ["Python Tests"] @@ -6,10 +6,9 @@ on: - completed jobs: pypi-publish: - if: | - github.repository_owner == 'oauthlib' && - ${{ github.event.workflow_run.conclusion == 'success' }} && - ${{ github.ref_type == 'tag' }} + if: ${{ github.repository_owner == 'oauthlib' && + github.event.workflow_run.conclusion == 'success' && + github.ref_type == 'tag' }} name: Upload release to PyPI runs-on: ubuntu-latest environment: diff --git a/Makefile b/Makefile index 550525c6..9622f70d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ # # Please specify your library as well as primary contacts. # Since these contacts will be addressed with Github mentions they -# need to be Github users (for now)(sorry Bitbucket). +# need to be Github users. # clean: clean-eggs clean-build @find . -iname '*.pyc' -delete @@ -45,25 +45,18 @@ test: bottle: #--------------------------- - # Library thomsonreuters/bottle-oauthlib + # Library refinitiv/bottle-oauthlib # Contacts: Jonathan.Huot - cd bottle-oauthlib 2>/dev/null || git clone https://github.com/thomsonreuters/bottle-oauthlib.git + cd bottle-oauthlib 2>/dev/null || git clone https://github.com/refinitiv/bottle-oauthlib.git cd bottle-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox -flask: - #--------------------------- - # Library: lepture/flask-oauthLib - # Contacts: lepture,widnyana - cd flask-oauthlib 2>/dev/null || git clone https://github.com/lepture/flask-oauthlib.git - cd flask-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox - django: #--------------------------- # Library: evonove/django-oauth-toolkit # Contacts: evonove,masci # (note: has tox.ini already) cd django-oauth-toolkit 2>/dev/null || git clone https://github.com/evonove/django-oauth-toolkit.git - cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox -e py27,py35,py36 + cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox requests: #--------------------------- From aac6c64895bf2bf307d71a10eb997c08fa4d1126 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 14 May 2025 21:58:38 +0200 Subject: [PATCH 093/115] Handle expires_at with best effort basis --- oauthlib/oauth2/rfc6749/clients/base.py | 28 ++++++++-- oauthlib/oauth2/rfc6749/parameters.py | 4 +- requirements-test.txt | 1 + tests/oauth2/rfc6749/clients/test_base.py | 65 +++++++++-------------- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index a94e73c4..66ebc3e8 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -586,10 +586,30 @@ def populate_token_attributes(self, response): self._expires_at = round(time.time()) + int(self.expires_in) if 'expires_at' in response: - try: - self._expires_at = round(float(response.get('expires_at'))) - except: - self._expires_at = None + # expires_at is not in specification + # so it does its best to : + # convert into a float, or + # convert into an integer, or + # reuse the type as-is + + if isinstance(response.get('expires_at'), str): + try: + self.expires_at = int(response.get('expires_at')) + except ValueError: + self.expires_at = response.get('expires_at') + try: + self.expires_at = float(response.get('expires_at')) + except ValueError: + self.expires_at = response.get('expires_at') + else: + self.expires_at = response.get('expires_at') + + # we preserve internal capability to raise TokenExpiredError + # for valid types only + if isinstance(self.expires_at, float): + self._expires_at = round(self.expires_at) + elif isinstance(self.expires_at, int): + self._expires_at = self.expires_at if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 6c55000c..f6e55a16 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -434,10 +434,8 @@ def parse_token_response(body, scope=None): if params['expires_in'] is None: params.pop('expires_in') else: - params['expires_at'] = time.time() + int(params['expires_in']) + params['expires_at'] = round(time.time()) + int(params['expires_in']) - if isinstance(params.get('expires_at'), float): - params['expires_at'] = round(params['expires_at']) params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) diff --git a/requirements-test.txt b/requirements-test.txt index 6d8d6e9d..2761eca0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -r requirements.txt pytest>=4.0 pytest-cov>=2.6 +pytest-subtests==0.14.1 diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index b0b6372b..cddd1a14 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import datetime +import json from unittest.mock import patch from oauthlib import common @@ -303,31 +304,6 @@ def test_prepare_refresh_token_request(self): self.assertEqual(h, {'Content-Type': 'application/x-www-form-urlencoded'}) self.assertFormBodyEqual(b, 'grant_type=refresh_token&scope={}&refresh_token={}'.format(scope, token)) - def test_parse_token_response_invalid_expires_at(self): - token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' - ' "token_type":"example",' - ' "expires_at":"2006-01-02T15:04:05Z",' - ' "scope":"/profile",' - ' "example_parameter":"example_value"}') - token = { - "access_token": "2YotnFZFEjr1zCsicMWpAA", - "token_type": "example", - "expires_at": "2006-01-02T15:04:05Z", - "scope": ["/profile"], - "example_parameter": "example_value" - } - - client = Client(self.client_id) - - # Parse code and state - response = client.parse_request_body_response(token_json, scope=["/profile"]) - self.assertEqual(response, token) - self.assertEqual(None, client._expires_at) - self.assertEqual(client.access_token, response.get("access_token")) - self.assertEqual(client.refresh_token, response.get("refresh_token")) - self.assertEqual(client.token_type, response.get("token_type")) - - def test_create_code_verifier_min_length(self): client = Client(self.client_id) length = 43 @@ -361,20 +337,31 @@ def test_create_code_challenge_s256(self): code_challenge_s256 = client.create_code_challenge(code_verifier=code_verifier, code_challenge_method='S256') self.assertEqual(code_challenge_s256, client.code_challenge) - def test_parse_token_response_expires_at_is_int(self): - expected_expires_at = 1661185149 - token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' - ' "token_type":"example",' - ' "expires_at":1661185148.6437678,' - ' "scope":"/profile",' - ' "example_parameter":"example_value"}') - - client = Client(self.client_id) - - response = client.parse_request_body_response(token_json, scope=["/profile"]) - - self.assertEqual(response['expires_at'], expected_expires_at) - self.assertEqual(client._expires_at, expected_expires_at) + def test_parse_token_response_expires_at_types(self): + for data in [ # title, expected, expected_valid, fieldjson + ('int', 1661185148, 1661185148, 1661185148), + ('float', 1661185148.6437678, 1661185149, 1661185148.6437678), + ('str', "2006-01-02T15:04:05Z", None, "\"2006-01-02T15:04:05Z\""), + ('str-as-int', 1661185148, 1661185148, "\"1661185148\""), + ('str-as-float', 1661185148.42, 1661185148, "\"1661185148.42\""), + ]: + with self.subTest(msg=data[0]): + expected_expires_at = data[1] + expected_valid_expires_at = data[2] + token_json = ('{{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_at":{expires_at},' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}}'.format(expires_at=data[3])) + + client = Client(self.client_id) + + response = client.parse_request_body_response(token_json, scope=["/profile"]) + + self.assertEqual(response['expires_at'], json.loads('{{"foo":{}}}'.format(data[3]))["foo"], "response attribute wrong") + self.assertEqual(client.expires_at, expected_expires_at, "client attribute wrong") + if expected_valid_expires_at: + self.assertEqual(client._expires_at, expected_valid_expires_at, "internal expiration wrong") @patch('time.time') def test_parse_token_response_generated_expires_at_is_int(self, t): From e1672e498905b505830ebb69155bec5e99afb847 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 14 May 2025 22:02:27 +0200 Subject: [PATCH 094/115] Removed version ping to work with all py versions --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2761eca0..521036a4 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -r requirements.txt pytest>=4.0 pytest-cov>=2.6 -pytest-subtests==0.14.1 +pytest-subtests From a8566919393418926cc1aef00cf31575c5235285 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 15 May 2025 23:26:01 +0200 Subject: [PATCH 095/115] Simplified str convertion of expires_at Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- oauthlib/oauth2/rfc6749/clients/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 66ebc3e8..ad273a84 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -594,13 +594,13 @@ def populate_token_attributes(self, response): if isinstance(response.get('expires_at'), str): try: + # Attempt to convert to int first, then float if int fails self.expires_at = int(response.get('expires_at')) except ValueError: - self.expires_at = response.get('expires_at') - try: - self.expires_at = float(response.get('expires_at')) - except ValueError: - self.expires_at = response.get('expires_at') + try: + self.expires_at = float(response.get('expires_at')) + except ValueError: + self.expires_at = response.get('expires_at') else: self.expires_at = response.get('expires_at') From 35480caefa84019d9b5ab504bda7d3ed987a7f81 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 28 May 2025 21:40:36 +0200 Subject: [PATCH 096/115] Does not round if float is provided --- oauthlib/oauth2/rfc6749/clients/base.py | 2 +- tests/oauth2/rfc6749/clients/test_base.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index ad273a84..e104cb3a 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -607,7 +607,7 @@ def populate_token_attributes(self, response): # we preserve internal capability to raise TokenExpiredError # for valid types only if isinstance(self.expires_at, float): - self._expires_at = round(self.expires_at) + self._expires_at = self.expires_at elif isinstance(self.expires_at, int): self._expires_at = self.expires_at diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index cddd1a14..f059db9c 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -340,10 +340,10 @@ def test_create_code_challenge_s256(self): def test_parse_token_response_expires_at_types(self): for data in [ # title, expected, expected_valid, fieldjson ('int', 1661185148, 1661185148, 1661185148), - ('float', 1661185148.6437678, 1661185149, 1661185148.6437678), + ('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678), ('str', "2006-01-02T15:04:05Z", None, "\"2006-01-02T15:04:05Z\""), ('str-as-int', 1661185148, 1661185148, "\"1661185148\""), - ('str-as-float', 1661185148.42, 1661185148, "\"1661185148.42\""), + ('str-as-float', 1661185148.42, 1661185148.42, "\"1661185148.42\""), ]: with self.subTest(msg=data[0]): expected_expires_at = data[1] From 6a30bcf6386d79521eadb342768d936fe76b1e98 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:04:24 +0200 Subject: [PATCH 097/115] Factorized parsing of expires_at Added similar behaviors to all interfaces where expires_at is parsed, this will facilitate the implementations. Note this is breaking change for those which are expecting the "default" `expires_at` (as in, not provided) to be a float. This will now default to a int. --- oauthlib/oauth2/rfc6749/clients/base.py | 37 ++------ .../rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/parameters.py | 87 +++++++++++++++---- tests/oauth2/rfc6749/clients/test_base.py | 23 +++-- tests/oauth2/rfc6749/test_parameters.py | 22 +++++ 5 files changed, 113 insertions(+), 58 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index e104cb3a..17f833d2 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -17,6 +17,7 @@ InsecureTransportError, TokenExpiredError, ) from oauthlib.oauth2.rfc6749.parameters import ( + parse_expires, parse_token_response, prepare_token_request, prepare_token_revocation_request, ) @@ -581,35 +582,13 @@ def populate_token_attributes(self, response): 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 = round(time.time()) + int(self.expires_in) - - if 'expires_at' in response: - # expires_at is not in specification - # so it does its best to : - # convert into a float, or - # convert into an integer, or - # reuse the type as-is - - if isinstance(response.get('expires_at'), str): - try: - # Attempt to convert to int first, then float if int fails - self.expires_at = int(response.get('expires_at')) - except ValueError: - try: - self.expires_at = float(response.get('expires_at')) - except ValueError: - self.expires_at = response.get('expires_at') - else: - self.expires_at = response.get('expires_at') - - # we preserve internal capability to raise TokenExpiredError - # for valid types only - if isinstance(self.expires_at, float): - self._expires_at = self.expires_at - elif isinstance(self.expires_at, int): - self._expires_at = self.expires_at + vin, vat, v_at = parse_expires(response) + if vin: + self.expires_in = vin + if vat: + self.expires_at = vat + if v_at: + self._expires_at = v_at if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 8fb17377..9c223840 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -91,7 +91,7 @@ def prepare_request_body(self, ``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``. + to an hour from now, i.e. ``round(time.time()) + 3600``. :param issued_at: A unix timestamp of when the JWT was created. Defaults to now, i.e. ``time.time()``. diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index f6e55a16..7a0b2d3e 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -336,15 +336,18 @@ def parse_implicit_response(uri, state=None, scope=None): 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'] = round(time.time()) + int(params['expires_in']) + vin, vat, v_at = parse_expires(params) + if vin: + params['expires_in'] = vin + elif 'expires_in' in params: + params.pop('expires_in') + if vat: + params['expires_at'] = vat + elif 'expires_at' in params: + params.pop('expires_at') if state and params.get('state') != state: raise ValueError("Mismatching or missing state in params.") @@ -423,19 +426,19 @@ def parse_token_response(body, scope=None): # 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'] = round(time.time()) + int(params['expires_in']) - + vin, vat, v_at = parse_expires(params) + if vin: + params['expires_in'] = vin + elif 'expires_in' in params: + params.pop('expires_in') + if vat: + params['expires_at'] = vat + elif 'expires_at' in params: + params.pop('expires_at') params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) @@ -468,3 +471,57 @@ def validate_token_parameters(params): w.old_scope = params.old_scopes w.new_scope = params.scopes raise w + +def parse_expires(params): + """Parse `expires_in`, `expires_at` fields from params + + Parse following these rules: + - `expires_in` must be either blank or a valid integer. + - `expires_at` is not in specification so it does its best to: + - convert into a int, else + - convert into a float, else + - reuse the same type as-is (usually string) + - `_expires_at` is a special internal value returned to be always an `int`, based + either on the presence of `expires_at`, or reuse the current time plus + `expires_in`. This is typically used to validate token expiry. + + :param params: Dict with expires_in and expires_at optionally set + :return: Tuple of `expires_in`, `expires_at`, and `_expires_at`. None if not set. + """ + expires_in = None + expires_at = None + _expires_at = None + + if 'expires_in' in params: + if isinstance(params.get('expires_in'), int): + expires_in = params.get('expires_in') + elif isinstance(params.get('expires_in'), str): + try: + # Attempt to convert to int + expires_in = int(params.get('expires_in')) + except ValueError: + raise ValueError("expires_int must be an int") + elif params.get('expires_in') is not None: + raise ValueError("expires_int must be an int") + + if 'expires_at' in params: + if isinstance(params.get('expires_at'), float) or \ + isinstance(params.get('expires_at'), int): + expires_at = params.get('expires_at') + _expires_at = expires_at + elif isinstance(params.get('expires_at'), str): + try: + # Attempt to convert to int first, then float if int fails + expires_at = int(params.get('expires_at')) + _expires_at = expires_at + except ValueError: + try: + expires_at = float(params.get('expires_at')) + _expires_at = expires_at + except ValueError: + # no change from str + expires_at = params.get('expires_at') + if _expires_at is None and expires_in: + expires_at = round(time.time()) + expires_in + _expires_at = expires_at + return expires_in, expires_at, _expires_at diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index f059db9c..0e68d3bb 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -338,30 +338,27 @@ def test_create_code_challenge_s256(self): self.assertEqual(code_challenge_s256, client.code_challenge) def test_parse_token_response_expires_at_types(self): - for data in [ # title, expected, expected_valid, fieldjson + for title, fieldjson, expected, generated in [ # title, fieldjson, expected, expected_valid ('int', 1661185148, 1661185148, 1661185148), ('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678), - ('str', "2006-01-02T15:04:05Z", None, "\"2006-01-02T15:04:05Z\""), - ('str-as-int', 1661185148, 1661185148, "\"1661185148\""), - ('str-as-float', 1661185148.42, 1661185148.42, "\"1661185148.42\""), + ('str', "\"2006-01-02T15:04:05Z\"", "2006-01-02T15:04:05Z", None), + ('str-as-int', "\"1661185148\"", 1661185148, 1661185148), + ('str-as-float', "\"1661185148.42\"", 1661185148.42, 1661185148.42), ]: - with self.subTest(msg=data[0]): - expected_expires_at = data[1] - expected_valid_expires_at = data[2] + with self.subTest(msg=title): token_json = ('{{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' ' "token_type":"example",' ' "expires_at":{expires_at},' ' "scope":"/profile",' - ' "example_parameter":"example_value"}}'.format(expires_at=data[3])) + ' "example_parameter":"example_value"}}'.format(expires_at=fieldjson)) client = Client(self.client_id) - response = client.parse_request_body_response(token_json, scope=["/profile"]) - self.assertEqual(response['expires_at'], json.loads('{{"foo":{}}}'.format(data[3]))["foo"], "response attribute wrong") - self.assertEqual(client.expires_at, expected_expires_at, "client attribute wrong") - if expected_valid_expires_at: - self.assertEqual(client._expires_at, expected_valid_expires_at, "internal expiration wrong") + self.assertEqual(response['expires_at'], expected, "response attribute wrong") + self.assertEqual(client.expires_at, expected, "client attribute wrong") + if generated: + self.assertEqual(client._expires_at, generated, "internal expiration wrong") @patch('time.time') def test_parse_token_response_generated_expires_at_is_int(self, t): diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index cd8c9e95..cdc7164b 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -302,3 +302,25 @@ def record_scope_change(sender, message, old, new): finally: signals.scope_changed.disconnect(record_scope_change) del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] + + + def test_parse_expires(self): + for title, arg, expected in [ # title, arg_tuple, returned_expected_tuple, + ('none', (None, None), (None, None, None)), + ('expires_in only', (3600, None), (3600, 4600, 4600)), + ('expires_in and expires_at', (3600, 200), (3600, 200, 200)), + ('expires_in and expires_at float', (3600, 200.42), (3600, 200.42, 200.42)), + ('expires_in and expires_at str-int', (3600, "200"), (3600, 200, 200)), + ('expires_in and expires_at str-float', (3600, "200.42"), (3600, 200.42, 200.42)), + ('expires_in str only', ("3600", None), (3600, 4600, 4600)), + ('expires_in str and expires_at', ("3600", 200), (3600, 200, 200)), + ('expires_in str and expires_at float', ("3600", 200.42), (3600, 200.42, 200.42)), + ('expires_in str and expires_at str-int', ("3600", "200"), (3600, 200, 200)), + ('expires_in str and expires_at str-float', ("3600", "200.42"), (3600, 200.42, 200.42)), + ]: + with self.subTest(msg=title): + params = { + "expires_in": arg[0], + "expires_at": arg[1] + } + self.assertEqual(expected, parse_expires(params)) From 51259c22d856d6c8c8612cd6d1af03632db6bd13 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:15:35 +0200 Subject: [PATCH 098/115] Added expires_at changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ec62bdf..c0f02515 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,7 @@ OAuth2.0 Common: OAuth2.0 Client: * #745: expires_at is forced to be an int +* #899: expires_at clarification General: * Removed Python 3.5, 3.6, 3.7 support From ebbe68297d7115b80fa5bbd4688a406222a06fe4 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:15:52 +0200 Subject: [PATCH 099/115] Removed unnecessary comments --- tests/oauth2/rfc6749/clients/test_base.py | 2 +- tests/oauth2/rfc6749/test_parameters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index 0e68d3bb..b0970f2d 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -338,7 +338,7 @@ def test_create_code_challenge_s256(self): self.assertEqual(code_challenge_s256, client.code_challenge) def test_parse_token_response_expires_at_types(self): - for title, fieldjson, expected, generated in [ # title, fieldjson, expected, expected_valid + for title, fieldjson, expected, generated in [ ('int', 1661185148, 1661185148, 1661185148), ('float', 1661185148.6437678, 1661185148.6437678, 1661185148.6437678), ('str', "\"2006-01-02T15:04:05Z\"", "2006-01-02T15:04:05Z", None), diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index cdc7164b..63b74c37 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -305,7 +305,7 @@ def record_scope_change(sender, message, old, new): def test_parse_expires(self): - for title, arg, expected in [ # title, arg_tuple, returned_expected_tuple, + for title, arg, expected in [ ('none', (None, None), (None, None, None)), ('expires_in only', (3600, None), (3600, 4600, 4600)), ('expires_in and expires_at', (3600, 200), (3600, 200, 200)), From 7293d0cbae2179a06d338d7d717fa18df3d76293 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 12 Jun 2025 20:16:23 +0200 Subject: [PATCH 100/115] Simplify isinstance calls to one/Ruff/SIM101 --- oauthlib/oauth2/rfc6749/parameters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 7a0b2d3e..4675a31f 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -505,8 +505,7 @@ def parse_expires(params): raise ValueError("expires_int must be an int") if 'expires_at' in params: - if isinstance(params.get('expires_at'), float) or \ - isinstance(params.get('expires_at'), int): + if isinstance(params.get('expires_at'), (float, int)): expires_at = params.get('expires_at') _expires_at = expires_at elif isinstance(params.get('expires_at'), str): From 78e2c43aae8f655deb15dd1357ae5cd93130937a Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 19:22:20 +0200 Subject: [PATCH 101/115] Updated release date --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c0f02515..8064cd0d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -3.3.0 (2025-05-11): +3.3.0 (2025-06-17): ------------------ OAuth2.0 Provider: * OIDC: #879 Changed in how ui_locales is parsed From e4d4a0d0eab5c305712ff08244ab81d533fc118f Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 19:22:26 +0200 Subject: [PATCH 102/115] Bumped release --- SECURITY.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 3ddddebc..f1a3fb80 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,13 +2,14 @@ ## Supported Versions -following versions are currently being supported with security updates. +The following versions are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | +| 3.3.x | :white_check_mark: | | 3.2.x | :white_check_mark: | | 3.1.x | :x: | -| < 3.2.0 | :x: | +| < 3.1 | :x: | ## Reporting a Vulnerability From be152364ed71c7b9fc228e328f7ff503fda4ea26 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:09:57 +0200 Subject: [PATCH 103/115] Updated GH actions --- .github/workflows/python-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 110e5f60..dd4630bd 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -18,8 +18,8 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - name: Check out repository code - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install prereq From e5b3617504f5c6d4f492ce4251a2376ec969e904 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:10:14 +0200 Subject: [PATCH 104/115] Added maintainer instructions --- docs/release_process.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/release_process.rst b/docs/release_process.rst index 2796f29c..e915f938 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -51,3 +51,26 @@ Minor point (1.1.0) releases will introduce non API breaking new features and changes. Bug releases (1.0.1) will include minor fixes that needs to be released quickly (e.g. after a bigger release unintentionally introduced a bug). + +For maintainer - Publishing a newer version +-------------------------------------------- + +List of tasks to do a release from a maintainer point of view: + + - Create a Branch ``xyz-release`` + - Update ``oauthlib/__init__.py`` version + - Update ``CHANGELOG.rst`` accordingly + - Review Github Issues and PR, and associate the milestone of the version + - Run ``make`` to cover the release readiness + - Create a PR to let downstreams developers test their apps and comments + - Create a tag and push tag, it will automatically publish the release to pypi + - Create a release with GitHub Releases + - Merge PR, close Github milestone + + +Initial setup: + - Because we currently use "trusted publisher", it does not require to setup + token. However, OIDC Authorization flow has to be configured in `pypi publishing`. + - During setup, refer to the environment and name of the workflow directly in the code. + +.. _`pypi publishing`: https://pypi.org/manage/project/oauthlib/settings/publishing/ From 76585febf8d029e7f30d0571737f164691a6d483 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:10:33 +0200 Subject: [PATCH 105/115] Fixed ruff findings about function import --- oauthlib/common.py | 4 ++-- oauthlib/oauth1/rfc5849/signature.py | 2 +- oauthlib/oauth2/rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/errors.py | 4 ++-- oauthlib/openid/connect/core/exceptions.py | 5 +++-- tests/oauth2/rfc6749/endpoints/test_metadata.py | 4 ++-- tests/test_uri_validate.py | 2 +- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/oauthlib/common.py b/oauthlib/common.py index fd9cad09..dfa85179 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -198,7 +198,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): def generate_signed_token(private_pem, request): - import jwt + import jwt # noqa: PLC0415 now = datetime.datetime.utcnow() @@ -216,7 +216,7 @@ def generate_signed_token(private_pem, request): def verify_signed_token(public_pem, token): - import jwt + import jwt # noqa: PLC0415 return jwt.decode(token, public_pem, algorithms=['RS256']) diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 8916782b..a27cb2e7 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -568,7 +568,7 @@ def _get_jwt_rsa_algorithm(hash_algorithm_name: str): # Not in cache: instantiate a new RSAAlgorithm # PyJWT has some nice pycrypto/cryptography abstractions - import jwt.algorithms as jwt_algorithms + import jwt.algorithms as jwt_algorithms # noqa: PLC0415 m = { 'SHA-1': jwt_algorithms.hashes.SHA1, 'SHA-256': jwt_algorithms.hashes.SHA256, diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 9c223840..abf22d2d 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -149,7 +149,7 @@ def prepare_request_body(self, .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ - import jwt + import jwt # noqa: PLC0415 key = private_key or self.private_key if not key: diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 3b415748..be8e7a1e 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -6,6 +6,8 @@ defined error responses for all four core grant types. """ import json +import inspect +import sys from oauthlib.common import add_params_to_uri, urlencode @@ -386,8 +388,6 @@ def __init__(self, error, *args, **kwargs): def raise_from_error(error, params=None): - import inspect - import sys kwargs = { 'description': params.get('error_description'), 'uri': params.get('error_uri'), diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py index 8a3e79f4..291cf137 100644 --- a/oauthlib/openid/connect/core/exceptions.py +++ b/oauthlib/openid/connect/core/exceptions.py @@ -5,6 +5,9 @@ Error used both by OAuth 2 clients and providers to represent the spec defined error responses for all four core grant types. """ +import inspect +import sys + from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error @@ -137,8 +140,6 @@ class InsufficientScopeError(OAuth2Error): def raise_from_error(error, params=None): - import inspect - import sys kwargs = { 'description': params.get('error_description'), 'uri': params.get('error_uri'), diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index facf69d0..c36d94bf 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -20,8 +20,8 @@ def test_openid_oauth2_preconfigured(self): "introspection_endpoint": "https://foo.bar/introspect", "token_endpoint": "https://foo.bar/token" } - from oauthlib.oauth2 import Server as OAuth2Server - from oauthlib.openid import Server as OpenIDServer + from oauthlib.oauth2 import Server as OAuth2Server # noqa: PLC0415 + from oauthlib.openid import Server as OpenIDServer # noqa: PLC0415 endpoint = OAuth2Server(None) metadata = MetadataEndpoint([endpoint], default_claims) diff --git a/tests/test_uri_validate.py b/tests/test_uri_validate.py index 04138d60..f1ac404e 100644 --- a/tests/test_uri_validate.py +++ b/tests/test_uri_validate.py @@ -1,3 +1,4 @@ +from datetime import datetime import unittest from oauthlib.uri_validate import is_absolute_uri @@ -77,7 +78,6 @@ def test_failures(self): self.assertIsNone(is_absolute_uri('http://[abcd:efgh::1]/')) def test_recursive_regex(self): - from datetime import datetime t0 = datetime.now() is_absolute_uri('http://[::::::::::::::::::::::::::]/path') t1 = datetime.now() From 854670b3fc14e612ef6ea746483a90aa80167c32 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:24:22 +0200 Subject: [PATCH 106/115] Fix linter after rebase --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c4e38b66..840c33d5 100644 --- a/README.rst +++ b/README.rst @@ -76,8 +76,8 @@ 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. - - `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support. + - `django-oauth-toolkit`_, which includes `Django REST framework`_ support. + - `django-allauth`_, which includes `Django REST framework`_ as well as `Django Ninja`_ support. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Pyramid there is `pyramid-oauthlib`_. - For Bottle there is `bottle-oauthlib`_. From 68b2dfc52ffe0697b677d5856dbbb1d9eaa6520b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jun 2025 23:34:34 +0200 Subject: [PATCH 107/115] Updated recommended filter for publish --- .github/workflows/python-publish.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index dd4630bd..fc9181d4 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -6,9 +6,7 @@ on: - completed jobs: pypi-publish: - if: ${{ github.repository_owner == 'oauthlib' && - github.event.workflow_run.conclusion == 'success' && - github.ref_type == 'tag' }} + if: github.repository_owner == 'oauthlib' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') name: Upload release to PyPI runs-on: ubuntu-latest environment: From 967bc5024680a587db909cbc1143a94552c76c90 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 18 Jun 2025 00:12:41 +0200 Subject: [PATCH 108/115] Execute publish workflow of the tagged source. Not of master branch. --- .github/workflows/python-publish.yml | 2 ++ docs/release_process.rst | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index fc9181d4..b36e018a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v4 + with: # by default, this event will trigger a workflow on the default branch. + ref: ${{ github.event.workflow_run.head_ref }} # set source branch - uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/docs/release_process.rst b/docs/release_process.rst index e915f938..70cc1a5f 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -71,6 +71,9 @@ List of tasks to do a release from a maintainer point of view: Initial setup: - Because we currently use "trusted publisher", it does not require to setup token. However, OIDC Authorization flow has to be configured in `pypi publishing`. - - During setup, refer to the environment and name of the workflow directly in the code. + + - During setup, refer to the environment and name of the workflow directly in the code. + - GitHub Restrictions: tag protection must be enabled + .. _`pypi publishing`: https://pypi.org/manage/project/oauthlib/settings/publishing/ From 892953211edcaa2c106144364c926fbaaa436918 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 18 Jun 2025 22:20:56 +0200 Subject: [PATCH 109/115] Add unit test sample based on 3.3.0 regression of expires_in --- oauthlib/oauth2/rfc6749/parameters.py | 4 ++-- .../rfc6749/clients/test_web_application.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 4675a31f..ea5821fd 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -500,9 +500,9 @@ def parse_expires(params): # Attempt to convert to int expires_in = int(params.get('expires_in')) except ValueError: - raise ValueError("expires_int must be an int") + raise ValueError("expires_in must be an int") elif params.get('expires_in') is not None: - raise ValueError("expires_int must be an int") + raise ValueError("expires_in must be an int") if 'expires_at' in params: if isinstance(params.get('expires_at'), (float, int)): diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 2a7a8ff3..1f7c4138 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -262,3 +262,19 @@ def test_prepare_request_body(self): with self.assertWarns(DeprecationWarning), self.assertRaises(ValueError): client.prepare_request_body(client_id='different_client_id') # testing the exact exception message in Python2&Python3 is a pain + + def test_expires_in_as_str(self): + """ + see regression issue #906 + """ + + client = WebApplicationClient( + client_id="dummy", + token={"access_token": "xyz", "expires_in": "3600"} # ← str on purpose + ) + self.assertIsNotNone(client) + client = WebApplicationClient( + client_id="dummy", + token={"access_token": "xyz", "expires_in": 3600} # ← str on purpose + ) + self.assertIsNotNone(client) From 136b3e72586286d04f8a53ae230539af4d99b69e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 18 Jun 2025 22:44:22 +0200 Subject: [PATCH 110/115] Add mandatory RTD configuration --- .readthedocs.yaml | 16 ++++++++++++++++ docs/requirements.txt | 3 +++ 2 files changed, 19 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..d76e42c7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" +sphinx: + builder: html + configuration: docs/conf.py + fail_on_warning: true +# the requirements.txt override some RTD defaults. +# ideally it has to be updated from time to time +# with latest libraries versions. +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..812eac27 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx +sphinx_rtd_theme +readthedocs-sphinx-ext From 34c90128402e24460ec4bce54ef4d9de8a0cd24c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 19 Jun 2025 11:47:10 +0200 Subject: [PATCH 111/115] Handle expires_in as float to be backward compatible with 3.2.* --- oauthlib/oauth2/rfc6749/parameters.py | 4 +++- tests/oauth2/rfc6749/clients/test_web_application.py | 9 +++++++-- tests/oauth2/rfc6749/test_parameters.py | 5 +++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index ea5821fd..8268ef92 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -476,7 +476,7 @@ def parse_expires(params): """Parse `expires_in`, `expires_at` fields from params Parse following these rules: - - `expires_in` must be either blank or a valid integer. + - `expires_in` must be either integer, float or None. If a float, it is converted into an integer. - `expires_at` is not in specification so it does its best to: - convert into a int, else - convert into a float, else @@ -495,6 +495,8 @@ def parse_expires(params): if 'expires_in' in params: if isinstance(params.get('expires_in'), int): expires_in = params.get('expires_in') + elif isinstance(params.get('expires_in'), float): + expires_in = int(params.get('expires_in')) elif isinstance(params.get('expires_in'), str): try: # Attempt to convert to int diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 1f7c4138..f9a4c9d1 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -270,11 +270,16 @@ def test_expires_in_as_str(self): client = WebApplicationClient( client_id="dummy", - token={"access_token": "xyz", "expires_in": "3600"} # ← str on purpose + token={"access_token": "xyz", "expires_in": "3600"} ) self.assertIsNotNone(client) client = WebApplicationClient( client_id="dummy", - token={"access_token": "xyz", "expires_in": 3600} # ← str on purpose + token={"access_token": "xyz", "expires_in": 3600} + ) + self.assertIsNotNone(client) + client = WebApplicationClient( + client_id="dummy", + token={"access_token": "xyz", "expires_in": 3600.12} ) self.assertIsNotNone(client) diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index 63b74c37..bd8a8b61 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -312,6 +312,11 @@ def test_parse_expires(self): ('expires_in and expires_at float', (3600, 200.42), (3600, 200.42, 200.42)), ('expires_in and expires_at str-int', (3600, "200"), (3600, 200, 200)), ('expires_in and expires_at str-float', (3600, "200.42"), (3600, 200.42, 200.42)), + ('expires_in float only', (3600.12, None), (3600, 4600, 4600)), + ('expires_in float and expires_at', (3600.12, 200), (3600, 200, 200)), + ('expires_in float and expires_at float', (3600.12, 200.42), (3600, 200.42, 200.42)), + ('expires_in float and expires_at str-int', (3600.12, "200"), (3600, 200, 200)), + ('expires_in float and expires_at str-float', (3600.12, "200.42"), (3600, 200.42, 200.42)), ('expires_in str only', ("3600", None), (3600, 4600, 4600)), ('expires_in str and expires_at', ("3600", 200), (3600, 200, 200)), ('expires_in str and expires_at float', ("3600", 200.42), (3600, 200.42, 200.42)), From 93fdf9144060751d555915960054431cadb6679a Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 19 Jun 2025 17:39:49 +0200 Subject: [PATCH 112/115] Add twine manual instructions --- docs/release_process.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/release_process.rst b/docs/release_process.rst index 70cc1a5f..8588ce28 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -67,6 +67,12 @@ List of tasks to do a release from a maintainer point of view: - Create a release with GitHub Releases - Merge PR, close Github milestone +In case of issues with CICD and a manual publish is required, follow these steps: + + - Install dependencies `pip install build twine` + - Run `python -m build` + - Run `twine check dist/*` + - Run `twine upload dist/*` Initial setup: - Because we currently use "trusted publisher", it does not require to setup From 9b65baf2432e33f51c4e9a834dd6973277385ad1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 19 Jun 2025 17:39:55 +0200 Subject: [PATCH 113/115] Bump version --- CHANGELOG.rst | 6 ++++++ oauthlib/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8064cd0d..2b2b6d56 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog ========= +3.3.1 (2025-06-19): +------------------ +OAuth2.0 Client: +* #906: fix regression of expires_in parsing when float in string. + + 3.3.0 (2025-06-17): ------------------ OAuth2.0 Provider: diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 2920cf44..462612f8 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.3.0' +__version__ = '3.3.1' logging.getLogger('oauthlib').addHandler(NullHandler()) From 38c2a8e859a0d0890b76fbb1a42e4d72ce84ae04 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 20 Jun 2025 00:46:38 +0200 Subject: [PATCH 114/115] Merge publish into build workflow Having two workflows are usually required when different events are used. Calling from one workflow another workflow of the same repository is not efficient and clear. --- .github/workflows/lint_python.yml | 5 +++-- .github/workflows/python-build.yml | 33 ++++++++++++++++++++++++++++ .github/workflows/python-publish.yml | 32 --------------------------- 3 files changed, 36 insertions(+), 34 deletions(-) delete mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 683a3283..25e7f88b 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -3,6 +3,8 @@ on: [pull_request, push] jobs: lint_python: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -10,9 +12,8 @@ jobs: python-version: 3.x check-latest: true - run: pip install --upgrade pip setuptools wheel - - run: pip install black codespell mypy pytest ruff safety + - run: pip install codespell mypy pytest ruff safety - run: ruff check --output-format=github . - - run: black --check . || true - run: codespell --ignore-words-list="implementor,mimiced,provicers,re-use,THIRDPARTY,assertIn" # --skip="*.css,*.js,*.lock" - run: pip install -r requirements-test.txt - run: pip install --editable . diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index 8108b6dc..51623fad 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -9,6 +9,8 @@ jobs: matrix: python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 @@ -32,6 +34,8 @@ jobs: needs: tests runs-on: ubuntu-latest container: python:3-slim + permissions: + contents: read steps: - name: Finished run: | @@ -44,6 +48,8 @@ jobs: matrix: toxenv: ["docs", "readme"] runs-on: ubuntu-latest + permissions: + contents: read steps: - run: sudo apt install -y graphviz - name: Set up Python @@ -56,3 +62,30 @@ jobs: run: pip install tox - name: Run python tests run: tox -e ${{ matrix.toxenv }} + pypi-publish: + needs: + - tests + - docs + - coveralls + if: ${{ success() }} && github.repository == 'oauthlib/oauthlib' && startsWith(github.ref, 'refs/tags') + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/oauthlib + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install prereq + run: pip install build twine + - name: Build python package + run: python -m build + - name: Check python package + run: twine check dist/* + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index b36e018a..00000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Publish release -on: - workflow_run: - workflows: ["Python Tests"] - types: - - completed -jobs: - pypi-publish: - if: github.repository_owner == 'oauthlib' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - name: Upload release to PyPI - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/oauthlib - permissions: - id-token: write # IMPORTANT: this permission is mandatory for trusted publishing - steps: - - name: Check out repository code - uses: actions/checkout@v4 - with: # by default, this event will trigger a workflow on the default branch. - ref: ${{ github.event.workflow_run.head_ref }} # set source branch - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install prereq - run: pip install wheel - - name: Build python package - run: python setup.py build - - name: Package python package - run: python setup.py sdist bdist_wheel - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 From a2779c65b3e961f1b49c05a2417c0ae819f3f8c4 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 20 Jun 2025 00:49:42 +0200 Subject: [PATCH 115/115] Bump dependency-review --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4e751977..0d4a0136 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,4 +17,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v3 + uses: actions/dependency-review-action@v4