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..477234b8 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 @@ -17,6 +19,8 @@ jobs: allow-prereleases: true - name: Check out repository code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install prereq run: pip install tox coveralls - name: Run python tests @@ -32,6 +36,8 @@ jobs: needs: tests runs-on: ubuntu-latest container: python:3-slim + permissions: + contents: read steps: - name: Finished run: | @@ -44,6 +50,8 @@ jobs: matrix: toxenv: ["docs", "readme"] runs-on: ubuntu-latest + permissions: + contents: read steps: - run: sudo apt install -y graphviz - name: Set up Python @@ -52,7 +60,59 @@ jobs: python-version: "3.11" - name: Check out repository code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install prereq run: pip install tox - name: Run python tests run: tox -e ${{ matrix.toxenv }} + build: + name: Build oauthlib distribution + needs: + - tests + - docs + - coveralls + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Check out repository code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build wheel and tarball + run: python3 -m build + - name: Store the package's artifact + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + pypi-publish: + if: success() && github.repository == 'oauthlib/oauthlib' && github.ref_type == 'tag' + needs: + - build + 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: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: 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..6bdea12d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" + apt_packages: + - graphviz # used for "dot" graphs +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/Makefile b/Makefile index 9622f70d..5313c80a 100644 --- a/Makefile +++ b/Makefile @@ -34,44 +34,37 @@ clean-build: @rm -fr dist/ @rm -fr *.egg-info -format fmt black: - black . - -lint ruff: - ruff check . - test: - tox + uvx --with tox-uv tox bottle: #--------------------------- # Library refinitiv/bottle-oauthlib # Contacts: Jonathan.Huot 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 + cd bottle-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && uvx --with tox-uv 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 + cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && uvx --with tox-uv tox requests: #--------------------------- # Library requests/requests-oauthlib # Contacts: ib-lundgren,lukasa cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git - cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + cd requests-oauthlib && sed -i.old 's,oauthlib.*,--editable=file://{toxinidir}/../../[signedtoken],' requirements.txt && uvx --with tox-uv tox dance: #--------------------------- # Library singingwolfboy/flask-dance # Contacts: singingwolfboy cd flask-dance 2>/dev/null || git clone https://github.com/singingwolfboy/flask-dance.git - cd flask-dance && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + cd flask-dance && sed -i.old 's;"oauthlib.*";"oauthlib @ file://'`pwd`'/../";' pyproject.toml && uv venv && uv pip install -e '.[test]' && ./.venv/bin/coverage run -m pytest .DEFAULT_GOAL := all .PHONY: clean test bottle dance django flask requests -all: lint test bottle dance django flask requests +all: test bottle dance django flask requests 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/contributing.rst b/docs/contributing.rst index 9da1370c..19c08190 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -159,24 +159,22 @@ all versions conveniently at once can be done using `Tox`_. $ tox -Tox requires you to have `virtualenv`_ installed as well as respective python -version. We recommend using `pyenv`_ to install those Python versions. +Tox requires you to have respective python versions. We recommend using `uv`_ to install those Python versions. -We recommend using the latest patch version for each Python version we support and the latest PyPy versions. -The versions beloew may not be up to date. .. sourcecode:: bash - $ 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 + $ uv tool install tox --with tox-uv + $ uv python list # check which versions you want to use + $ uv python install 3.8 3.9 3.10 3.11 3.12 3.13 + $ uv python install pypy3 + $ uvx --with tox-uv tox # that run all tests with all python versions + .. _`Tox`: https://tox.readthedocs.io/en/latest/install.html -.. _`virtualenv`: https://virtualenv.pypa.io/en/latest/installation/ -.. _`pyenv`: https://github.com/pyenv/pyenv +.. _`uv`: https://docs.astral.sh/uv/#python-versions -Test upstream applications +Test downstream applications ----------------------------------- Remember, OAuthLib is used by several 3rd party projects. If you think you @@ -186,6 +184,14 @@ submit a breaking change, confirm that other projects builds are not affected. $ make +Note be sure you are using ``uv`` as explained before with all python versions, including those from downstream libraries, to have all test cases running. + +As of 2025, additional downstreams python versions are as below: + +.. sourcecode:: bash + + $ uv python install pypy3.10 + If you add code, add tests! -------------------------------------- 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/ruff.toml b/ruff.toml index ec45dd4b..9dffa813 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,6 +5,7 @@ # When switching from ruff.toml to pyproject.toml, use the section names that # start with [tool.ruff +exclude = ["flask-dance", "requests-oauthlib", "bottle-oauthlib", "django-oauth-toolkit"] # [tool.ruff] lint.select = [ 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)),