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 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 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/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/SECURITY.md b/SECURITY.md index 7d0f5250..f1a3fb80 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,4 +12,16 @@ The following versions are currently being supported with security updates. | < 3.1 | :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 + +## 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. 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 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 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()) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 4675a31f..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,14 +495,16 @@ 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 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/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, diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 2a7a8ff3..f9a4c9d1 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -262,3 +262,24 @@ 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"} + ) + self.assertIsNotNone(client) + client = WebApplicationClient( + client_id="dummy", + 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)),