pax_global_header00006660000000000000000000000064151550550750014522gustar00rootroot0000000000000052 comment=a4e1a3d1218b01c5806420b8f16d9308ac4adc30 jpadilla-pyjwt-a4e1a3d/000077500000000000000000000000001515505507500151415ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/.github/000077500000000000000000000000001515505507500165015ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/.github/FUNDING.yml000066400000000000000000000001021515505507500203070ustar00rootroot00000000000000# These are supported funding model platforms github: [jpadilla] jpadilla-pyjwt-a4e1a3d/.github/ISSUE_TEMPLATE.md000066400000000000000000000005241515505507500212070ustar00rootroot00000000000000Summary. ## Expected Result What you expected. ## Actual Result What happened instead. ## Reproduction Steps ```python import jwt ``` ## System Information $ python -m jwt.help ``` ``` This command is only available on PyJWT v1.6.3 and greater. Otherwise, please provide some basic information about your system. jpadilla-pyjwt-a4e1a3d/.github/ISSUE_TEMPLATE/000077500000000000000000000000001515505507500206645ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/.github/ISSUE_TEMPLATE/Bug.md000066400000000000000000000006301515505507500217220ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- Summary. ## Expected Result What you expected. ## Actual Result What happened instead. ## Reproduction Steps ```python import jwt ``` ## System Information $ python -m jwt.help ``` ``` This command is only available on PyJWT v1.6.3 and greater. Otherwise, please provide some basic information about your system. jpadilla-pyjwt-a4e1a3d/.github/ISSUE_TEMPLATE/Custom.md000066400000000000000000000002501515505507500224550ustar00rootroot00000000000000--- name: Request for Help about: Guidance on using PyJWT. --- Please refer to our [StackOverflow tag](https://stackoverflow.com/questions/tagged/pyjwt) for guidance. jpadilla-pyjwt-a4e1a3d/.github/ISSUE_TEMPLATE/Feature.md000066400000000000000000000001511515505507500225760ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- Suggest an idea for this project. jpadilla-pyjwt-a4e1a3d/.github/dependabot.yml000066400000000000000000000001651515505507500213330ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" jpadilla-pyjwt-a4e1a3d/.github/workflows/000077500000000000000000000000001515505507500205365ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/.github/workflows/enforce-changelog-entry.yml000066400000000000000000000011461515505507500257700ustar00rootroot00000000000000name: "Update unreleased section in CHANGELOG" on: pull_request: # By default labeled/unlabeled are not included in the pull_request even so we need to list out what we want types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] permissions: contents: read jobs: changelog: runs-on: ubuntu-latest steps: - uses: dangoslen/changelog-enforcer@204e7d3ef26579f4cd0fd759c57032656fdf23c7 # v3.6.1 with: skipLabels: 'Skip-Changelog,dependencies,tests' versionPattern: ^`(v\\d?\\.\\d?\\.\\d|Unreleased) <\\S+>`__ changeLogPath: CHANGELOG.rst jpadilla-pyjwt-a4e1a3d/.github/workflows/main.yml000066400000000000000000000066071515505507500222160ustar00rootroot00000000000000--- name: CI on: push: branches: ["master"] pull_request: branches: ["master"] workflow_dispatch: permissions: contents: read jobs: tests: name: "Python ${{ matrix.python-version }} on ${{ matrix.platform }}" runs-on: "${{ matrix.platform }}" env: USING_COVERAGE: '3.9' strategy: fail-fast: false matrix: platform: ["ubuntu-latest", "windows-latest"] python-version: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" - "pypy3.9" - "pypy3.10" - "pypy3.11" steps: - uses: "actions/checkout@v6" - uses: "actions/setup-python@v6" with: python-version: "${{ matrix.python-version }}" allow-prereleases: true - name: "Install dependencies" run: | python -VV python -m site python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade coverage[toml] virtualenv tox tox-gh-actions - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" # We always use a modern Python version for combining coverage to prevent # parsing errors in older versions for modern code. - uses: "actions/setup-python@v6" with: python-version: "3.9" - name: "Combine coverage" run: | set -xe python -m pip install coverage[toml] python -m coverage combine python -m coverage xml if: "contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest'" - name: "Upload coverage to Codecov" if: "contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest'" uses: "codecov/codecov-action@v5" with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} verbose: true package: name: "Build & verify package" runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v6" - uses: "actions/setup-python@v6" with: python-version: "3.9" - name: "Install build and twine" run: "python -m pip install build twine" - name: "Build package" run: "python -m build" - name: "List result" run: "ls -l dist" - name: "Check long_description" run: "python -m twine check dist/*" install-dev: strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] python-version: ["3.9", "3.13"] name: "Verify dev env (Python ${{ matrix.python-version }}, ${{ matrix.os }})" runs-on: "${{ matrix.os }}" steps: - uses: "actions/checkout@v6" - uses: "actions/setup-python@v6" with: python-version: "${{ matrix.python-version }}" - name: "Install with no extras" run: "python -m pip install ." - name: "Import package" run: "python -c 'import jwt; print(jwt.__version__)'" - name: "Install with crypto extra" run: "python -m pip install .[crypto]" - name: "Import package with crypto" run: "python -c 'import jwt; print(jwt.__version__)'" - name: "Install in dev mode" run: "python -m pip install -e .[dev]" - name: "Import package in dev mode" run: "python -c 'import jwt; print(jwt.__version__)'" jpadilla-pyjwt-a4e1a3d/.github/workflows/pypi-package.yml000066400000000000000000000033541515505507500236400ustar00rootroot00000000000000--- name: Build & maybe upload PyPI package on: push: branches: [master] tags: ["*"] pull_request: branches: [master] release: types: - published workflow_dispatch: permissions: contents: read # Needed for trusted publishing. id-token: write jobs: # Always build & lint package. build-package: name: Build & verify package runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 # Upload to Test PyPI on every commit on master. release-test-pypi: name: Publish in-dev package to test.pypi.org environment: release-test-pypi if: github.repository_owner == 'jpadilla' && github.event_name == 'push' && github.ref == 'refs/heads/master' runs-on: ubuntu-latest needs: build-package steps: - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v8 with: name: Packages path: dist - name: Upload package to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ # Upload to real PyPI on GitHub Releases. release-pypi: name: Publish released package to pypi.org environment: release-pypi if: github.repository_owner == 'jpadilla' && github.event.action == 'published' runs-on: ubuntu-latest needs: build-package steps: - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v8 with: name: Packages path: dist - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 jpadilla-pyjwt-a4e1a3d/.github/workflows/stale.yml000066400000000000000000000012771515505507500224000ustar00rootroot00000000000000name: 'Stale issue handler' on: workflow_dispatch: schedule: - cron: '30 1 * * *' permissions: issues: write pull-requests: write jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v10 id: stale with: stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days' days-before-stale: 120 days-before-close: 7 stale-issue-label: stale stale-pr-label: stale exempt-issue-labels: 'blocked,must,should,keep' - name: Print outputs run: echo ${{ join(steps.stale.outputs.*, ',') }} jpadilla-pyjwt-a4e1a3d/.gitignore000066400000000000000000000014611515505507500171330ustar00rootroot00000000000000# Created by https://www.gitignore.io ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ .venv/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ .pytest_cache .mypy_cache pip-wheel-metadata/ .venv/ .idea jpadilla-pyjwt-a4e1a3d/.pre-commit-config.yaml000066400000000000000000000024761515505507500214330ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 hooks: - id: pyupgrade name: Enforce Python 3.9+ idioms args: ["--py39-plus"] - repo: https://github.com/asottile/blacken-docs rev: 1.20.0 hooks: - id: blacken-docs args: ["--target-version=py39"] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/mgedmin/check-manifest rev: "0.51" hooks: - id: check-manifest args: [--no-build-isolation] - repo: https://github.com/abravalheri/validate-pyproject rev: "v0.25" hooks: - id: validate-pyproject - repo: https://github.com/kieran-ryan/pyprojectsort rev: "v0.4.0" hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema rev: "0.37.0" hooks: - id: check-github-workflows - id: check-readthedocs - repo: https://github.com/regebro/pyroma rev: "5.0.1" hooks: - id: pyroma - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.15.4 hooks: # Run the linter. - id: ruff args: [ --fix ] # Run the formatter. - id: ruff-format jpadilla-pyjwt-a4e1a3d/.readthedocs.yaml000066400000000000000000000004751515505507500203760ustar00rootroot00000000000000# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: "ubuntu-lts-latest" tools: python: "3.11" python: install: - method: "pip" path: "." extra_requirements: - "docs" - "crypto" sphinx: configuration: "docs/conf.py" fail_on_warning: true jpadilla-pyjwt-a4e1a3d/AUTHORS.rst000066400000000000000000000005021515505507500170150ustar00rootroot00000000000000Authors ======= ``pyjwt`` is currently written and maintained by `Jose Padilla `_. Originally written and maintained by `Jeff Lindsay `_. A full list of contributors can be found on GitHub’s `overview `_. jpadilla-pyjwt-a4e1a3d/CHANGELOG.rst000066400000000000000000001206441515505507500171710ustar00rootroot00000000000000Changelog ========= All notable changes to this project will be documented in this file. This project adheres to `Semantic Versioning `__. `Unreleased `__ ------------------------------------------------------------------------ `v2.12.1 `__ ------------------------------------------------------------------------ Fixed ~~~~~ - Add missing ``typing_extensions`` dependency for Python < 3.11 in `#1150 `__ `v2.12.0 `__ ----------------------------------------------------------------------- Fixed ~~~~~ - Annotate PyJWKSet.keys for pyright by @tamird in `#1134 `__ - Close ``HTTPError`` response to prevent ``ResourceWarning`` on Python 3.14 by @veeceey in `#1133 `__ - Do not keep ``algorithms`` dict in PyJWK instances by @akx in `#1143 `__ - Validate the crit (Critical) Header Parameter defined in RFC 7515 §4.1.11. by @dmbs335 in `GHSA-752w-5fwx-jx9f `__ - Use PyJWK algorithm when encoding without explicit algorithm in `#1148 `__ Added ~~~~~ - Docs: Add ``PyJWKClient`` API reference and document the two-tier caching system (JWK Set cache and signing key LRU cache). `v2.11.0 `__ ----------------------------------------------------------------------- Fixed ~~~~~ - Enforce ECDSA curve validation per RFC 7518 Section 3.4. - Fix build system warnings by @kurtmckee in `#1105 `__ - Validate key against allowed types for Algorithm family in `#964 `__ - Add iterator for JWKSet in `#1041 `__ - Validate `iss` claim is a string during encoding and decoding by @pachewise in `#1040 `__ - Improve typing/logic for `options` in decode, decode_complete by @pachewise in `#1045 `__ - Declare float supported type for lifespan and timeout by @nikitagashkov in `#1068 `__ - Fix ``SyntaxWarning``\s/``DeprecationWarning``\s caused by invalid escape sequences by @kurtmckee in `#1103 `__ - Development: Build a shared wheel once to speed up test suite setup times by @kurtmckee in `#1114 `__ - Development: Test type annotations across all supported Python versions, increase the strictness of the type checking, and remove the mypy pre-commit hook by @kurtmckee in `#1112 `__ Added ~~~~~ - Support Python 3.14, and test against PyPy 3.10 and 3.11 by @kurtmckee in `#1104 `__ - Development: Migrate to ``build`` to test package building in CI by @kurtmckee in `#1108 `__ - Development: Improve coverage config and eliminate unused test suite code by @kurtmckee in `#1115 `__ - Docs: Standardize CHANGELOG links to PRs by @kurtmckee in `#1110 `__ - Docs: Fix Read the Docs builds by @kurtmckee in `#1111 `__ - Docs: Add example of using leeway with nbf by @djw8605 in `#1034 `__ - Docs: Refactored docs with ``autodoc``; added ``PyJWS`` and ``jwt.algorithms`` docs by @pachewise in `#1045 `__ - Docs: Documentation improvements for "sub" and "jti" claims by @cleder in `#1088 `__ - Development: Add pyupgrade as a pre-commit hook by @kurtmckee in `#1109 `__ - Add minimum key length validation for HMAC and RSA keys (CWE-326). Warns by default via ``InsecureKeyLengthWarning`` when keys are below minimum recommended lengths per RFC 7518 Section 3.2 (HMAC) and NIST SP 800-131A (RSA). Pass ``enforce_minimum_key_length=True`` in options to ``PyJWT`` or ``PyJWS`` to raise ``InvalidKeyError`` instead. - Refactor ``PyJWT`` to own an internal ``PyJWS`` instance instead of calling global ``api_jws`` functions. `v2.10.1 `__ ----------------------------------------------------------------------- Fixed ~~~~~ - Prevent partial matching of `iss` claim by @fabianbadoi in `GHSA-75c5-xw7c-p5pm `__ `v2.10.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - Remove algorithm requirement from JWT API, instead relying on JWS API for enforcement, by @luhn in `#975 `__ - Use ``Sequence`` for parameter types rather than ``List`` where applicable by @imnotjames in `#970 `__ - Add JWK support to JWT encode by @luhn in `#979 `__ - Encoding and decoding payloads using the `none` algorithm by @jpadilla in `#c2629f6 `__ Before: .. code-block:: pycon >>> import jwt >>> jwt.encode({"payload": "abc"}, key=None, algorithm=None) After: .. code-block:: pycon >>> import jwt >>> jwt.encode({"payload": "abc"}, key=None, algorithm="none") - Added validation for 'sub' (subject) and 'jti' (JWT ID) claims in tokens by @Divan009 in `#1005 `__ - Refactor project configuration files from ``setup.cfg`` to ``pyproject.toml`` by @cleder in `#995 `__ - Ruff linter and formatter changes by @gagandeepp in `#1001 `__ - Drop support for Python 3.8 (EOL) by @kkirsche in `#1007 `__ Fixed ~~~~~ - Encode EC keys with a fixed bit length by @etianen in `#990 `__ - Add an RTD config file to resolve Read the Docs build failures by @kurtmckee in `#977 `__ - Docs: Update ``iat`` exception docs by @pachewise in `#974 `__ - Docs: Fix ``decode_complete`` scope and algorithms by @RbnRncn in `#982 `__ - Fix doctest for ``docs/usage.rst`` by @pachewise in `#986 `__ - Fix ``test_utils.py`` not to xfail by @pachewise in `#987 `__ - Docs: Correct `jwt.decode` audience param doc expression by @peter279k in `#994 `__ Added ~~~~~ - Add support for python 3.13 by @hugovk in `#972 `__ - Create SECURITY.md by @auvipy and @jpadilla in `#973 `__ - Docs: Add PS256 encoding and decoding usage by @peter279k in `#992 `__ - Docs: Add API docs for PyJWK by @luhn in `#980 `__ - Docs: Add EdDSA algorithm encoding/decoding usage by @peter279k in `#993 `__ - Include checkers and linters for ``pyproject.toml`` in ``pre-commit`` by @cleder in `#1002 `__ - Docs: Add ES256 decoding usage by @Gautam-Hegde in `#1003 `__ `v2.9.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - Drop support for Python 3.7 (EOL) by @hugovk in `#910 `__ - Allow JWT issuer claim validation to accept a list of strings too by @mattpollak in `#913 `__ Fixed ~~~~~ - Fix unnecessary string concatenation by @sirosen in `#904 `__ - Fix docs for ``jwt.decode_complete`` to include ``strict_aud`` option by @woodruffw in `#923 `__ - Fix docs step by @jpadilla in `#950 `__ - Fix: Remove an unused variable from example code block by @kenkoooo in `#958 `__ Added ~~~~~ - Add support for Python 3.12 by @hugovk in `#910 `__ - Improve performance of ``is_ssh_key`` + add unit test by @bdraco in `#940 `__ - Allow ``jwt.decode()`` to accept a PyJWK object by @luhn in `#886 `__ - Make ``algorithm_name`` attribute available on PyJWK by @luhn in `#886 `__ - Raise ``InvalidKeyError`` on invalid PEM keys to be compatible with cryptography 42.x.x by @CollinEMac in `#952 `__ - Raise an exception when required cryptography dependency is missing by @tobloef in ``__ `v2.8.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - Update python version test matrix by @auvipy in `#895 `__ Fixed ~~~~~ Added ~~~~~ - Add ``strict_aud`` as an option to ``jwt.decode`` by @woodruffw in `#902 `__ - Export PyJWKClientConnectionError class by @daviddavis in `#887 `__ - Allows passing of ssl.SSLContext to PyJWKClient by @juur in `#891 `__ `v2.7.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - Changed the error message when the token audience doesn't match the expected audience by @irdkwmnsb `#809 `__ - Improve error messages when cryptography isn't installed by @Viicos in `#846 `__ - Make `Algorithm` an abstract base class by @Viicos in `#845 `__ - ignore invalid keys in a jwks by @timw6n in `#863 `__ Fixed ~~~~~ - Add classifier for Python 3.11 by @eseifert in `#818 `__ - Fix ``_validate_iat`` validation by @Viicos in `#847 `__ - fix: use datetime.datetime.timestamp function to have a milliseconds by @daillouf `#821 `__ - docs: correct mistake in the changelog about verify param by @gbillig in `#866 `__ Added ~~~~~ - Add ``compute_hash_digest`` as a method of ``Algorithm`` objects, which uses the underlying hash algorithm to compute a digest. If there is no appropriate hash algorithm, a ``NotImplementedError`` will be raised in `#775 `__ - Add optional ``headers`` argument to ``PyJWKClient``. If provided, the headers will be included in requests that the client uses when fetching the JWK set by @thundercat1 in `#823 `__ - Add PyJWT._{de,en}code_payload hooks by @akx in `#829 `__ - Add `sort_headers` parameter to `api_jwt.encode` by @evroon in `#832 `__ - Make mypy configuration stricter and improve typing by @akx in `#830 `__ - Add more types by @Viicos in `#843 `__ - Add a timeout for PyJWKClient requests by @daviddavis in `#875 `__ - Add client connection error exception by @daviddavis in `#876 `__ - Add complete types to take all allowed keys into account by @Viicos in `#873 `__ - Add `as_dict` option to `Algorithm.to_jwk` by @fluxth in `#881 `__ `v2.6.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - bump up cryptography >= 3.4.0 by @jpadilla in `#807 `__ - Remove `types-cryptography` from `crypto` extra by @lautat in `#805 `__ Fixed ~~~~~ - Invalidate token on the exact second the token expires `#797 `__ - fix: version 2.5.0 heading typo by @c0state in `#803 `__ Added ~~~~~ - Adding validation for `issued_at` when `iat > (now + leeway)` as `ImmatureSignatureError` by @sriharan16 in `#794 `__ `v2.5.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - Skip keys with incompatible alg when loading JWKSet by @DaGuich in `#762 `__ - Remove support for python3.6 by @sirosen in `#777 `__ - Emit a deprecation warning for unsupported kwargs by @sirosen in `#776 `__ - Remove redundant wheel dep from pyproject.toml by @mgorny in `#765 `__ - Do not fail when an unusable key occurs by @DaGuich in `#762 `__ - Update audience typing by @JulianMaurin in `#782 `__ - Improve PyJWKSet error accuracy by @JulianMaurin in `#786 `__ - Mypy as pre-commit check + api_jws typing by @JulianMaurin in `#787 `__ Fixed ~~~~~ - Adjust expected exceptions in option merging tests for PyPy3 by @mgorny in `#763 `__ - Fixes for pyright on strict mode by @brandon-leapyear in `#747 `__ - docs: fix simple typo, iinstance -> isinstance by @timgates42 in `#774 `__ - Fix typo: priot -> prior by @jdufresne in `#780 `__ - Fix for headers disorder issue by @kadabusha in `#721 `__ Added ~~~~~ - Add to_jwk static method to ECAlgorithm by @leonsmith in `#732 `__ - Expose get_algorithm_by_name as new method by @sirosen in `#773 `__ - Add type hints to jwt/help.py and add missing types dependency by @kkirsche in `#784 `__ - Add cacheing functionality for JWK set by @wuhaoyujerry in `#781 `__ `v2.4.0 `__ ----------------------------------------------------------------------- Security ~~~~~~~~ - [CVE-2022-29217] Prevent key confusion through non-blocklisted public key formats. https://github.com/jpadilla/pyjwt/security/advisories/GHSA-ffqj-6fqr-9h24 Changed ~~~~~~~ - Explicit check the key for ECAlgorithm by @estin in `#713 `__ - Raise DeprecationWarning for jwt.decode(verify=...) by @akx in `#742 `__ Fixed ~~~~~ - Don't use implicit optionals by @rekyungmin in `#705 `__ - documentation fix: show correct scope for decode_complete() by @sseering in `#661 `__ - fix: Update copyright information by @kkirsche in `#729 `__ - Don't mutate options dictionary in .decode_complete() by @akx in `#743 `__ Added ~~~~~ - Add support for Python 3.10 by @hugovk in `#699 `__ - api_jwk: Add PyJWKSet.__getitem__ by @woodruffw in `#725 `__ - Update usage.rst by @guneybilen in `#727 `__ - Docs: mention performance reasons for reusing RSAPrivateKey when encoding by @dmahr1 in `#734 `__ - Fixed typo in usage.rst by @israelabraham in `#738 `__ - Add detached payload support for JWS encoding and decoding by @fviard in `#723 `__ - Replace various string interpolations with f-strings by @akx in `#744 `__ - Update CHANGELOG.rst by @hipertracker in `#751 `__ `v2.3.0 `__ ----------------------------------------------------------------------- Fixed ~~~~~ - Revert "Remove arbitrary kwargs." `#701 `__ Added ~~~~~ - Add exception chaining `#702 `__ `v2.2.0 `__ ----------------------------------------------------------------------- Changed ~~~~~~~ - Remove arbitrary kwargs. `#657 `__ - Use timezone package as Python 3.5+ is required. `#694 `__ Fixed ~~~~~ - Assume JWK without the "use" claim is valid for signing as per RFC7517 `#668 `__ - Prefer `headers["alg"]` to `algorithm` in `jwt.encode()`. `#673 `__ - Fix aud validation to support {'aud': null} case. `#670 `__ - Make `typ` optional in JWT to be compliant with RFC7519. `#644 `__ - Remove upper bound on cryptography version. `#693 `__ Added ~~~~~ - Add support for Ed448/EdDSA. `#675 `__ `v2.1.0 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Allow claims validation without making JWT signature validation mandatory. `#608 `__ Fixed ~~~~~ - Remove padding from JWK test data. `#628 `__ - Make `kty` mandatory in JWK to be compliant with RFC7517. `#624 `__ - Allow JWK without `alg` to be compliant with RFC7517. `#624 `__ - Allow to verify with private key on ECAlgorithm, as well as on Ed25519Algorithm. `#645 `__ Added ~~~~~ - Add caching by default to PyJWKClient `#611 `__ - Add missing exceptions.InvalidKeyError to jwt module __init__ imports `#620 `__ - Add support for ES256K algorithm `#629 `__ - Add `from_jwk()` to Ed25519Algorithm `#621 `__ - Add `to_jwk()` to Ed25519Algorithm `#643 `__ - Export `PyJWK` and `PyJWKSet` `#652 `__ `v2.0.1 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Rename CHANGELOG.md to CHANGELOG.rst and include in docs `#597 `__ Fixed ~~~~~ - Fix `from_jwk()` for all algorithms `#598 `__ Added ~~~~~ `v2.0.0 `__ -------------------------------------------------------------------- Changed ~~~~~~~ Drop support for Python 2 and Python 3.0-3.5 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Python 3.5 is EOL so we decide to drop its support. Version ``1.7.1`` is the last one supporting Python 3.0-3.5. Require cryptography >= 3 ^^^^^^^^^^^^^^^^^^^^^^^^^ Drop support for PyCrypto and ECDSA ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We've kept this around for a long time, mostly for environments that didn't allow installing cryptography. Drop CLI ^^^^^^^^ Dropped the included cli entry point. Improve typings ^^^^^^^^^^^^^^^ We no longer need to use mypy Python 2 compatibility mode (comments) ``jwt.encode(...)`` return type ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Tokens are returned as string instead of a byte string Dropped deprecated errors ^^^^^^^^^^^^^^^^^^^^^^^^^ Removed ``ExpiredSignature``, ``InvalidAudience``, and ``InvalidIssuer``. Use ``ExpiredSignatureError``, ``InvalidAudienceError``, and ``InvalidIssuerError`` instead. Dropped deprecated ``verify_expiration`` param in ``jwt.decode(...)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use ``jwt.decode(encoded, key, algorithms=["HS256"], options={"verify_exp": False})`` instead. Dropped deprecated ``verify`` param in ``jwt.decode(...)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use ``jwt.decode(encoded, key, options={"verify_signature": False})`` instead. Require explicit ``algorithms`` in ``jwt.decode(...)`` by default ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example: ``jwt.decode(encoded, key, algorithms=["HS256"])``. Dropped deprecated ``require_*`` options in ``jwt.decode(...)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For example, instead of ``jwt.decode(encoded, key, algorithms=["HS256"], options={"require_exp": True})``, use ``jwt.decode(encoded, key, algorithms=["HS256"], options={"require": ["exp"]})``. And the old v1.x syntax ``jwt.decode(token, verify=False)`` is now: ``jwt.decode(jwt=token, key='secret', algorithms=['HS256'], options={"verify_signature": False})`` Added ~~~~~ Introduce better experience for JWKs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Introduce ``PyJWK``, ``PyJWKSet``, and ``PyJWKClient``. .. code:: python import jwt from jwt import PyJWKClient token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) signing_key = jwks_client.get_signing_key_from_jwt(token) data = jwt.decode( token, signing_key.key, algorithms=["RS256"], audience="https://expenses-api", options={"verify_exp": False}, ) print(data) Support for JWKs containing ECDSA keys ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Add support for Ed25519 / EdDSA ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pull Requests ~~~~~~~~~~~~~ - Add PyPy3 to the test matrix (#550) by @jdufresne - Require tweak (#280) by @psafont - Decode return type is dict[str, Any] (#393) by @jacopofar - Fix linter error in test\_cli (#414) by @jaraco - Run mypy with tox (#421) by @jpadilla - Document (and prefer) pyjwt[crypto] req format (#426) by @gthb - Correct type for json\_encoder argument (#438) by @jdufresne - Prefer https:// links where available (#439) by @jdufresne - Pass python\_requires argument to setuptools (#440) by @jdufresne - Rename [wheel] section to [bdist\_wheel] as the former is legacy (#441) by @jdufresne - Remove setup.py test command in favor of pytest and tox (#442) by @jdufresne - Fix mypy errors (#449) by @jpadilla - DX Tweaks (#450) by @jpadilla - Add support of python 3.8 (#452) by @Djailla - Fix 406 (#454) by @justinbaur - Add support for Ed25519 / EdDSA, with unit tests (#455) by @Someguy123 - Remove Python 2.7 compatibility (#457) by @Djailla - Fix simple typo: encododed -> encoded (#462) by @timgates42 - Enhance tracebacks. (#477) by @JulienPalard - Simplify ``python_requires`` (#478) by @michael-k - Document top-level .encode and .decode to close #459 (#482) by @dimaqq - Improve documentation for audience usage (#484) by @CorreyL - Correct README on how to run tests locally (#489) by @jdufresne - Fix ``tox -e lint`` warnings and errors (#490) by @jdufresne - Run pyupgrade across project to use modern Python 3 conventions (#491) by @jdufresne - Add Python-3-only trove classifier and remove "universal" from wheel (#492) by @jdufresne - Emit warnings about user code, not pyjwt code (#494) by @mgedmin - Move setup information to declarative setup.cfg (#495) by @jdufresne - CLI options for verifying audience and issuer (#496) by @GeoffRichards - Specify the target Python version for mypy (#497) by @jdufresne - Remove unnecessary compatibility shims for Python 2 (#498) by @jdufresne - Setup GH Actions (#499) by @jpadilla - Implementation of ECAlgorithm.from\_jwk (#500) by @jpadilla - Remove cli entry point (#501) by @jpadilla - Expose InvalidKeyError on jwt module (#503) by @russellcardullo - Avoid loading token twice in pyjwt.decode (#506) by @CaselIT - Default links to stable version of documentation (#508) by @salcedo - Update README.md badges (#510) by @jpadilla - Introduce better experience for JWKs (#511) by @jpadilla - Fix tox conditional extras (#512) by @jpadilla - Return tokens as string not bytes (#513) by @jpadilla - Drop support for legacy contrib algorithms (#514) by @jpadilla - Drop deprecation warnings (#515) by @jpadilla - Update Auth0 sponsorship link (#519) by @Sambego - Update return type for jwt.encode (#521) by @moomoolive - Run tests against Python 3.9 and add trove classifier (#522) by @michael-k - Removed redundant ``default_backend()`` (#523) by @rohitkg98 - Documents how to use private keys with passphrases (#525) by @rayluo - Update version to 2.0.0a1 (#528) by @jpadilla - Fix usage example (#530) by @nijel - add EdDSA to docs (#531) by @CircleOnCircles - Remove support for EOL Python 3.5 (#532) by @jdufresne - Upgrade to isort 5 and adjust configurations (#533) by @jdufresne - Remove unused argument "verify" from PyJWS.decode() (#534) by @jdufresne - Update typing syntax and usage for Python 3.6+ (#535) by @jdufresne - Run pyupgrade to simplify code and use Python 3.6 syntax (#536) by @jdufresne - Drop unknown pytest config option: strict (#537) by @jdufresne - Upgrade black version and usage (#538) by @jdufresne - Remove "Command line" sections from docs (#539) by @jdufresne - Use existing key\_path() utility function throughout tests (#540) by @jdufresne - Replace force\_bytes()/force\_unicode() in tests with literals (#541) by @jdufresne - Remove unnecessary Unicode decoding before json.loads() (#542) by @jdufresne - Remove unnecessary force\_bytes() calls prior to base64url\_decode() (#543) by @jdufresne - Remove deprecated arguments from docs (#544) by @jdufresne - Update code blocks in docs (#545) by @jdufresne - Refactor jwt/jwks\_client.py without requests dependency (#546) by @jdufresne - Tighten bytes/str boundaries and remove unnecessary coercing (#547) by @jdufresne - Replace codecs.open() with builtin open() (#548) by @jdufresne - Replace int\_from\_bytes() with builtin int.from\_bytes() (#549) by @jdufresne - Enforce .encode() return type using mypy (#551) by @jdufresne - Prefer direct indexing over options.get() (#552) by @jdufresne - Cleanup "noqa" comments (#553) by @jdufresne - Replace merge\_dict() with builtin dict unpacking generalizations (#555) by @jdufresne - Do not mutate the input payload in PyJWT.encode() (#557) by @jdufresne - Use direct indexing in PyJWKClient.get\_signing\_key\_from\_jwt() (#558) by @jdufresne - Split PyJWT/PyJWS classes to tighten type interfaces (#559) by @jdufresne - Simplify mocked\_response test utility function (#560) by @jdufresne - Autoupdate pre-commit hooks and apply them (#561) by @jdufresne - Remove unused argument "payload" from PyJWS.\ *verify*\ signature() (#562) by @jdufresne - Add utility functions to assist test skipping (#563) by @jdufresne - Type hint jwt.utils module (#564) by @jdufresne - Prefer ModuleNotFoundError over ImportError (#565) by @jdufresne - Fix tox "manifest" environment to pass (#566) by @jdufresne - Fix tox "docs" environment to pass (#567) by @jdufresne - Simplify black configuration to be closer to upstream defaults (#568) by @jdufresne - Use generator expressions (#569) by @jdufresne - Simplify from\_base64url\_uint() (#570) by @jdufresne - Drop lint environment from GitHub actions in favor of pre-commit.ci (#571) by @jdufresne - [pre-commit.ci] pre-commit autoupdate (#572) - Simplify tox configuration (#573) by @jdufresne - Combine identical test functions using pytest.mark.parametrize() (#574) by @jdufresne - Complete type hinting of jwks\_client.py (#578) by @jdufresne `v1.7.1 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Update test dependencies with pinned ranges - Fix pytest deprecation warnings `v1.7.0 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Remove CRLF line endings `#353 `__ Fixed ~~~~~ - Update usage.rst `#360 `__ Added ~~~~~ - Support for Python 3.7 `#375 `__ `#379 `__ `#384 `__ `v1.6.4 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Reverse an unintentional breaking API change to .decode() `#352 `__ `v1.6.3 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - All exceptions inherit from PyJWTError `#340 `__ Added ~~~~~ - Add type hints `#344 `__ - Add help module `7ca41e `__ Docs ~~~~ - Added section to usage docs for jwt.get\_unverified\_header() `#350 `__ - Update legacy instructions for using pycrypto `#337 `__ `v1.6.1 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Audience parameter throws ``InvalidAudienceError`` when application does not specify an audience, but the token does. `#336 `__ `v1.6.0 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Dropped support for python 2.6 and 3.3 `#301 `__ - An invalid signature now raises an ``InvalidSignatureError`` instead of ``DecodeError`` `#316 `__ Fixed ~~~~~ - Fix over-eager fallback to stdin `#304 `__ Added ~~~~~ - Audience parameter now supports iterables `#306 `__ `v1.5.3 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Increase required version of the cryptography package to >=1.4.0. Fixed ~~~~~ - Remove uses of deprecated functions from the cryptography package. - Warn about missing ``algorithms`` param to ``decode()`` only when ``verify`` param is ``True`` `#281 `__ `v1.5.2 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Ensure correct arguments order in decode super call `7c1e61d `__ `v1.5.1 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Change optparse for argparse. `#238 `__ Fixed ~~~~~ - Guard against PKCS1 PEM encoded public keys `#277 `__ - Add deprecation warning when decoding without specifying ``algorithms`` `#277 `__ - Improve deprecation messages `#270 `__ - PyJWT.decode: move verify param into options `#271 `__ Added ~~~~~ - Support for Python 3.6 `#262 `__ - Expose jwt.InvalidAlgorithmError `#264 `__ `v1.5.0 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Add support for ECDSA public keys in RFC 4253 (OpenSSH) format `#244 `__ - Renamed commandline script ``jwt`` to ``jwt-cli`` to avoid issues with the script clobbering the ``jwt`` module in some circumstances. `#187 `__ - Better error messages when using an algorithm that requires the cryptography package, but it isn't available `#230 `__ - Tokens with future 'iat' values are no longer rejected `#190 `__ - Non-numeric 'iat' values now raise InvalidIssuedAtError instead of DecodeError - Remove rejection of future 'iat' claims `#252 `__ Fixed ~~~~~ - Add back 'ES512' for backward compatibility (for now) `#225 `__ - Fix incorrectly named ECDSA algorithm `#219 `__ - Fix rpm build `#196 `__ Added ~~~~~ - Add JWK support for HMAC and RSA keys `#202 `__ `v1.4.2 `__ -------------------------------------------------------------------- Fixed ~~~~~ - A PEM-formatted key encoded as bytes could cause a ``TypeError`` to be raised `#213 `__ `v1.4.1 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Newer versions of Pytest could not detect warnings properly `#182 `__ - Non-string 'kid' value now raises ``InvalidTokenError`` `#174 `__ - ``jwt.decode(None)`` now gracefully fails with ``InvalidTokenError`` `#183 `__ `v1.4 `__ ------------------------------------------------------------------ Fixed ~~~~~ - Exclude Python cache files from PyPI releases. Added ~~~~~ - Added new options to require certain claims (require\_nbf, require\_iat, require\_exp) and raise ``MissingRequiredClaimError`` if they are not present. - If ``audience=`` or ``issuer=`` is specified but the claim is not present, ``MissingRequiredClaimError`` is now raised instead of ``InvalidAudienceError`` and ``InvalidIssuerError`` `v1.3 `__ ------------------------------------------------------------------ Fixed ~~~~~ - ECDSA (ES256, ES384, ES512) signatures are now being properly serialized `#158 `__ - RSA-PSS (PS256, PS384, PS512) signatures now use the proper salt length for PSS padding. `#163 `__ Added ~~~~~ - Added a new ``jwt.get_unverified_header()`` to parse and return the header portion of a token prior to signature verification. Removed ~~~~~~~ - Python 3.2 is no longer a supported platform. This version of Python is rarely used. Users affected by this should upgrade to 3.3+. `v1.2.0 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Added back ``verify_expiration=`` argument to ``jwt.decode()`` that was erroneously removed in `v1.1.0 `__. Changed ~~~~~~~ - Refactored JWS-specific logic out of PyJWT and into PyJWS superclass. `#141 `__ Deprecated ~~~~~~~~~~ - ``verify_expiration=`` argument to ``jwt.decode()`` is now deprecated and will be removed in a future version. Use the ``option=`` argument instead. `v1.1.0 `__ -------------------------------------------------------------------- Added ~~~~~ - Added support for PS256, PS384, and PS512 algorithms. `#132 `__ - Added flexible and complete verification options during decode. `#131 `__ - Added this CHANGELOG.md file. Deprecated ~~~~~~~~~~ - Deprecated usage of the .decode(..., verify=False) parameter. Fixed ~~~~~ - Fixed command line encoding. `#128 `__ `v1.0.1 `__ -------------------------------------------------------------------- Fixed ~~~~~ - Include jwt/contrib' and jwt/contrib/algorithms\` in setup.py so that they will actually be included when installing. `882524d `__ - Fix bin/jwt after removing jwt.header(). `bd57b02 `__ `v1.0.0 `__ -------------------------------------------------------------------- Changed ~~~~~~~ - Moved ``jwt.api.header`` out of the public API. `#85 `__ - Added README details how to extract public / private keys from an x509 certificate. `#100 `__ - Refactor api.py functions into an object (``PyJWT``). `#101 `__ - Added support for PyCrypto and ecdsa when cryptography isn't available. `#101 `__ Fixed ~~~~~ - Fixed a security vulnerability where ``alg=None`` header could bypass signature verification. `#109 `__ - Fixed a security vulnerability by adding support for a whitelist of allowed ``alg`` values ``jwt.decode(algorithms=[])``. `#110 `__ jpadilla-pyjwt-a4e1a3d/CODE_OF_CONDUCT.md000066400000000000000000000063101515505507500177400ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@jpadilla.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] [homepage]: https://www.contributor-covenant.org/ [version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html jpadilla-pyjwt-a4e1a3d/LICENSE000066400000000000000000000020751515505507500161520ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2022 José Padilla Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. jpadilla-pyjwt-a4e1a3d/MANIFEST.in000066400000000000000000000005651515505507500167050ustar00rootroot00000000000000include .pre-commit-config.yaml include .readthedocs.yaml include CODE_OF_CONDUCT.md include AUTHORS.rst include CHANGELOG.rst include LICENSE include README.rst include SECURITY.md include ruff.toml include tox.ini include jwt/py.typed graft docs graft tests exclude codecov.yml recursive-exclude docs/_build * recursive-exclude * *.py[co] recursive-exclude * __pycache__ jpadilla-pyjwt-a4e1a3d/README.rst000066400000000000000000000043511515505507500166330ustar00rootroot00000000000000PyJWT ===== .. image:: https://github.com/jpadilla/pyjwt/workflows/CI/badge.svg :target: https://github.com/jpadilla/pyjwt/actions?query=workflow%3ACI .. image:: https://img.shields.io/pypi/v/pyjwt.svg :target: https://pypi.python.org/pypi/pyjwt .. image:: https://codecov.io/gh/jpadilla/pyjwt/branch/master/graph/badge.svg :target: https://codecov.io/gh/jpadilla/pyjwt .. image:: https://readthedocs.org/projects/pyjwt/badge/?version=stable :target: https://pyjwt.readthedocs.io/en/stable/ A Python implementation of `RFC 7519 `_. Original implementation was written by `@progrium `_. Sponsor ------- .. |auth0-logo| image:: https://github.com/user-attachments/assets/ee98379e-ee76-4bcb-943a-e25c4ea6d174 :width: 160px +--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |auth0-logo| | If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at `auth0.com/signup `_. | +--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Installing ---------- Install with **pip**: .. code-block:: console $ pip install PyJWT Usage ----- .. code-block:: pycon >>> import jwt >>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") >>> print(encoded) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg >>> jwt.decode(encoded, "secret", algorithms=["HS256"]) {'some': 'payload'} Documentation ------------- View the full docs online at https://pyjwt.readthedocs.io/en/stable/ Tests ----- You can run tests from the project root after cloning with: .. code-block:: console $ tox jpadilla-pyjwt-a4e1a3d/SECURITY.md000066400000000000000000000014111515505507500167270ustar00rootroot00000000000000# Security Policy ## Supported Versions The following versions of this project are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 2.10.x | :white_check_mark: | | 2.9 | :x: | ## Reporting a Vulnerability In order for the vulnerability reports to reach maintainers as soon as possible, the preferred way is to use the "Report a vulnerability" button under the "Security" tab of the associated GitHub project. This creates a private communication channel between the reporter and the maintainers. If you are absolutely unable to or have strong reasons not to use GitHub's vulnerability reporting workflow, please reach out to [security@jpadilla.com](mailto:security@jpadilla.com). jpadilla-pyjwt-a4e1a3d/codecov.yml000066400000000000000000000003141515505507500173040ustar00rootroot00000000000000comment: false coverage: status: patch: default: target: "100" project: default: target: "100" threshold: "10%" jpadilla-pyjwt-a4e1a3d/docs/000077500000000000000000000000001515505507500160715ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/docs/Makefile000066400000000000000000000127131515505507500175350ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -n -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/structlog.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/structlog.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/structlog" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/structlog" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." jpadilla-pyjwt-a4e1a3d/docs/_static/000077500000000000000000000000001515505507500175175ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/docs/_static/theme_overrides.css000066400000000000000000000005531515505507500234200ustar00rootroot00000000000000img.auth0-logo { max-width: 45px !important; } @media screen and (min-width: 767px) { .wy-table-responsive table td { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } } jpadilla-pyjwt-a4e1a3d/docs/algorithms.rst000066400000000000000000000103431515505507500207750ustar00rootroot00000000000000Digital Signature Algorithms ============================ The JWT specification supports several algorithms for cryptographic signing. This library currently supports: * HS256 - HMAC using SHA-256 hash algorithm (default) * HS384 - HMAC using SHA-384 hash algorithm * HS512 - HMAC using SHA-512 hash algorithm * ES256 - ECDSA signature algorithm using SHA-256 hash algorithm * ES256K - ECDSA signature algorithm with secp256k1 curve using SHA-256 hash algorithm * ES384 - ECDSA signature algorithm using SHA-384 hash algorithm * ES512 - ECDSA signature algorithm using SHA-512 hash algorithm * RS256 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm * RS384 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm * RS512 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm * PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256 * PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384 * PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512 * EdDSA - Both Ed25519 signature using SHA-512 and Ed448 signature using SHA-3 are supported. Ed25519 and Ed448 provide 128-bit and 224-bit security respectively. Minimum Key Length Requirements ------------------------------- PyJWT enforces minimum key lengths per industry standards. Keys below these minimums will trigger an ``InsecureKeyLengthWarning`` by default, or raise ``InvalidKeyError`` if ``enforce_minimum_key_length`` is enabled. .. list-table:: :header-rows: 1 :widths: auto * - Algorithm - Minimum Key Length - Standard * - HS256 - 32 bytes (256 bits) - RFC 7518 Section 3.2 * - HS384 - 48 bytes (384 bits) - RFC 7518 Section 3.2 * - HS512 - 64 bytes (512 bits) - RFC 7518 Section 3.2 * - RS256/384/512 - 2048 bits - NIST SP 800-131A * - PS256/384/512 - 2048 bits - NIST SP 800-131A See :ref:`key-length-validation` for configuration details. Asymmetric (Public-key) Algorithms ---------------------------------- Usage of RSA (RS\*) and EC (EC\*) algorithms require a basic understanding of how public-key cryptography is used with regards to digital signatures. If you are unfamiliar, you may want to read `this article `_. When using the RSASSA-PKCS1-v1_5 algorithms, the `key` argument in both ``jwt.encode()`` and ``jwt.decode()`` (``"secret"`` in the examples) is expected to be either an RSA public or private key in PEM or SSH format. The type of key (private or public) depends on whether you are signing or verifying a token. When using the ECDSA algorithms, the ``key`` argument is expected to be an Elliptic Curve public or private key in PEM format. The type of key (private or public) depends on whether you are signing or verifying. Specifying an Algorithm ----------------------- You can specify which algorithm you would like to use to sign the JWT by using the `algorithm` parameter: .. code-block:: pycon >>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS512") >>> print(encoded) eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.WTzLzFO079PduJiFIyzrOah54YaM8qoxH9fLMQoQhKtw3_fMGjImIOokijDkXVbyfBqhMo2GCNu4w9v7UXvnpA When decoding, you can also specify which algorithms you would like to permit when validating the JWT by using the `algorithms` parameter which takes a list of allowed algorithms: .. code-block:: pycon >>> jwt.decode(encoded, "secret", algorithms=["HS512", "HS256"]) {'some': 'payload'} In the above case, if the JWT has any value for its alg header other than HS512 or HS256, the claim will be rejected with an ``InvalidAlgorithmError``. .. warning:: Do **not** compute the ``algorithms`` parameter based on the ``alg`` from the token itself, or on any other data that an attacker may be able to influence, as that might expose you to various vulnerabilities (see `RFC 8725 §2.1 `_). Instead, either hard-code a fixed value for ``algorithms``, or configure it in the same place you configure the ``key``. Make sure not to mix symmetric and asymmetric algorithms that interpret the ``key`` in different ways (e.g. HS\* and RS\*). jpadilla-pyjwt-a4e1a3d/docs/api.rst000066400000000000000000000030451515505507500173760ustar00rootroot00000000000000API Reference ============= .. module:: jwt .. autofunction:: encode(payload, key, algorithm="HS256", headers=None, json_encoder=None) -> str .. autofunction:: decode(jwt, key="", algorithms=None, options=None, audience=None, issuer=None, leeway=0) -> dict[str, typing.Any] .. autofunction:: decode_complete(jwt, key="", algorithms=None, options=None, verify=None, detached_payload=None, audience=None, issuer=None, subject=None, leeway=0) -> dict[str, typing.Any] .. autoclass:: PyJWT :class-doc-from: init :members: .. autoclass:: PyJWK :class-doc-from: init :members: .. property:: algorithm_name :type: str The name of the algorithm used by the key. .. property:: Algorithm The :py:class:`Algorithm` class associated with the key. .. autoclass:: PyJWKSet :members: .. autoclass:: PyJWKClient :class-doc-from: init :members: .. note:: TODO: Finish documenting PyJWS class .. module:: jwt.api_jws .. autoclass:: jwt.api_jws.PyJWS :members: Algorithms ---------- .. automodule:: jwt.algorithms :members: Algorithm, AllowedPrivateKeys, AllowedPublicKeys Types ---------- .. module:: jwt.types :synopsis: Type validation used in the JWT API .. autoclass:: jwt.types.SigOptions :members: :undoc-members: .. autoclass:: jwt.types.Options :members: :undoc-members: Warnings ---------- .. automodule:: jwt.warnings :members: :show-inheritance: Exceptions ---------- .. automodule:: jwt.exceptions :members: :inherited-members: :show-inheritance: jpadilla-pyjwt-a4e1a3d/docs/changelog.rst000066400000000000000000000000361515505507500205510ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst jpadilla-pyjwt-a4e1a3d/docs/conf.py000066400000000000000000000102271515505507500173720ustar00rootroot00000000000000import os import re def read(*parts) -> str: """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, *parts), encoding="utf-8") as f: return f.read() def find_version(*file_paths) -> str: """ Build a path from *file_paths* and search for a ``__version__`` string inside. """ version_file = read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") # -- General configuration ------------------------------------------------ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "PyJWT" copyright = "2015-2022, José Padilla" author = "José Padilla" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = find_version("../jwt/__init__.py") # The short X.Y version. version = release.rsplit(".", 1)[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. # language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # Suppress nitpick warnings for type aliases that Sphinx cannot resolve. nitpick_ignore = [ ("py:class", "JWKDict"), ("py:class", "PublicKeyTypes"), ("py:class", "PrivateKeyTypes"), ] # Intersphinx extension. intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "cryptography": ("https://cryptography.io/en/latest/", None), } # Hack for allowing aliases within TYPE_CHECKING to be documented os.environ["SPHINX_BUILD"] = "1" # -- Options for HTML output ---------------------------------------------- html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # These paths are either relative to html_static_path # or fully qualified paths (eg. https://...) html_css_files = [ "theme_overrides.css", ] # Output file base name for HTML help builder. htmlhelp_basename = "PyJWTdoc" # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "pyjwt", "PyJWT Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "PyJWT", "PyJWT Documentation", author, "PyJWT", "One line description of project.", "Miscellaneous", ) ] jpadilla-pyjwt-a4e1a3d/docs/faq.rst000066400000000000000000000011551515505507500173740ustar00rootroot00000000000000Frequently Asked Questions ========================== How can I extract a public / private key from a x509 certificate? ----------------------------------------------------------------- The ``load_pem_x509_certificate()`` function from ``cryptography`` can be used to extract the public or private keys from a x509 certificate in PEM format. .. code-block:: python from cryptography.x509 import load_pem_x509_certificate cert_str = b"-----BEGIN CERTIFICATE-----MIIDETCCAfm..." cert_obj = load_pem_x509_certificate(cert_str) public_key = cert_obj.public_key() private_key = cert_obj.private_key() jpadilla-pyjwt-a4e1a3d/docs/index.rst000066400000000000000000000034411515505507500177340ustar00rootroot00000000000000Welcome to ``PyJWT`` ==================== ``PyJWT`` is a Python library which allows you to encode and decode JSON Web Tokens (JWT). JWT is an open, industry-standard (`RFC 7519`_) for representing claims securely between two parties. Sponsor ------- .. |auth0-logo| image:: https://github.com/user-attachments/assets/ee98379e-ee76-4bcb-943a-e25c4ea6d174 :width: 160px +--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |auth0-logo| | If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at `auth0.com/signup `_. | +--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Installation ------------ You can install ``pyjwt`` with ``pip``: .. code-block:: console $ pip install pyjwt See :doc:`Installation ` for more information. Example Usage ------------- .. doctest:: >>> import jwt >>> encoded_jwt = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") >>> jwt.decode(encoded_jwt, "secret", algorithms=["HS256"]) {'some': 'payload'} See :doc:`Usage Examples ` for more examples. Index ----- .. toctree:: :maxdepth: 2 installation usage faq algorithms api changelog .. _`RFC 7519`: https://tools.ietf.org/html/rfc7519 jpadilla-pyjwt-a4e1a3d/docs/installation.rst000066400000000000000000000014271515505507500213300ustar00rootroot00000000000000Installation ============ You can install ``PyJWT`` with ``pip``: .. code-block:: console $ pip install pyjwt .. _installation_cryptography: Cryptographic Dependencies (Optional) ------------------------------------- If you are planning on encoding or decoding tokens using certain digital signature algorithms (like RSA or ECDSA), you will need to install the cryptography_ library. This can be installed explicitly, or as a required extra in the ``pyjwt`` requirement: .. code-block:: console $ pip install pyjwt[crypto] The ``pyjwt[crypto]`` format is recommended in requirements files in projects using ``PyJWT``, as a separate ``cryptography`` requirement line may later be mistaken for an unused requirement and removed. .. _`cryptography`: https://cryptography.io jpadilla-pyjwt-a4e1a3d/docs/requirements-docs.txt000066400000000000000000000000301515505507500222740ustar00rootroot00000000000000sphinx sphinx_rtd_theme jpadilla-pyjwt-a4e1a3d/docs/usage.rst000066400000000000000000000710061515505507500177330ustar00rootroot00000000000000Usage Examples ============== Encoding & Decoding Tokens with HS256 ------------------------------------- .. code-block:: pycon >>> import jwt >>> key = "secret" >>> encoded = jwt.encode({"some": "payload"}, key, algorithm="HS256") >>> jwt.decode(encoded, key, algorithms="HS256") {'some': 'payload'} Encoding & Decoding Tokens with RS256 (RSA) ------------------------------------------- RSA encoding and decoding require the ``cryptography`` module. See :ref:`installation_cryptography`. .. code-block:: pycon >>> import jwt >>> private_key = b"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwhvqCC+37A+UXgcvDl+7nbVjDI3QErdZBkI1VypVBMkKKWHM\nNLMdHk0bIKL+1aDYTRRsCKBy9ZmSSX1pwQlO/3+gRs/MWG27gdRNtf57uLk1+lQI\n6hBDozuyBR0YayQDIx6VsmpBn3Y8LS13p4pTBvirlsdX+jXrbOEaQphn0OdQo0WD\noOwwsPCNCKoIMbUOtUCowvjesFXlWkwG1zeMzlD1aDDS478PDZdckPjT96ICzqe4\nO1Ok6fRGnor2UTmuPy0f1tI0F7Ol5DHAD6pZbkhB70aTBuWDGLDR0iLenzyQecmD\n4aU19r1XC9AHsVbQzxHrP8FveZGlV/nJOBJwFwIDAQABAoIBAFCVFBA39yvJv/dV\nFiTqe1HahnckvFe4w/2EKO65xTfKWiyZzBOotBLrQbLH1/FJ5+H/82WVboQlMATQ\nSsH3olMRYbFj/NpNG8WnJGfEcQpb4Vu93UGGZP3z/1B+Jq/78E15Gf5KfFm91PeQ\nY5crJpLDU0CyGwTls4ms3aD98kNXuxhCGVbje5lCARizNKfm/+2qsnTYfKnAzN+n\nnm0WCjcHmvGYO8kGHWbFWMWvIlkoZ5YubSX2raNeg+YdMJUHz2ej1ocfW0A8/tmL\nwtFoBSuBe1Z2ykhX4t6mRHp0airhyc+MO0bIlW61vU/cPGPos16PoS7/V08S7ZED\nX64rkyECgYEA4iqeJZqny/PjOcYRuVOHBU9nEbsr2VJIf34/I9hta/mRq8hPxOdD\n/7ES/ZTZynTMnOdKht19Fi73Sf28NYE83y5WjGJV/JNj5uq2mLR7t2R0ZV8uK8tU\n4RR6b2bHBbhVLXZ9gqWtu9bWtsxWOkG1bs0iONgD3k5oZCXp+IWuklECgYEA27bA\n7UW+iBeB/2z4x1p/0wY+whBOtIUiZy6YCAOv/HtqppsUJM+W9GeaiMpPHlwDUWxr\n4xr6GbJSHrspkMtkX5bL9e7+9zBguqG5SiQVIzuues9Jio3ZHG1N2aNrr87+wMiB\nxX6Cyi0x1asmsmIBO7MdP/tSNB2ebr8qM6/6mecCgYBA82ZJfFm1+8uEuvo6E9/R\nyZTbBbq5BaVmX9Y4MB50hM6t26/050mi87J1err1Jofgg5fmlVMn/MLtz92uK/hU\nS9V1KYRyLc3h8gQQZLym1UWMG0KCNzmgDiZ/Oa/sV5y2mrG+xF/ZcwBkrNgSkO5O\n7MBoPLkXrcLTCARiZ9nTkQKBgQCsaBGnnkzOObQWnIny1L7s9j+UxHseCEJguR0v\nXMVh1+5uYc5CvGp1yj5nDGldJ1KrN+rIwMh0FYt+9dq99fwDTi8qAqoridi9Wl4t\nIXc8uH5HfBT3FivBtLucBjJgOIuK90ttj8JNp30tbynkXCcfk4NmS23L21oRCQyy\nlmqNDQKBgQDRvzEB26isJBr7/fwS0QbuIlgzEZ9T3ZkrGTFQNfUJZWcUllYI0ptv\ny7ShHOqyvjsC3LPrKGyEjeufaM5J8EFrqwtx6UB/tkGJ2bmd1YwOWFHvfHgHCZLP\n34ZNURCvxRV9ZojS1zmDRBJrSo7+/K0t28hXbiaTOjJA18XAyyWmGg==\n-----END RSA PRIVATE KEY-----\n" >>> public_key = b"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhvqCC+37A+UXgcvDl+7\nnbVjDI3QErdZBkI1VypVBMkKKWHMNLMdHk0bIKL+1aDYTRRsCKBy9ZmSSX1pwQlO\n/3+gRs/MWG27gdRNtf57uLk1+lQI6hBDozuyBR0YayQDIx6VsmpBn3Y8LS13p4pT\nBvirlsdX+jXrbOEaQphn0OdQo0WDoOwwsPCNCKoIMbUOtUCowvjesFXlWkwG1zeM\nzlD1aDDS478PDZdckPjT96ICzqe4O1Ok6fRGnor2UTmuPy0f1tI0F7Ol5DHAD6pZ\nbkhB70aTBuWDGLDR0iLenzyQecmD4aU19r1XC9AHsVbQzxHrP8FveZGlV/nJOBJw\nFwIDAQAB\n-----END PUBLIC KEY-----\n" >>> encoded = jwt.encode({"some": "payload"}, private_key, algorithm="RS256") >>> jwt.decode(encoded, public_key, algorithms=["RS256"]) {'some': 'payload'} If your private key needs a passphrase, you need to pass in a ``PrivateKey`` object from ``cryptography``. .. code-block:: python from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend pem_bytes = b"-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,C9C8F89EC68D15F26EB9B9695216C6DC\nE3lvX0dYjDxC0DIDitwNj+mEvU48Cqlp9esIeVmfcFmM6KpuQEA4asg/19kldbRq\ntOAYwmMuzz6GNYtX6sQXcStUE3pKMiMaTuP9WXzTc0boSYsGpGoQLtGv3h+0lkPu\nTGaktEhIfplAYlmsS/twr9Jh9QZjEs3dEMwpuF8A/iDZFeIE2thZL0bo38VWorgZ\nTCoOlC7qGtaeDvXXYrMvAUw3lN9A+DvxuPvbGqfqiHVBhxRcQEcR5p65lKP/V0WQ\nDe0AqCx1ghYGnExT7I4GLfr7Ux3F1UcVldPPsNeCTR/5YMOYDw7o5CZZ2TM39T33\nDBwfRhDqKe4bMUQcvcD54S2tfW7tEekm6mx5JwzW11sd0Gprj2uggDTOj3ce2yzM\nzl/dfbyFgh6v4jFeblIgvQ4VPg9nfCaRhatw5KXnfHBvmvdxlQ1Qp5P43ThXjI2a\njaJdm2lu1DLhf1OYGeQ0ytDDPzvhrZrdEJ8jbB3VCn4O/hvCtdsp7jVw2Djxmw2A\niRz2zlZJUlaytbi/DMpEVFwIzpuiDkpJ+ekzAsBbm/rGR/tjCEtHzVuoQNUWI93k\n0FML+Zzb6AkBWYjBXDZtzwJpMdNr8Vvh3krZySbRzQstqL2PYuNoSZ8/1xnnVqTV\nA0pDX7OS856AXQzQ1FRjjk/Jd0k6jGj8d7LzVgMnb8VknKvshlLmZDz8Sqa1coN4\n0Z1VfiT0Hzlk0fkoGtRjhSc3MB6ZLg7vVlY5vb4bRrTX79s/p8Y/OecYnGC6qhTi\n+VyJiMfwXyjFjIWYH8Y3G0QLkvOrTxLAY/3B2TU5wVSD7lfnPKOatMK1W0DHu5jp\nG9PPTzK9ol3v6Pk0prYg1fiApb6CCBUeZBvCIbJCzYrL/yBV/xYlCwAekLNGz9Vj\nNQUoiJqi27fOQi+ZXCrF7gYj8afo/xrg0tf7YqoOty8qfsozXzqwHKn+PcZOcqa5\n5rIqjLOO2f6KO2dxBeZK6zmzg7K/8RjvsNkEuXffec/nwnC10OVoMbE4wyPmNUQi\ndSuZ6xWBqiREjodLL+Ez/N1Qa52kuLSigrrSBTM2e42PWDV1sNW5V2wwlnolXFF6\n2Xp74WaGdnwF4Afrm7AnaBxdmfjk/a+c2uzPkZkpVnxrW3l8afphhKpRoTLzqDPp\nZGc5Fx9UZsmX18B8D1OGbf4aVLUkoqPPHbccCI+wByoAgIoq+y2391fP/Db6fY9A\nR4t2uuP2sNqDfYtzPYikePBXhYlldE1UHJ378g8pTiRHOI9BhuKIOIbVngPUYk4I\nwhYct2K84HjvR3iRnobK0UmmNOqtK0AtUqne+xaj1f3OwMZSvTUe7/jESgw1e1tn\nulKiWnKnmTSZkeTIp6itui2T7ewfNyitPtvnhoH1fBnMyUVACip0SLXp1fwQ7iCc\namPFFKo7p+C7P3l0ItegaMHywOSTBvK39DQTIpF9ml8VCQ+UyPOv/LnSJk1mbJN/\nc2Hdoj5dMa6T7ysIwZGEissJ/MEP+dpRs7VmCjWrHCDHfeAIO0n32g4zbzlNc/OA\nIdCXTvi4xUEn2n3JPt5Ba9qDUevaHSERlLxI+9a4ZaZeg4t+AzY0ur6+RWx+PaXB\n-----END RSA PRIVATE KEY-----\n" passphrase = b"abc123" private_key = serialization.load_pem_private_key( pem_bytes, password=passphrase, backend=default_backend() ) encoded = jwt.encode({"some": "payload"}, private_key, algorithm="RS256") If you are repeatedly encoding with the same private key, reusing the same ``RSAPrivateKey`` also has performance benefits because it avoids the CPU-intensive ``RSA_check_key`` primality test. Encoding & Decoding Tokens with PS256 (RSA) ------------------------------------------- RSA encoding and decoding require the ``cryptography`` module. See :ref:`installation_cryptography`. .. code-block:: pycon >>> import jwt >>> private_key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAuNhCS6bodtd+PvKqNj+tYZYqTNMDkf0rcptgHhecSsMP9Vay\n+6NvJk1tC+IajPaE4yRJVY4jFqEt3A0MJ9sKe5mWDYFmzW/L6VzQvQ+0nrMc1YTE\nDpOf7BQhlW5W0mDj5SwSR50Lxg/acb+SMWq6zmhuAoLRapH17K2RWONA2vr2frox\nJ6N9TGtrQHygDb0p9D6jPnXEe4y+zBuj6o0bCkJgCVNM+CU19xBepj5caetYV28/\n49yl5XPi93n1ATU+7aGAKxuvjudODuHhF/UsZScMFSHeZW367eQldTB2w9uoIIzW\nO46tKimr21zYifMimjwnBQ/PLDqc7HqY0Y/rLQIDAQABAoIBAAdu0CD7/Iu61/LE\nDfV8fgZXOYA5WVgSLCBsVbh1Y+2FsStBFJVrLwRanLCbo6GuJWMqNGC3ryWGebJI\nPAg7lfepEhBHodClAY1yvq9mOvHJa2Fn+KegEWWMMbAxQwCBW5NS6waXhBUE0i3n\ncYOB3TKA9IYuqH52kW22VQqT/imlWEb28pJJT49YfggmOOtAkrKerokO53lAfrJA\ntm8lYvxXnfnuYh7zI835RpZJ1PeaYrMqyAwT+StD9hPKGWGpN1gCJijjcK0aapvq\nMLET/JxMxxcLsINOeLtGhMKawmET3J/esJTumOE2L77MFG83rlCPbsSfLdSAI2WD\nSe3Q2ikCgYEA7JzmVrPh7G/oILLzIfk8GHFACRTtlE5SDEpFq+ARMprfcBXpkl+Q\naWqQ3vuSH7oiAQKlvo3We6XXohCMMDU2DyMaXiQMk73R83fMwbFnFcqFhbzx2zpm\nj/neHIViEi/N69SHPxl+vnUTfeVZptibNGS+ch3Ubawt3wCaWr+IdAcCgYEAx/19\ns5ryq2oTQCD5GfIqW73LAUly5RqENLvKHZ2z+mZ0pp7dc5449aDsHPLXLl1YC3mO\nlZZk+8Jh5yrpHyljiIYwh/1y0WsbungMlH6lG9JigcN8R2Tk9hWT7DQL0fm0dYoQ\njkwr/gJv6PW0piLsR0vsQQpm/F/ucZolVPQIoisCgYA5XXzWznvax/LeYqRhuzxf\nrK1axlEnYKmxwxwLJKLmwvejBB0B2Nt5Q1XmSdXOjWELH6oxfc/fYIDcEOj8ExqN\nJvSQmGrYMvBA9+2TlEAq31Pp7boxbYJKK8k23vu87wwcvgUgPj0lTdsw7bcDpYZT\neI1Xu3WyNUlVxJ6nm8IoZwKBgG6YPjVekKg+htrF4Tt58fa95E+X4JPVsBrBZqou\nFeN5WTTzUZ+odfNPxILVwC2BrTjbRgBvJPUcr6t4zWZQKxzKqHfrrt0kkDb0QHC2\nAHR8ScFc65NHtl5n3F+ZAJhjsGn3qeQnN4TGsEBx8C6XzXY4BDSLnhweqOvlxJNQ\nSJ31AoGAX/UN5xR6PlCgPw5HWfGd7+4sArkjA36DAXvrAgW/6/mxZZzoGA1swYdZ\nq2uGp38UEKkxKTrhR4J6eR5DsLAfl/KQBbNC42vqZwe9YrS4hNQFR14GwlyJhdLx\nKQD/JzHwNQN5+o+hy0lJavTw9NwAAb1ZzTgvq6fPwEG0b9hn0SI=\n-----END RSA PRIVATE KEY-----\n" >>> public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNhCS6bodtd+PvKqNj+t\nYZYqTNMDkf0rcptgHhecSsMP9Vay+6NvJk1tC+IajPaE4yRJVY4jFqEt3A0MJ9sK\ne5mWDYFmzW/L6VzQvQ+0nrMc1YTEDpOf7BQhlW5W0mDj5SwSR50Lxg/acb+SMWq6\nzmhuAoLRapH17K2RWONA2vr2froxJ6N9TGtrQHygDb0p9D6jPnXEe4y+zBuj6o0b\nCkJgCVNM+CU19xBepj5caetYV28/49yl5XPi93n1ATU+7aGAKxuvjudODuHhF/Us\nZScMFSHeZW367eQldTB2w9uoIIzWO46tKimr21zYifMimjwnBQ/PLDqc7HqY0Y/r\nLQIDAQAB\n-----END PUBLIC KEY-----\n" >>> encoded = jwt.encode({"some": "payload"}, private_key, algorithm="PS256") >>> jwt.decode(encoded, public_key, algorithms=["PS256"]) {'some': 'payload'} Encoding & Decoding Tokens with EdDSA (Ed25519) ----------------------------------------------- EdDSA encoding and decoding require the ``cryptography`` module. See :ref:`installation_cryptography`. .. code-block:: pycon >>> import jwt >>> private_key = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIPtUxyxlhjOWetjIYmc98dmB2GxpeaMPP64qBhZmG13r\n-----END PRIVATE KEY-----\n" >>> public_key = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA7p4c1IU6aA65FWn6YZ+Bya5dRbfd4P6d4a6H0u9+gCg=\n-----END PUBLIC KEY-----\n" >>> encoded = jwt.encode({"some": "payload"}, private_key, algorithm="EdDSA") >>> jwt.decode(encoded, public_key, algorithms=["EdDSA"]) {'some': 'payload'} Encoding & Decoding Tokens with ES256 (ECDSA) --------------------------------------------- ECDSA encoding and decoding require the ``cryptography`` module. See :ref:`installation_cryptography`. .. code-block:: pycon >>> import jwt >>> private_key = b"-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIHAhM7P6HG3LgkDvgvfDeaMA6uELj+jEKWsSeOpS/SfYoAoGCCqGSM49\nAwEHoUQDQgAEXHVxB7s5SR7I9cWwry/JkECIRekaCwG3uOLCYbw5gVzn4dRmwMyY\nUJFcQWuFSfECRK+uQOOXD0YSEucBq0p5tA==\n-----END EC PRIVATE KEY-----\n" >>> public_key = b"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXHVxB7s5SR7I9cWwry/JkECIReka\nCwG3uOLCYbw5gVzn4dRmwMyYUJFcQWuFSfECRK+uQOOXD0YSEucBq0p5tA==\n-----END PUBLIC KEY-----\n" >>> encoded = jwt.encode({"some": "payload"}, private_key, algorithm="ES256") >>> jwt.decode(encoded, public_key, algorithms=["ES256"]) {'some': 'payload'} Specifying Additional Headers ----------------------------- .. code-block:: pycon >>> jwt.encode( ... {"some": "payload"}, ... "secret", ... algorithm="HS256", ... headers={"kid": "230498151c214b788dd97f22b85410a5"}, ... ) 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1IiwidHlwIjoiSldUIn0.eyJzb21lIjoicGF5bG9hZCJ9.0n16c-shKKnw6gervyk1Dge35tvzbzQ_KCV3H3bgoJ0' By default the ``typ`` is attaching to the headers. In case when you don't need to pass this header to the token, you have to explicitly null it. .. code-block:: pycon >>> jwt.encode( ... {"some": "payload"}, ... "secret", ... algorithm="HS256", ... headers={"typ": None}, ... ) # doctest: +ELLIPSIS 'eyJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9...' Reading the Claimset without Validation --------------------------------------- If you wish to read the claimset of a JWT without performing validation of the signature or any of the registered claim names, you can set the ``verify_signature`` option to ``False``. Note: It is generally ill-advised to use this functionality unless you clearly understand what you are doing. Without digital signature information, the integrity or authenticity of the claimset cannot be trusted. .. code-block:: pycon >>> jwt.decode(encoded, options={"verify_signature": False}) {'some': 'payload'} Reading Headers without Validation ---------------------------------- Some APIs require you to read a JWT header without validation. For example, in situations where the token issuer uses multiple keys and you have no way of knowing in advance which one of the issuer's public keys or shared secrets to use for validation, the issuer may include an identifier for the key in the header. .. code-block:: pycon >>> encoded = jwt.encode( ... {"some": "payload"}, ... "secret", ... algorithm="HS256", ... headers={"kid": "230498151c214b788dd97f22b85410a5"}, ... ) >>> jwt.get_unverified_header(encoded) {'alg': 'HS256', 'kid': '230498151c214b788dd97f22b85410a5', 'typ': 'JWT'} Registered Claim Names ---------------------- The JWT specification defines some registered claim names and defines how they should be used. PyJWT supports these registered claim names: - "exp" (Expiration Time) Claim - "nbf" (Not Before Time) Claim - "iss" (Issuer) Claim - "aud" (Audience) Claim - "iat" (Issued At) Claim - "sub" (Subject) Claim - "jti" (JWT ID) Claim Expiration Time Claim (exp) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL. You can pass the expiration time as a UTC UNIX timestamp (an int) or as a datetime, which will be converted into an int. For example: .. code-block:: pycon >>> from datetime import datetime, timezone >>> token = jwt.encode({"exp": 1371720939}, "secret") >>> token = jwt.encode({"exp": datetime.now(tz=timezone.utc)}, "secret") Expiration time is automatically verified in `jwt.decode()` and raises `jwt.ExpiredSignatureError` if the expiration time is in the past: .. code-block:: pycon >>> try: ... jwt.decode(token, "secret", algorithms=["HS256"]) ... except jwt.ExpiredSignatureError: ... print("expired") ... expired Expiration time will be compared to the current UTC time (as given by `timegm(datetime.now(tz=timezone.utc).utctimetuple())`), so be sure to use a UTC timestamp or datetime in encoding. You can turn off expiration time verification with the `verify_exp` parameter in the options argument. PyJWT also supports the leeway part of the expiration time definition, which means you can validate a expiration time which is in the past but not very far. For example, if you have a JWT payload with a expiration time set to 30 seconds after creation but you know that sometimes you will process it after 30 seconds, you can set a leeway of 5 seconds in order to have some margin: .. code-block:: pycon >>> import time, datetime >>> from datetime import timezone >>> payload = { ... "exp": datetime.datetime.now(tz=timezone.utc) + datetime.timedelta(seconds=1) ... } >>> token = jwt.encode(payload, "secret") >>> time.sleep(2) >>> # JWT payload is now expired >>> # But with some leeway, it will still validate >>> decoded = jwt.decode(token, "secret", leeway=5, algorithms=["HS256"]) Instead of specifying the leeway as a number of seconds, a `datetime.timedelta` instance can be used. The last line in the example above is equivalent to: .. code-block:: pycon >>> decoded = jwt.decode( ... token, "secret", leeway=datetime.timedelta(seconds=5), algorithms=["HS256"] ... ) Not Before Time Claim (nbf) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL. The `nbf` claim works similarly to the `exp` claim above. .. code-block:: pycon >>> token = jwt.encode({"nbf": 1371720939}, "secret") >>> token = jwt.encode({"nbf": datetime.datetime.now(tz=timezone.utc)}, "secret") The `nbf` claim also supports the leeway feature similar to the `exp` claim. This allows you to validate a “not before” time that is slightly in the future. Using leeway with the nbf claim can be particularly helpful in scenarios where clock synchronization between the token issuer and the validator is imprecise. .. code-block:: pycon >>> import time, datetime >>> from datetime import timezone >>> payload = { ... "nbf": datetime.datetime.now(tz=timezone.utc) - datetime.timedelta(seconds=3) ... } >>> token = jwt.encode(payload, "secret") >>> # JWT payload is not valid yet >>> # But with some leeway, it will still validate >>> decoded = jwt.decode(token, "secret", leeway=5, algorithms=["HS256"]) Issuer Claim (iss) ~~~~~~~~~~~~~~~~~~ The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL. .. code-block:: pycon >>> payload = {"some": "payload", "iss": "urn:foo"} >>> token = jwt.encode(payload, "secret") >>> try: ... jwt.decode(token, "secret", issuer="urn:invalid", algorithms=["HS256"]) ... except jwt.InvalidIssuerError: ... print("invalid issuer") ... invalid issuer If the issuer claim is incorrect, `jwt.InvalidIssuerError` will be raised. Audience Claim (aud) ~~~~~~~~~~~~~~~~~~~~ The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case- sensitive strings, each containing a StringOrURI value. .. code-block:: pycon >>> payload = {"some": "payload", "aud": ["urn:foo", "urn:bar"]} >>> token = jwt.encode(payload, "secret") >>> decoded = jwt.decode(token, "secret", audience="urn:foo", algorithms=["HS256"]) >>> decoded = jwt.decode(token, "secret", audience="urn:bar", algorithms=["HS256"]) In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. .. code-block:: pycon >>> payload = {"some": "payload", "aud": "urn:foo"} >>> token = jwt.encode(payload, "secret") >>> decoded = jwt.decode(token, "secret", audience="urn:foo", algorithms=["HS256"]) If multiple audiences are accepted, the ``audience`` parameter for ``jwt.decode`` can also be an iterable .. code-block:: pycon >>> payload = {"some": "payload", "aud": "urn:foo"} >>> token = jwt.encode(payload, "secret") >>> decoded = jwt.decode( ... token, "secret", audience=["urn:foo", "urn:bar"], algorithms=["HS256"] ... ) >>> try: ... jwt.decode(token, "secret", audience=["urn:invalid"], algorithms=["HS256"]) ... except jwt.InvalidAudienceError: ... print("invalid audience") ... invalid audience The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL. If the audience claim is incorrect, `jwt.InvalidAudienceError` will be raised. Issued At Claim (iat) ~~~~~~~~~~~~~~~~~~~~~ The iat (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL. If the `iat` claim is not a number, an `jwt.InvalidIssuedAtError` exception will be raised. .. code-block:: pycon >>> token = jwt.encode({"iat": 1371720939}, "secret") >>> token = jwt.encode({"iat": datetime.datetime.now(tz=timezone.utc)}, "secret") Subject Claim (sub) ~~~~~~~~~~~~~~~~~~~ The "sub" (subject) claim identifies the principal that is the subject of the JWT. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. Use of this claim is OPTIONAL. .. code-block:: pycon >>> payload = {"some": "payload", "sub": "1234567890"} >>> token = jwt.encode(payload, "secret") >>> decoded = jwt.decode(token, "secret", algorithms=["HS256"]) >>> decoded["sub"] '1234567890' Think of the `sub` claim as the **"who"** of the JWT. It identifies the subject of the token — the user or entity that the token is about. The claims inside a JWT are essentially statements about this subject. For example, if you have a JWT for a logged-in user, the `sub` claim would typically be their unique user ID, like `1234567890`. This value needs to be unique within your application's context so you can reliably identify who the token belongs to. While the `sub` claim is optional, it's a fundamental part of most JWT-based authentication systems. JWT ID Claim (jti) ~~~~~~~~~~~~~~~~~~ The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object. If the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" value is a case-sensitive string. Use of this claim is OPTIONAL. .. code-block:: pycon >>> import uuid >>> payload = {"some": "payload", "jti": str(uuid.uuid4())} >>> token = jwt.encode(payload, "secret") >>> decoded = jwt.decode(token, "secret", algorithms=["HS256"]) >>> decoded["jti"] # doctest: +SKIP '3fa85f64-5717-4562-b3fc-2c963f66afa6' The `jti` claim is giving your JWT a unique identifier. Think of it like a serial number for the token. This ID must be assigned in a way that makes it virtually impossible for two different tokens to have the same `jti` value. A common practice is to use a Universally Unique Identifier (UUID). The `jti` claim is used to **prevent replay attacks**. A replay attack happens when a bad actor intercepts a valid token and uses it to make a request again. By storing the `jti` of every token you've already processed in a database or cache, you can check if a token has been used before. If a token with a previously-seen `jti` shows up, you can reject the request, stopping the attack. .. _key-length-validation: Key Length Validation --------------------- PyJWT validates that cryptographic keys meet minimum recommended lengths. By default, a warning (``InsecureKeyLengthWarning``) is emitted when a key is too short. You can configure PyJWT to raise an ``InvalidKeyError`` instead. The minimum key lengths are: * **HMAC** (HS256, HS384, HS512): Key must be at least as long as the hash output (32, 48, or 64 bytes respectively), per `RFC 7518 Section 3.2 `_. * **RSA** (RS256, RS384, RS512, PS256, PS384, PS512): Key must be at least 2048 bits, per `NIST SP 800-131A `_. By default, short keys produce an ``InsecureKeyLengthWarning``: .. code-block:: pycon >>> import jwt >>> encoded = jwt.encode({"some": "payload"}, "short", algorithm="HS256") To enforce minimum key lengths (raise ``InvalidKeyError`` on short keys), pass ``enforce_minimum_key_length=True`` in the options when creating a ``PyJWT`` or ``PyJWS`` instance: .. code-block:: pycon >>> strict_jwt = jwt.PyJWT(options={"enforce_minimum_key_length": True}) >>> try: ... strict_jwt.encode({"some": "payload"}, "short", algorithm="HS256") ... except jwt.InvalidKeyError: ... print("key too short") ... key too short To suppress the warning without enforcing, use Python's standard ``warnings`` module: .. code-block:: python import warnings import jwt warnings.filterwarnings("ignore", category=jwt.InsecureKeyLengthWarning) Requiring Presence of Claims ---------------------------- If you wish to require one or more claims to be present in the claimset, you can set the ``require`` parameter to include these claims. .. code-block:: pycon >>> token = jwt.encode({"sub": "1234567890", "iat": 1371720939}, "secret") >>> try: ... jwt.decode( ... token, ... "secret", ... options={"require": ["exp", "iss", "sub"]}, ... algorithms=["HS256"], ... ) ... except jwt.MissingRequiredClaimError as e: ... print(e) ... Token is missing the "exp" claim Retrieve RSA signing keys from a JWKS endpoint ---------------------------------------------- ``PyJWKClient`` fetches and manages JSON Web Key Sets (JWKS) from a remote endpoint. Identity providers such as Auth0, Okta, and any OpenID Connect server publish a JWKS endpoint containing the public keys used to sign JWTs. Instead of hard-coding public keys, you can point ``PyJWKClient`` at that URL and let it resolve the correct key for each token automatically. Pass the JWKS URL to create a client, then call ``get_signing_key_from_jwt()`` with a token to look up the matching key by its ``kid`` header claim: .. code-block:: pycon >>> import jwt >>> from jwt import PyJWKClient >>> token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" >>> url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" >>> optional_custom_headers = {"User-agent": "custom-user-agent"} >>> jwks_client = PyJWKClient(url, headers=optional_custom_headers) >>> signing_key = jwks_client.get_signing_key_from_jwt(token) >>> jwt.decode( ... token, ... signing_key, ... audience="https://expenses-api", ... options={"verify_exp": False}, ... algorithms=["RS256"], ... ) {'iss': 'https://dev-87evx9ru.auth0.com/', 'sub': 'aW4Cca79xReLWUz0aE2H6kD0O3cXBVtC@clients', 'aud': 'https://expenses-api', 'iat': 1572006954, 'exp': 1572006964, 'azp': 'aW4Cca79xReLWUz0aE2H6kD0O3cXBVtC', 'gty': 'client-credentials'} If the ``kid`` is not found in the current key set, ``PyJWKClient`` automatically refreshes the JWKS from the endpoint and retries before raising an error. ``PyJWKClient`` also includes built-in caching to avoid unnecessary network requests. See :class:`~jwt.PyJWKClient` in the API reference for details on the caching parameters. OIDC Login Flow --------------- The following usage demonstrates an OIDC login flow using pyjwt. Further reading about the OIDC spec is recommended for implementers. In particular, this demonstrates validation of the ``at_hash`` claim. This claim relies on data from outside of the the JWT for validation. Methods are provided which support computation and validation of this claim, but it is not built into pyjwt. .. code-block:: python import base64 import jwt import requests # Part 1: setup # get the OIDC config and JWKs to use # in OIDC, you must know your client_id (this is the OAuth 2.0 client_id) client_id = ... # example of fetching data from your OIDC server # see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig oidc_server = ... oidc_config = requests.get( f"https://{oidc_server}/.well-known/openid-configuration" ).json() signing_algos = oidc_config["id_token_signing_alg_values_supported"] # setup a PyJWKClient to get the appropriate signing key jwks_client = jwt.PyJWKClient(oidc_config["jwks_uri"]) # Part 2: login / authorization # when a user completes an OIDC login flow, there will be a well-formed # response object to parse/handle # data from the login flow # see: https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse token_response = ... id_token = token_response["id_token"] access_token = token_response["access_token"] # Part 3: decode and validate at_hash # after the login is complete, the id_token needs to be decoded # this is the stage at which an OIDC client must verify the at_hash # get signing_key from id_token signing_key = jwks_client.get_signing_key_from_jwt(id_token) # now, decode_complete to get payload + header data = jwt.decode_complete( id_token, key=signing_key, audience=client_id, algorithms=signing_algos, ) payload, header = data["payload"], data["header"] # get the pyjwt algorithm object alg_obj = jwt.get_algorithm_by_name(header["alg"]) # compute at_hash, then validate / assert digest = alg_obj.compute_hash_digest(access_token) at_hash = base64.urlsafe_b64encode(digest[: (len(digest) // 2)]).rstrip("=") assert at_hash == payload["at_hash"] jpadilla-pyjwt-a4e1a3d/jwt/000077500000000000000000000000001515505507500157455ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/jwt/__init__.py000066400000000000000000000034151515505507500200610ustar00rootroot00000000000000from .api_jwk import PyJWK, PyJWKSet from .api_jws import ( PyJWS, get_algorithm_by_name, get_unverified_header, register_algorithm, unregister_algorithm, ) from .api_jwt import PyJWT, decode, decode_complete, encode from .exceptions import ( DecodeError, ExpiredSignatureError, ImmatureSignatureError, InvalidAlgorithmError, InvalidAudienceError, InvalidIssuedAtError, InvalidIssuerError, InvalidKeyError, InvalidSignatureError, InvalidTokenError, MissingRequiredClaimError, PyJWKClientConnectionError, PyJWKClientError, PyJWKError, PyJWKSetError, PyJWTError, ) from .jwks_client import PyJWKClient from .warnings import InsecureKeyLengthWarning __version__ = "2.12.1" __title__ = "PyJWT" __description__ = "JSON Web Token implementation in Python" __url__ = "https://pyjwt.readthedocs.io" __uri__ = __url__ __doc__ = f"{__description__} <{__uri__}>" __author__ = "José Padilla" __email__ = "hello@jpadilla.com" __license__ = "MIT" __copyright__ = "Copyright 2015-2026 José Padilla" __all__ = [ "PyJWS", "PyJWT", "PyJWKClient", "PyJWK", "PyJWKSet", "decode", "decode_complete", "encode", "get_unverified_header", "register_algorithm", "unregister_algorithm", "get_algorithm_by_name", # Warnings "InsecureKeyLengthWarning", # Exceptions "DecodeError", "ExpiredSignatureError", "ImmatureSignatureError", "InvalidAlgorithmError", "InvalidAudienceError", "InvalidIssuedAtError", "InvalidIssuerError", "InvalidKeyError", "InvalidSignatureError", "InvalidTokenError", "MissingRequiredClaimError", "PyJWKClientConnectionError", "PyJWKClientError", "PyJWKError", "PyJWKSetError", "PyJWTError", ] jpadilla-pyjwt-a4e1a3d/jwt/algorithms.py000066400000000000000000001056011515505507500204730ustar00rootroot00000000000000from __future__ import annotations import hashlib import hmac import json import os import sys from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, Union, cast, get_args, overload, ) from .exceptions import InvalidKeyError from .types import HashlibHash, JWKDict from .utils import ( base64url_decode, base64url_encode, der_to_raw_signature, force_bytes, from_base64url_uint, is_pem_format, is_ssh_key, raw_to_der_signature, to_base64url_uint, ) try: from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric.ec import ( ECDSA, SECP256K1, SECP256R1, SECP384R1, SECP521R1, EllipticCurve, EllipticCurvePrivateKey, EllipticCurvePrivateNumbers, EllipticCurvePublicKey, EllipticCurvePublicNumbers, ) from cryptography.hazmat.primitives.asymmetric.ed448 import ( Ed448PrivateKey, Ed448PublicKey, ) from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, ) from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPrivateKey, RSAPrivateNumbers, RSAPublicKey, RSAPublicNumbers, rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp, rsa_recover_prime_factors, ) from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key, load_pem_public_key, load_ssh_public_key, ) if sys.version_info >= (3, 10): from typing import TypeAlias else: # Python 3.9 and lower from typing_extensions import TypeAlias # Type aliases for convenience in algorithms method signatures AllowedRSAKeys: TypeAlias = Union[RSAPrivateKey, RSAPublicKey] AllowedECKeys: TypeAlias = Union[EllipticCurvePrivateKey, EllipticCurvePublicKey] AllowedOKPKeys: TypeAlias = Union[ Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey ] AllowedKeys: TypeAlias = Union[AllowedRSAKeys, AllowedECKeys, AllowedOKPKeys] #: Type alias for allowed ``cryptography`` private keys (requires ``cryptography`` to be installed) AllowedPrivateKeys: TypeAlias = Union[ RSAPrivateKey, EllipticCurvePrivateKey, Ed25519PrivateKey, Ed448PrivateKey ] #: Type alias for allowed ``cryptography`` public keys (requires ``cryptography`` to be installed) AllowedPublicKeys: TypeAlias = Union[ RSAPublicKey, EllipticCurvePublicKey, Ed25519PublicKey, Ed448PublicKey ] if TYPE_CHECKING or bool(os.getenv("SPHINX_BUILD", "")): from cryptography.hazmat.primitives.asymmetric.types import ( PrivateKeyTypes, PublicKeyTypes, ) has_crypto = True except ModuleNotFoundError: if sys.version_info >= (3, 11): from typing import Never else: from typing_extensions import Never AllowedRSAKeys = Never # type: ignore[misc] AllowedECKeys = Never # type: ignore[misc] AllowedOKPKeys = Never # type: ignore[misc] AllowedKeys = Never # type: ignore[misc] AllowedPrivateKeys = Never # type: ignore[misc] AllowedPublicKeys = Never # type: ignore[misc] has_crypto = False requires_cryptography = { "RS256", "RS384", "RS512", "ES256", "ES256K", "ES384", "ES521", "ES512", "PS256", "PS384", "PS512", "EdDSA", } def get_default_algorithms() -> dict[str, Algorithm]: """ Returns the algorithms that are implemented by the library. """ default_algorithms: dict[str, Algorithm] = { "none": NoneAlgorithm(), "HS256": HMACAlgorithm(HMACAlgorithm.SHA256), "HS384": HMACAlgorithm(HMACAlgorithm.SHA384), "HS512": HMACAlgorithm(HMACAlgorithm.SHA512), } if has_crypto: default_algorithms.update( { "RS256": RSAAlgorithm(RSAAlgorithm.SHA256), "RS384": RSAAlgorithm(RSAAlgorithm.SHA384), "RS512": RSAAlgorithm(RSAAlgorithm.SHA512), "ES256": ECAlgorithm(ECAlgorithm.SHA256, SECP256R1), "ES256K": ECAlgorithm(ECAlgorithm.SHA256, SECP256K1), "ES384": ECAlgorithm(ECAlgorithm.SHA384, SECP384R1), "ES521": ECAlgorithm(ECAlgorithm.SHA512, SECP521R1), "ES512": ECAlgorithm( ECAlgorithm.SHA512, SECP521R1 ), # Backward compat for #219 fix "PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), "PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), "PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512), "EdDSA": OKPAlgorithm(), } ) return default_algorithms class Algorithm(ABC): """ The interface for an algorithm used to sign and verify tokens. """ # pyjwt-964: Validate to ensure the key passed in was decoded to the correct cryptography key family _crypto_key_types: tuple[type[AllowedKeys], ...] | None = None def compute_hash_digest(self, bytestr: bytes) -> bytes: """ Compute a hash digest using the specified algorithm's hash algorithm. If there is no hash algorithm, raises a NotImplementedError. """ # lookup self.hash_alg if defined in a way that mypy can understand hash_alg = getattr(self, "hash_alg", None) if hash_alg is None: raise NotImplementedError if ( has_crypto and isinstance(hash_alg, type) and issubclass(hash_alg, hashes.HashAlgorithm) ): digest = hashes.Hash(hash_alg(), backend=default_backend()) digest.update(bytestr) return bytes(digest.finalize()) else: return bytes(hash_alg(bytestr).digest()) def check_crypto_key_type(self, key: PublicKeyTypes | PrivateKeyTypes) -> None: """Check that the key belongs to the right cryptographic family. Note that this method only works when ``cryptography`` is installed. :param key: Potentially a cryptography key :type key: :py:data:`PublicKeyTypes ` | :py:data:`PrivateKeyTypes ` :raises ValueError: if ``cryptography`` is not installed, or this method is called by a non-cryptography algorithm :raises InvalidKeyError: if the key doesn't match the expected key classes """ if not has_crypto or self._crypto_key_types is None: raise ValueError( "This method requires the cryptography library, and should only be used by cryptography-based algorithms." ) if not isinstance(key, self._crypto_key_types): valid_classes = (cls.__name__ for cls in self._crypto_key_types) actual_class = key.__class__.__name__ self_class = self.__class__.__name__ raise InvalidKeyError( f"Expected one of {valid_classes}, got: {actual_class}. Invalid Key type for {self_class}" ) @abstractmethod def prepare_key(self, key: Any) -> Any: """ Performs necessary validation and conversions on the key and returns the key value in the proper format for sign() and verify(). """ @abstractmethod def sign(self, msg: bytes, key: Any) -> bytes: """ Returns a digital signature for the specified message using the specified key value. """ @abstractmethod def verify(self, msg: bytes, key: Any, sig: bytes) -> bool: """ Verifies that the specified digital signature is valid for the specified message and key values. """ @overload @staticmethod @abstractmethod def to_jwk(key_obj: Any, as_dict: Literal[True]) -> JWKDict: ... # pragma: no cover @overload @staticmethod @abstractmethod def to_jwk( key_obj: Any, as_dict: Literal[False] = False ) -> str: ... # pragma: no cover @staticmethod @abstractmethod def to_jwk(key_obj: Any, as_dict: bool = False) -> JWKDict | str: """ Serializes a given key into a JWK """ @staticmethod @abstractmethod def from_jwk(jwk: str | JWKDict) -> Any: """ Deserializes a given key from JWK back into a key object """ def check_key_length(self, key: Any) -> str | None: """ Return a warning message if the key is below the minimum recommended length for this algorithm, or None if adequate. """ return None class NoneAlgorithm(Algorithm): """ Placeholder for use when no signing or verification operations are required. """ def prepare_key(self, key: str | None) -> None: if key == "": key = None if key is not None: raise InvalidKeyError('When alg = "none", key value must be None.') return key def sign(self, msg: bytes, key: None) -> bytes: return b"" def verify(self, msg: bytes, key: None, sig: bytes) -> bool: return False @staticmethod def to_jwk(key_obj: Any, as_dict: bool = False) -> NoReturn: raise NotImplementedError() @staticmethod def from_jwk(jwk: str | JWKDict) -> NoReturn: raise NotImplementedError() class HMACAlgorithm(Algorithm): """ Performs signing and verification operations using HMAC and the specified hash function. """ SHA256: ClassVar[HashlibHash] = hashlib.sha256 SHA384: ClassVar[HashlibHash] = hashlib.sha384 SHA512: ClassVar[HashlibHash] = hashlib.sha512 def __init__(self, hash_alg: HashlibHash) -> None: self.hash_alg = hash_alg def prepare_key(self, key: str | bytes) -> bytes: key_bytes = force_bytes(key) if is_pem_format(key_bytes) or is_ssh_key(key_bytes): raise InvalidKeyError( "The specified key is an asymmetric key or x509 certificate and" " should not be used as an HMAC secret." ) return key_bytes @overload @staticmethod def to_jwk(key_obj: str | bytes, as_dict: Literal[True]) -> JWKDict: ... @overload @staticmethod def to_jwk(key_obj: str | bytes, as_dict: Literal[False] = False) -> str: ... @staticmethod def to_jwk(key_obj: str | bytes, as_dict: bool = False) -> JWKDict | str: jwk = { "k": base64url_encode(force_bytes(key_obj)).decode(), "kty": "oct", } if as_dict: return jwk else: return json.dumps(jwk) @staticmethod def from_jwk(jwk: str | JWKDict) -> bytes: try: if isinstance(jwk, str): obj: JWKDict = json.loads(jwk) elif isinstance(jwk, dict): obj = jwk else: raise ValueError except ValueError: raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "oct": raise InvalidKeyError("Not an HMAC key") return base64url_decode(obj["k"]) def check_key_length(self, key: bytes) -> str | None: min_length = self.hash_alg().digest_size if len(key) < min_length: return ( f"The HMAC key is {len(key)} bytes long, which is below " f"the minimum recommended length of {min_length} bytes for " f"{self.hash_alg().name.upper()}. " f"See RFC 7518 Section 3.2." ) return None def sign(self, msg: bytes, key: bytes) -> bytes: return hmac.new(key, msg, self.hash_alg).digest() def verify(self, msg: bytes, key: bytes, sig: bytes) -> bool: return hmac.compare_digest(sig, self.sign(msg, key)) if has_crypto: class RSAAlgorithm(Algorithm): """ Performs signing and verification operations using RSASSA-PKCS-v1_5 and the specified hash function. """ SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 _crypto_key_types = cast( tuple[type[AllowedKeys], ...], get_args(Union[RSAPrivateKey, RSAPublicKey]), ) _MIN_KEY_SIZE: ClassVar[int] = 2048 def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: self.hash_alg = hash_alg def check_key_length(self, key: AllowedRSAKeys) -> str | None: if key.key_size < self._MIN_KEY_SIZE: return ( f"The RSA key is {key.key_size} bits long, which is below " f"the minimum recommended size of {self._MIN_KEY_SIZE} bits. " f"See NIST SP 800-131A." ) return None def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: if isinstance(key, self._crypto_key_types): return cast(AllowedRSAKeys, key) if not isinstance(key, (bytes, str)): raise TypeError("Expecting a PEM-formatted key.") key_bytes = force_bytes(key) try: if key_bytes.startswith(b"ssh-rsa"): public_key: PublicKeyTypes = load_ssh_public_key(key_bytes) self.check_crypto_key_type(public_key) return cast(RSAPublicKey, public_key) else: private_key: PrivateKeyTypes = load_pem_private_key( key_bytes, password=None ) self.check_crypto_key_type(private_key) return cast(RSAPrivateKey, private_key) except ValueError: try: public_key = load_pem_public_key(key_bytes) self.check_crypto_key_type(public_key) return cast(RSAPublicKey, public_key) except (ValueError, UnsupportedAlgorithm): raise InvalidKeyError( "Could not parse the provided public key." ) from None @overload @staticmethod def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[True]) -> JWKDict: ... @overload @staticmethod def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[False] = False) -> str: ... @staticmethod def to_jwk(key_obj: AllowedRSAKeys, as_dict: bool = False) -> JWKDict | str: obj: dict[str, Any] | None = None if hasattr(key_obj, "private_numbers"): # Private key numbers = key_obj.private_numbers() obj = { "kty": "RSA", "key_ops": ["sign"], "n": to_base64url_uint(numbers.public_numbers.n).decode(), "e": to_base64url_uint(numbers.public_numbers.e).decode(), "d": to_base64url_uint(numbers.d).decode(), "p": to_base64url_uint(numbers.p).decode(), "q": to_base64url_uint(numbers.q).decode(), "dp": to_base64url_uint(numbers.dmp1).decode(), "dq": to_base64url_uint(numbers.dmq1).decode(), "qi": to_base64url_uint(numbers.iqmp).decode(), } elif hasattr(key_obj, "verify"): # Public key numbers = key_obj.public_numbers() obj = { "kty": "RSA", "key_ops": ["verify"], "n": to_base64url_uint(numbers.n).decode(), "e": to_base64url_uint(numbers.e).decode(), } else: raise InvalidKeyError("Not a public or private key") if as_dict: return obj else: return json.dumps(obj) @staticmethod def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: try: if isinstance(jwk, str): obj = json.loads(jwk) elif isinstance(jwk, dict): obj = jwk else: raise ValueError except ValueError: raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "RSA": raise InvalidKeyError("Not an RSA key") from None if "d" in obj and "e" in obj and "n" in obj: # Private key if "oth" in obj: raise InvalidKeyError( "Unsupported RSA private key: > 2 primes not supported" ) other_props = ["p", "q", "dp", "dq", "qi"] props_found = [prop in obj for prop in other_props] any_props_found = any(props_found) if any_props_found and not all(props_found): raise InvalidKeyError( "RSA key must include all parameters if any are present besides d" ) from None public_numbers = RSAPublicNumbers( from_base64url_uint(obj["e"]), from_base64url_uint(obj["n"]), ) if any_props_found: numbers = RSAPrivateNumbers( d=from_base64url_uint(obj["d"]), p=from_base64url_uint(obj["p"]), q=from_base64url_uint(obj["q"]), dmp1=from_base64url_uint(obj["dp"]), dmq1=from_base64url_uint(obj["dq"]), iqmp=from_base64url_uint(obj["qi"]), public_numbers=public_numbers, ) else: d = from_base64url_uint(obj["d"]) p, q = rsa_recover_prime_factors( public_numbers.n, d, public_numbers.e ) numbers = RSAPrivateNumbers( d=d, p=p, q=q, dmp1=rsa_crt_dmp1(d, p), dmq1=rsa_crt_dmq1(d, q), iqmp=rsa_crt_iqmp(p, q), public_numbers=public_numbers, ) return numbers.private_key() elif "n" in obj and "e" in obj: # Public key return RSAPublicNumbers( from_base64url_uint(obj["e"]), from_base64url_uint(obj["n"]), ).public_key() else: raise InvalidKeyError("Not a public or private key") def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: signature: bytes = key.sign(msg, padding.PKCS1v15(), self.hash_alg()) return signature def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: try: key.verify(sig, msg, padding.PKCS1v15(), self.hash_alg()) return True except InvalidSignature: return False class ECAlgorithm(Algorithm): """ Performs signing and verification operations using ECDSA and the specified hash function """ SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 _crypto_key_types = cast( tuple[type[AllowedKeys], ...], get_args(Union[EllipticCurvePrivateKey, EllipticCurvePublicKey]), ) def __init__( self, hash_alg: type[hashes.HashAlgorithm], expected_curve: type[EllipticCurve] | None = None, ) -> None: self.hash_alg = hash_alg self.expected_curve = expected_curve def _validate_curve(self, key: AllowedECKeys) -> None: """Validate that the key's curve matches the expected curve.""" if self.expected_curve is None: return if not isinstance(key.curve, self.expected_curve): raise InvalidKeyError( f"The key's curve '{key.curve.name}' does not match the expected " f"curve '{self.expected_curve.name}' for this algorithm" ) def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys: if isinstance(key, self._crypto_key_types): ec_key = cast(AllowedECKeys, key) self._validate_curve(ec_key) return ec_key if not isinstance(key, (bytes, str)): raise TypeError("Expecting a PEM-formatted key.") key_bytes = force_bytes(key) # Attempt to load key. We don't know if it's # a Signing Key or a Verifying Key, so we try # the Verifying Key first. try: if key_bytes.startswith(b"ecdsa-sha2-"): public_key: PublicKeyTypes = load_ssh_public_key(key_bytes) else: public_key = load_pem_public_key(key_bytes) # Explicit check the key to prevent confusing errors from cryptography self.check_crypto_key_type(public_key) ec_public_key = cast(EllipticCurvePublicKey, public_key) self._validate_curve(ec_public_key) return ec_public_key except ValueError: private_key = load_pem_private_key(key_bytes, password=None) self.check_crypto_key_type(private_key) ec_private_key = cast(EllipticCurvePrivateKey, private_key) self._validate_curve(ec_private_key) return ec_private_key def sign(self, msg: bytes, key: EllipticCurvePrivateKey) -> bytes: der_sig = key.sign(msg, ECDSA(self.hash_alg())) return der_to_raw_signature(der_sig, key.curve) def verify(self, msg: bytes, key: AllowedECKeys, sig: bytes) -> bool: try: der_sig = raw_to_der_signature(sig, key.curve) except ValueError: return False try: public_key = ( key.public_key() if isinstance(key, EllipticCurvePrivateKey) else key ) public_key.verify(der_sig, msg, ECDSA(self.hash_alg())) return True except InvalidSignature: return False @overload @staticmethod def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[True]) -> JWKDict: ... @overload @staticmethod def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[False] = False) -> str: ... @staticmethod def to_jwk(key_obj: AllowedECKeys, as_dict: bool = False) -> JWKDict | str: if isinstance(key_obj, EllipticCurvePrivateKey): public_numbers = key_obj.public_key().public_numbers() elif isinstance(key_obj, EllipticCurvePublicKey): public_numbers = key_obj.public_numbers() else: raise InvalidKeyError("Not a public or private key") if isinstance(key_obj.curve, SECP256R1): crv = "P-256" elif isinstance(key_obj.curve, SECP384R1): crv = "P-384" elif isinstance(key_obj.curve, SECP521R1): crv = "P-521" elif isinstance(key_obj.curve, SECP256K1): crv = "secp256k1" else: raise InvalidKeyError(f"Invalid curve: {key_obj.curve}") obj: dict[str, Any] = { "kty": "EC", "crv": crv, "x": to_base64url_uint( public_numbers.x, bit_length=key_obj.curve.key_size, ).decode(), "y": to_base64url_uint( public_numbers.y, bit_length=key_obj.curve.key_size, ).decode(), } if isinstance(key_obj, EllipticCurvePrivateKey): obj["d"] = to_base64url_uint( key_obj.private_numbers().private_value, bit_length=key_obj.curve.key_size, ).decode() if as_dict: return obj else: return json.dumps(obj) @staticmethod def from_jwk(jwk: str | JWKDict) -> AllowedECKeys: try: if isinstance(jwk, str): obj = json.loads(jwk) elif isinstance(jwk, dict): obj = jwk else: raise ValueError except ValueError: raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "EC": raise InvalidKeyError("Not an Elliptic curve key") from None if "x" not in obj or "y" not in obj: raise InvalidKeyError("Not an Elliptic curve key") from None x = base64url_decode(obj.get("x")) y = base64url_decode(obj.get("y")) curve = obj.get("crv") curve_obj: EllipticCurve if curve == "P-256": if len(x) == len(y) == 32: curve_obj = SECP256R1() else: raise InvalidKeyError( "Coords should be 32 bytes for curve P-256" ) from None elif curve == "P-384": if len(x) == len(y) == 48: curve_obj = SECP384R1() else: raise InvalidKeyError( "Coords should be 48 bytes for curve P-384" ) from None elif curve == "P-521": if len(x) == len(y) == 66: curve_obj = SECP521R1() else: raise InvalidKeyError( "Coords should be 66 bytes for curve P-521" ) from None elif curve == "secp256k1": if len(x) == len(y) == 32: curve_obj = SECP256K1() else: raise InvalidKeyError( "Coords should be 32 bytes for curve secp256k1" ) else: raise InvalidKeyError(f"Invalid curve: {curve}") public_numbers = EllipticCurvePublicNumbers( x=int.from_bytes(x, byteorder="big"), y=int.from_bytes(y, byteorder="big"), curve=curve_obj, ) if "d" not in obj: return public_numbers.public_key() d = base64url_decode(obj.get("d")) if len(d) != len(x): raise InvalidKeyError( "D should be {} bytes for curve {}", len(x), curve ) return EllipticCurvePrivateNumbers( int.from_bytes(d, byteorder="big"), public_numbers ).private_key() class RSAPSSAlgorithm(RSAAlgorithm): """ Performs a signature using RSASSA-PSS with MGF1 """ def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: signature: bytes = key.sign( msg, padding.PSS( mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg().digest_size, ), self.hash_alg(), ) return signature def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: try: key.verify( sig, msg, padding.PSS( mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg().digest_size, ), self.hash_alg(), ) return True except InvalidSignature: return False class OKPAlgorithm(Algorithm): """ Performs signing and verification operations using EdDSA This class requires ``cryptography>=2.6`` to be installed. """ _crypto_key_types = cast( tuple[type[AllowedKeys], ...], get_args( Union[ Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey, ] ), ) def __init__(self, **kwargs: Any) -> None: pass def prepare_key(self, key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys: if not isinstance(key, (str, bytes)): self.check_crypto_key_type(key) return key key_str = key.decode("utf-8") if isinstance(key, bytes) else key key_bytes = key.encode("utf-8") if isinstance(key, str) else key loaded_key: PublicKeyTypes | PrivateKeyTypes if "-----BEGIN PUBLIC" in key_str: loaded_key = load_pem_public_key(key_bytes) elif "-----BEGIN PRIVATE" in key_str: loaded_key = load_pem_private_key(key_bytes, password=None) elif key_str[0:4] == "ssh-": loaded_key = load_ssh_public_key(key_bytes) else: raise InvalidKeyError("Not a public or private key") # Explicit check the key to prevent confusing errors from cryptography self.check_crypto_key_type(loaded_key) return cast("AllowedOKPKeys", loaded_key) def sign( self, msg: str | bytes, key: Ed25519PrivateKey | Ed448PrivateKey ) -> bytes: """ Sign a message ``msg`` using the EdDSA private key ``key`` :param str|bytes msg: Message to sign :param Ed25519PrivateKey}Ed448PrivateKey key: A :class:`.Ed25519PrivateKey` or :class:`.Ed448PrivateKey` isinstance :return bytes signature: The signature, as bytes """ msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg signature: bytes = key.sign(msg_bytes) return signature def verify( self, msg: str | bytes, key: AllowedOKPKeys, sig: str | bytes ) -> bool: """ Verify a given ``msg`` against a signature ``sig`` using the EdDSA key ``key`` :param str|bytes sig: EdDSA signature to check ``msg`` against :param str|bytes msg: Message to sign :param Ed25519PrivateKey|Ed25519PublicKey|Ed448PrivateKey|Ed448PublicKey key: A private or public EdDSA key instance :return bool verified: True if signature is valid, False if not. """ try: msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg sig_bytes = sig.encode("utf-8") if isinstance(sig, str) else sig public_key = ( key.public_key() if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)) else key ) public_key.verify(sig_bytes, msg_bytes) return True # If no exception was raised, the signature is valid. except InvalidSignature: return False @overload @staticmethod def to_jwk(key: AllowedOKPKeys, as_dict: Literal[True]) -> JWKDict: ... @overload @staticmethod def to_jwk(key: AllowedOKPKeys, as_dict: Literal[False] = False) -> str: ... @staticmethod def to_jwk(key: AllowedOKPKeys, as_dict: bool = False) -> JWKDict | str: if isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): x = key.public_bytes( encoding=Encoding.Raw, format=PublicFormat.Raw, ) crv = "Ed25519" if isinstance(key, Ed25519PublicKey) else "Ed448" obj = { "x": base64url_encode(force_bytes(x)).decode(), "kty": "OKP", "crv": crv, } if as_dict: return obj else: return json.dumps(obj) if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): d = key.private_bytes( encoding=Encoding.Raw, format=PrivateFormat.Raw, encryption_algorithm=NoEncryption(), ) x = key.public_key().public_bytes( encoding=Encoding.Raw, format=PublicFormat.Raw, ) crv = "Ed25519" if isinstance(key, Ed25519PrivateKey) else "Ed448" obj = { "x": base64url_encode(force_bytes(x)).decode(), "d": base64url_encode(force_bytes(d)).decode(), "kty": "OKP", "crv": crv, } if as_dict: return obj else: return json.dumps(obj) raise InvalidKeyError("Not a public or private key") @staticmethod def from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys: try: if isinstance(jwk, str): obj = json.loads(jwk) elif isinstance(jwk, dict): obj = jwk else: raise ValueError except ValueError: raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "OKP": raise InvalidKeyError("Not an Octet Key Pair") curve = obj.get("crv") if curve != "Ed25519" and curve != "Ed448": raise InvalidKeyError(f"Invalid curve: {curve}") if "x" not in obj: raise InvalidKeyError('OKP should have "x" parameter') x = base64url_decode(obj.get("x")) try: if "d" not in obj: if curve == "Ed25519": return Ed25519PublicKey.from_public_bytes(x) return Ed448PublicKey.from_public_bytes(x) d = base64url_decode(obj.get("d")) if curve == "Ed25519": return Ed25519PrivateKey.from_private_bytes(d) return Ed448PrivateKey.from_private_bytes(d) except ValueError as err: raise InvalidKeyError("Invalid key parameter") from err jpadilla-pyjwt-a4e1a3d/jwt/api_jwk.py000066400000000000000000000140121515505507500177410ustar00rootroot00000000000000from __future__ import annotations import json import time from collections.abc import Iterator from typing import Any from .algorithms import get_default_algorithms, has_crypto, requires_cryptography from .exceptions import ( InvalidKeyError, MissingCryptographyError, PyJWKError, PyJWKSetError, PyJWTError, ) from .types import JWKDict class PyJWK: def __init__(self, jwk_data: JWKDict, algorithm: str | None = None) -> None: """A class that represents a `JSON Web Key `_. :param jwk_data: The decoded JWK data. :type jwk_data: dict[str, typing.Any] :param algorithm: The key algorithm. If not specified, the key's ``alg`` will be used. :type algorithm: str or None :raises InvalidKeyError: If the key type (``kty``) is not found or unsupported, or if the curve (``crv``) is not found or unsupported. :raises MissingCryptographyError: If the algorithm requires ``cryptography`` to be installed and it is not available. :raises PyJWKError: If unable to find an algorithm for the key. """ self._jwk_data = jwk_data kty = self._jwk_data.get("kty", None) if not kty: raise InvalidKeyError(f"kty is not found: {self._jwk_data}") if not algorithm and isinstance(self._jwk_data, dict): algorithm = self._jwk_data.get("alg", None) if not algorithm: # Determine alg with kty (and crv). crv = self._jwk_data.get("crv", None) if kty == "EC": if crv == "P-256" or not crv: algorithm = "ES256" elif crv == "P-384": algorithm = "ES384" elif crv == "P-521": algorithm = "ES512" elif crv == "secp256k1": algorithm = "ES256K" else: raise InvalidKeyError(f"Unsupported crv: {crv}") elif kty == "RSA": algorithm = "RS256" elif kty == "oct": algorithm = "HS256" elif kty == "OKP": if not crv: raise InvalidKeyError(f"crv is not found: {self._jwk_data}") if crv == "Ed25519": algorithm = "EdDSA" else: raise InvalidKeyError(f"Unsupported crv: {crv}") else: raise InvalidKeyError(f"Unsupported kty: {kty}") if not has_crypto and algorithm in requires_cryptography: raise MissingCryptographyError( f"{algorithm} requires 'cryptography' to be installed." ) self.algorithm_name = algorithm try: self.Algorithm = get_default_algorithms()[algorithm] except KeyError: raise PyJWKError( f"Unable to find an algorithm for key: {self._jwk_data}", ) from None self.key = self.Algorithm.from_jwk(self._jwk_data) @staticmethod def from_dict(obj: JWKDict, algorithm: str | None = None) -> PyJWK: """Creates a :class:`PyJWK` object from a JSON-like dictionary. :param obj: The JWK data, as a dictionary :type obj: dict[str, typing.Any] :param algorithm: The key algorithm. If not specified, the key's ``alg`` will be used. :type algorithm: str or None :rtype: PyJWK """ return PyJWK(obj, algorithm) @staticmethod def from_json(data: str, algorithm: None = None) -> PyJWK: """Create a :class:`PyJWK` object from a JSON string. Implicitly calls :meth:`PyJWK.from_dict()`. :param str data: The JWK data, as a JSON string. :param algorithm: The key algorithm. If not specific, the key's ``alg`` will be used. :type algorithm: str or None :rtype: PyJWK """ obj = json.loads(data) return PyJWK.from_dict(obj, algorithm) @property def key_type(self) -> str | None: """The `kty` property from the JWK. :rtype: str or None """ return self._jwk_data.get("kty", None) @property def key_id(self) -> str | None: """The `kid` property from the JWK. :rtype: str or None """ return self._jwk_data.get("kid", None) @property def public_key_use(self) -> str | None: """The `use` property from the JWK. :rtype: str or None """ return self._jwk_data.get("use", None) class PyJWKSet: def __init__(self, keys: list[JWKDict]) -> None: self.keys: list[PyJWK] = [] if not keys: raise PyJWKSetError("The JWK Set did not contain any keys") if not isinstance(keys, list): raise PyJWKSetError("Invalid JWK Set value") for key in keys: try: self.keys.append(PyJWK(key)) except PyJWTError as error: if isinstance(error, MissingCryptographyError): raise error # skip unusable keys continue if len(self.keys) == 0: raise PyJWKSetError( "The JWK Set did not contain any usable keys. Perhaps 'cryptography' is not installed?" ) @staticmethod def from_dict(obj: dict[str, Any]) -> PyJWKSet: keys = obj.get("keys", []) return PyJWKSet(keys) @staticmethod def from_json(data: str) -> PyJWKSet: obj = json.loads(data) return PyJWKSet.from_dict(obj) def __getitem__(self, kid: str) -> PyJWK: for key in self.keys: if key.key_id == kid: return key raise KeyError(f"keyset has no key for kid: {kid}") def __iter__(self) -> Iterator[PyJWK]: return iter(self.keys) class PyJWTSetWithTimestamp: def __init__(self, jwk_set: PyJWKSet): self.jwk_set = jwk_set self.timestamp = time.monotonic() def get_jwk_set(self) -> PyJWKSet: return self.jwk_set def get_timestamp(self) -> float: return self.timestamp jpadilla-pyjwt-a4e1a3d/jwt/api_jws.py000066400000000000000000000336501515505507500177620ustar00rootroot00000000000000from __future__ import annotations import binascii import json import warnings from collections.abc import Sequence from typing import TYPE_CHECKING, Any from .algorithms import ( Algorithm, get_default_algorithms, has_crypto, requires_cryptography, ) from .api_jwk import PyJWK from .exceptions import ( DecodeError, InvalidAlgorithmError, InvalidKeyError, InvalidSignatureError, InvalidTokenError, ) from .utils import base64url_decode, base64url_encode from .warnings import InsecureKeyLengthWarning, RemovedInPyjwt3Warning if TYPE_CHECKING: from .algorithms import AllowedPrivateKeys, AllowedPublicKeys from .types import SigOptions _ALGORITHM_UNSET = object() class PyJWS: header_typ = "JWT" def __init__( self, algorithms: Sequence[str] | None = None, options: SigOptions | None = None, ) -> None: self._algorithms = get_default_algorithms() self._valid_algs = ( set(algorithms) if algorithms is not None else set(self._algorithms) ) # Remove algorithms that aren't on the whitelist for key in list(self._algorithms.keys()): if key not in self._valid_algs: del self._algorithms[key] self.options: SigOptions = self._get_default_options() if options is not None: self.options = {**self.options, **options} @staticmethod def _get_default_options() -> SigOptions: return {"verify_signature": True, "enforce_minimum_key_length": False} def register_algorithm(self, alg_id: str, alg_obj: Algorithm) -> None: """ Registers a new Algorithm for use when creating and verifying tokens. :param str alg_id: the ID of the Algorithm :param alg_obj: the Algorithm object :type alg_obj: Algorithm """ if alg_id in self._algorithms: raise ValueError("Algorithm already has a handler.") if not isinstance(alg_obj, Algorithm): raise TypeError("Object is not of type `Algorithm`") self._algorithms[alg_id] = alg_obj self._valid_algs.add(alg_id) def unregister_algorithm(self, alg_id: str) -> None: """ Unregisters an Algorithm for use when creating and verifying tokens :param str alg_id: the ID of the Algorithm :raises KeyError: if algorithm is not registered. """ if alg_id not in self._algorithms: raise KeyError( "The specified algorithm could not be removed" " because it is not registered." ) del self._algorithms[alg_id] self._valid_algs.remove(alg_id) def get_algorithms(self) -> list[str]: """ Returns a list of supported values for the `alg` parameter. :rtype: list[str] """ return list(self._valid_algs) def get_algorithm_by_name(self, alg_name: str) -> Algorithm: """ For a given string name, return the matching Algorithm object. Example usage: >>> jws_obj = PyJWS() >>> jws_obj.get_algorithm_by_name("RS256") :param alg_name: The name of the algorithm to retrieve :type alg_name: str :rtype: Algorithm """ try: return self._algorithms[alg_name] except KeyError as e: if not has_crypto and alg_name in requires_cryptography: raise NotImplementedError( f"Algorithm '{alg_name}' could not be found. Do you have cryptography installed?" ) from e raise NotImplementedError("Algorithm not supported") from e def encode( self, payload: bytes, key: AllowedPrivateKeys | PyJWK | str | bytes, algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, is_payload_detached: bool = False, sort_headers: bool = True, ) -> str: segments: list[bytes] = [] # declare a new var to narrow the type for type checkers if algorithm is _ALGORITHM_UNSET: if isinstance(key, PyJWK): algorithm_ = key.algorithm_name else: algorithm_ = "HS256" elif algorithm is None: if isinstance(key, PyJWK): algorithm_ = key.algorithm_name else: algorithm_ = "none" else: algorithm_ = algorithm # Prefer headers values if present to function parameters. if headers: headers_alg = headers.get("alg") if headers_alg: algorithm_ = headers["alg"] headers_b64 = headers.get("b64") if headers_b64 is False: is_payload_detached = True # Header header: dict[str, Any] = {"typ": self.header_typ, "alg": algorithm_} if headers: self._validate_headers(headers, encoding=True) header.update(headers) if not header["typ"]: del header["typ"] if is_payload_detached: header["b64"] = False elif "b64" in header: # True is the standard value for b64, so no need for it del header["b64"] json_header = json.dumps( header, separators=(",", ":"), cls=json_encoder, sort_keys=sort_headers ).encode() segments.append(base64url_encode(json_header)) if is_payload_detached: msg_payload = payload else: msg_payload = base64url_encode(payload) segments.append(msg_payload) # Segments signing_input = b".".join(segments) alg_obj = self.get_algorithm_by_name(algorithm_) if isinstance(key, PyJWK): key = key.key key = alg_obj.prepare_key(key) key_length_msg = alg_obj.check_key_length(key) if key_length_msg: if self.options.get("enforce_minimum_key_length", False): raise InvalidKeyError(key_length_msg) else: warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=2) signature = alg_obj.sign(signing_input, key) segments.append(base64url_encode(signature)) # Don't put the payload content inside the encoded token when detached if is_payload_detached: segments[1] = b"" encoded_string = b".".join(segments) return encoded_string.decode("utf-8") def decode_complete( self, jwt: str | bytes, key: AllowedPublicKeys | PyJWK | str | bytes = "", algorithms: Sequence[str] | None = None, options: SigOptions | None = None, detached_payload: bytes | None = None, **kwargs: dict[str, Any], ) -> dict[str, Any]: if kwargs: warnings.warn( "passing additional kwargs to decode_complete() is deprecated " "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, stacklevel=2, ) merged_options: SigOptions if options is None: merged_options = self.options else: merged_options = {**self.options, **options} verify_signature = merged_options["verify_signature"] if verify_signature and not algorithms and not isinstance(key, PyJWK): raise DecodeError( 'It is required that you pass in a value for the "algorithms" argument when calling decode().' ) payload, signing_input, header, signature = self._load(jwt) self._validate_headers(header) if header.get("b64", True) is False: if detached_payload is None: raise DecodeError( 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' ) payload = detached_payload signing_input = b".".join([signing_input.rsplit(b".", 1)[0], payload]) if verify_signature: self._verify_signature(signing_input, header, signature, key, algorithms) return { "payload": payload, "header": header, "signature": signature, } def decode( self, jwt: str | bytes, key: AllowedPublicKeys | PyJWK | str | bytes = "", algorithms: Sequence[str] | None = None, options: SigOptions | None = None, detached_payload: bytes | None = None, **kwargs: dict[str, Any], ) -> Any: if kwargs: warnings.warn( "passing additional kwargs to decode() is deprecated " "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, stacklevel=2, ) decoded = self.decode_complete( jwt, key, algorithms, options, detached_payload=detached_payload ) return decoded["payload"] def get_unverified_header(self, jwt: str | bytes) -> dict[str, Any]: """Returns back the JWT header parameters as a `dict` Note: The signature is not verified so the header parameters should not be fully trusted until signature verification is complete """ headers = self._load(jwt)[2] self._validate_headers(headers) return headers def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict[str, Any], bytes]: if isinstance(jwt, str): jwt = jwt.encode("utf-8") if not isinstance(jwt, bytes): raise DecodeError(f"Invalid token type. Token must be a {bytes}") try: signing_input, crypto_segment = jwt.rsplit(b".", 1) header_segment, payload_segment = signing_input.split(b".", 1) except ValueError as err: raise DecodeError("Not enough segments") from err try: header_data = base64url_decode(header_segment) except (TypeError, binascii.Error) as err: raise DecodeError("Invalid header padding") from err try: header: dict[str, Any] = json.loads(header_data) except ValueError as e: raise DecodeError(f"Invalid header string: {e}") from e if not isinstance(header, dict): raise DecodeError("Invalid header string: must be a json object") try: payload = base64url_decode(payload_segment) except (TypeError, binascii.Error) as err: raise DecodeError("Invalid payload padding") from err try: signature = base64url_decode(crypto_segment) except (TypeError, binascii.Error) as err: raise DecodeError("Invalid crypto padding") from err return (payload, signing_input, header, signature) def _verify_signature( self, signing_input: bytes, header: dict[str, Any], signature: bytes, key: AllowedPublicKeys | PyJWK | str | bytes = "", algorithms: Sequence[str] | None = None, ) -> None: if algorithms is None and isinstance(key, PyJWK): algorithms = [key.algorithm_name] try: alg = header["alg"] except KeyError: raise InvalidAlgorithmError("Algorithm not specified") from None if not alg or (algorithms is not None and alg not in algorithms): raise InvalidAlgorithmError("The specified alg value is not allowed") if isinstance(key, PyJWK): alg_obj = key.Algorithm prepared_key = key.key else: try: alg_obj = self.get_algorithm_by_name(alg) except NotImplementedError as e: raise InvalidAlgorithmError("Algorithm not supported") from e prepared_key = alg_obj.prepare_key(key) key_length_msg = alg_obj.check_key_length(prepared_key) if key_length_msg: if self.options.get("enforce_minimum_key_length", False): raise InvalidKeyError(key_length_msg) else: warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=4) if not alg_obj.verify(signing_input, prepared_key, signature): raise InvalidSignatureError("Signature verification failed") # Extensions that PyJWT actually understands and supports _supported_crit: set[str] = {"b64"} def _validate_headers( self, headers: dict[str, Any], *, encoding: bool = False ) -> None: if "kid" in headers: self._validate_kid(headers["kid"]) if not encoding and "crit" in headers: self._validate_crit(headers) def _validate_kid(self, kid: Any) -> None: if not isinstance(kid, str): raise InvalidTokenError("Key ID header parameter must be a string") def _validate_crit(self, headers: dict[str, Any]) -> None: crit = headers["crit"] if not isinstance(crit, list) or len(crit) == 0: raise InvalidTokenError("Invalid 'crit' header: must be a non-empty list") for ext in crit: if not isinstance(ext, str): raise InvalidTokenError("Invalid 'crit' header: values must be strings") if ext not in self._supported_crit: raise InvalidTokenError(f"Unsupported critical extension: {ext}") if ext not in headers: raise InvalidTokenError( f"Critical extension '{ext}' is missing from headers" ) _jws_global_obj = PyJWS() encode = _jws_global_obj.encode decode_complete = _jws_global_obj.decode_complete decode = _jws_global_obj.decode register_algorithm = _jws_global_obj.register_algorithm unregister_algorithm = _jws_global_obj.unregister_algorithm get_algorithm_by_name = _jws_global_obj.get_algorithm_by_name get_unverified_header = _jws_global_obj.get_unverified_header jpadilla-pyjwt-a4e1a3d/jwt/api_jwt.py000066400000000000000000000526721515505507500177700ustar00rootroot00000000000000from __future__ import annotations import json import os import warnings from calendar import timegm from collections.abc import Container, Iterable, Sequence from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Union, cast from .api_jws import PyJWS, _ALGORITHM_UNSET, _jws_global_obj from .exceptions import ( DecodeError, ExpiredSignatureError, ImmatureSignatureError, InvalidAudienceError, InvalidIssuedAtError, InvalidIssuerError, InvalidJTIError, InvalidSubjectError, MissingRequiredClaimError, ) from .warnings import RemovedInPyjwt3Warning if TYPE_CHECKING or bool(os.getenv("SPHINX_BUILD", "")): import sys if sys.version_info >= (3, 10): from typing import TypeAlias else: # Python 3.9 and lower from typing_extensions import TypeAlias from .algorithms import AllowedPrivateKeys, AllowedPublicKeys from .api_jwk import PyJWK from .types import FullOptions, Options, SigOptions AllowedPrivateKeyTypes: TypeAlias = Union[AllowedPrivateKeys, PyJWK, str, bytes] AllowedPublicKeyTypes: TypeAlias = Union[AllowedPublicKeys, PyJWK, str, bytes] class PyJWT: def __init__(self, options: Options | None = None) -> None: self.options: FullOptions self.options = self._get_default_options() if options is not None: self.options = self._merge_options(options) self._jws = PyJWS(options=self._get_sig_options()) @staticmethod def _get_default_options() -> FullOptions: return { "verify_signature": True, "verify_exp": True, "verify_nbf": True, "verify_iat": True, "verify_aud": True, "verify_iss": True, "verify_sub": True, "verify_jti": True, "require": [], "strict_aud": False, "enforce_minimum_key_length": False, } def _get_sig_options(self) -> SigOptions: return { "verify_signature": self.options["verify_signature"], "enforce_minimum_key_length": self.options.get( "enforce_minimum_key_length", False ), } def _merge_options(self, options: Options | None = None) -> FullOptions: if options is None: return self.options # (defensive) set defaults for verify_x to False if verify_signature is False if not options.get("verify_signature", True): options["verify_exp"] = options.get("verify_exp", False) options["verify_nbf"] = options.get("verify_nbf", False) options["verify_iat"] = options.get("verify_iat", False) options["verify_aud"] = options.get("verify_aud", False) options["verify_iss"] = options.get("verify_iss", False) options["verify_sub"] = options.get("verify_sub", False) options["verify_jti"] = options.get("verify_jti", False) return {**self.options, **options} def encode( self, payload: dict[str, Any], key: AllowedPrivateKeyTypes, algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, sort_headers: bool = True, ) -> str: """Encode the ``payload`` as JSON Web Token. :param payload: JWT claims, e.g. ``dict(iss=..., aud=..., sub=...)`` :type payload: dict[str, typing.Any] :param key: a key suitable for the chosen algorithm: * for **asymmetric algorithms**: PEM-formatted private key, a multiline string * for **symmetric algorithms**: plain string, sufficiently long for security :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPrivateKeys` :param algorithm: algorithm to sign the token with, e.g. ``"ES256"``. If ``headers`` includes ``alg``, it will be preferred to this parameter. If ``key`` is a :class:`PyJWK` object, by default the key algorithm will be used. :type algorithm: str or None :param headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")``. :type headers: dict[str, typing.Any] or None :param json_encoder: custom JSON encoder for ``payload`` and ``headers`` :type json_encoder: json.JSONEncoder or None :rtype: str :returns: a JSON Web Token :raises TypeError: if ``payload`` is not a ``dict`` """ # Check that we get a dict if not isinstance(payload, dict): raise TypeError( "Expecting a dict object, as JWT only supports " "JSON objects as payloads." ) # Payload payload = payload.copy() for time_claim in ["exp", "iat", "nbf"]: # Convert datetime to a intDate value in known time-format claims if isinstance(payload.get(time_claim), datetime): payload[time_claim] = timegm(payload[time_claim].utctimetuple()) # Issue #1039, iss being set to non-string if "iss" in payload and not isinstance(payload["iss"], str): raise TypeError("Issuer (iss) must be a string.") json_payload = self._encode_payload( payload, headers=headers, json_encoder=json_encoder, ) return self._jws.encode( json_payload, key, algorithm, headers, json_encoder, sort_headers=sort_headers, ) def _encode_payload( self, payload: dict[str, Any], headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, ) -> bytes: """ Encode a given payload to the bytes to be signed. This method is intended to be overridden by subclasses that need to encode the payload in a different way, e.g. compress the payload. """ return json.dumps( payload, separators=(",", ":"), cls=json_encoder, ).encode("utf-8") def decode_complete( self, jwt: str | bytes, key: AllowedPublicKeyTypes = "", algorithms: Sequence[str] | None = None, options: Options | None = None, # deprecated arg, remove in pyjwt3 verify: bool | None = None, # could be used as passthrough to api_jws, consider removal in pyjwt3 detached_payload: bytes | None = None, # passthrough arguments to _validate_claims # consider putting in options audience: str | Iterable[str] | None = None, issuer: str | Container[str] | None = None, subject: str | None = None, leeway: float | timedelta = 0, # kwargs **kwargs: Any, ) -> dict[str, Any]: """Identical to ``jwt.decode`` except for return value which is a dictionary containing the token header (JOSE Header), the token payload (JWT Payload), and token signature (JWT Signature) on the keys "header", "payload", and "signature" respectively. :param jwt: the token to be decoded :type jwt: str or bytes :param key: the key suitable for the allowed algorithm :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPublicKeys` :param algorithms: allowed algorithms, e.g. ``["ES256"]`` .. warning:: Do **not** compute the ``algorithms`` parameter based on the ``alg`` from the token itself, or on any other data that an attacker may be able to influence, as that might expose you to various vulnerabilities (see `RFC 8725 §2.1 `_). Instead, either hard-code a fixed value for ``algorithms``, or configure it in the same place you configure the ``key``. Make sure not to mix symmetric and asymmetric algorithms that interpret the ``key`` in different ways (e.g. HS\\* and RS\\*). :type algorithms: typing.Sequence[str] or None :param jwt.types.Options options: extended decoding and validation options Refer to :py:class:`jwt.types.Options` for more information. :param audience: optional, the value for ``verify_aud`` check :type audience: str or typing.Iterable[str] or None :param issuer: optional, the value for ``verify_iss`` check :type issuer: str or typing.Container[str] or None :param leeway: a time margin in seconds for the expiration check :type leeway: float or datetime.timedelta :rtype: dict[str, typing.Any] :returns: Decoded JWT with the JOSE Header on the key ``header``, the JWS Payload on the key ``payload``, and the JWS Signature on the key ``signature``. """ if kwargs: warnings.warn( "passing additional kwargs to decode_complete() is deprecated " "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, stacklevel=2, ) if options is None: verify_signature = True else: verify_signature = options.get("verify_signature", True) # If the user has set the legacy `verify` argument, and it doesn't match # what the relevant `options` entry for the argument is, inform the user # that they're likely making a mistake. if verify is not None and verify != verify_signature: warnings.warn( "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. " "The equivalent is setting `verify_signature` to False in the `options` dictionary. " "This invocation has a mismatch between the kwarg and the option entry.", category=DeprecationWarning, stacklevel=2, ) merged_options = self._merge_options(options) sig_options: SigOptions = { "verify_signature": verify_signature, } decoded = self._jws.decode_complete( jwt, key=key, algorithms=algorithms, options=sig_options, detached_payload=detached_payload, ) payload = self._decode_payload(decoded) self._validate_claims( payload, merged_options, audience=audience, issuer=issuer, leeway=leeway, subject=subject, ) decoded["payload"] = payload return decoded def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]: """ Decode the payload from a JWS dictionary (payload, signature, header). This method is intended to be overridden by subclasses that need to decode the payload in a different way, e.g. decompress compressed payloads. """ try: payload: dict[str, Any] = json.loads(decoded["payload"]) except ValueError as e: raise DecodeError(f"Invalid payload string: {e}") from e if not isinstance(payload, dict): raise DecodeError("Invalid payload string: must be a json object") return payload def decode( self, jwt: str | bytes, key: AllowedPublicKeys | PyJWK | str | bytes = "", algorithms: Sequence[str] | None = None, options: Options | None = None, # deprecated arg, remove in pyjwt3 verify: bool | None = None, # could be used as passthrough to api_jws, consider removal in pyjwt3 detached_payload: bytes | None = None, # passthrough arguments to _validate_claims # consider putting in options audience: str | Iterable[str] | None = None, subject: str | None = None, issuer: str | Container[str] | None = None, leeway: float | timedelta = 0, # kwargs **kwargs: Any, ) -> dict[str, Any]: """Verify the ``jwt`` token signature and return the token claims. :param jwt: the token to be decoded :type jwt: str or bytes :param key: the key suitable for the allowed algorithm :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPublicKeys` :param algorithms: allowed algorithms, e.g. ``["ES256"]`` If ``key`` is a :class:`PyJWK` object, allowed algorithms will default to the key algorithm. .. warning:: Do **not** compute the ``algorithms`` parameter based on the ``alg`` from the token itself, or on any other data that an attacker may be able to influence, as that might expose you to various vulnerabilities (see `RFC 8725 §2.1 `_). Instead, either hard-code a fixed value for ``algorithms``, or configure it in the same place you configure the ``key``. Make sure not to mix symmetric and asymmetric algorithms that interpret the ``key`` in different ways (e.g. HS\\* and RS\\*). :type algorithms: typing.Sequence[str] or None :param jwt.types.Options options: extended decoding and validation options Refer to :py:class:`jwt.types.Options` for more information. :param audience: optional, the value for ``verify_aud`` check :type audience: str or typing.Iterable[str] or None :param subject: optional, the value for ``verify_sub`` check :type subject: str or None :param issuer: optional, the value for ``verify_iss`` check :type issuer: str or typing.Container[str] or None :param leeway: a time margin in seconds for the expiration check :type leeway: float or datetime.timedelta :rtype: dict[str, typing.Any] :returns: the JWT claims """ if kwargs: warnings.warn( "passing additional kwargs to decode() is deprecated " "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, stacklevel=2, ) decoded = self.decode_complete( jwt, key, algorithms, options, verify=verify, detached_payload=detached_payload, audience=audience, subject=subject, issuer=issuer, leeway=leeway, ) return cast(dict[str, Any], decoded["payload"]) def _validate_claims( self, payload: dict[str, Any], options: FullOptions, audience: Iterable[str] | str | None = None, issuer: Container[str] | str | None = None, subject: str | None = None, leeway: float | timedelta = 0, ) -> None: if isinstance(leeway, timedelta): leeway = leeway.total_seconds() if audience is not None and not isinstance(audience, (str, Iterable)): raise TypeError("audience must be a string, iterable or None") self._validate_required_claims(payload, options["require"]) now = datetime.now(tz=timezone.utc).timestamp() if "iat" in payload and options["verify_iat"]: self._validate_iat(payload, now, leeway) if "nbf" in payload and options["verify_nbf"]: self._validate_nbf(payload, now, leeway) if "exp" in payload and options["verify_exp"]: self._validate_exp(payload, now, leeway) if options["verify_iss"]: self._validate_iss(payload, issuer) if options["verify_aud"]: self._validate_aud( payload, audience, strict=options.get("strict_aud", False) ) if options["verify_sub"]: self._validate_sub(payload, subject) if options["verify_jti"]: self._validate_jti(payload) def _validate_required_claims( self, payload: dict[str, Any], claims: Iterable[str], ) -> None: for claim in claims: if payload.get(claim) is None: raise MissingRequiredClaimError(claim) def _validate_sub( self, payload: dict[str, Any], subject: str | None = None ) -> None: """ Checks whether "sub" if in the payload is valid or not. This is an Optional claim :param payload(dict): The payload which needs to be validated :param subject(str): The subject of the token """ if "sub" not in payload: return if not isinstance(payload["sub"], str): raise InvalidSubjectError("Subject must be a string") if subject is not None: if payload.get("sub") != subject: raise InvalidSubjectError("Invalid subject") def _validate_jti(self, payload: dict[str, Any]) -> None: """ Checks whether "jti" if in the payload is valid or not This is an Optional claim :param payload(dict): The payload which needs to be validated """ if "jti" not in payload: return if not isinstance(payload.get("jti"), str): raise InvalidJTIError("JWT ID must be a string") def _validate_iat( self, payload: dict[str, Any], now: float, leeway: float, ) -> None: try: iat = int(payload["iat"]) except ValueError: raise InvalidIssuedAtError( "Issued At claim (iat) must be an integer." ) from None if iat > (now + leeway): raise ImmatureSignatureError("The token is not yet valid (iat)") def _validate_nbf( self, payload: dict[str, Any], now: float, leeway: float, ) -> None: try: nbf = int(payload["nbf"]) except ValueError: raise DecodeError("Not Before claim (nbf) must be an integer.") from None if nbf > (now + leeway): raise ImmatureSignatureError("The token is not yet valid (nbf)") def _validate_exp( self, payload: dict[str, Any], now: float, leeway: float, ) -> None: try: exp = int(payload["exp"]) except ValueError: raise DecodeError( "Expiration Time claim (exp) must be an integer." ) from None if exp <= (now - leeway): raise ExpiredSignatureError("Signature has expired") def _validate_aud( self, payload: dict[str, Any], audience: str | Iterable[str] | None, *, strict: bool = False, ) -> None: if audience is None: if "aud" not in payload or not payload["aud"]: return # Application did not specify an audience, but # the token has the 'aud' claim raise InvalidAudienceError("Invalid audience") if "aud" not in payload or not payload["aud"]: # Application specified an audience, but it could not be # verified since the token does not contain a claim. raise MissingRequiredClaimError("aud") audience_claims = payload["aud"] # In strict mode, we forbid list matching: the supplied audience # must be a string, and it must exactly match the audience claim. if strict: # Only a single audience is allowed in strict mode. if not isinstance(audience, str): raise InvalidAudienceError("Invalid audience (strict)") # Only a single audience claim is allowed in strict mode. if not isinstance(audience_claims, str): raise InvalidAudienceError("Invalid claim format in token (strict)") if audience != audience_claims: raise InvalidAudienceError("Audience doesn't match (strict)") return if isinstance(audience_claims, str): audience_claims = [audience_claims] if not isinstance(audience_claims, list): raise InvalidAudienceError("Invalid claim format in token") if any(not isinstance(c, str) for c in audience_claims): raise InvalidAudienceError("Invalid claim format in token") if isinstance(audience, str): audience = [audience] if all(aud not in audience_claims for aud in audience): raise InvalidAudienceError("Audience doesn't match") def _validate_iss( self, payload: dict[str, Any], issuer: Container[str] | str | None ) -> None: if issuer is None: return if "iss" not in payload: raise MissingRequiredClaimError("iss") iss = payload["iss"] if not isinstance(iss, str): raise InvalidIssuerError("Payload Issuer (iss) must be a string") if isinstance(issuer, str): if iss != issuer: raise InvalidIssuerError("Invalid issuer") else: try: if iss not in issuer: raise InvalidIssuerError("Invalid issuer") except TypeError: raise InvalidIssuerError( 'Issuer param must be "str" or "Container[str]"' ) from None _jwt_global_obj = PyJWT() _jwt_global_obj._jws = _jws_global_obj encode = _jwt_global_obj.encode decode_complete = _jwt_global_obj.decode_complete decode = _jwt_global_obj.decode jpadilla-pyjwt-a4e1a3d/jwt/exceptions.py000066400000000000000000000045071515505507500205060ustar00rootroot00000000000000class PyJWTError(Exception): """ Base class for all exceptions """ pass class InvalidTokenError(PyJWTError): """Base exception when ``decode()`` fails on a token""" pass class DecodeError(InvalidTokenError): """Raised when a token cannot be decoded because it failed validation""" pass class InvalidSignatureError(DecodeError): """Raised when a token's signature doesn't match the one provided as part of the token.""" pass class ExpiredSignatureError(InvalidTokenError): """Raised when a token's ``exp`` claim indicates that it has expired""" pass class InvalidAudienceError(InvalidTokenError): """Raised when a token's ``aud`` claim does not match one of the expected audience values""" pass class InvalidIssuerError(InvalidTokenError): """Raised when a token's ``iss`` claim does not match the expected issuer""" pass class InvalidIssuedAtError(InvalidTokenError): """Raised when a token's ``iat`` claim is non-numeric""" pass class ImmatureSignatureError(InvalidTokenError): """Raised when a token's ``nbf`` or ``iat`` claims represent a time in the future""" pass class InvalidKeyError(PyJWTError): """Raised when the specified key is not in the proper format""" pass class InvalidAlgorithmError(InvalidTokenError): """Raised when the specified algorithm is not recognized by PyJWT""" pass class MissingRequiredClaimError(InvalidTokenError): """Raised when a claim that is required to be present is not contained in the claimset""" def __init__(self, claim: str) -> None: self.claim = claim def __str__(self) -> str: return f'Token is missing the "{self.claim}" claim' class PyJWKError(PyJWTError): pass class MissingCryptographyError(PyJWKError): """Raised if the algorithm requires ``cryptography`` to be installed and it is not available.""" pass class PyJWKSetError(PyJWTError): pass class PyJWKClientError(PyJWTError): pass class PyJWKClientConnectionError(PyJWKClientError): pass class InvalidSubjectError(InvalidTokenError): """Raised when a token's ``sub`` claim is not a string or doesn't match the expected ``subject``""" pass class InvalidJTIError(InvalidTokenError): """Raised when a token's ``jti`` claim is not a string""" pass jpadilla-pyjwt-a4e1a3d/jwt/help.py000066400000000000000000000033701515505507500172520ustar00rootroot00000000000000import json import platform import sys from . import __version__ as pyjwt_version try: import cryptography cryptography_version = cryptography.__version__ except ModuleNotFoundError: cryptography_version = "" def info() -> dict[str, dict[str, str]]: """ Generate information for a bug report. Based on the requests package help utility module. """ try: platform_info = { "system": platform.system(), "release": platform.release(), } except OSError: platform_info = {"system": "Unknown", "release": "Unknown"} implementation = platform.python_implementation() if implementation == "CPython": implementation_version = platform.python_version() elif implementation == "PyPy": pypy_version_info = sys.pypy_version_info # type: ignore[attr-defined] implementation_version = ( f"{pypy_version_info.major}." f"{pypy_version_info.minor}." f"{pypy_version_info.micro}" ) if pypy_version_info.releaselevel != "final": implementation_version = "".join( [ implementation_version, pypy_version_info.releaselevel, ] ) else: implementation_version = "Unknown" return { "platform": platform_info, "implementation": { "name": implementation, "version": implementation_version, }, "cryptography": {"version": cryptography_version}, "pyjwt": {"version": pyjwt_version}, } def main() -> None: """Pretty-print the bug information as JSON.""" print(json.dumps(info(), sort_keys=True, indent=2)) if __name__ == "__main__": main() jpadilla-pyjwt-a4e1a3d/jwt/jwk_set_cache.py000066400000000000000000000017011515505507500211070ustar00rootroot00000000000000import time from typing import Optional from .api_jwk import PyJWKSet, PyJWTSetWithTimestamp class JWKSetCache: def __init__(self, lifespan: float) -> None: self.jwk_set_with_timestamp: Optional[PyJWTSetWithTimestamp] = None self.lifespan = lifespan def put(self, jwk_set: PyJWKSet) -> None: if jwk_set is not None: self.jwk_set_with_timestamp = PyJWTSetWithTimestamp(jwk_set) else: # clear cache self.jwk_set_with_timestamp = None def get(self) -> Optional[PyJWKSet]: if self.jwk_set_with_timestamp is None or self.is_expired(): return None return self.jwk_set_with_timestamp.get_jwk_set() def is_expired(self) -> bool: return ( self.jwk_set_with_timestamp is not None and self.lifespan > -1 and time.monotonic() > self.jwk_set_with_timestamp.get_timestamp() + self.lifespan ) jpadilla-pyjwt-a4e1a3d/jwt/jwks_client.py000066400000000000000000000206271515505507500206420ustar00rootroot00000000000000from __future__ import annotations import json import urllib.request from functools import lru_cache from ssl import SSLContext from typing import Any from urllib.error import HTTPError, URLError from .api_jwk import PyJWK, PyJWKSet from .api_jwt import decode_complete as decode_token from .exceptions import PyJWKClientConnectionError, PyJWKClientError from .jwk_set_cache import JWKSetCache class PyJWKClient: def __init__( self, uri: str, cache_keys: bool = False, max_cached_keys: int = 16, cache_jwk_set: bool = True, lifespan: float = 300, headers: dict[str, Any] | None = None, timeout: float = 30, ssl_context: SSLContext | None = None, ): """A client for retrieving signing keys from a JWKS endpoint. ``PyJWKClient`` uses a two-tier caching system to avoid unnecessary network requests: **Tier 1 — JWK Set cache** (enabled by default): Caches the entire JSON Web Key Set response from the endpoint. Controlled by: - ``cache_jwk_set``: Set to ``True`` (the default) to enable this cache. When enabled, the JWK Set is fetched from the network only when the cache is empty or expired. - ``lifespan``: Time in seconds before the cached JWK Set expires. Defaults to ``300`` (5 minutes). Must be greater than 0. **Tier 2 — Signing key cache** (disabled by default): Caches individual signing keys (looked up by ``kid``) using an LRU cache with **no time-based expiration**. Keys are evicted only when the cache reaches its maximum size. Controlled by: - ``cache_keys``: Set to ``True`` to enable this cache. Defaults to ``False``. - ``max_cached_keys``: Maximum number of signing keys to keep in the LRU cache. Defaults to ``16``. :param uri: The URL of the JWKS endpoint. :type uri: str :param cache_keys: Enable the per-key LRU cache (Tier 2). :type cache_keys: bool :param max_cached_keys: Max entries in the signing key LRU cache. :type max_cached_keys: int :param cache_jwk_set: Enable the JWK Set response cache (Tier 1). :type cache_jwk_set: bool :param lifespan: TTL in seconds for the JWK Set cache. :type lifespan: float :param headers: Optional HTTP headers to include in requests. :type headers: dict or None :param timeout: HTTP request timeout in seconds. :type timeout: float :param ssl_context: Optional SSL context for the request. :type ssl_context: ssl.SSLContext or None """ if headers is None: headers = {} self.uri = uri self.jwk_set_cache: JWKSetCache | None = None self.headers = headers self.timeout = timeout self.ssl_context = ssl_context if cache_jwk_set: # Init jwt set cache with default or given lifespan. # Default lifespan is 300 seconds (5 minutes). if lifespan <= 0: raise PyJWKClientError( f'Lifespan must be greater than 0, the input is "{lifespan}"' ) self.jwk_set_cache = JWKSetCache(lifespan) else: self.jwk_set_cache = None if cache_keys: # Cache signing keys get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # Ignore mypy (https://github.com/python/mypy/issues/2427) self.get_signing_key = get_signing_key # type: ignore[method-assign] def fetch_data(self) -> Any: """Fetch the JWK Set from the JWKS endpoint. Makes an HTTP request to the configured ``uri`` and returns the parsed JSON response. If the JWK Set cache is enabled, the response is stored in the cache. :returns: The parsed JWK Set as a dictionary. :raises PyJWKClientConnectionError: If the HTTP request fails. """ jwk_set: Any = None try: r = urllib.request.Request(url=self.uri, headers=self.headers) with urllib.request.urlopen( r, timeout=self.timeout, context=self.ssl_context ) as response: jwk_set = json.load(response) except (URLError, TimeoutError) as e: if isinstance(e, HTTPError): e.close() raise PyJWKClientConnectionError( f'Fail to fetch data from the url, err: "{e}"' ) from e else: return jwk_set finally: if self.jwk_set_cache is not None: self.jwk_set_cache.put(jwk_set) def get_jwk_set(self, refresh: bool = False) -> PyJWKSet: """Return the JWK Set, using the cache when available. :param refresh: Force a fresh fetch from the endpoint, bypassing the cache. :type refresh: bool :returns: The JWK Set. :rtype: PyJWKSet :raises PyJWKClientError: If the endpoint does not return a JSON object. """ data = None if self.jwk_set_cache is not None and not refresh: data = self.jwk_set_cache.get() if data is None: data = self.fetch_data() if not isinstance(data, dict): raise PyJWKClientError("The JWKS endpoint did not return a JSON object") return PyJWKSet.from_dict(data) def get_signing_keys(self, refresh: bool = False) -> list[PyJWK]: """Return all signing keys from the JWK Set. Filters the JWK Set to keys whose ``use`` is ``"sig"`` (or unspecified) and that have a ``kid``. :param refresh: Force a fresh fetch from the endpoint, bypassing the cache. :type refresh: bool :returns: A list of signing keys. :rtype: list[PyJWK] :raises PyJWKClientError: If no signing keys are found. """ jwk_set = self.get_jwk_set(refresh) signing_keys = [ jwk_set_key for jwk_set_key in jwk_set.keys if jwk_set_key.public_key_use in ["sig", None] and jwk_set_key.key_id ] if not signing_keys: raise PyJWKClientError("The JWKS endpoint did not contain any signing keys") return signing_keys def get_signing_key(self, kid: str) -> PyJWK: """Return the signing key matching the given ``kid``. If no match is found in the current JWK Set, the set is refreshed from the endpoint and the lookup is retried once. :param kid: The key ID to look up. :type kid: str :returns: The matching signing key. :rtype: PyJWK :raises PyJWKClientError: If no matching key is found after refreshing. """ signing_keys = self.get_signing_keys() signing_key = self.match_kid(signing_keys, kid) if not signing_key: # If no matching signing key from the jwk set, refresh the jwk set and try again. signing_keys = self.get_signing_keys(refresh=True) signing_key = self.match_kid(signing_keys, kid) if not signing_key: raise PyJWKClientError( f'Unable to find a signing key that matches: "{kid}"' ) return signing_key def get_signing_key_from_jwt(self, token: str | bytes) -> PyJWK: """Return the signing key for a JWT by reading its ``kid`` header. Extracts the ``kid`` from the token's unverified header and delegates to :meth:`get_signing_key`. :param token: The encoded JWT. :type token: str or bytes :returns: The matching signing key. :rtype: PyJWK """ unverified = decode_token(token, options={"verify_signature": False}) header = unverified["header"] return self.get_signing_key(header.get("kid")) @staticmethod def match_kid(signing_keys: list[PyJWK], kid: str) -> PyJWK | None: """Find a key in *signing_keys* that matches *kid*. :param signing_keys: The list of keys to search. :type signing_keys: list[PyJWK] :param kid: The key ID to match. :type kid: str :returns: The matching key, or ``None`` if not found. :rtype: PyJWK or None """ signing_key = None for key in signing_keys: if key.key_id == kid: signing_key = key break return signing_key jpadilla-pyjwt-a4e1a3d/jwt/py.typed000066400000000000000000000000001515505507500174320ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/jwt/types.py000066400000000000000000000061461515505507500174720ustar00rootroot00000000000000from typing import Any, Callable, TypedDict JWKDict = dict[str, Any] HashlibHash = Callable[..., Any] class SigOptions(TypedDict, total=False): """Options for PyJWS class (TypedDict). Note that this is a smaller set of options than for :py:func:`jwt.decode()`.""" verify_signature: bool """verify the JWT cryptographic signature""" enforce_minimum_key_length: bool """Default: ``False``. Raise :py:class:`jwt.exceptions.InvalidKeyError` instead of warning when keys are below minimum recommended length.""" class Options(TypedDict, total=False): """Options for :py:func:`jwt.decode()` and :py:func:`jwt.decode_complete()` (TypedDict). .. warning:: Some claims, such as ``exp``, ``iat``, ``jti``, ``nbf``, and ``sub``, will only be verified if present. Please refer to the documentation below for which ones, and make sure to include them in the ``require`` param if you want to make sure that they are always present (and therefore always verified if ``verify_{claim} = True`` for that claim). """ verify_signature: bool """Default: ``True``. Verify the JWT cryptographic signature.""" require: list[str] """Default: ``[]``. List of claims that must be present. Example: ``require=["exp", "iat", "nbf"]``. **Only verifies that the claims exists**. Does not verify that the claims are valid.""" strict_aud: bool """Default: ``False``. (requires ``verify_aud=True``) Check that the ``aud`` claim is a single value (not a list), and matches ``audience`` exactly.""" verify_aud: bool """Default: ``verify_signature``. Check that ``aud`` (audience) claim matches ``audience``.""" verify_exp: bool """Default: ``verify_signature``. Check that ``exp`` (expiration) claim value is in the future (if present in payload). """ verify_iat: bool """Default: ``verify_signature``. Check that ``iat`` (issued at) claim value is an integer (if present in payload). """ verify_iss: bool """Default: ``verify_signature``. Check that ``iss`` (issuer) claim matches ``issuer``. """ verify_jti: bool """Default: ``verify_signature``. Check that ``jti`` (JWT ID) claim is a string (if present in payload). """ verify_nbf: bool """Default: ``verify_signature``. Check that ``nbf`` (not before) claim value is in the past (if present in payload). """ verify_sub: bool """Default: ``verify_signature``. Check that ``sub`` (subject) claim is a string and matches ``subject`` (if present in payload). """ enforce_minimum_key_length: bool """Default: ``False``. Raise :py:class:`jwt.exceptions.InvalidKeyError` instead of warning when keys are below minimum recommended length.""" # The only difference between Options and FullOptions is that FullOptions # required _every_ value to be there; Options doesn't require any class FullOptions(TypedDict): verify_signature: bool require: list[str] strict_aud: bool verify_aud: bool verify_exp: bool verify_iat: bool verify_iss: bool verify_jti: bool verify_nbf: bool verify_sub: bool enforce_minimum_key_length: bool jpadilla-pyjwt-a4e1a3d/jwt/utils.py000066400000000000000000000070701515505507500174630ustar00rootroot00000000000000import base64 import binascii import re from typing import Optional, Union try: from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve from cryptography.hazmat.primitives.asymmetric.utils import ( decode_dss_signature, encode_dss_signature, ) except ModuleNotFoundError: pass def force_bytes(value: Union[bytes, str]) -> bytes: if isinstance(value, str): return value.encode("utf-8") elif isinstance(value, bytes): return value else: raise TypeError("Expected a string value") def base64url_decode(input: Union[bytes, str]) -> bytes: input_bytes = force_bytes(input) rem = len(input_bytes) % 4 if rem > 0: input_bytes += b"=" * (4 - rem) return base64.urlsafe_b64decode(input_bytes) def base64url_encode(input: bytes) -> bytes: return base64.urlsafe_b64encode(input).replace(b"=", b"") def to_base64url_uint(val: int, *, bit_length: Optional[int] = None) -> bytes: if val < 0: raise ValueError("Must be a positive integer") int_bytes = bytes_from_int(val, bit_length=bit_length) if len(int_bytes) == 0: int_bytes = b"\x00" return base64url_encode(int_bytes) def from_base64url_uint(val: Union[bytes, str]) -> int: data = base64url_decode(force_bytes(val)) return int.from_bytes(data, byteorder="big") def number_to_bytes(num: int, num_bytes: int) -> bytes: padded_hex = "%0*x" % (2 * num_bytes, num) return binascii.a2b_hex(padded_hex.encode("ascii")) def bytes_to_number(string: bytes) -> int: return int(binascii.b2a_hex(string), 16) def bytes_from_int(val: int, *, bit_length: Optional[int] = None) -> bytes: if bit_length is None: bit_length = val.bit_length() byte_length = (bit_length + 7) // 8 return val.to_bytes(byte_length, "big", signed=False) def der_to_raw_signature(der_sig: bytes, curve: "EllipticCurve") -> bytes: num_bits = curve.key_size num_bytes = (num_bits + 7) // 8 r, s = decode_dss_signature(der_sig) return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes) def raw_to_der_signature(raw_sig: bytes, curve: "EllipticCurve") -> bytes: num_bits = curve.key_size num_bytes = (num_bits + 7) // 8 if len(raw_sig) != 2 * num_bytes: raise ValueError("Invalid signature") r = bytes_to_number(raw_sig[:num_bytes]) s = bytes_to_number(raw_sig[num_bytes:]) return bytes(encode_dss_signature(r, s)) # Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252 _PEMS = { b"CERTIFICATE", b"TRUSTED CERTIFICATE", b"PRIVATE KEY", b"PUBLIC KEY", b"ENCRYPTED PRIVATE KEY", b"OPENSSH PRIVATE KEY", b"DSA PRIVATE KEY", b"RSA PRIVATE KEY", b"RSA PUBLIC KEY", b"EC PRIVATE KEY", b"DH PARAMETERS", b"NEW CERTIFICATE REQUEST", b"CERTIFICATE REQUEST", b"SSH2 PUBLIC KEY", b"SSH2 ENCRYPTED PRIVATE KEY", b"X509 CRL", } _PEM_RE = re.compile( b"----[- ]BEGIN (" + b"|".join(_PEMS) + b""")[- ]----\r? .+?\r? ----[- ]END \\1[- ]----\r?\n?""", re.DOTALL, ) def is_pem_format(key: bytes) -> bool: return bool(_PEM_RE.search(key)) # Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46 _SSH_KEY_FORMATS = ( b"ssh-ed25519", b"ssh-rsa", b"ssh-dss", b"ecdsa-sha2-nistp256", b"ecdsa-sha2-nistp384", b"ecdsa-sha2-nistp521", ) def is_ssh_key(key: bytes) -> bool: return key.startswith(_SSH_KEY_FORMATS) jpadilla-pyjwt-a4e1a3d/jwt/warnings.py000066400000000000000000000005121515505507500201450ustar00rootroot00000000000000class RemovedInPyjwt3Warning(DeprecationWarning): """Warning for features that will be removed in PyJWT 3.""" pass class InsecureKeyLengthWarning(UserWarning): """Warning emitted when a cryptographic key is shorter than the minimum recommended length. See :ref:`key-length-validation` for details.""" pass jpadilla-pyjwt-a4e1a3d/pyproject.toml000066400000000000000000000047161515505507500200650ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools>=77.0.3", ] [project] authors = [ { email = "hello@jpadilla.com", name = "Jose Padilla" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Utilities", ] dependencies = [ "typing_extensions >= 4.0; python_version < '3.11'", ] description = "JSON Web Token implementation in Python" dynamic = [ "version", ] keywords = [ "json", "jwt", "security", "signing", "token", "web", ] license = "MIT" name = "PyJWT" requires-python = ">=3.9" [project.optional-dependencies] crypto = [ "cryptography>=3.4.0", ] dev = [ "coverage[toml]==7.10.7", "cryptography>=3.4.0", "pre-commit", "pytest>=8.4.2,<9.0.0", "sphinx", "sphinx-rtd-theme", "zope.interface", ] docs = [ "sphinx", "sphinx-rtd-theme", "zope.interface", ] tests = [ "coverage[toml]==7.10.7", "pytest>=8.4.2,<9.0.0", ] [project.readme] content-type = "text/x-rst" file = "README.rst" [project.urls] Homepage = "https://github.com/jpadilla/pyjwt" [tool.coverage.paths] source = [ "*/site-packages", "jwt", ] [tool.coverage.report] exclude_also = [ "if TYPE_CHECKING", ] skip_covered = true [tool.coverage.run] branch = true parallel = true relative_files = true source = [ "jwt", "tests", ] [tool.mypy] files = [ "jwt", "tests", ] strict = true warn_return_any = true warn_unused_ignores = true [[tool.mypy.overrides]] ignore_missing_imports = true module = [ "cryptography", "cryptography.*", ] [tool.pytest.ini_options] addopts = "-ra" filterwarnings = [ "error", "ignore::jwt.warnings.InsecureKeyLengthWarning", ] testpaths = [ "tests", ] [tool.setuptools] include-package-data = true zip-safe = false [tool.setuptools.dynamic.version] attr = "jwt.__version__" [tool.setuptools.package-data] "*" = [ "py.typed", ] [tool.setuptools.packages.find] exclude = [ "tests", "tests.*", ] namespaces = false jpadilla-pyjwt-a4e1a3d/ruff.toml000066400000000000000000000034421515505507500170030ustar00rootroot00000000000000# Exclude a variety of commonly ignored directories. exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", ] # Same as Black. line-length = 88 indent-width = 4 # Assume Python 3.9 target-version = "py39" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. select = ["E4", "E7", "E9", "F", "B"] ignore = ["E501"] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [format] # Like Black, use double quotes for strings. quote-style = "double" # Like Black, indent with spaces, rather than tabs. indent-style = "space" # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto" # Enable auto-formatting of code examples in docstrings. Markdown, # reStructuredText code/literal blocks and doctests are all supported. # # This is currently disabled by default, but it is planned for this # to be opt-out in the future. docstring-code-format = false # Set the line length limit used when formatting code snippets in # docstrings. # # This only has an effect when the `docstring-code-format` setting is # enabled. docstring-code-line-length = "dynamic" jpadilla-pyjwt-a4e1a3d/tests/000077500000000000000000000000001515505507500163035ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/tests/__init__.py000066400000000000000000000000001515505507500204020ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/tests/keys/000077500000000000000000000000001515505507500172565ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/tests/keys/__init__.py000066400000000000000000000030231515505507500213650ustar00rootroot00000000000000import json import os from typing import Union from jwt.algorithms import has_crypto, AllowedRSAKeys from jwt.utils import base64url_decode BASE_PATH = os.path.dirname(os.path.abspath(__file__)) def decode_value(val: Union[str, bytes]) -> int: decoded = base64url_decode(val) return int.from_bytes(decoded, byteorder="big") def load_hmac_key() -> bytes: with open(os.path.join(BASE_PATH, "jwk_hmac.json")) as infile: keyobj = json.load(infile) return base64url_decode(keyobj["k"]) if has_crypto: from cryptography.hazmat.primitives.asymmetric import ec from jwt.algorithms import RSAAlgorithm def load_rsa_pub_key() -> AllowedRSAKeys: with open(os.path.join(BASE_PATH, "jwk_rsa_pub.json")) as infile: return RSAAlgorithm.from_jwk(infile.read()) def load_ec_pub_key_p_521() -> ec.EllipticCurvePublicKey: with open(os.path.join(BASE_PATH, "jwk_ec_pub_P-521.json")) as infile: keyobj = json.load(infile) return ec.EllipticCurvePublicNumbers( x=decode_value(keyobj["x"]), y=decode_value(keyobj["y"]), curve=ec.SECP521R1(), ).public_key() else: import sys if sys.version_info >= (3, 11): from typing import Never else: from typing_extensions import Never def load_rsa_pub_key() -> AllowedRSAKeys: raise RuntimeError("cryptography is not available") def load_ec_pub_key_p_521() -> Never: # type: ignore[misc] raise RuntimeError("cryptography is not available") jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_key_P-256.json000066400000000000000000000003651515505507500230600ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256@hobbiton.example", "crv": "P-256", "x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4", "y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU", "d": "9GJquUJf57a9sev-u8-PoYlIezIPqI_vGpIaiu4zyZk" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_key_P-384.json000066400000000000000000000004641515505507500230620ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.384@hobbiton.example", "crv": "P-384", "x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J", "y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy", "d": "xKPj5IXjiHpQpLOgyMGo6lg_DUp738SuXkiugCFMxbGNKTyTprYPfJz42wTOXbtd" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_key_P-521.json000066400000000000000000000005741515505507500230550ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.521@hobbiton.example", "crv": "P-521", "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_key_secp256k1.json000066400000000000000000000003721515505507500237700ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256k@hobbiton.example", "crv": "secp256k1", "x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs", "y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI", "d": "XV7LOlEOANIaSxyil8yE8NPDT5jmVw_HQeCwNDzochQ" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_pub_P-256.json000066400000000000000000000002771515505507500230600ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256@hobbiton.example", "crv": "P-256", "x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4", "y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_pub_P-384.json000066400000000000000000000003511515505507500230530ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.384@hobbiton.example", "crv": "P-384", "x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J", "y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_pub_P-521.json000066400000000000000000000004311515505507500230430ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.521@hobbiton.example", "crv": "P-521", "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_ec_pub_secp256k1.json000066400000000000000000000003041515505507500237610ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256k@hobbiton.example", "crv": "secp256k1", "x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs", "y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_empty.json000066400000000000000000000000001515505507500221500ustar00rootroot00000000000000jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_hmac.json000066400000000000000000000002531515505507500217340ustar00rootroot00000000000000{ "kty": "oct", "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "use": "sig", "alg": "HS256", "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_keyset_only_unknown_alg.json000066400000000000000000000026761515505507500260060ustar00rootroot00000000000000{"keys":[{"kid":"lYXxnemSzWNBUoPug_h0hZnjPi5oKCmQ9awQJaZCWWM","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"k75Ghd4r8h_fdydTAXyMjrGYNnuiG7yevoW1ZIIuegEUK3LLGY0Z3Q8PhCrkmi6LpkPwwR1C8ck9plvSs4vZ9GqmUoi5YcQEile6HjPG3NBwQ-cHWY4ZH_D-ItdzcZUKDxjHYaY-GW1yLeJ1RAh8wMPM7cenA2v0eNIq4HaIXzZJ2Hgxh4Ei-CSYcD0f_TYEySqUEb8jd0dC8frpkYDkOUCVizRBDUEg_hkPSpVqfLP8ekxIHxkC9wcfL-d2FhptxBQYN8NFnIuG9NFXbZ5mdzdmIuN6WPr_CECcgL9qXsph9U-L829dU67ufeBvzEejJ8qwiswslRdx4ZcYjtaBdQ","e":"AQAB","x5c":["MIICnTCCAYUCBgGAUN05KzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdUZXN0aW5nMB4XDTIyMDQyMjEwNDAxN1oXDTMyMDQyMjEwNDE1N1owEjEQMA4GA1UEAwwHVGVzdGluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJO+RoXeK/If33cnUwF8jI6xmDZ7ohu8nr6FtWSCLnoBFCtyyxmNGd0PD4Qq5Joui6ZD8MEdQvHJPaZb0rOL2fRqplKIuWHEBIpXuh4zxtzQcEPnB1mOGR/w/iLXc3GVCg8Yx2GmPhltci3idUQIfMDDzO3HpwNr9HjSKuB2iF82Sdh4MYeBIvgkmHA9H/02BMkqlBG/I3dHQvH66ZGA5DlAlYs0QQ1BIP4ZD0qVanyz/HpMSB8ZAvcHHy/ndhYabcQUGDfDRZyLhvTRV22eZnc3ZiLjelj6/whAnIC/al7KYfVPi/NvXVOu7n3gb8xHoyfKsIrMLJUXceGXGI7WgXUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeMUFrCX4eAfF8i6wILOP5dDJOBN10nPP63VNliQ7+YHu1ZI0VGB7TNrImRE9riH2IWenSXD21DxK31qBlZKNEgaH7rVwwvOZ22qCyWacv1+QdanxAiljD03rU7HOR/tyqcvjl6U2Yadxcq6OWlKKVaa0fNtbPigqAwQ3iVpg9N+OthANYyKHxlmzJKGeEaDA69/uJ6UwektHlv/9BnNFh8We6EwJxYG7/rejI02EgbJFxGO1RlcmigTxRc5l3Dw4WldBIRxWiJgSEkKSfUy5S7sQdFQokZjTyqy6h1ldb/tgrWLIE0srGQ2u/fQeSgPTbAzihaeOf+WKq5RDXoq5bw=="],"x5t":"FaWinuPZQiDMljn3x9DMAuepBYQ","x5t#S256":"_0B--Hh1KgNtdyZqAp1NWUAikRPvlt2HGm__xXpjTi0"}]} jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_keyset_with_unknown_alg.json000066400000000000000000000055561515505507500260000ustar00rootroot00000000000000{"keys":[{"kid":"U1MayerhVuRj8xtFR8hyMH9lCfVMKlb3TG7mbQAS19M","kty":"RSA","alg":"RS256","use":"sig","n":"omef3NkXf4--6BtUPKjhlV7pf6Vv7HMg-VL-ITX8KQZTD4LTzWO3x9RPwVepKjgfvJe_IiZFaJX78-a7zpcG9mpZG8czp3C8nZSvAJKphvYLd9s9qYrGMFW9t1eHyGwmIQN02VXwHeZ0JDd5X4i7sO4XPkNycfzSoxaQbv7wANYBTcvcWcjYVxIj4ZpYkSsQqrrOTm69G7FyurtfExGc7jlSRcv-Gubq_K3IQLHGHTlil20wqZmis1dLJwpAjgTxY7uQSwEdqJHCJR3q76bsDelIBZpbR07kqIOXqYu52w0wkC_1W7_HcVPLNp6T_ML09P8jGsOWfMO95_zchkseQw","e":"AQAB","x5c":["MIICnTCCAYUCBgGAUN03JTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdUZXN0aW5nMB4XDTIyMDQyMjEwNDAxNloXDTMyMDQyMjEwNDE1NlowEjEQMA4GA1UEAwwHVGVzdGluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKJnn9zZF3+PvugbVDyo4ZVe6X+lb+xzIPlS/iE1/CkGUw+C081jt8fUT8FXqSo4H7yXvyImRWiV+/Pmu86XBvZqWRvHM6dwvJ2UrwCSqYb2C3fbPamKxjBVvbdXh8hsJiEDdNlV8B3mdCQ3eV+Iu7DuFz5DcnH80qMWkG7+8ADWAU3L3FnI2FcSI+GaWJErEKq6zk5uvRuxcrq7XxMRnO45UkXL/hrm6vytyECxxh05YpdtMKmZorNXSycKQI4E8WO7kEsBHaiRwiUd6u+m7A3pSAWaW0dO5KiDl6mLudsNMJAv9Vu/x3FTyzaek/zC9PT/IxrDlnzDvef83IZLHkMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAi7ZppYbkpt0ALn5NXIIPgA04svRwAmsUJWKLBS5iKVXq6HOJPsz0GAB9oKpjar83rUomwK2UE0XFJLMDvrB0nTZJBjm2DCANLL1GtTKUd+mdvhyHCIMrUApkhAYzv2Rk1c4+Jt7f5/h8FnM8jdl9FGc5TBy5ixS0OxnyW1JOakClYQz8vNS7LrC4hmLWwy7GAmUdemNLEefQcECaNzaLN5gGk1ht5lJyNCsHu9STZeYM2UXdDAtMtu9HAepfzh2CAOscSDtZr89SmFSwxKaOfbJyXH4PivMgWK4zO0P6ofuv8d8gRbUAUgnysKHQc0isTVWOxgmzI69EUe/iVXJHig=="],"x5t":"0C94xr3ayzaC9OUcSSLyrwDGdmI","x5t#S256":"O6ntIrYkVK0hX-_AwnrwJW1CO97lP3D2_aKnELuNLSo"},{"kid":"lYXxnemSzWNBUoPug_h0hZnjPi5oKCmQ9awQJaZCWWM","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"k75Ghd4r8h_fdydTAXyMjrGYNnuiG7yevoW1ZIIuegEUK3LLGY0Z3Q8PhCrkmi6LpkPwwR1C8ck9plvSs4vZ9GqmUoi5YcQEile6HjPG3NBwQ-cHWY4ZH_D-ItdzcZUKDxjHYaY-GW1yLeJ1RAh8wMPM7cenA2v0eNIq4HaIXzZJ2Hgxh4Ei-CSYcD0f_TYEySqUEb8jd0dC8frpkYDkOUCVizRBDUEg_hkPSpVqfLP8ekxIHxkC9wcfL-d2FhptxBQYN8NFnIuG9NFXbZ5mdzdmIuN6WPr_CECcgL9qXsph9U-L829dU67ufeBvzEejJ8qwiswslRdx4ZcYjtaBdQ","e":"AQAB","x5c":["MIICnTCCAYUCBgGAUN05KzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdUZXN0aW5nMB4XDTIyMDQyMjEwNDAxN1oXDTMyMDQyMjEwNDE1N1owEjEQMA4GA1UEAwwHVGVzdGluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJO+RoXeK/If33cnUwF8jI6xmDZ7ohu8nr6FtWSCLnoBFCtyyxmNGd0PD4Qq5Joui6ZD8MEdQvHJPaZb0rOL2fRqplKIuWHEBIpXuh4zxtzQcEPnB1mOGR/w/iLXc3GVCg8Yx2GmPhltci3idUQIfMDDzO3HpwNr9HjSKuB2iF82Sdh4MYeBIvgkmHA9H/02BMkqlBG/I3dHQvH66ZGA5DlAlYs0QQ1BIP4ZD0qVanyz/HpMSB8ZAvcHHy/ndhYabcQUGDfDRZyLhvTRV22eZnc3ZiLjelj6/whAnIC/al7KYfVPi/NvXVOu7n3gb8xHoyfKsIrMLJUXceGXGI7WgXUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeMUFrCX4eAfF8i6wILOP5dDJOBN10nPP63VNliQ7+YHu1ZI0VGB7TNrImRE9riH2IWenSXD21DxK31qBlZKNEgaH7rVwwvOZ22qCyWacv1+QdanxAiljD03rU7HOR/tyqcvjl6U2Yadxcq6OWlKKVaa0fNtbPigqAwQ3iVpg9N+OthANYyKHxlmzJKGeEaDA69/uJ6UwektHlv/9BnNFh8We6EwJxYG7/rejI02EgbJFxGO1RlcmigTxRc5l3Dw4WldBIRxWiJgSEkKSfUy5S7sQdFQokZjTyqy6h1ldb/tgrWLIE0srGQ2u/fQeSgPTbAzihaeOf+WKq5RDXoq5bw=="],"x5t":"FaWinuPZQiDMljn3x9DMAuepBYQ","x5t#S256":"_0B--Hh1KgNtdyZqAp1NWUAikRPvlt2HGm__xXpjTi0"}]} jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_okp_key_Ed25519.json000066400000000000000000000002171515505507500235030ustar00rootroot00000000000000{ "kty":"OKP", "crv":"Ed25519", "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_okp_key_Ed448.json000066400000000000000000000004161515505507500233360ustar00rootroot00000000000000{ "kty": "OKP", "kid": "sig_ed448_01", "crv": "Ed448", "use": "sig", "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", "d": "Zh5xx0r_0tq39xj-8jGuCwAA6wsDim2ME7cX_iXzqDRgPN8lsZZHu60AO7m31Fa4NtHO07eU63q8", "alg": "EdDSA" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_okp_pub_Ed25519.json000066400000000000000000000001321515505507500234750ustar00rootroot00000000000000{ "kty":"OKP", "crv":"Ed25519", "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_okp_pub_Ed448.json000066400000000000000000000002671515505507500233400ustar00rootroot00000000000000{ "kty": "OKP", "kid": "sig_ed448_01", "crv": "Ed448", "use": "sig", "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", "alg": "EdDSA" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_rsa_key.json000066400000000000000000000033211515505507500224600ustar00rootroot00000000000000{ "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ", "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik", "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4" } jpadilla-pyjwt-a4e1a3d/tests/keys/jwk_rsa_pub.json000066400000000000000000000007151515505507500224620ustar00rootroot00000000000000{ "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB" } jpadilla-pyjwt-a4e1a3d/tests/keys/testkey2_rsa.pub.pem000066400000000000000000000007031515505507500231650ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1tUH3/0v8fvLensHO1g2 6+U4r7jBg43DVOgqmXAWQa8ArAb4NfTrsYX8YkVhZZYwuLmKczRj0GhXUVY9iDbT sIGmgG+ySj6eiREz5VLqofFkAvRZ6y7yNv8PIGgXEhQTiDDNIkHGaFNMvn/eZ54H is70pdTjR5Ko+/y/wg71df1nb/5KwttSvy0YsTu/XpkduonPruYfAVRG3HK+3GZd xTygLcdamwe9jj+kjxtXRlrXVMQiXGFSU8U6bjafWnQiQ9XzjxvygBt0ZD0kRorr p74XGyQY5ThkN8DlpJbTTFsxOnBUAQz4zhohjobIGBRimi5yVlyLOwTlpaKGFC7O 7wIDAQAB -----END PUBLIC KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ec.priv000066400000000000000000000003611515505507500223170ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2nninfu2jMHDwAbn 9oERUhRADS6duQaJEadybLaa0YShRANCAAQfMBxRZKUYEdy5/fLdGI2tYj6kTr50 PZPt8jOD23rAR7dhtNpG1ojqopmH0AH5wEXadgk8nLCT4cAPK59Qp9Ek -----END PRIVATE KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ec.pub000066400000000000000000000002621515505507500221250ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHzAcUWSlGBHcuf3y3RiNrWI+pE6+ dD2T7fIzg9t6wEe3YbTaRtaI6qKZh9AB+cBF2nYJPJywk+HADyufUKfRJA== -----END PUBLIC KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ec_secp192r1.priv000066400000000000000000000003211515505507500240240ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBiON6kYcPu8ZUDRTu8W eXJ2FmX7e9yq0hahNAMyAARHecLjkXWDUJfZ4wiFH61JpmonCYH1GpinVlqw68Sf wtDHg2F6SifQEFC6VKj1ZXw= -----END PRIVATE KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ec_ssh.pub000066400000000000000000000002411515505507500227770ustar00rootroot00000000000000ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB8wHFFkpRgR3Ln98t0Yja1iPqROvnQ9k+3yM4PbesBHt2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ= jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ed25519000066400000000000000000000001671515505507500217530ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIBy9N4xfv/9qOiKrxwRKeGfO5ab6lSukKHbuC5vaJ1Mg -----END PRIVATE KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ed25519.pem000066400000000000000000000001671515505507500225330ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIJb2MBNIWqpJ2zwLlbw8JkHNPIBkFCv/g127aQI7dQ1Q -----END PRIVATE KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ed25519.pub000066400000000000000000000001211515505507500225260ustar00rootroot00000000000000ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC4pK2dePGgctIAsh0H/tmUrLzx2Vc4Ltc8TN9nfuChG jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_ed25519.pub.pem000066400000000000000000000001611515505507500233120ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEASmyuOjH4q3bPqsOwf61G4jBH5L2g9kWnCDOp/7IOHKg= -----END PUBLIC KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_pkcs1.pub.pem000066400000000000000000000003671515505507500233450ustar00rootroot00000000000000-----BEGIN RSA PUBLIC KEY----- MIGHAoGBAOV/0Vl/5VdHcYpnILYzBGWo5JQVzo9wBkbxzjAStcAnTwvv1ZJTMXs6 fjz91f9hiMM4Z/5qNTE/EHlDWxVdj1pyRaQulZPUs0r9qJ02ogRRGLG3jjrzzbzF yj/pdNBwym0UJYC/Jmn/kMLwGiWI2nfa9vM5SovqZiAy2FD7eOtVAgED -----END RSA PUBLIC KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_rsa.cer000066400000000000000000000024011515505507500223030ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDhTCCAm2gAwIBAgIJANE4sir3EkX8MA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMQ4wDAYDVQQK DAVQeUpXVDEZMBcGA1UECwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0xNTAzMTgwMTE2 MTRaFw0xODAzMTcwMTE2MTRaMFkxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhh czEPMA0GA1UEBwwGQXVzdGluMQ4wDAYDVQQKDAVQeUpXVDEZMBcGA1UECwwQVGVz dCBDZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANR4 MwXyb9nDo0K8gsHvDRHpa4jkzRVimVIr3r1K0YZanJmSXQr7giUa/sQjfjpjvKsI CSUffH3jbo8VYPifS7N/1DgOB3BfZ2B+mqlVxCwBPB5PwC78YveprNQw7gL0BmmG fpQDcZb8XkBTmUm45M//ZofGi3hisKiS6d6fjoVAUKcLwFAD4PNvjlLYE1t50pY4 3ha9eAfKgJ3hknP8JdJ4vvtUkWVFxUqL83KkDpJWt1tu66y36w+i14I/07A7OLw9 T5yJtc3FXpyk+032CNe27Bvzv1nnMM9jZdfaS+4A6LDa7hd6ICVjatS8p/4oz0J5 Dy6WR8ob7osnGHCNw4kCAwEAAaNQME4wHQYDVR0OBBYEFDR6fVdFxZED6YMmD62W LlBW+qEBMB8GA1UdIwQYMBaAFDR6fVdFxZED6YMmD62WLlBW+qEBMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFwDNwm+lU/kGfWwiWM0Lv2aosXotoiG TsBSWIn2iYphq0vzlgChcNocN9zkaOz3zc9pcREP6lyqHpE0OEbNucHHDdU1L2he lLFOLOmkpP5fyPDXs9nKYhO8ygMByEonHm3K/VvCgrsSgJ3JuxMLUxnE55jQXGWV OqYQNo2J5h93Zd2HTTe19jCz+bbWnRBP5VvLAAAo5YSmk3iroWSPWAKkWOOecJ2Q /xnRyuWERsfvZiF/m9q7yDJ55LXVVm3Rufmy76SoTnJ2acap+XQNXBH/AxayeLUS OYmHWH61dUcsQtwXYHYRB8TTtMIwUCXGmthXkDJydEfrGcD0y6APIh8= -----END CERTIFICATE----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_rsa.priv000066400000000000000000000032171515505507500225200ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA1HgzBfJv2cOjQryCwe8NEelriOTNFWKZUivevUrRhlqcmZJd CvuCJRr+xCN+OmO8qwgJJR98feNujxVg+J9Ls3/UOA4HcF9nYH6aqVXELAE8Hk/A Lvxi96ms1DDuAvQGaYZ+lANxlvxeQFOZSbjkz/9mh8aLeGKwqJLp3p+OhUBQpwvA UAPg82+OUtgTW3nSljjeFr14B8qAneGSc/wl0ni++1SRZUXFSovzcqQOkla3W27r rLfrD6LXgj/TsDs4vD1PnIm1zcVenKT7TfYI17bsG/O/Wecwz2Nl19pL7gDosNru F3ogJWNq1Lyn/ijPQnkPLpZHyhvuiycYcI3DiQIDAQABAoIBAQCt9uzwBZ0HVGQs lGULnUu6SsC9iXlR9TVMTpdFrij4NODb7Tc5cs0QzJWkytrjvB4Se7XhK3KnMLyp cvu/Fc7J3fRJIVN98t+V5pOD6rGAxlIPD4Vv8z6lQcw8wQNgb6WAaZriXh93XJNf YBO2hSj0FU5CBZLUsxmqLQBIQ6RR/OUGAvThShouE9K4N0vKB2UPOCu5U+d5zS3W 44Q5uatxYiSHBTYIZDN4u27Nfo5WA+GTvFyeNsO6tNNWlYfRHSBtnm6SZDY/5i4J fxP2JY0waM81KRvuHTazY571lHM/TTvFDRUX5nvHIu7GToBKahfVLf26NJuTZYXR 5c09GAXBAoGBAO7a9M/dvS6eDhyESYyCjP6w61jD7UYJ1fudaYFrDeqnaQ857Pz4 BcKx3KMmLFiDvuMgnVVj8RToBGfMV0zP7sDnuFRJnWYcOeU8e2sWGbZmWGWzv0SD +AhppSZThU4mJ8aa/tgsepCHkJnfoX+3wN7S9NfGhM8GDGxTHJwBpxINAoGBAOO4 ZVtn9QEblmCX/Q5ejInl43Y9nRsfTy9lB9Lp1cyWCJ3eep6lzT60K3OZGVOuSgKQ vZ/aClMCMbqsAAG4fKBjREA6p7k4/qaMApHQum8APCh9WPsKLaavxko8ZDc41kZt hgKyUs2XOhW/BLjmzqwGryidvOfszDwhH7rNVmRtAoGBALYGdvrSaRHVsbtZtRM3 imuuOCx1Y6U0abZOx9Cw3PIukongAxLlkL5G/XX36WOrQxWkDUK930OnbXQM7ZrD +5dW/8p8L09Zw2VHKmb5eK7gYA1hZim4yJTgrdL/Y1+jBDz+cagcfWsXZMNfAZxr VLh628x0pVF/sof67pqVR9UhAoGBAMcQiLoQ9GJVhW1HMBYBnQVnCyJv1gjBo+0g emhrtVQ0y6+FrtdExVjNEzboXPWD5Hq9oKY+aswJnQM8HH1kkr16SU2EeN437pQU zKI/PtqN8AjNGp3JVgLioYp/pHOJofbLA10UGcJTMpmT9ELWsVA8P55X1a1AmYDu y9f2bFE5AoGAdjo95mB0LVYikNPa+NgyDwLotLqrueb9IviMmn6zKHCwiOXReqXD X9slB8RA15uv56bmN04O//NyVFcgJ2ef169GZHiRFIgIy0Pl8LYkMhCYKKhyqM7g xN+SqGqDTKDC22j00S7jcvCaa1qadn1qbdfukZ4NXv7E2d/LO0Y2Kkc= -----END RSA PRIVATE KEY----- jpadilla-pyjwt-a4e1a3d/tests/keys/testkey_rsa.pub000066400000000000000000000006211515505507500223220ustar00rootroot00000000000000ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUeDMF8m/Zw6NCvILB7w0R6WuI5M0VYplSK969StGGWpyZkl0K+4IlGv7EI346Y7yrCAklH3x9426PFWD4n0uzf9Q4DgdwX2dgfpqpVcQsATweT8Au/GL3qazUMO4C9AZphn6UA3GW/F5AU5lJuOTP/2aHxot4YrCokunen46FQFCnC8BQA+Dzb45S2BNbedKWON4WvXgHyoCd4ZJz/CXSeL77VJFlRcVKi/NypA6SVrdbbuust+sPoteCP9OwOzi8PU+cibXNxV6cpPtN9gjXtuwb879Z5zDPY2XX2kvuAOiw2u4XeiAlY2rUvKf+KM9CeQ8ulkfKG+6LJxhwjcOJ aasmundo@mair.local jpadilla-pyjwt-a4e1a3d/tests/test_advisory.py000066400000000000000000000117531515505507500215630ustar00rootroot00000000000000import pytest import jwt from jwt.algorithms import get_default_algorithms from jwt.exceptions import InvalidKeyError from .utils import crypto_required priv_key_bytes = b"""-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIIbBhdo2ah7X32i50GOzrCr4acZTe6BezUdRIixjTAdL -----END PRIVATE KEY-----""" pub_key_bytes = ( b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPL1I9oiq+B8crkmuV4YViiUnhdLjCp3hvy1bNGuGfNL" ) ssh_priv_key_bytes = b"""-----BEGIN EC PRIVATE KEY----- MHcCAQEEIOWc7RbaNswMtNtc+n6WZDlUblMr2FBPo79fcGXsJlGQoAoGCCqGSM49 AwEHoUQDQgAElcy2RSSSgn2RA/xCGko79N+7FwoLZr3Z0ij/ENjow2XpUDwwKEKk Ak3TDXC9U8nipMlGcY7sDpXp2XyhHEM+Rw== -----END EC PRIVATE KEY-----""" ssh_key_bytes = b"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJXMtkUkkoJ9kQP8QhpKO/TfuxcKC2a92dIo/xDY6MNl6VA8MChCpAJN0w1wvVPJ4qTJRnGO7A6V6dl8oRxDPkc=""" class TestAdvisory: @crypto_required def test_ghsa_ffqj_6fqr_9h24(self) -> None: # Generate ed25519 private key # private_key = ed25519.Ed25519PrivateKey.generate() # Get private key bytes as they would be stored in a file # priv_key_bytes = private_key.private_bytes( # encoding=serialization.Encoding.PEM, # format=serialization.PrivateFormat.PKCS8, # encryption_algorithm=serialization.NoEncryption(), # ) # Get public key bytes as they would be stored in a file # pub_key_bytes = private_key.public_key().public_bytes( # encoding=serialization.Encoding.OpenSSH, # format=serialization.PublicFormat.OpenSSH, # ) # Making a good jwt token that should work by signing it # with the private key # encoded_good = jwt.encode({"test": 1234}, priv_key_bytes, algorithm="EdDSA") encoded_good = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJ0ZXN0IjoxMjM0fQ.M5y1EEavZkHSlj9i8yi9nXKKyPBSAUhDRTOYZi3zZY11tZItDaR3qwAye8pc74_lZY3Ogt9KPNFbVOSGnUBHDg" # Using HMAC with the public key to trick the receiver to think that the # public key is a HMAC secret encoded_bad = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoxMjM0fQ.6ulDpqSlbHmQ8bZXhZRLFko9SwcHrghCwh8d-exJEE4" algorithm_names = list(get_default_algorithms()) # Both of the jwt tokens are validated as valid jwt.decode( encoded_good, pub_key_bytes, algorithms=algorithm_names, ) with pytest.raises(InvalidKeyError): jwt.decode( encoded_bad, pub_key_bytes, algorithms=algorithm_names, ) # Of course the receiver should specify ed25519 algorithm to be used if # they specify ed25519 public key. However, if other algorithms are used, # the POC does not work # HMAC specifies illegal strings for the HMAC secret in jwt/algorithms.py # # invalid_str ings = [ # b"-----BEGIN PUBLIC KEY-----", # b"-----BEGIN CERTIFICATE-----", # b"-----BEGIN RSA PUBLIC KEY-----", # b"ssh-rsa", # ] # # However, OKPAlgorithm (ed25519) accepts the following in jwt/algorithms.py: # # if "-----BEGIN PUBLIC" in str_key: # return load_pem_public_key(key) # if "-----BEGIN PRIVATE" in str_key: # return load_pem_private_key(key, password=None) # if str_key[0:4] == "ssh-": # return load_ssh_public_key(key) # # These should most likely made to match each other to prevent this behavior # POC for the ecdsa-sha2-nistp256 format. # openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-priv.pem # openssl ec -in ec256-key-priv.pem -pubout > ec256-key-pub.pem # ssh-keygen -y -f ec256-key-priv.pem > ec256-key-ssh.pub # Making a good jwt token that should work by signing it with the private key # encoded_good = jwt.encode({"test": 1234}, ssh_priv_key_bytes, algorithm="ES256") encoded_good = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxMjM0fQ.NX42mS8cNqYoL3FOW9ZcKw8Nfq2mb6GqJVADeMA1-kyHAclilYo_edhdM_5eav9tBRQTlL0XMeu_WFE_mz3OXg" # Using HMAC with the ssh public key to trick the receiver to think that the public key is a HMAC secret # encoded_bad = jwt.encode({"test": 1234}, ssh_key_bytes, algorithm="HS256") encoded_bad = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxMjM0fQ.5eYfbrbeGYmWfypQ6rMWXNZ8bdHcqKng5GPr9MJZITU" algorithm_names = list(get_default_algorithms()) # Both of the jwt tokens are validated as valid jwt.decode( encoded_good, ssh_key_bytes, algorithms=algorithm_names, ) with pytest.raises(InvalidKeyError): jwt.decode( encoded_bad, ssh_key_bytes, algorithms=algorithm_names, ) jpadilla-pyjwt-a4e1a3d/tests/test_algorithms.py000066400000000000000000001711311515505507500220710ustar00rootroot00000000000000import base64 import json from typing import Union, cast import pytest from jwt.algorithms import HMACAlgorithm, NoneAlgorithm, has_crypto from jwt.exceptions import InvalidKeyError from jwt.utils import base64url_decode from .keys import load_ec_pub_key_p_521, load_hmac_key, load_rsa_pub_key from .utils import crypto_required, key_path if has_crypto: from cryptography.hazmat.primitives.asymmetric.ec import ( EllipticCurvePrivateKey, EllipticCurvePublicKey, ) from cryptography.hazmat.primitives.asymmetric.ed448 import ( Ed448PrivateKey, Ed448PublicKey, ) from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, ) from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPrivateKey, RSAPublicKey, ) from jwt.algorithms import ECAlgorithm, OKPAlgorithm, RSAAlgorithm, RSAPSSAlgorithm class TestAlgorithms: def test_check_crypto_key_type_should_fail_when_not_using_crypto(self) -> None: """If has_crypto is False, or if _crypto_key_types is None, then this method should throw.""" algo = NoneAlgorithm() with pytest.raises(ValueError): algo.check_crypto_key_type("key") # type: ignore[arg-type,unused-ignore] def test_none_algorithm_should_throw_exception_if_key_is_not_none(self) -> None: algo = NoneAlgorithm() with pytest.raises(InvalidKeyError): algo.prepare_key("123") def test_none_algorithm_should_throw_exception_on_to_jwk(self) -> None: algo = NoneAlgorithm() with pytest.raises(NotImplementedError): algo.to_jwk("dummy") # Using a dummy argument as is it not relevant def test_none_algorithm_should_throw_exception_on_from_jwk(self) -> None: algo = NoneAlgorithm() with pytest.raises(NotImplementedError): algo.from_jwk({}) # Using a dummy argument as is it not relevant def test_hmac_should_reject_nonstring_key(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) with pytest.raises(TypeError) as context: algo.prepare_key(object()) # type: ignore[arg-type] exception = context.value assert str(exception) == "Expected a string value" def test_hmac_should_accept_unicode_key(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) algo.prepare_key("awesome") @pytest.mark.parametrize( "key", [ "testkey2_rsa.pub.pem", "testkey2_rsa.pub.pem", "testkey_pkcs1.pub.pem", "testkey_rsa.cer", "testkey_rsa.pub", ], ) def test_hmac_should_throw_exception(self, key: str) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) with pytest.raises(InvalidKeyError): with open(key_path(key)) as keyfile: algo.prepare_key(keyfile.read()) def test_hmac_jwk_should_parse_and_verify(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) with open(key_path("jwk_hmac.json")) as keyfile: key = algo.from_jwk(keyfile.read()) signature = algo.sign(b"Hello World!", key) assert algo.verify(b"Hello World!", key, signature) @pytest.mark.parametrize("as_dict", (False, True)) def test_hmac_to_jwk_returns_correct_values(self, as_dict: bool) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) if as_dict: key = algo.to_jwk("secret", as_dict=True) else: key = json.loads(algo.to_jwk("secret", as_dict=False)) assert key == {"kty": "oct", "k": "c2VjcmV0"} def test_hmac_from_jwk_should_raise_exception_if_not_hmac_key(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: with pytest.raises(InvalidKeyError): algo.from_jwk(keyfile.read()) def test_hmac_from_jwk_should_raise_exception_if_empty_json(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) with open(key_path("jwk_empty.json")) as keyfile: with pytest.raises(InvalidKeyError): algo.from_jwk(keyfile.read()) @crypto_required def test_rsa_should_parse_pem_public_key(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey2_rsa.pub.pem")) as pem_key: algo.prepare_key(pem_key.read()) @crypto_required def test_rsa_should_accept_pem_private_key_bytes(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.priv"), "rb") as pem_key: algo.prepare_key(pem_key.read()) @crypto_required def test_rsa_should_accept_unicode_key(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.priv")) as rsa_key: algo.prepare_key(rsa_key.read()) @crypto_required def test_rsa_should_reject_non_string_key(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with pytest.raises(TypeError): algo.prepare_key(None) # type: ignore[arg-type,unused-ignore] @crypto_required def test_rsa_verify_should_return_false_if_signature_invalid(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) message = b"Hello World!" sig = base64.b64decode( b"yS6zk9DBkuGTtcBzLUzSpo9gGJxJFOGvUqN01iLhWHrzBQ9ZEz3+Ae38AXp" b"10RWwscp42ySC85Z6zoN67yGkLNWnfmCZSEv+xqELGEvBJvciOKsrhiObUl" b"2mveSc1oeO/2ujkGDkkkJ2epn0YliacVjZF5+/uDmImUfAAj8lzjnHlzYix" b"sn5jGz1H07jYYbi9diixN8IUhXeTafwFg02IcONhum29V40Wu6O5tAKWlJX" b"fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA" b"APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA==" ) sig += b"123" # Signature is now invalid with open(key_path("testkey_rsa.pub")) as keyfile: pub_key = cast(RSAPublicKey, algo.prepare_key(keyfile.read())) result = algo.verify(message, pub_key, sig) assert not result @crypto_required def test_ec_jwk_public_and_private_keys_should_parse_and_verify(self) -> None: tests = { "P-256": ECAlgorithm.SHA256, "P-384": ECAlgorithm.SHA384, "P-521": ECAlgorithm.SHA512, "secp256k1": ECAlgorithm.SHA256, } for curve, hash in tests.items(): algo = ECAlgorithm(hash) with open(key_path(f"jwk_ec_pub_{curve}.json")) as keyfile: pub_key = cast(EllipticCurvePublicKey, algo.from_jwk(keyfile.read())) with open(key_path(f"jwk_ec_key_{curve}.json")) as keyfile: priv_key = cast(EllipticCurvePrivateKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", priv_key) assert algo.verify(b"Hello World!", pub_key, signature) @crypto_required def test_ec_jwk_fails_on_invalid_json(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA512) valid_points = { "P-256": { "x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4", "y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU", }, "P-384": { "x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J", "y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy", }, "P-521": { "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", }, "secp256k1": { "x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs", "y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI", }, } # Invalid JSON with pytest.raises(InvalidKeyError): algo.from_jwk("") # Bad key type with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "RSA"}') # Missing data with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "EC"}') with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "EC", "x": "1"}') with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "EC", "y": "1"}') # Missing curve with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "EC", "x": "dGVzdA==", "y": "dGVzdA=="}') # EC coordinates not equally long with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "EC", "x": "dGVzdHRlc3Q=", "y": "dGVzdA=="}') # EC coordinates length invalid for curve in ("P-256", "P-384", "P-521", "secp256k1"): with pytest.raises(InvalidKeyError): algo.from_jwk( f'{{"kty": "EC", "crv": "{curve}", "x": "dGVzdA==", "y": "dGVzdA=="}}' ) # EC private key length invalid for curve, point in valid_points.items(): with pytest.raises(InvalidKeyError): algo.from_jwk( f'{{"kty": "EC", "crv": "{curve}", "x": "{point["x"]}", "y": "{point["y"]}", "d": "dGVzdA=="}}' ) @crypto_required def test_ec_private_key_to_jwk_works_with_from_jwk(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.priv")) as ec_key: orig_key = cast(EllipticCurvePrivateKey, algo.prepare_key(ec_key.read())) parsed_key = cast(EllipticCurvePrivateKey, algo.from_jwk(algo.to_jwk(orig_key))) assert parsed_key.private_numbers() == orig_key.private_numbers() assert ( parsed_key.private_numbers().public_numbers == orig_key.private_numbers().public_numbers ) @crypto_required def test_ec_public_key_to_jwk_works_with_from_jwk(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.pub")) as ec_key: orig_key = cast(EllipticCurvePublicKey, algo.prepare_key(ec_key.read())) parsed_key = cast(EllipticCurvePublicKey, algo.from_jwk(algo.to_jwk(orig_key))) assert parsed_key.public_numbers() == orig_key.public_numbers() @crypto_required @pytest.mark.parametrize("as_dict", (False, True)) def test_ec_to_jwk_returns_correct_values_for_public_key( self, as_dict: bool ) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.pub")) as keyfile: pub_key = algo.prepare_key(keyfile.read()) if as_dict: key = algo.to_jwk(pub_key, as_dict=True) else: key = json.loads(algo.to_jwk(pub_key, as_dict=False)) expected = { "kty": "EC", "crv": "P-256", "x": "HzAcUWSlGBHcuf3y3RiNrWI-pE6-dD2T7fIzg9t6wEc", "y": "t2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ", } assert key == expected @crypto_required @pytest.mark.parametrize("as_dict", (False, True)) def test_ec_to_jwk_returns_correct_values_for_private_key( self, as_dict: bool ) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.priv")) as keyfile: priv_key = algo.prepare_key(keyfile.read()) if as_dict: key = algo.to_jwk(priv_key, as_dict=True) else: key = json.loads(algo.to_jwk(priv_key, as_dict=False)) expected = { "kty": "EC", "crv": "P-256", "x": "HzAcUWSlGBHcuf3y3RiNrWI-pE6-dD2T7fIzg9t6wEc", "y": "t2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ", "d": "2nninfu2jMHDwAbn9oERUhRADS6duQaJEadybLaa0YQ", } assert key == expected @crypto_required def test_ec_to_jwk_raises_exception_on_invalid_key(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with pytest.raises(InvalidKeyError): # crypto-mypy reports call-overload; no-crypto-mypy reports # unused-ignore because mypy resolves the argument type to Any. algo.to_jwk({"not": "a valid key"}) # type: ignore[call-overload,unused-ignore] @crypto_required @pytest.mark.parametrize("as_dict", (False, True)) def test_ec_to_jwk_with_valid_curves(self, as_dict: bool) -> None: tests = { "P-256": ECAlgorithm.SHA256, "P-384": ECAlgorithm.SHA384, "P-521": ECAlgorithm.SHA512, "secp256k1": ECAlgorithm.SHA256, } for curve, hash in tests.items(): algo = ECAlgorithm(hash) with open(key_path(f"jwk_ec_pub_{curve}.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) if as_dict: jwk = algo.to_jwk(pub_key, as_dict=True) else: jwk = json.loads(algo.to_jwk(pub_key, as_dict=False)) assert jwk["crv"] == curve with open(key_path(f"jwk_ec_key_{curve}.json")) as keyfile: priv_key = algo.from_jwk(keyfile.read()) if as_dict: jwk = algo.to_jwk(priv_key, as_dict=True) else: jwk = json.loads(algo.to_jwk(priv_key, as_dict=False)) assert jwk["crv"] == curve @crypto_required def test_ec_to_jwk_with_invalid_curve(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec_secp192r1.priv")) as keyfile: priv_key = algo.prepare_key(keyfile.read()) with pytest.raises(InvalidKeyError): algo.to_jwk(priv_key) @crypto_required def test_rsa_jwk_public_and_private_keys_should_parse_and_verify(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = cast(RSAPublicKey, algo.from_jwk(keyfile.read())) with open(key_path("jwk_rsa_key.json")) as keyfile: priv_key = cast(RSAPrivateKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", priv_key) assert algo.verify(b"Hello World!", pub_key, signature) @crypto_required def test_rsa_private_key_to_jwk_works_with_from_jwk(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.priv")) as rsa_key: orig_key = cast(RSAPrivateKey, algo.prepare_key(rsa_key.read())) parsed_key = cast(RSAPrivateKey, algo.from_jwk(algo.to_jwk(orig_key))) assert parsed_key.private_numbers() == orig_key.private_numbers() assert ( parsed_key.private_numbers().public_numbers == orig_key.private_numbers().public_numbers ) @crypto_required def test_rsa_public_key_to_jwk_works_with_from_jwk(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.pub")) as rsa_key: orig_key = cast(RSAPublicKey, algo.prepare_key(rsa_key.read())) parsed_key = cast(RSAPublicKey, algo.from_jwk(algo.to_jwk(orig_key))) assert parsed_key.public_numbers() == orig_key.public_numbers() @crypto_required def test_rsa_jwk_private_key_with_other_primes_is_invalid(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_key.json")) as keyfile: with pytest.raises(InvalidKeyError): keydata = json.loads(keyfile.read()) keydata["oth"] = [] algo.from_jwk(json.dumps(keydata)) @crypto_required def test_rsa_jwk_private_key_with_missing_values_is_invalid(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_key.json")) as keyfile: with pytest.raises(InvalidKeyError): keydata = json.loads(keyfile.read()) del keydata["p"] algo.from_jwk(json.dumps(keydata)) @crypto_required def test_rsa_jwk_private_key_can_recover_prime_factors(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_key.json")) as keyfile: keybytes = keyfile.read() control_key = cast(RSAPrivateKey, algo.from_jwk(keybytes)).private_numbers() keydata = json.loads(keybytes) delete_these = ["p", "q", "dp", "dq", "qi"] for field in delete_these: del keydata[field] parsed_key = cast( RSAPrivateKey, algo.from_jwk(json.dumps(keydata)) ).private_numbers() assert control_key.d == parsed_key.d assert control_key.p == parsed_key.p assert control_key.q == parsed_key.q assert control_key.dmp1 == parsed_key.dmp1 assert control_key.dmq1 == parsed_key.dmq1 assert control_key.iqmp == parsed_key.iqmp @crypto_required def test_rsa_jwk_private_key_with_missing_required_values_is_invalid(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_key.json")) as keyfile: with pytest.raises(InvalidKeyError): keydata = json.loads(keyfile.read()) del keydata["p"] algo.from_jwk(json.dumps(keydata)) @crypto_required def test_rsa_jwk_raises_exception_if_not_a_valid_key(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) # Invalid JSON with pytest.raises(InvalidKeyError): algo.from_jwk("{not-a-real-key") # Missing key parts with pytest.raises(InvalidKeyError): algo.from_jwk('{"kty": "RSA"}') @crypto_required @pytest.mark.parametrize("as_dict", (False, True)) def test_rsa_to_jwk_returns_correct_values_for_public_key( self, as_dict: bool ) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.pub")) as keyfile: pub_key = algo.prepare_key(keyfile.read()) if as_dict: key = algo.to_jwk(pub_key, as_dict=True) else: key = json.loads(algo.to_jwk(pub_key, as_dict=False)) expected = { "e": "AQAB", "key_ops": ["verify"], "kty": "RSA", "n": ( "1HgzBfJv2cOjQryCwe8NEelriOTNFWKZUivevUrRhlqcmZJdCvuCJRr-xCN-" "OmO8qwgJJR98feNujxVg-J9Ls3_UOA4HcF9nYH6aqVXELAE8Hk_ALvxi96ms" "1DDuAvQGaYZ-lANxlvxeQFOZSbjkz_9mh8aLeGKwqJLp3p-OhUBQpwvAUAPg" "82-OUtgTW3nSljjeFr14B8qAneGSc_wl0ni--1SRZUXFSovzcqQOkla3W27r" "rLfrD6LXgj_TsDs4vD1PnIm1zcVenKT7TfYI17bsG_O_Wecwz2Nl19pL7gDo" "sNruF3ogJWNq1Lyn_ijPQnkPLpZHyhvuiycYcI3DiQ" ), } assert key == expected @crypto_required @pytest.mark.parametrize("as_dict", (False, True)) def test_rsa_to_jwk_returns_correct_values_for_private_key( self, as_dict: bool ) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.priv")) as keyfile: priv_key = algo.prepare_key(keyfile.read()) if as_dict: key = algo.to_jwk(priv_key, as_dict=True) else: key = json.loads(algo.to_jwk(priv_key, as_dict=False)) expected = { "key_ops": ["sign"], "kty": "RSA", "e": "AQAB", "n": ( "1HgzBfJv2cOjQryCwe8NEelriOTNFWKZUivevUrRhlqcmZJdCvuCJRr-xCN-" "OmO8qwgJJR98feNujxVg-J9Ls3_UOA4HcF9nYH6aqVXELAE8Hk_ALvxi96ms" "1DDuAvQGaYZ-lANxlvxeQFOZSbjkz_9mh8aLeGKwqJLp3p-OhUBQpwvAUAPg" "82-OUtgTW3nSljjeFr14B8qAneGSc_wl0ni--1SRZUXFSovzcqQOkla3W27r" "rLfrD6LXgj_TsDs4vD1PnIm1zcVenKT7TfYI17bsG_O_Wecwz2Nl19pL7gDo" "sNruF3ogJWNq1Lyn_ijPQnkPLpZHyhvuiycYcI3DiQ" ), "d": ( "rfbs8AWdB1RkLJRlC51LukrAvYl5UfU1TE6XRa4o-DTg2-03OXLNEMyVpMr" "a47weEnu14StypzC8qXL7vxXOyd30SSFTffLfleaTg-qxgMZSDw-Fb_M-pU" "HMPMEDYG-lgGma4l4fd1yTX2ATtoUo9BVOQgWS1LMZqi0ASEOkUfzlBgL04" "UoaLhPSuDdLygdlDzgruVPnec0t1uOEObmrcWIkhwU2CGQzeLtuzX6OVgPh" "k7xcnjbDurTTVpWH0R0gbZ5ukmQ2P-YuCX8T9iWNMGjPNSkb7h02s2Oe9ZR" "zP007xQ0VF-Z7xyLuxk6ASmoX1S39ujSbk2WF0eXNPRgFwQ" ), "q": ( "47hlW2f1ARuWYJf9Dl6MieXjdj2dGx9PL2UH0unVzJYInd56nqXNPrQrc5k" "ZU65KApC9n9oKUwIxuqwAAbh8oGNEQDqnuTj-powCkdC6bwA8KH1Y-wotpq" "_GSjxkNzjWRm2GArJSzZc6Fb8EuObOrAavKJ285-zMPCEfus1WZG0" ), "p": ( "7tr0z929Lp4OHIRJjIKM_rDrWMPtRgnV-51pgWsN6qdpDzns_PgFwrHcoyY" "sWIO-4yCdVWPxFOgEZ8xXTM_uwOe4VEmdZhw55Tx7axYZtmZYZbO_RIP4CG" "mlJlOFTiYnxpr-2Cx6kIeQmd-hf7fA3tL018aEzwYMbFMcnAGnEg0" ), "qi": ( "djo95mB0LVYikNPa-NgyDwLotLqrueb9IviMmn6zKHCwiOXReqXDX9slB8" "RA15uv56bmN04O__NyVFcgJ2ef169GZHiRFIgIy0Pl8LYkMhCYKKhyqM7g" "xN-SqGqDTKDC22j00S7jcvCaa1qadn1qbdfukZ4NXv7E2d_LO0Y2Kkc" ), "dp": ( "tgZ2-tJpEdWxu1m1EzeKa644LHVjpTRptk7H0LDc8i6SieADEuWQvkb9df" "fpY6tDFaQNQr3fQ6dtdAztmsP7l1b_ynwvT1nDZUcqZvl4ruBgDWFmKbjI" "lOCt0v9jX6MEPP5xqBx9axdkw18BnGtUuHrbzHSlUX-yh_rumpVH1SE" ), "dq": ( "xxCIuhD0YlWFbUcwFgGdBWcLIm_WCMGj7SB6aGu1VDTLr4Wu10TFWM0TNu" "hc9YPker2gpj5qzAmdAzwcfWSSvXpJTYR43jfulBTMoj8-2o3wCM0anclW" "AuKhin-kc4mh9ssDXRQZwlMymZP0QtaxUDw_nlfVrUCZgO7L1_ZsUTk" ), } assert key == expected @crypto_required def test_rsa_to_jwk_raises_exception_on_invalid_key(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with pytest.raises(InvalidKeyError): # crypto-mypy reports call-overload; no-crypto-mypy reports # unused-ignore because mypy resolves the argument type to Any. algo.to_jwk({"not": "a valid key"}) # type: ignore[call-overload,unused-ignore] @crypto_required def test_rsa_from_jwk_raises_exception_on_invalid_key(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_hmac.json")) as keyfile: with pytest.raises(InvalidKeyError): algo.from_jwk(keyfile.read()) @crypto_required def test_ec_should_reject_non_string_key(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with pytest.raises(TypeError): algo.prepare_key(None) # type: ignore[arg-type,unused-ignore] @crypto_required def test_ec_should_accept_pem_private_key_bytes(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.priv"), "rb") as ec_key: algo.prepare_key(ec_key.read()) @crypto_required def test_ec_should_accept_ssh_public_key_bytes(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec_ssh.pub")) as ec_key: algo.prepare_key(ec_key.read()) @crypto_required def test_ec_verify_should_return_false_if_signature_invalid(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) message = b"Hello World!" # Mess up the signature by replacing a known byte sig = base64.b64decode( b"AC+m4Jf/xI3guAC6w0w37t5zRpSCF6F4udEz5LiMiTIjCS4vcVe6dDOxK+M" b"mvkF8PxJuvqxP2CO3TR3okDPCl/NjATTO1jE+qBZ966CRQSSzcCM+tzcHzw" b"LZS5kbvKu0Acd/K6Ol2/W3B1NeV5F/gjvZn/jOwaLgWEUYsg0o4XVrAg65".replace( b"r", b"s" ) ) with open(key_path("testkey_ec.pub")) as keyfile: pub_key = algo.prepare_key(keyfile.read()) result = algo.verify(message, pub_key, sig) assert not result @crypto_required def test_ec_verify_should_return_false_if_signature_wrong_length(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) message = b"Hello World!" sig = base64.b64decode(b"AC+m4Jf/xI3guAC6w0w3") with open(key_path("testkey_ec.pub")) as keyfile: pub_key = algo.prepare_key(keyfile.read()) result = algo.verify(message, pub_key, sig) assert not result @crypto_required def test_ec_should_throw_exception_on_wrong_key(self) -> None: algo = ECAlgorithm(ECAlgorithm.SHA256) with pytest.raises(InvalidKeyError): with open(key_path("testkey_rsa.priv")) as keyfile: algo.prepare_key(keyfile.read()) with pytest.raises(InvalidKeyError): with open(key_path("testkey2_rsa.pub.pem")) as pem_key: algo.prepare_key(pem_key.read()) @crypto_required def test_rsa_pss_sign_then_verify_should_return_true(self) -> None: algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256) message = b"Hello World!" with open(key_path("testkey_rsa.priv")) as keyfile: priv_key = cast(RSAPrivateKey, algo.prepare_key(keyfile.read())) sig = algo.sign(message, priv_key) with open(key_path("testkey_rsa.pub")) as keyfile: pub_key = cast(RSAPublicKey, algo.prepare_key(keyfile.read())) result = algo.verify(message, pub_key, sig) assert result @crypto_required def test_rsa_pss_verify_should_return_false_if_signature_invalid(self) -> None: algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256) jwt_message = b"Hello World!" jwt_sig = base64.b64decode( b"ywKAUGRIDC//6X+tjvZA96yEtMqpOrSppCNfYI7NKyon3P7doud5v65oWNu" b"vQsz0fzPGfF7mQFGo9Cm9Vn0nljm4G6PtqZRbz5fXNQBH9k10gq34AtM02c" b"/cveqACQ8gF3zxWh6qr9jVqIpeMEaEBIkvqG954E0HT9s9ybHShgHX9mlWk" b"186/LopP4xe5c/hxOQjwhv6yDlTiwJFiqjNCvj0GyBKsc4iECLGIIO+4mC4" b"daOCWqbpZDuLb1imKpmm8Nsm56kAxijMLZnpCcnPgyb7CqG+B93W9GHglA5" b"drUeR1gRtO7vqbZMsCAQ4bpjXxwbYyjQlEVuMl73UL6sOWg==" ) jwt_sig += b"123" # Signature is now invalid with open(key_path("testkey_rsa.pub")) as keyfile: jwt_pub_key = cast(RSAPublicKey, algo.prepare_key(keyfile.read())) result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert not result class TestAlgorithmsRFC7520: """ These test vectors were taken from RFC 7520 (https://tools.ietf.org/html/rfc7520) """ def test_hmac_verify_should_return_true_for_test_vector(self) -> None: """ This test verifies that HMAC verification works with a known good signature and key. Reference: https://tools.ietf.org/html/rfc7520#section-4.4 """ signing_input = ( b"eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZ" b"jMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ" b"29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIG" b"lmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmc" b"gd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4" ) signature = base64url_decode(b"s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0") algo = HMACAlgorithm(HMACAlgorithm.SHA256) key = algo.prepare_key(load_hmac_key()) result = algo.verify(signing_input, key, signature) assert result @crypto_required def test_rsa_verify_should_return_true_for_test_vector(self) -> None: """ This test verifies that RSA PKCS v1.5 verification works with a known good signature and key. Reference: https://tools.ietf.org/html/rfc7520#section-4.1 """ signing_input = ( b"eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb" b"XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb" b"3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS" b"Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU" b"geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4" ) signature = base64url_decode( b"MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmKZop" b"dHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJ" b"K3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4" b"QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic" b"1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_fcIe8u9ipH84ogor" b"ee7vjbU5y18kDquDg" ) algo = RSAAlgorithm(RSAAlgorithm.SHA256) key = cast(RSAPublicKey, algo.prepare_key(load_rsa_pub_key())) result = algo.verify(signing_input, key, signature) assert result @crypto_required def test_rsapss_verify_should_return_true_for_test_vector(self) -> None: """ This test verifies that RSA-PSS verification works with a known good signature and key. Reference: https://tools.ietf.org/html/rfc7520#section-4.2 """ signing_input = ( b"eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb" b"XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb" b"3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS" b"Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU" b"geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4" ) signature = base64url_decode( b"cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2IpN6" b"-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXUvdvWXz" b"g-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRXe8P_ijQ7p" b"8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT0qI0n6uiP1aC" b"N_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a6GYmJUAfmWjwZ6o" b"D4ifKo8DYM-X72Eaw" ) algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384) key = cast(RSAPublicKey, algo.prepare_key(load_rsa_pub_key())) result = algo.verify(signing_input, key, signature) assert result @crypto_required def test_ec_verify_should_return_true_for_test_vector(self) -> None: """ This test verifies that ECDSA verification works with a known good signature and key. Reference: https://tools.ietf.org/html/rfc7520#section-4.3 """ signing_input = ( b"eyJhbGciOiJFUzUxMiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhb" b"XBsZSJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb" b"3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdS" b"Bkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmU" b"geW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4" ) signature = base64url_decode( b"AE_R_YZCChjn4791jSQCrdPZCNYqHXCTZH0-JZGYNlaAjP2kqaluUIIUnC9qvbu9P" b"lon7KRTzoNEuT4Va2cmL1eJAQy3mtPBu_u_sDDyYjnAMDxXPn7XrT0lw-kvAD890j" b"l8e2puQens_IEKBpHABlsbEPX6sFY8OcGDqoRuBomu9xQ2" ) algo = ECAlgorithm(ECAlgorithm.SHA512) key = algo.prepare_key(load_ec_pub_key_p_521()) result = algo.verify(signing_input, key, signature) assert result # private key can also be used. with open(key_path("jwk_ec_key_P-521.json")) as keyfile: private_key = algo.from_jwk(keyfile.read()) result = algo.verify(signing_input, private_key, signature) assert result @crypto_required class TestOKPAlgorithms: hello_world_sig = b"Qxa47mk/azzUgmY2StAOguAd4P7YBLpyCfU3JdbaiWnXM4o4WibXwmIHvNYgN3frtE2fcyd8OYEaOiD/KiwkCg==" hello_world_sig_pem = b"9ueQE7PT8uudHIQb2zZZ7tB7k1X3jeTnIfOVvGCINZejrqQbru1EXPeuMlGcQEZrGkLVcfMmr99W/+byxfppAg==" hello_world = b"Hello World!" def test_okp_ed25519_should_reject_non_string_key(self) -> None: algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): algo.prepare_key(None) # type: ignore[arg-type,unused-ignore] with open(key_path("testkey_ed25519")) as keyfile: algo.prepare_key(keyfile.read()) with open(key_path("testkey_ed25519.pub")) as keyfile: algo.prepare_key(keyfile.read()) @pytest.mark.parametrize( "private_key_file,public_key_file,sig_attr", [ ("testkey_ed25519", "testkey_ed25519.pub", "hello_world_sig"), ("testkey_ed25519.pem", "testkey_ed25519.pub.pem", "hello_world_sig_pem"), ], ) def test_okp_ed25519_sign_should_generate_correct_signature_value( self, private_key_file: str, public_key_file: str, sig_attr: str ) -> None: algo = OKPAlgorithm() jwt_message = self.hello_world expected_sig = base64.b64decode(getattr(self, sig_attr)) with open(key_path(private_key_file)) as keyfile: jwt_key = cast(Ed25519PrivateKey, algo.prepare_key(keyfile.read())) with open(key_path(public_key_file)) as keyfile: jwt_pub_key = cast(Ed25519PublicKey, algo.prepare_key(keyfile.read())) algo.sign(jwt_message, jwt_key) result = algo.verify(jwt_message, jwt_pub_key, expected_sig) assert result @pytest.mark.parametrize( "public_key_file,sig_attr", [ ("testkey_ed25519.pub", "hello_world_sig"), ("testkey_ed25519.pub.pem", "hello_world_sig_pem"), ], ) def test_okp_ed25519_verify_should_return_false_if_signature_invalid( self, public_key_file: str, sig_attr: str ) -> None: algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(getattr(self, sig_attr)) jwt_sig += b"123" # Signature is now invalid with open(key_path(public_key_file)) as keyfile: jwt_pub_key = algo.prepare_key(keyfile.read()) result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert not result @pytest.mark.parametrize( "public_key_file,sig_attr", [ ("testkey_ed25519.pub", "hello_world_sig"), ("testkey_ed25519.pub.pem", "hello_world_sig_pem"), ], ) def test_okp_ed25519_verify_should_return_true_if_signature_valid( self, public_key_file: str, sig_attr: str ) -> None: algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(getattr(self, sig_attr)) with open(key_path(public_key_file)) as keyfile: jwt_pub_key = algo.prepare_key(keyfile.read()) result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert result @pytest.mark.parametrize( "public_key_file", ("testkey_ed25519.pub", "testkey_ed25519.pub.pem") ) def test_okp_ed25519_prepare_key_should_be_idempotent( self, public_key_file: str ) -> None: algo = OKPAlgorithm() with open(key_path(public_key_file)) as keyfile: jwt_pub_key_first = algo.prepare_key(keyfile.read()) jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first) assert jwt_pub_key_first == jwt_pub_key_second def test_okp_ed25519_prepare_key_should_reject_invalid_key(self) -> None: algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): algo.prepare_key("not a valid key") def test_okp_ed25519_jwk_private_key_should_parse_and_verify(self) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: key = cast(Ed25519PrivateKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", key) assert algo.verify(b"Hello World!", key.public_key(), signature) def test_okp_ed25519_jwk_private_key_should_parse_and_verify_with_private_key_as_is( self, ) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: key = cast(Ed25519PrivateKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", key) assert algo.verify(b"Hello World!", key, signature) def test_okp_ed25519_jwk_public_key_should_parse_and_verify(self) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: priv_key = cast(Ed25519PrivateKey, algo.from_jwk(keyfile.read())) with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: pub_key = cast(Ed25519PublicKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", priv_key) assert algo.verify(b"Hello World!", pub_key, signature) def test_okp_ed25519_jwk_fails_on_invalid_json(self) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: valid_pub = json.loads(keyfile.read()) with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: valid_key = json.loads(keyfile.read()) # Invalid instance type with pytest.raises(InvalidKeyError): algo.from_jwk(123) # type: ignore[arg-type] # Invalid JSON with pytest.raises(InvalidKeyError): algo.from_jwk("") # Invalid kty, not "OKP" v = valid_pub.copy() v["kty"] = "oct" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid crv, not "Ed25519" v = valid_pub.copy() v["crv"] = "P-256" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid crv, "Ed448" v = valid_pub.copy() v["crv"] = "Ed448" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Missing x v = valid_pub.copy() del v["x"] with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid x v = valid_pub.copy() v["x"] = "123" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid d v = valid_key.copy() v["d"] = "123" with pytest.raises(InvalidKeyError): algo.from_jwk(v) @pytest.mark.parametrize("as_dict", (False, True)) def test_okp_ed25519_to_jwk_works_with_from_jwk(self, as_dict: bool) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed25519.json")) as keyfile: priv_key_1 = cast(Ed25519PrivateKey, algo.from_jwk(keyfile.read())) with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: pub_key_1 = cast(Ed25519PublicKey, algo.from_jwk(keyfile.read())) pub_jwk: Union[dict[str, object], str] pri_jwk: Union[dict[str, object], str] if as_dict: pub_jwk = algo.to_jwk(pub_key_1, as_dict=True) pri_jwk = algo.to_jwk(priv_key_1, as_dict=True) else: pub_jwk = algo.to_jwk(pub_key_1, as_dict=False) pri_jwk = algo.to_jwk(priv_key_1, as_dict=False) pub_key_2 = algo.from_jwk(pub_jwk) priv_key_2 = cast(Ed25519PrivateKey, algo.from_jwk(pri_jwk)) signature_1 = algo.sign(b"Hello World!", priv_key_1) signature_2 = algo.sign(b"Hello World!", priv_key_2) assert algo.verify(b"Hello World!", pub_key_2, signature_1) assert algo.verify(b"Hello World!", pub_key_2, signature_2) def test_okp_to_jwk_raises_exception_on_invalid_key(self) -> None: algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): # crypto-mypy reports call-overload; no-crypto-mypy reports # unused-ignore because mypy resolves the argument type to Any. algo.to_jwk({"not": "a valid key"}) # type: ignore[call-overload,unused-ignore] def test_okp_ed448_jwk_private_key_should_parse_and_verify(self) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: key = cast(Ed448PrivateKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", key) assert algo.verify(b"Hello World!", key.public_key(), signature) def test_okp_ed448_jwk_private_key_should_parse_and_verify_with_private_key_as_is( self, ) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: key = cast(Ed448PrivateKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", key) assert algo.verify(b"Hello World!", key, signature) def test_okp_ed448_jwk_public_key_should_parse_and_verify(self) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: priv_key = cast(Ed448PrivateKey, algo.from_jwk(keyfile.read())) with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: pub_key = cast(Ed448PublicKey, algo.from_jwk(keyfile.read())) signature = algo.sign(b"Hello World!", priv_key) assert algo.verify(b"Hello World!", pub_key, signature) def test_okp_ed448_jwk_fails_on_invalid_json(self) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: valid_pub = json.loads(keyfile.read()) with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: valid_key = json.loads(keyfile.read()) # Invalid instance type with pytest.raises(InvalidKeyError): algo.from_jwk(123) # type: ignore[arg-type] # Invalid JSON with pytest.raises(InvalidKeyError): algo.from_jwk("") # Invalid kty, not "OKP" v = valid_pub.copy() v["kty"] = "oct" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid crv, not "Ed448" v = valid_pub.copy() v["crv"] = "P-256" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid crv, "Ed25519" v = valid_pub.copy() v["crv"] = "Ed25519" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Missing x v = valid_pub.copy() del v["x"] with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid x v = valid_pub.copy() v["x"] = "123" with pytest.raises(InvalidKeyError): algo.from_jwk(v) # Invalid d v = valid_key.copy() v["d"] = "123" with pytest.raises(InvalidKeyError): algo.from_jwk(v) @pytest.mark.parametrize("as_dict", (False, True)) def test_okp_ed448_to_jwk_works_with_from_jwk(self, as_dict: bool) -> None: algo = OKPAlgorithm() with open(key_path("jwk_okp_key_Ed448.json")) as keyfile: priv_key_1 = cast(Ed448PrivateKey, algo.from_jwk(keyfile.read())) with open(key_path("jwk_okp_pub_Ed448.json")) as keyfile: pub_key_1 = cast(Ed448PublicKey, algo.from_jwk(keyfile.read())) pub_jwk: Union[dict[str, object], str] pri_jwk: Union[dict[str, object], str] if as_dict: pub_jwk = algo.to_jwk(pub_key_1, as_dict=True) pri_jwk = algo.to_jwk(priv_key_1, as_dict=True) else: pub_jwk = algo.to_jwk(pub_key_1, as_dict=False) pri_jwk = algo.to_jwk(priv_key_1, as_dict=False) pub_key_2 = algo.from_jwk(pub_jwk) priv_key_2 = cast(Ed448PrivateKey, algo.from_jwk(pri_jwk)) signature_1 = algo.sign(b"Hello World!", priv_key_1) signature_2 = algo.sign(b"Hello World!", priv_key_2) assert algo.verify(b"Hello World!", pub_key_2, signature_1) assert algo.verify(b"Hello World!", pub_key_2, signature_2) @crypto_required def test_rsa_can_compute_digest(self) -> None: # this is the well-known sha256 hash of "foo" foo_hash = base64.b64decode(b"LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=") algo = RSAAlgorithm(RSAAlgorithm.SHA256) computed_hash = algo.compute_hash_digest(b"foo") assert computed_hash == foo_hash def test_hmac_can_compute_digest(self) -> None: # this is the well-known sha256 hash of "foo" foo_hash = base64.b64decode(b"LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=") algo = HMACAlgorithm(HMACAlgorithm.SHA256) computed_hash = algo.compute_hash_digest(b"foo") assert computed_hash == foo_hash @crypto_required def test_rsa_prepare_key_raises_invalid_key_error_on_invalid_pem(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) invalid_key = "invalid key" with pytest.raises(InvalidKeyError) as excinfo: algo.prepare_key(invalid_key) # Check that the exception message is correct assert "Could not parse the provided public key." in str(excinfo.value) @crypto_required class TestECCurveValidation: """Tests for ECDSA curve validation per RFC 7518 Section 3.4.""" def test_ec_curve_validation_rejects_wrong_curve_for_es256(self) -> None: """ES256 should reject keys that are not P-256.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 algo = ECAlgorithm(ECAlgorithm.SHA256, SECP256R1) # P-384 key should be rejected with open(key_path("jwk_ec_key_P-384.json")) as keyfile: p384_key = ECAlgorithm.from_jwk(keyfile.read()) with pytest.raises(InvalidKeyError) as excinfo: algo.prepare_key(p384_key) assert "secp384r1" in str(excinfo.value) assert "secp256r1" in str(excinfo.value) def test_ec_curve_validation_rejects_wrong_curve_for_es384(self) -> None: """ES384 should reject keys that are not P-384.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP384R1 algo = ECAlgorithm(ECAlgorithm.SHA384, SECP384R1) # P-256 key should be rejected with open(key_path("jwk_ec_key_P-256.json")) as keyfile: p256_key = ECAlgorithm.from_jwk(keyfile.read()) with pytest.raises(InvalidKeyError) as excinfo: algo.prepare_key(p256_key) assert "secp256r1" in str(excinfo.value) assert "secp384r1" in str(excinfo.value) def test_ec_curve_validation_rejects_wrong_curve_for_es512(self) -> None: """ES512 should reject keys that are not P-521.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP521R1 algo = ECAlgorithm(ECAlgorithm.SHA512, SECP521R1) # P-256 key should be rejected with open(key_path("jwk_ec_key_P-256.json")) as keyfile: p256_key = ECAlgorithm.from_jwk(keyfile.read()) with pytest.raises(InvalidKeyError) as excinfo: algo.prepare_key(p256_key) assert "secp256r1" in str(excinfo.value) assert "secp521r1" in str(excinfo.value) def test_ec_curve_validation_rejects_wrong_curve_for_es256k(self) -> None: """ES256K should reject keys that are not secp256k1.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP256K1 algo = ECAlgorithm(ECAlgorithm.SHA256, SECP256K1) # P-256 key should be rejected with open(key_path("jwk_ec_key_P-256.json")) as keyfile: p256_key = ECAlgorithm.from_jwk(keyfile.read()) with pytest.raises(InvalidKeyError) as excinfo: algo.prepare_key(p256_key) assert "secp256r1" in str(excinfo.value) assert "secp256k1" in str(excinfo.value) def test_ec_curve_validation_accepts_correct_curve_for_es256(self) -> None: """ES256 should accept P-256 keys.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 algo = ECAlgorithm(ECAlgorithm.SHA256, SECP256R1) with open(key_path("jwk_ec_key_P-256.json")) as keyfile: key = algo.from_jwk(keyfile.read()) prepared = algo.prepare_key(key) assert prepared is key def test_ec_curve_validation_accepts_correct_curve_for_es384(self) -> None: """ES384 should accept P-384 keys.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP384R1 algo = ECAlgorithm(ECAlgorithm.SHA384, SECP384R1) with open(key_path("jwk_ec_key_P-384.json")) as keyfile: key = algo.from_jwk(keyfile.read()) prepared = algo.prepare_key(key) assert prepared is key def test_ec_curve_validation_accepts_correct_curve_for_es512(self) -> None: """ES512 should accept P-521 keys.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP521R1 algo = ECAlgorithm(ECAlgorithm.SHA512, SECP521R1) with open(key_path("jwk_ec_key_P-521.json")) as keyfile: key = algo.from_jwk(keyfile.read()) prepared = algo.prepare_key(key) assert prepared is key def test_ec_curve_validation_accepts_correct_curve_for_es256k(self) -> None: """ES256K should accept secp256k1 keys.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP256K1 algo = ECAlgorithm(ECAlgorithm.SHA256, SECP256K1) with open(key_path("jwk_ec_key_secp256k1.json")) as keyfile: key = algo.from_jwk(keyfile.read()) prepared = algo.prepare_key(key) assert prepared is key def test_ec_curve_validation_rejects_p192_for_es256(self) -> None: """ES256 should reject P-192 keys (weaker than P-256).""" from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 algo = ECAlgorithm(ECAlgorithm.SHA256, SECP256R1) with open(key_path("testkey_ec_secp192r1.priv")) as keyfile: with pytest.raises(InvalidKeyError) as excinfo: algo.prepare_key(keyfile.read()) assert "secp192r1" in str(excinfo.value) assert "secp256r1" in str(excinfo.value) def test_ec_algorithm_without_expected_curve_accepts_any_curve(self) -> None: """ECAlgorithm without expected_curve should accept any curve (backwards compat).""" algo = ECAlgorithm(ECAlgorithm.SHA256) # Should accept P-256 with open(key_path("jwk_ec_key_P-256.json")) as keyfile: p256_key = algo.from_jwk(keyfile.read()) algo.prepare_key(p256_key) # Should accept P-384 with open(key_path("jwk_ec_key_P-384.json")) as keyfile: p384_key = algo.from_jwk(keyfile.read()) algo.prepare_key(p384_key) # Should accept P-521 with open(key_path("jwk_ec_key_P-521.json")) as keyfile: p521_key = algo.from_jwk(keyfile.read()) algo.prepare_key(p521_key) # Should accept secp256k1 with open(key_path("jwk_ec_key_secp256k1.json")) as keyfile: secp256k1_key = algo.from_jwk(keyfile.read()) algo.prepare_key(secp256k1_key) def test_default_algorithms_have_correct_expected_curve(self) -> None: """Default algorithms returned by get_default_algorithms should have expected_curve set.""" from cryptography.hazmat.primitives.asymmetric.ec import ( SECP256K1, SECP256R1, SECP384R1, SECP521R1, ) from jwt.algorithms import get_default_algorithms algorithms = get_default_algorithms() es256 = algorithms["ES256"] assert isinstance(es256, ECAlgorithm) assert es256.expected_curve == SECP256R1 es256k = algorithms["ES256K"] assert isinstance(es256k, ECAlgorithm) assert es256k.expected_curve == SECP256K1 es384 = algorithms["ES384"] assert isinstance(es384, ECAlgorithm) assert es384.expected_curve == SECP384R1 es521 = algorithms["ES521"] assert isinstance(es521, ECAlgorithm) assert es521.expected_curve == SECP521R1 es512 = algorithms["ES512"] assert isinstance(es512, ECAlgorithm) assert es512.expected_curve == SECP521R1 def test_ec_curve_validation_with_pem_key(self) -> None: """Curve validation should work with PEM-formatted keys.""" from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 algo = ECAlgorithm(ECAlgorithm.SHA256, SECP256R1) # P-256 PEM key should be accepted with open(key_path("testkey_ec.priv")) as keyfile: algo.prepare_key(keyfile.read()) # P-192 PEM key should be rejected with open(key_path("testkey_ec_secp192r1.priv")) as keyfile: with pytest.raises(InvalidKeyError): algo.prepare_key(keyfile.read()) def test_jwt_encode_decode_rejects_wrong_curve(self) -> None: """Integration test: jwt.encode/decode should reject wrong curve keys.""" import jwt # Use P-384 key with ES256 algorithm (expects P-256) with open(key_path("jwk_ec_key_P-384.json")) as keyfile: p384_key = ECAlgorithm.from_jwk(keyfile.read()) assert isinstance(p384_key, EllipticCurvePrivateKey) # Encoding should fail with pytest.raises(InvalidKeyError): jwt.encode({"hello": "world"}, p384_key, algorithm="ES256") # Create a valid token with P-256 key with open(key_path("jwk_ec_key_P-256.json")) as keyfile: p256_key = ECAlgorithm.from_jwk(keyfile.read()) assert isinstance(p256_key, EllipticCurvePrivateKey) token = jwt.encode({"hello": "world"}, p256_key, algorithm="ES256") # Decoding with wrong curve key should fail with open(key_path("jwk_ec_pub_P-384.json")) as keyfile: p384_pub_key = ECAlgorithm.from_jwk(keyfile.read()) assert isinstance(p384_pub_key, EllipticCurvePublicKey) with pytest.raises(InvalidKeyError): jwt.decode(token, p384_pub_key, algorithms=["ES256"]) # Decoding with correct curve key should succeed with open(key_path("jwk_ec_pub_P-256.json")) as keyfile: p256_pub_key = ECAlgorithm.from_jwk(keyfile.read()) assert isinstance(p256_pub_key, EllipticCurvePublicKey) decoded = jwt.decode(token, p256_pub_key, algorithms=["ES256"]) assert decoded == {"hello": "world"} class TestKeyLengthValidation: """Tests for minimum key length validation (CWE-326).""" # --- HMAC tests --- def test_hmac_short_key_warns_by_default_hs256(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) key = algo.prepare_key(b"short") msg = algo.check_key_length(key) assert msg is not None assert "below" in msg assert "32" in msg def test_hmac_short_key_warns_by_default_hs384(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA384) key = algo.prepare_key(b"a" * 47) msg = algo.check_key_length(key) assert msg is not None assert "48" in msg def test_hmac_short_key_warns_by_default_hs512(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA512) key = algo.prepare_key(b"a" * 63) msg = algo.check_key_length(key) assert msg is not None assert "64" in msg def test_hmac_empty_key_returns_warning_message(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) key = algo.prepare_key(b"") msg = algo.check_key_length(key) assert msg is not None def test_hmac_exact_minimum_no_warning(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA256) key = algo.prepare_key(b"a" * 32) assert algo.check_key_length(key) is None def test_hmac_above_minimum_no_warning(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA512) key = algo.prepare_key(b"a" * 128) assert algo.check_key_length(key) is None def test_hmac_exact_minimum_hs384(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA384) key = algo.prepare_key(b"a" * 48) assert algo.check_key_length(key) is None def test_hmac_exact_minimum_hs512(self) -> None: algo = HMACAlgorithm(HMACAlgorithm.SHA512) key = algo.prepare_key(b"a" * 64) assert algo.check_key_length(key) is None # --- RSA tests --- @crypto_required def test_rsa_small_key_returns_warning_message(self) -> None: from cryptography.hazmat.primitives.asymmetric import rsa as rsa_module small_key = rsa_module.generate_private_key( public_exponent=65537, key_size=1024, ) algo = RSAAlgorithm(RSAAlgorithm.SHA256) msg = algo.check_key_length(small_key) assert msg is not None assert "1024" in msg assert "2048" in msg @crypto_required def test_rsa_small_public_key_returns_warning_message(self) -> None: from cryptography.hazmat.primitives.asymmetric import rsa as rsa_module small_key = rsa_module.generate_private_key( public_exponent=65537, key_size=1024, ) algo = RSAAlgorithm(RSAAlgorithm.SHA256) msg = algo.check_key_length(small_key.public_key()) assert msg is not None @crypto_required def test_rsa_2048_key_no_warning(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.priv")) as f: key = algo.prepare_key(f.read()) assert algo.check_key_length(key) is None @crypto_required def test_rsa_pss_inherits_validation(self) -> None: from cryptography.hazmat.primitives.asymmetric import rsa as rsa_module small_key = rsa_module.generate_private_key( public_exponent=65537, key_size=1024, ) algo = RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256) msg = algo.check_key_length(small_key) assert msg is not None @crypto_required def test_rsa_pem_weak_key_validated(self) -> None: from cryptography.hazmat.primitives.asymmetric import rsa as rsa_module from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, ) small_key = rsa_module.generate_private_key( public_exponent=65537, key_size=1024, ) pem = small_key.private_bytes( Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption() ) algo = RSAAlgorithm(RSAAlgorithm.SHA256) prepared = algo.prepare_key(pem) msg = algo.check_key_length(prepared) assert msg is not None # --- PyJWS integration tests --- def test_pyjws_encode_warns_short_hmac_key(self) -> None: import jwt jws = jwt.PyJWS() with pytest.warns(jwt.InsecureKeyLengthWarning, match="below"): jws.encode(b'{"test":"payload"}', b"short", algorithm="HS256") def test_pyjws_encode_enforces_short_hmac_key(self) -> None: import jwt jws = jwt.PyJWS(options={"enforce_minimum_key_length": True}) with pytest.raises(InvalidKeyError, match="below"): jws.encode(b'{"test":"payload"}', b"short", algorithm="HS256") def test_pyjws_encode_no_warning_adequate_key(self) -> None: import warnings import jwt jws = jwt.PyJWS() with warnings.catch_warnings(): warnings.simplefilter("error", jwt.InsecureKeyLengthWarning) jws.encode(b'{"test":"payload"}', b"a" * 32, algorithm="HS256") # --- PyJWT integration tests --- def test_pyjwt_encode_warns_short_hmac_key(self) -> None: import jwt with pytest.warns(jwt.InsecureKeyLengthWarning): jwt.encode({"hello": "world"}, "short", algorithm="HS256") def test_pyjwt_encode_enforces_short_hmac_key(self) -> None: import jwt pyjwt = jwt.PyJWT(options={"enforce_minimum_key_length": True}) with pytest.raises(InvalidKeyError, match="below"): pyjwt.encode({"hello": "world"}, "short", algorithm="HS256") def test_pyjwt_decode_enforces_short_hmac_key(self) -> None: import jwt adequate_key = "a" * 32 token = jwt.encode({"hello": "world"}, adequate_key, algorithm="HS256") pyjwt = jwt.PyJWT(options={"enforce_minimum_key_length": True}) # Decoding with adequate key should work result = pyjwt.decode(token, adequate_key, algorithms=["HS256"]) assert result == {"hello": "world"} # Decoding with short key should raise pyjwt_enforce = jwt.PyJWT(options={"enforce_minimum_key_length": True}) with pytest.raises(InvalidKeyError): pyjwt_enforce.decode(token, "short", algorithms=["HS256"]) def test_pyjwt_encode_no_warning_adequate_key(self) -> None: import warnings import jwt with warnings.catch_warnings(): warnings.simplefilter("error", jwt.InsecureKeyLengthWarning) jwt.encode({"hello": "world"}, "a" * 32, algorithm="HS256") def test_global_register_algorithm_works_with_encode(self) -> None: """Backward compat: jwt.register_algorithm + jwt.encode use the same JWS.""" import jwt # This test just verifies the global path still works # (register_algorithm and encode share the same JWS instance) token = jwt.encode({"hello": "world"}, "a" * 32, algorithm="HS256") decoded = jwt.decode(token, "a" * 32, algorithms=["HS256"]) assert decoded == {"hello": "world"} jpadilla-pyjwt-a4e1a3d/tests/test_api_jwk.py000066400000000000000000000264221515505507500213460ustar00rootroot00000000000000import json import pytest from jwt.algorithms import has_crypto from jwt.api_jwk import PyJWK, PyJWKSet from jwt.exceptions import ( InvalidKeyError, MissingCryptographyError, PyJWKError, PyJWKSetError, ) from .utils import crypto_required, key_path, no_crypto_required if has_crypto: from jwt.algorithms import ECAlgorithm, HMACAlgorithm, OKPAlgorithm, RSAAlgorithm class TestPyJWK: @crypto_required def test_should_load_key_from_jwk_data_dict(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) key_data_str = algo.to_jwk(pub_key) key_data = json.loads(key_data_str) # TODO Should `to_jwk` set these? key_data["alg"] = "RS256" key_data["use"] = "sig" key_data["kid"] = "keyid-abc123" jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "RSA" assert jwk.key_id == "keyid-abc123" assert jwk.public_key_use == "sig" @crypto_required def test_should_load_key_from_jwk_data_json_string(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) key_data_str = algo.to_jwk(pub_key) key_data = json.loads(key_data_str) # TODO Should `to_jwk` set these? key_data["alg"] = "RS256" key_data["use"] = "sig" key_data["kid"] = "keyid-abc123" jwk = PyJWK.from_json(json.dumps(key_data)) assert jwk.key_type == "RSA" assert jwk.key_id == "keyid-abc123" assert jwk.public_key_use == "sig" @crypto_required def test_should_load_key_without_alg_from_dict(self) -> None: with open(key_path("jwk_rsa_pub.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "RSA" assert isinstance(jwk.Algorithm, RSAAlgorithm) assert jwk.Algorithm.hash_alg == RSAAlgorithm.SHA256 assert jwk.algorithm_name == "RS256" @crypto_required def test_should_load_key_from_dict_with_algorithm(self) -> None: with open(key_path("jwk_rsa_pub.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data, algorithm="RS256") assert jwk.key_type == "RSA" assert isinstance(jwk.Algorithm, RSAAlgorithm) assert jwk.Algorithm.hash_alg == RSAAlgorithm.SHA256 assert jwk.algorithm_name == "RS256" @crypto_required def test_should_load_key_ec_p256_from_dict(self) -> None: with open(key_path("jwk_ec_pub_P-256.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "EC" assert isinstance(jwk.Algorithm, ECAlgorithm) assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA256 assert jwk.algorithm_name == "ES256" @crypto_required def test_should_load_key_ec_p384_from_dict(self) -> None: with open(key_path("jwk_ec_pub_P-384.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "EC" assert isinstance(jwk.Algorithm, ECAlgorithm) assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA384 assert jwk.algorithm_name == "ES384" @crypto_required def test_should_load_key_ec_p521_from_dict(self) -> None: with open(key_path("jwk_ec_pub_P-521.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "EC" assert isinstance(jwk.Algorithm, ECAlgorithm) assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA512 assert jwk.algorithm_name == "ES512" @crypto_required def test_should_load_key_ec_secp256k1_from_dict(self) -> None: with open(key_path("jwk_ec_pub_secp256k1.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "EC" assert isinstance(jwk.Algorithm, ECAlgorithm) assert jwk.Algorithm.hash_alg == ECAlgorithm.SHA256 assert jwk.algorithm_name == "ES256K" @crypto_required def test_should_load_key_hmac_from_dict(self) -> None: with open(key_path("jwk_hmac.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "oct" assert isinstance(jwk.Algorithm, HMACAlgorithm) assert jwk.Algorithm.hash_alg == HMACAlgorithm.SHA256 assert jwk.algorithm_name == "HS256" @crypto_required def test_should_load_key_hmac_without_alg_from_dict(self) -> None: with open(key_path("jwk_hmac.json")) as keyfile: key_data = json.loads(keyfile.read()) del key_data["alg"] jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "oct" assert isinstance(jwk.Algorithm, HMACAlgorithm) assert jwk.Algorithm.hash_alg == HMACAlgorithm.SHA256 assert jwk.algorithm_name == "HS256" @crypto_required def test_should_load_key_okp_without_alg_from_dict(self) -> None: with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: key_data = json.loads(keyfile.read()) jwk = PyJWK.from_dict(key_data) assert jwk.key_type == "OKP" assert isinstance(jwk.Algorithm, OKPAlgorithm) assert jwk.algorithm_name == "EdDSA" @crypto_required def test_from_dict_should_throw_exception_if_arg_is_invalid(self) -> None: with open(key_path("jwk_rsa_pub.json")) as keyfile: valid_rsa_pub = json.loads(keyfile.read()) with open(key_path("jwk_ec_pub_P-256.json")) as keyfile: valid_ec_pub = json.loads(keyfile.read()) with open(key_path("jwk_okp_pub_Ed25519.json")) as keyfile: valid_okp_pub = json.loads(keyfile.read()) # Unknown algorithm with pytest.raises(PyJWKError): PyJWK.from_dict(valid_rsa_pub, algorithm="unknown") # Missing kty v = valid_rsa_pub.copy() del v["kty"] with pytest.raises(InvalidKeyError): PyJWK.from_dict(v) # Unknown kty v = valid_rsa_pub.copy() v["kty"] = "unknown" with pytest.raises(InvalidKeyError): PyJWK.from_dict(v) # Unknown EC crv v = valid_ec_pub.copy() v["crv"] = "unknown" with pytest.raises(InvalidKeyError): PyJWK.from_dict(v) # Unknown OKP crv v = valid_okp_pub.copy() v["crv"] = "unknown" with pytest.raises(InvalidKeyError): PyJWK.from_dict(v) # Missing OKP crv v = valid_okp_pub.copy() del v["crv"] with pytest.raises(InvalidKeyError): PyJWK.from_dict(v) @no_crypto_required def test_missing_crypto_library_good_error_message(self) -> None: with pytest.raises(PyJWKError) as exc: PyJWK({"kty": "dummy"}, algorithm="RS256") assert "cryptography" in str(exc.value) @no_crypto_required def test_missing_crypto_library_raises_missing_cryptography_error(self) -> None: with pytest.raises(MissingCryptographyError): PyJWK({"kty": "dummy"}, algorithm="RS256") class TestPyJWKSet: @crypto_required def test_should_load_keys_from_jwk_data_dict(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) key_data_str = algo.to_jwk(pub_key) key_data = json.loads(key_data_str) # TODO Should `to_jwk` set these? key_data["alg"] = "RS256" key_data["use"] = "sig" key_data["kid"] = "keyid-abc123" jwk_set = PyJWKSet.from_dict({"keys": [key_data]}) jwk = jwk_set.keys[0] assert jwk.key_type == "RSA" assert jwk.key_id == "keyid-abc123" assert jwk.public_key_use == "sig" @crypto_required def test_should_load_keys_from_jwk_data_json_string(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) key_data_str = algo.to_jwk(pub_key) key_data = json.loads(key_data_str) # TODO Should `to_jwk` set these? key_data["alg"] = "RS256" key_data["use"] = "sig" key_data["kid"] = "keyid-abc123" jwk_set = PyJWKSet.from_json(json.dumps({"keys": [key_data]})) jwk = jwk_set.keys[0] assert jwk.key_type == "RSA" assert jwk.key_id == "keyid-abc123" assert jwk.public_key_use == "sig" @crypto_required def test_keyset_should_index_by_kid(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) key_data_str = algo.to_jwk(pub_key) key_data = json.loads(key_data_str) # TODO Should `to_jwk` set these? key_data["alg"] = "RS256" key_data["use"] = "sig" key_data["kid"] = "keyid-abc123" jwk_set = PyJWKSet.from_dict({"keys": [key_data]}) jwk = jwk_set.keys[0] assert jwk == jwk_set["keyid-abc123"] with pytest.raises(KeyError): _ = jwk_set["this-kid-does-not-exist"] @crypto_required def test_keyset_iterator(self) -> None: algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("jwk_rsa_pub.json")) as keyfile: pub_key = algo.from_jwk(keyfile.read()) key_data_str = algo.to_jwk(pub_key) key_data = json.loads(key_data_str) jwk_set = PyJWKSet.from_dict({"keys": [key_data]}) assert jwk_set.keys == [jwk for jwk in jwk_set] @crypto_required def test_keyset_with_unknown_alg(self) -> None: # first keyset with unusable key and usable key with open(key_path("jwk_keyset_with_unknown_alg.json")) as keyfile: jwks_text = keyfile.read() jwks = json.loads(jwks_text) assert len(jwks.get("keys")) == 2 keyset = PyJWKSet.from_json(jwks_text) assert len(keyset.keys) == 1 # second keyset with only unusable key -> catch exception with open(key_path("jwk_keyset_only_unknown_alg.json")) as keyfile: jwks_text = keyfile.read() jwks = json.loads(jwks_text) assert len(jwks.get("keys")) == 1 with pytest.raises(PyJWKSetError): _ = PyJWKSet.from_json(jwks_text) @crypto_required def test_invalid_keys_list(self) -> None: with pytest.raises(PyJWKSetError) as err: PyJWKSet(keys="string") # type: ignore[arg-type] assert str(err.value) == "Invalid JWK Set value" @crypto_required def test_empty_keys_list(self) -> None: with pytest.raises(PyJWKSetError) as err: PyJWKSet(keys=[]) assert str(err.value) == "The JWK Set did not contain any keys" @no_crypto_required def test_missing_crypto_library_raises_when_required(self) -> None: with pytest.raises(MissingCryptographyError): PyJWKSet(keys=[{"kty": "RSA"}]) jpadilla-pyjwt-a4e1a3d/tests/test_api_jws.py000066400000000000000000001160251515505507500213550ustar00rootroot00000000000000import json from decimal import Decimal import pytest from jwt.algorithms import NoneAlgorithm, has_crypto from jwt.api_jwk import PyJWK from jwt.api_jws import PyJWS from jwt.exceptions import ( DecodeError, InvalidAlgorithmError, InvalidSignatureError, InvalidTokenError, ) from jwt.utils import base64url_decode from jwt.warnings import RemovedInPyjwt3Warning from .utils import crypto_required, key_path, no_crypto_required try: from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, load_pem_public_key, load_ssh_public_key, ) from cryptography.hazmat.primitives.asymmetric.ec import ( EllipticCurvePrivateKey, EllipticCurvePublicKey, ) from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPrivateKey, RSAPublicKey, ) except ModuleNotFoundError: pass @pytest.fixture def jws() -> PyJWS: return PyJWS() @pytest.fixture def payload() -> bytes: """Creates a sample jws claimset for use as a payload during tests""" return b"hello world" class TestJWS: def test_register_algo_does_not_allow_duplicate_registration( self, jws: PyJWS ) -> None: jws.register_algorithm("AAA", NoneAlgorithm()) with pytest.raises(ValueError): jws.register_algorithm("AAA", NoneAlgorithm()) def test_register_algo_rejects_non_algorithm_obj(self, jws: PyJWS) -> None: with pytest.raises(TypeError): jws.register_algorithm( "AAA123", {}, # type: ignore[arg-type] ) def test_unregister_algo_removes_algorithm(self, jws: PyJWS) -> None: supported = jws.get_algorithms() assert "none" in supported assert "HS256" in supported jws.unregister_algorithm("HS256") supported = jws.get_algorithms() assert "HS256" not in supported def test_unregister_algo_throws_error_if_not_registered(self, jws: PyJWS) -> None: with pytest.raises(KeyError): jws.unregister_algorithm("AAA") def test_algo_parameter_removes_alg_from_algorithms_list(self, jws: PyJWS) -> None: assert "none" in jws.get_algorithms() assert "HS256" in jws.get_algorithms() jws = PyJWS(algorithms=["HS256"]) assert "none" not in jws.get_algorithms() assert "HS256" in jws.get_algorithms() def test_override_options(self) -> None: jws = PyJWS(options={"verify_signature": False}) assert not jws.options["verify_signature"] def test_non_object_options_dont_persist(self, jws: PyJWS, payload: bytes) -> None: token = jws.encode(payload, "secret") jws.decode(token, "secret", options={"verify_signature": False}) assert jws.options["verify_signature"] def test_options_must_be_dict(self) -> None: pytest.raises(TypeError, PyJWS, options=object()) pytest.raises((TypeError, ValueError), PyJWS, options=("something")) def test_encode_decode(self, jws: PyJWS, payload: bytes) -> None: secret = "secret" jws_message = jws.encode(payload, secret, algorithm="HS256") decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) assert decoded_payload == payload def test_decode_fails_when_alg_is_not_on_method_algorithms_param( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" jws_token = jws.encode(payload, secret, algorithm="HS256") jws.decode(jws_token, secret, algorithms=["HS256"]) with pytest.raises(InvalidAlgorithmError): jws.decode(jws_token, secret, algorithms=["HS384"]) def test_decode_works_with_unicode_token(self, jws: PyJWS) -> None: secret = "secret" unicode_jws = ( "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) jws.decode(unicode_jws, secret, algorithms=["HS256"]) def test_decode_missing_segments_throws_exception(self, jws: PyJWS) -> None: secret = "secret" example_jws = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9" # Missing segment with pytest.raises(DecodeError) as context: jws.decode(example_jws, secret, algorithms=["HS256"]) exception = context.value assert str(exception) == "Not enough segments" def test_decode_invalid_token_type_is_none(self, jws: PyJWS) -> None: example_jws = None example_secret = "secret" with pytest.raises(DecodeError) as context: jws.decode( example_jws, # type: ignore[arg-type] example_secret, algorithms=["HS256"], ) exception = context.value assert "Invalid token type" in str(exception) def test_decode_invalid_token_type_is_int(self, jws: PyJWS) -> None: example_jws = 123 example_secret = "secret" with pytest.raises(DecodeError) as context: jws.decode( example_jws, # type: ignore[arg-type] example_secret, algorithms=["HS256"], ) exception = context.value assert "Invalid token type" in str(exception) def test_decode_with_non_mapping_header_throws_exception(self, jws: PyJWS) -> None: secret = "secret" example_jws = ( "MQ" # == 1 ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) with pytest.raises(DecodeError) as context: jws.decode(example_jws, secret, algorithms=["HS256"]) exception = context.value assert str(exception) == "Invalid header string: must be a json object" def test_encode_default_algorithm(self, jws: PyJWS, payload: bytes) -> None: msg = jws.encode(payload, "secret") decoded = jws.decode_complete(msg, "secret", algorithms=["HS256"]) assert decoded == { "header": {"alg": "HS256", "typ": "JWT"}, "payload": payload, "signature": ( b"H\x8a\xf4\xdf3:\xe1\xac\x16E\xd3\xeb\x00\xcf\xfa\xd5\x05\xac" b"e\xc8@\xb6\x00\xd5\xde\x9aa|s\xcfZB" ), } def test_encode_algorithm_param_should_be_case_sensitive( self, jws: PyJWS, payload: bytes ) -> None: jws.encode(payload, "secret", algorithm="HS256") with pytest.raises(NotImplementedError) as context: jws.encode(payload, "none", algorithm="hs256") exception = context.value assert str(exception) == "Algorithm not supported" def test_encode_with_headers_alg_none(self, jws: PyJWS, payload: bytes) -> None: msg = jws.encode( payload, # The type signature does not capture the fact that `key` may be # None when algorithm is "none". key=None, # type: ignore[arg-type,unused-ignore] headers={"alg": "none"}, ) with pytest.raises(DecodeError) as context: jws.decode(msg, algorithms=["none"]) assert str(context.value) == "Signature verification failed" @crypto_required def test_encode_with_headers_alg_es256(self, jws: PyJWS, payload: bytes) -> None: with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file: priv_key = load_pem_private_key(ec_priv_file.read(), password=None) with open(key_path("testkey_ec.pub"), "rb") as ec_pub_file: pub_key = load_pem_public_key(ec_pub_file.read()) assert isinstance(priv_key, EllipticCurvePrivateKey) assert isinstance(pub_key, EllipticCurvePublicKey) msg = jws.encode(payload, priv_key, headers={"alg": "ES256"}) assert b"hello world" == jws.decode(msg, pub_key, algorithms=["ES256"]) @crypto_required def test_encode_with_alg_hs256_and_headers_alg_es256( self, jws: PyJWS, payload: bytes ) -> None: with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file: priv_key = load_pem_private_key(ec_priv_file.read(), password=None) with open(key_path("testkey_ec.pub"), "rb") as ec_pub_file: pub_key = load_pem_public_key(ec_pub_file.read()) assert isinstance(priv_key, EllipticCurvePrivateKey) assert isinstance(pub_key, EllipticCurvePublicKey) msg = jws.encode(payload, priv_key, algorithm="HS256", headers={"alg": "ES256"}) assert b"hello world" == jws.decode(msg, pub_key, algorithms=["ES256"]) def test_encode_with_jwk(self, jws: PyJWS, payload: bytes) -> None: jwk = PyJWK( { "kty": "oct", "alg": "HS256", "k": "c2VjcmV0", # "secret" } ) msg = jws.encode(payload, key=jwk) decoded = jws.decode_complete(msg, key=jwk, algorithms=["HS256"]) assert decoded == { "header": {"alg": "HS256", "typ": "JWT"}, "payload": payload, "signature": ( b"H\x8a\xf4\xdf3:\xe1\xac\x16E\xd3\xeb\x00\xcf\xfa\xd5\x05\xac" b"e\xc8@\xb6\x00\xd5\xde\x9aa|s\xcfZB" ), } def test_encode_with_jwk_uses_key_algorithm( self, jws: PyJWS, payload: bytes ) -> None: """Test that encoding with a PyJWK key uses the key's algorithm when no algorithm is explicitly specified. Regression test for #1147.""" jwk = PyJWK( { "kty": "oct", "alg": "HS384", "k": "c2VjcmV0", # "secret" } ) # Should use HS384 from the key, not default to HS256 msg = jws.encode(payload, key=jwk) header = jws.get_unverified_header(msg) assert header["alg"] == "HS384" # Should also be decodable with the same key decoded = jws.decode(msg, key=jwk) assert decoded == payload def test_decode_algorithm_param_should_be_case_sensitive(self, jws: PyJWS) -> None: example_jws = ( "eyJhbGciOiJoczI1NiIsInR5cCI6IkpXVCJ9" # alg = hs256 ".eyJoZWxsbyI6IndvcmxkIn0" ".5R_FEPE7SW2dT9GgIxPgZATjFGXfUDOSwo7TtO_Kd_g" ) with pytest.raises(InvalidAlgorithmError) as context: jws.decode(example_jws, "secret", algorithms=["hs256"]) exception = context.value assert str(exception) == "Algorithm not supported" def test_bad_secret(self, jws: PyJWS, payload: bytes) -> None: right_secret = "foo" bad_secret = "bar" jws_message = jws.encode(payload, right_secret) with pytest.raises(DecodeError) as excinfo: # Backward compat for ticket #315 jws.decode(jws_message, bad_secret, algorithms=["HS256"]) assert "Signature verification failed" == str(excinfo.value) with pytest.raises(InvalidSignatureError) as excinfo: jws.decode(jws_message, bad_secret, algorithms=["HS256"]) assert "Signature verification failed" == str(excinfo.value) def test_decodes_valid_jws(self, jws: PyJWS, payload: bytes) -> None: example_secret = "secret" example_jws = ( b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." b"aGVsbG8gd29ybGQ." b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" ) decoded_payload = jws.decode(example_jws, example_secret, algorithms=["HS256"]) assert decoded_payload == payload def test_decodes_complete_valid_jws(self, jws: PyJWS, payload: bytes) -> None: example_secret = "secret" example_jws = ( b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." b"aGVsbG8gd29ybGQ." b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" ) decoded = jws.decode_complete(example_jws, example_secret, algorithms=["HS256"]) assert decoded == { "header": {"alg": "HS256", "typ": "JWT"}, "payload": payload, "signature": ( b"\x80E\xb4\xa5\xd58\x93\x13\xed\x86;^\x85\x87a\xc4" b"\x1ff0\xe1\x9a\x8e\xddq\x08\xa9F\x19p\xc9\xf0\xf3" ), } def test_decodes_with_jwk(self, jws: PyJWS, payload: bytes) -> None: jwk = PyJWK( { "kty": "oct", "alg": "HS256", "k": "c2VjcmV0", # "secret" } ) example_jws = ( b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." b"aGVsbG8gd29ybGQ." b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" ) decoded_payload = jws.decode(example_jws, jwk, algorithms=["HS256"]) assert decoded_payload == payload def test_decodes_with_jwk_and_no_algorithm( self, jws: PyJWS, payload: bytes ) -> None: jwk = PyJWK( { "kty": "oct", "alg": "HS256", "k": "c2VjcmV0", # "secret" } ) example_jws = ( b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." b"aGVsbG8gd29ybGQ." b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" ) decoded_payload = jws.decode(example_jws, jwk) assert decoded_payload == payload def test_decodes_with_jwk_and_mismatched_algorithm( self, jws: PyJWS, payload: bytes ) -> None: jwk = PyJWK( { "kty": "oct", "alg": "HS512", "k": "c2VjcmV0", # "secret" } ) example_jws = ( b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." b"aGVsbG8gd29ybGQ." b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" ) with pytest.raises(InvalidAlgorithmError): jws.decode(example_jws, jwk) # 'Control' Elliptic Curve jws created by another library. # Used to test for regressions that could affect both # encoding / decoding operations equally (causing tests # to still pass). @crypto_required def test_decodes_valid_es384_jws(self, jws: PyJWS) -> None: example_payload = {"hello": "world"} with open(key_path("testkey_ec.pub")) as fp: example_pubkey = fp.read() example_jws = ( b"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9." b"eyJoZWxsbyI6IndvcmxkIn0.TORyNQab_MoXM7DvNKaTwbrJr4UY" b"d2SsX8hhlnWelQFmPFSf_JzC2EbLnar92t-bXsDovzxp25ExazrVHkfPkQ" ) decoded_payload = jws.decode(example_jws, example_pubkey, algorithms=["ES256"]) json_payload = json.loads(decoded_payload) assert json_payload == example_payload # 'Control' RSA jws created by another library. # Used to test for regressions that could affect both # encoding / decoding operations equally (causing tests # to still pass). @crypto_required def test_decodes_valid_rs384_jws(self, jws: PyJWS) -> None: example_payload = {"hello": "world"} with open(key_path("testkey_rsa.pub")) as fp: example_pubkey = fp.read() example_jws = ( b"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9" b".eyJoZWxsbyI6IndvcmxkIn0" b".yNQ3nI9vEDs7lEh-Cp81McPuiQ4ZRv6FL4evTYYAh1X" b"lRTTR3Cz8pPA9Stgso8Ra9xGB4X3rlra1c8Jz10nTUju" b"O06OMm7oXdrnxp1KIiAJDerWHkQ7l3dlizIk1bmMA457" b"W2fNzNfHViuED5ISM081dgf_a71qBwJ_yShMMrSOfxDx" b"mX9c4DjRogRJG8SM5PvpLqI_Cm9iQPGMvmYK7gzcq2cJ" b"urHRJDJHTqIdpLWXkY7zVikeen6FhuGyn060Dz9gYq9t" b"uwmrtSWCBUjiN8sqJ00CDgycxKqHfUndZbEAOjcCAhBr" b"qWW3mSVivUfubsYbwUdUG3fSRPjaUPcpe8A" ) decoded_payload = jws.decode(example_jws, example_pubkey, algorithms=["RS384"]) json_payload = json.loads(decoded_payload) assert json_payload == example_payload def test_load_verify_valid_jws(self, jws: PyJWS, payload: bytes) -> None: example_secret = "secret" example_jws = ( b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." b"SIr03zM64awWRdPrAM_61QWsZchAtgDV3pphfHPPWkI" ) decoded_payload = jws.decode( example_jws, key=example_secret, algorithms=["HS256"] ) assert decoded_payload == payload def test_allow_skip_verification(self, jws: PyJWS, payload: bytes) -> None: right_secret = "foo" jws_message = jws.encode(payload, right_secret) decoded_payload = jws.decode(jws_message, options={"verify_signature": False}) assert decoded_payload == payload def test_decode_with_optional_algorithms(self, jws: PyJWS) -> None: example_secret = "secret" example_jws = ( b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." b"SIr03zM64awWRdPrAM_61QWsZchAtgDV3pphfHPPWkI" ) with pytest.raises(DecodeError) as exc: jws.decode(example_jws, key=example_secret) assert ( 'It is required that you pass in a value for the "algorithms" argument when calling decode().' in str(exc.value) ) def test_decode_no_algorithms_verify_signature_false(self, jws: PyJWS) -> None: example_secret = "secret" example_jws = ( b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." b"aGVsbG8gd29ybGQ." b"SIr03zM64awWRdPrAM_61QWsZchAtgDV3pphfHPPWkI" ) jws.decode( example_jws, key=example_secret, options={"verify_signature": False}, ) def test_load_no_verification(self, jws: PyJWS, payload: bytes) -> None: right_secret = "foo" jws_message = jws.encode(payload, right_secret) decoded_payload = jws.decode( jws_message, # The type signature does not capture the fact that `key` is not # used when options["verify_signature"] is False. key=None, # type: ignore[arg-type,unused-ignore] algorithms=["HS256"], options={"verify_signature": False}, ) assert decoded_payload == payload def test_no_secret(self, jws: PyJWS, payload: bytes) -> None: right_secret = "foo" jws_message = jws.encode(payload, right_secret) with pytest.raises(DecodeError): jws.decode(jws_message, algorithms=["HS256"]) def test_verify_signature_with_no_secret(self, jws: PyJWS, payload: bytes) -> None: right_secret = "foo" jws_message = jws.encode(payload, right_secret) with pytest.raises(DecodeError) as exc: jws.decode(jws_message, algorithms=["HS256"]) assert "Signature verification" in str(exc.value) def test_verify_signature_with_no_algo_header_throws_exception( self, jws: PyJWS, payload: bytes ) -> None: example_jws = b"e30.eyJhIjo1fQ.KEh186CjVw_Q8FadjJcaVnE7hO5Z9nHBbU8TgbhHcBY" with pytest.raises(InvalidAlgorithmError): jws.decode(example_jws, "secret", algorithms=["HS256"]) def test_invalid_crypto_alg(self, jws: PyJWS, payload: bytes) -> None: with pytest.raises(NotImplementedError): jws.encode(payload, "secret", algorithm="HS1024") @no_crypto_required def test_missing_crypto_library_better_error_messages( self, jws: PyJWS, payload: bytes ) -> None: with pytest.raises(NotImplementedError) as excinfo: jws.encode(payload, "secret", algorithm="RS256") assert "cryptography" in str(excinfo.value) def test_unicode_secret(self, jws: PyJWS, payload: bytes) -> None: secret = "\xc2" jws_message = jws.encode(payload, secret) decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) assert decoded_payload == payload def test_nonascii_secret(self, jws: PyJWS, payload: bytes) -> None: secret = "\xc2" # char value that ascii codec cannot decode jws_message = jws.encode(payload, secret) decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) assert decoded_payload == payload def test_bytes_secret(self, jws: PyJWS, payload: bytes) -> None: secret = b"\xc2" # char value that ascii codec cannot decode jws_message = jws.encode(payload, secret) decoded_payload = jws.decode(jws_message, secret, algorithms=["HS256"]) assert decoded_payload == payload @pytest.mark.parametrize("sort_headers", (False, True)) def test_sorting_of_headers( self, jws: PyJWS, payload: bytes, sort_headers: bool ) -> None: jws_message = jws.encode( payload, key="\xc2", headers={"b": "1", "a": "2"}, sort_headers=sort_headers, ) header_json = base64url_decode(jws_message.split(".")[0]) assert sort_headers == (header_json.index(b'"a"') < header_json.index(b'"b"')) def test_decode_invalid_header_padding(self, jws: PyJWS) -> None: example_jws = ( "aeyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) example_secret = "secret" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) assert "header padding" in str(exc.value) def test_decode_invalid_header_string(self, jws: PyJWS) -> None: example_jws = ( "eyJhbGciOiAiSFMyNTbpIiwgInR5cCI6ICJKV1QifQ==" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) example_secret = "secret" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) assert "Invalid header" in str(exc.value) def test_decode_invalid_payload_padding(self, jws: PyJWS) -> None: example_jws = ( "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".aeyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) example_secret = "secret" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) assert "Invalid payload padding" in str(exc.value) def test_decode_invalid_crypto_padding(self, jws: PyJWS) -> None: example_jws = ( "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".aatvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) example_secret = "secret" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) assert "Invalid crypto padding" in str(exc.value) def test_decode_with_algo_none_should_fail( self, jws: PyJWS, payload: bytes ) -> None: jws_message = jws.encode( payload, # The type signature does not capture the fact that `key` may be # None when algorithm is "none". key=None, # type: ignore[arg-type,unused-ignore] algorithm="none", ) with pytest.raises(DecodeError): jws.decode(jws_message, algorithms=["none"]) def test_decode_with_algo_none_and_verify_false_should_pass( self, jws: PyJWS, payload: bytes ) -> None: jws_message = jws.encode( payload, # The type signature does not capture the fact that `key` may be # None when algorithm is "none". key=None, # type: ignore[arg-type,unused-ignore] algorithm="none", ) jws.decode(jws_message, options={"verify_signature": False}) def test_get_unverified_header_returns_header_values( self, jws: PyJWS, payload: bytes ) -> None: jws_message = jws.encode( payload, key="secret", algorithm="HS256", headers={"kid": "toomanysecrets"}, ) header = jws.get_unverified_header(jws_message) assert "kid" in header assert header["kid"] == "toomanysecrets" def test_get_unverified_header_fails_on_bad_header_types( self, jws: PyJWS, payload: bytes ) -> None: # Contains a bad kid value (int 123 instead of string) example_jws = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6MTIzfQ" ".eyJzdWIiOiIxMjM0NTY3ODkwIn0" ".vs2WY54jfpKP3JGC73Vq5YlMsqM5oTZ1ZydT77SiZSk" ) with pytest.raises(InvalidTokenError) as exc: jws.get_unverified_header(example_jws) assert "Key ID header parameter must be a string" == str(exc.value) @pytest.mark.parametrize( "algo", [ "RS256", "RS384", "RS512", ], ) @crypto_required def test_encode_decode_rsa_related_algorithms( self, jws: PyJWS, payload: bytes, algo: str ) -> None: # PEM-formatted RSA key with open(key_path("testkey_rsa.priv"), "rb") as rsa_priv_file: priv_rsakey = load_pem_private_key(rsa_priv_file.read(), password=None) assert isinstance(priv_rsakey, RSAPrivateKey) jws_message = jws.encode(payload, priv_rsakey, algorithm=algo) with open(key_path("testkey_rsa.pub"), "rb") as rsa_pub_file: pub_rsakey = load_ssh_public_key(rsa_pub_file.read()) assert isinstance(pub_rsakey, RSAPublicKey) jws.decode(jws_message, pub_rsakey, algorithms=[algo]) # string-formatted key with open(key_path("testkey_rsa.priv")) as rsa_priv_file: priv_rsakey_str = rsa_priv_file.read() jws_message = jws.encode(payload, priv_rsakey_str, algorithm=algo) with open(key_path("testkey_rsa.pub")) as rsa_pub_file: pub_rsakey_str = rsa_pub_file.read() jws.decode(jws_message, pub_rsakey_str, algorithms=[algo]) def test_rsa_related_algorithms(self, jws: PyJWS) -> None: jws = PyJWS() jws_algorithms = jws.get_algorithms() if has_crypto: assert "RS256" in jws_algorithms assert "RS384" in jws_algorithms assert "RS512" in jws_algorithms assert "PS256" in jws_algorithms assert "PS384" in jws_algorithms assert "PS512" in jws_algorithms else: assert "RS256" not in jws_algorithms assert "RS384" not in jws_algorithms assert "RS512" not in jws_algorithms assert "PS256" not in jws_algorithms assert "PS384" not in jws_algorithms assert "PS512" not in jws_algorithms @pytest.mark.parametrize( "algo,priv_key_file,pub_key_file", [ ("ES256", "jwk_ec_key_P-256.json", "jwk_ec_pub_P-256.json"), ("ES256K", "jwk_ec_key_secp256k1.json", "jwk_ec_pub_secp256k1.json"), ("ES384", "jwk_ec_key_P-384.json", "jwk_ec_pub_P-384.json"), ("ES512", "jwk_ec_key_P-521.json", "jwk_ec_pub_P-521.json"), ], ) @crypto_required def test_encode_decode_ecdsa_related_algorithms( self, jws: PyJWS, payload: bytes, algo: str, priv_key_file: str, pub_key_file: str, ) -> None: from jwt.algorithms import ECAlgorithm # Load keys from JWK files (each algorithm requires its specific curve) with open(key_path(priv_key_file)) as priv_file: priv_eckey = ECAlgorithm.from_jwk(priv_file.read()) assert isinstance(priv_eckey, EllipticCurvePrivateKey) jws_message = jws.encode(payload, priv_eckey, algorithm=algo) with open(key_path(pub_key_file)) as pub_file: pub_eckey = ECAlgorithm.from_jwk(pub_file.read()) assert isinstance(pub_eckey, EllipticCurvePublicKey) jws.decode(jws_message, pub_eckey, algorithms=[algo]) def test_ecdsa_related_algorithms(self, jws: PyJWS) -> None: jws = PyJWS() jws_algorithms = jws.get_algorithms() if has_crypto: assert "ES256" in jws_algorithms assert "ES256K" in jws_algorithms assert "ES384" in jws_algorithms assert "ES512" in jws_algorithms else: assert "ES256" not in jws_algorithms assert "ES256K" not in jws_algorithms assert "ES384" not in jws_algorithms assert "ES512" not in jws_algorithms def test_skip_check_signature(self, jws: PyJWS) -> None: token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJzb21lIjoicGF5bG9hZCJ9" ".4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZA" ) jws.decode(token, "secret", options={"verify_signature": False}) def test_decode_options_must_be_dict(self, jws: PyJWS, payload: bytes) -> None: token = jws.encode(payload, "secret") with pytest.raises(TypeError): jws.decode(token, "secret", options=object()) # type: ignore[arg-type] with pytest.raises((TypeError, ValueError)): jws.decode(token, "secret", options="something") # type: ignore[arg-type] def test_custom_json_encoder(self, jws: PyJWS, payload: bytes) -> None: class CustomJSONEncoder(json.JSONEncoder): def default(self, o: object) -> str: assert isinstance(o, Decimal) return "it worked" data = {"some_decimal": Decimal("2.2")} with pytest.raises(TypeError): jws.encode(payload, "secret", headers=data) token = jws.encode( payload, "secret", headers=data, json_encoder=CustomJSONEncoder ) header, *_ = token.split(".") header = json.loads(base64url_decode(header)) assert "some_decimal" in header assert header["some_decimal"] == "it worked" def test_encode_headers_parameter_adds_headers( self, jws: PyJWS, payload: bytes ) -> None: headers = {"testheader": True} token = jws.encode(payload, "secret", headers=headers) encoded_header = token[0 : token.index(".")].encode() decoded_header = base64url_decode(encoded_header) header = decoded_header.decode() header_obj = json.loads(header) assert "testheader" in header_obj assert header_obj["testheader"] == headers["testheader"] def test_encode_with_typ(self, jws: PyJWS) -> None: payload = """ { "iss": "https://scim.example.com", "iat": 1458496404, "jti": "4d3559ec67504aaba65d40b0363faad8", "aud": [ "https://scim.example.com/Feeds/98d52461fa5bbc879593b7754", "https://scim.example.com/Feeds/5d7604516b1d08641d7676ee7" ], "events": { "urn:ietf:params:scim:event:create": { "ref": "https://scim.example.com/Users/44f6142df96bd6ab61e7521d9", "attributes": ["id", "name", "userName", "password", "emails"] } } } """ token = jws.encode( payload.encode("utf-8"), "secret", headers={"typ": "secevent+jwt"} ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) assert "typ" in header_obj assert header_obj["typ"] == "secevent+jwt" def test_encode_with_typ_empty_string(self, jws: PyJWS, payload: bytes) -> None: token = jws.encode(payload, "secret", headers={"typ": ""}) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) assert "typ" not in header_obj def test_encode_with_typ_none(self, jws: PyJWS, payload: bytes) -> None: token = jws.encode(payload, "secret", headers={"typ": None}) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) assert "typ" not in header_obj def test_encode_with_typ_without_keywords(self, jws: PyJWS, payload: bytes) -> None: headers = {"foo": "bar"} token = jws.encode(payload, "secret", "HS256", headers, None) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) assert "foo" in header_obj assert header_obj["foo"] == "bar" def test_encode_fails_on_invalid_kid_types( self, jws: PyJWS, payload: bytes ) -> None: with pytest.raises(InvalidTokenError) as exc: jws.encode(payload, "secret", headers={"kid": 123}) assert "Key ID header parameter must be a string" == str(exc.value) with pytest.raises(InvalidTokenError) as exc: jws.encode(payload, "secret", headers={"kid": None}) assert "Key ID header parameter must be a string" == str(exc.value) def test_encode_decode_with_detached_content( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" jws_message = jws.encode( payload, secret, algorithm="HS256", is_payload_detached=True ) jws.decode(jws_message, secret, algorithms=["HS256"], detached_payload=payload) def test_encode_detached_content_with_b64_header( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" # Check that detached content is automatically detected when b64 is false headers = {"b64": False} token = jws.encode(payload, secret, "HS256", headers) msg_header, msg_payload, _ = token.split(".") msg_header_bytes = base64url_decode(msg_header.encode()) msg_header_obj = json.loads(msg_header_bytes) assert "b64" in msg_header_obj assert msg_header_obj["b64"] is False # Check that the payload is not inside the token assert not msg_payload # Check that content is not detached and b64 header removed when b64 is true headers = {"b64": True} token = jws.encode(payload, secret, "HS256", headers) msg_header, msg_payload, _ = token.split(".") msg_header_bytes = base64url_decode(msg_header.encode()) msg_header_obj = json.loads(msg_header_bytes) assert "b64" not in msg_header_obj assert msg_payload def test_decode_detached_content_without_proper_argument(self, jws: PyJWS) -> None: example_jws = ( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2V9" "." ".65yNkX_ZH4A_6pHaTL_eI84OXOHtfl4K0k5UnlXZ8f4" ) example_secret = "secret" with pytest.raises(DecodeError) as exc: jws.decode(example_jws, example_secret, algorithms=["HS256"]) assert ( 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' in str(exc.value) ) def test_decode_warns_on_unsupported_kwarg( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" jws_message = jws.encode( payload, secret, algorithm="HS256", is_payload_detached=True ) with pytest.warns(RemovedInPyjwt3Warning) as record: jws.decode( jws_message, secret, algorithms=["HS256"], detached_payload=payload, foo="bar", # type: ignore[arg-type] ) deprecation_warnings = [ w for w in record if issubclass(w.category, RemovedInPyjwt3Warning) ] assert len(deprecation_warnings) == 1 assert "foo" in str(deprecation_warnings[0].message) def test_decode_complete_warns_on_unuspported_kwarg( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" jws_message = jws.encode( payload, secret, algorithm="HS256", is_payload_detached=True ) with pytest.warns(RemovedInPyjwt3Warning) as record: jws.decode_complete( jws_message, secret, algorithms=["HS256"], detached_payload=payload, foo="bar", # type: ignore[arg-type] ) deprecation_warnings = [ w for w in record if issubclass(w.category, RemovedInPyjwt3Warning) ] assert len(deprecation_warnings) == 1 assert "foo" in str(deprecation_warnings[0].message) def test_decode_rejects_unknown_crit_extension( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": ["x-custom-policy"], "x-custom-policy": "require-mfa"}, ) with pytest.raises(InvalidTokenError, match="Unsupported critical extension"): jws.decode(token, secret, algorithms=["HS256"]) def test_decode_rejects_empty_crit(self, jws: PyJWS, payload: bytes) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": []}, ) with pytest.raises(InvalidTokenError, match="must be a non-empty list"): jws.decode(token, secret, algorithms=["HS256"]) def test_decode_rejects_non_list_crit(self, jws: PyJWS, payload: bytes) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": "b64"}, ) with pytest.raises(InvalidTokenError, match="must be a non-empty list"): jws.decode(token, secret, algorithms=["HS256"]) def test_decode_rejects_crit_with_non_string_values( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": [123]}, ) with pytest.raises(InvalidTokenError, match="values must be strings"): jws.decode(token, secret, algorithms=["HS256"]) def test_decode_rejects_crit_extension_missing_from_header( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": ["b64"]}, ) with pytest.raises(InvalidTokenError, match="missing from headers"): jws.decode(token, secret, algorithms=["HS256"]) def test_decode_accepts_supported_crit_extension( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": ["b64"], "b64": False}, is_payload_detached=True, ) decoded = jws.decode( token, secret, algorithms=["HS256"], detached_payload=payload, ) assert decoded == payload def test_get_unverified_header_rejects_unknown_crit( self, jws: PyJWS, payload: bytes ) -> None: secret = "secret" token = jws.encode( payload, secret, algorithm="HS256", headers={"crit": ["x-unknown"], "x-unknown": "value"}, ) with pytest.raises(InvalidTokenError, match="Unsupported critical extension"): jws.get_unverified_header(token) jpadilla-pyjwt-a4e1a3d/tests/test_api_jwt.py000066400000000000000000001126561515505507500213640ustar00rootroot00000000000000import json import time from calendar import timegm from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest from jwt.types import Options from jwt.api_jwk import PyJWK from jwt.api_jwt import PyJWT from jwt.exceptions import ( DecodeError, ExpiredSignatureError, ImmatureSignatureError, InvalidAudienceError, InvalidIssuedAtError, InvalidIssuerError, InvalidJTIError, InvalidSubjectError, MissingRequiredClaimError, ) from jwt.utils import base64url_decode from jwt.warnings import RemovedInPyjwt3Warning from .utils import crypto_required, key_path, utc_timestamp @pytest.fixture def jwt() -> PyJWT: return PyJWT() @pytest.fixture def payload() -> dict[str, object]: """Creates a sample JWT claimset for use as a payload during tests""" return {"iss": "jeff", "exp": utc_timestamp() + 15, "claim": "insanity"} class TestJWT: def test_jwt_with_options(self) -> None: jwt = PyJWT(options={"verify_signature": False}) assert jwt.options["verify_signature"] is False # assert that unrelated option is unchanged from default assert jwt.options["strict_aud"] is False # assert that verify_signature is respected unless verify_exp is overridden assert jwt.options["verify_exp"] is False def test_encode_with_jwk_uses_key_algorithm(self, jwt: PyJWT) -> None: """Test that encoding with a PyJWK key uses the key's algorithm when no algorithm is explicitly specified. Regression test for #1147.""" jwk = PyJWK( { "kty": "oct", "alg": "HS384", "k": "c2VjcmV0", # "secret" } ) payload = {"hello": "world"} # Should use HS384 from the key, not default to HS256 token = jwt.encode(payload, jwk) header = jwt.decode_complete(token, jwk, algorithms=["HS384"])["header"] assert header["alg"] == "HS384" def test_decodes_valid_jwt(self, jwt: PyJWT) -> None: example_payload = {"hello": "world"} example_secret = "secret" example_jwt = ( b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" b".eyJoZWxsbyI6ICJ3b3JsZCJ9" b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) decoded_payload = jwt.decode(example_jwt, example_secret, algorithms=["HS256"]) assert decoded_payload == example_payload def test_decodes_complete_valid_jwt(self, jwt: PyJWT) -> None: example_payload = {"hello": "world"} example_secret = "secret" example_jwt = ( b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" b".eyJoZWxsbyI6ICJ3b3JsZCJ9" b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) decoded = jwt.decode_complete(example_jwt, example_secret, algorithms=["HS256"]) assert decoded == { "header": {"alg": "HS256", "typ": "JWT"}, "payload": example_payload, "signature": ( b'\xb6\xf6\xa0,2\xe8j"J\xc4\xe2\xaa\xa4\x15\xd2' b"\x10l\xbbI\x84\xa2}\x98c\x9e\xd8&\xf5\xcbi\xca?" ), } def test_load_verify_valid_jwt(self, jwt: PyJWT) -> None: example_payload = {"hello": "world"} example_secret = "secret" example_jwt = ( b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" b".eyJoZWxsbyI6ICJ3b3JsZCJ9" b".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) decoded_payload = jwt.decode( example_jwt, key=example_secret, algorithms=["HS256"] ) assert decoded_payload == example_payload def test_decode_invalid_payload_string(self, jwt: PyJWT) -> None: example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aGVsb" "G8gd29ybGQ.SIr03zM64awWRdPrAM_61QWsZchAtgDV" "3pphfHPPWkI" ) example_secret = "secret" with pytest.raises(DecodeError) as exc: jwt.decode(example_jwt, example_secret, algorithms=["HS256"]) assert "Invalid payload string" in str(exc.value) def test_decode_with_non_mapping_payload_throws_exception(self, jwt: PyJWT) -> None: secret = "secret" example_jwt = ( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." "MQ." # == 1 "AbcSR3DWum91KOgfKxUHm78rLs_DrrZ1CrDgpUFFzls" ) with pytest.raises(DecodeError) as context: jwt.decode(example_jwt, secret, algorithms=["HS256"]) exception = context.value assert str(exception) == "Invalid payload string: must be a json object" def test_decode_with_invalid_audience_param_throws_exception( self, jwt: PyJWT ) -> None: secret = "secret" example_jwt = ( "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) with pytest.raises(TypeError) as context: jwt.decode( example_jwt, secret, audience=1, # type: ignore[arg-type] algorithms=["HS256"], ) exception = context.value assert str(exception) == "audience must be a string, iterable or None" def test_decode_with_nonlist_aud_claim_throws_exception(self, jwt: PyJWT) -> None: secret = "secret" example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJoZWxsbyI6IndvcmxkIiwiYXVkIjoxfQ" # aud = 1 ".Rof08LBSwbm8Z_bhA2N3DFY-utZR1Gi9rbIS5Zthnnc" ) with pytest.raises(InvalidAudienceError) as context: jwt.decode( example_jwt, secret, audience="my_audience", algorithms=["HS256"], ) exception = context.value assert str(exception) == "Invalid claim format in token" def test_decode_with_invalid_aud_list_member_throws_exception( self, jwt: PyJWT ) -> None: secret = "secret" example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJoZWxsbyI6IndvcmxkIiwiYXVkIjpbMV19" ".iQgKpJ8shetwNMIosNXWBPFB057c2BHs-8t1d2CCM2A" ) with pytest.raises(InvalidAudienceError) as context: jwt.decode( example_jwt, secret, audience="my_audience", algorithms=["HS256"], ) exception = context.value assert str(exception) == "Invalid claim format in token" def test_encode_bad_type(self, jwt: PyJWT) -> None: types = ["string", tuple(), list(), 42, set()] for t in types: pytest.raises( TypeError, lambda t=t: jwt.encode(t, "secret", algorithm="HS256"), ) def test_encode_with_non_str_iss(self, jwt: PyJWT) -> None: """Regression test for Issue #1039.""" with pytest.raises(TypeError): jwt.encode( { "iss": 123, }, key="secret", ) def test_encode_with_typ(self, jwt: PyJWT) -> None: payload = { "iss": "https://scim.example.com", "iat": 1458496404, "jti": "4d3559ec67504aaba65d40b0363faad8", "aud": [ "https://scim.example.com/Feeds/98d52461fa5bbc879593b7754", "https://scim.example.com/Feeds/5d7604516b1d08641d7676ee7", ], "events": { "urn:ietf:params:scim:event:create": { "ref": "https://scim.example.com/Users/44f6142df96bd6ab61e7521d9", "attributes": ["id", "name", "userName", "password", "emails"], } }, } token = jwt.encode( payload, "secret", algorithm="HS256", headers={"typ": "secevent+jwt"} ) header = token[0 : token.index(".")].encode() header = base64url_decode(header) header_obj = json.loads(header) assert "typ" in header_obj assert header_obj["typ"] == "secevent+jwt" def test_decode_raises_exception_if_exp_is_not_int(self, jwt: PyJWT) -> None: # >>> jwt.encode({'exp': 'not-an-int'}, 'secret') example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJleHAiOiJub3QtYW4taW50In0." "P65iYgoHtBqB07PMtBSuKNUEIPPPfmjfJG217cEE66s" ) with pytest.raises(DecodeError) as exc: jwt.decode(example_jwt, "secret", algorithms=["HS256"]) assert "exp" in str(exc.value) def test_decode_raises_exception_if_iat_is_not_int(self, jwt: PyJWT) -> None: # >>> jwt.encode({'iat': 'not-an-int'}, 'secret') example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJpYXQiOiJub3QtYW4taW50In0." "H1GmcQgSySa5LOKYbzGm--b1OmRbHFkyk8pq811FzZM" ) with pytest.raises(InvalidIssuedAtError): jwt.decode(example_jwt, "secret", algorithms=["HS256"]) def test_decode_raises_exception_if_iat_is_greater_than_now( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["iat"] = utc_timestamp() + 10 secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.raises(ImmatureSignatureError): jwt.decode(jwt_message, secret, algorithms=["HS256"]) def test_decode_works_if_iat_is_str_of_a_number( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["iat"] = "1638202770" secret = "secret" jwt_message = jwt.encode(payload, secret) data = jwt.decode(jwt_message, secret, algorithms=["HS256"]) assert data["iat"] == "1638202770" def test_decode_raises_exception_if_nbf_is_not_int(self, jwt: PyJWT) -> None: # >>> jwt.encode({'nbf': 'not-an-int'}, 'secret') example_jwt = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." "eyJuYmYiOiJub3QtYW4taW50In0." "c25hldC8G2ZamC8uKpax9sYMTgdZo3cxrmzFHaAAluw" ) with pytest.raises(DecodeError): jwt.decode(example_jwt, "secret", algorithms=["HS256"]) def test_decode_allows_aud_to_be_none(self, jwt: PyJWT) -> None: # >>> jwt.encode({'aud': None}, 'secret') example_jwt = ( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." "eyJhdWQiOm51bGx9." "-Peqc-pTugGvrc5C8Bnl0-X1V_5fv-aVb_7y7nGBVvQ" ) decoded = jwt.decode(example_jwt, "secret", algorithms=["HS256"]) assert decoded["aud"] is None def test_encode_datetime(self, jwt: PyJWT) -> None: secret = "secret" current_datetime = datetime.now(tz=timezone.utc) payload = { "exp": current_datetime, "iat": current_datetime, "nbf": current_datetime, } jwt_message = jwt.encode(payload, secret) decoded_payload = jwt.decode( jwt_message, secret, leeway=1, algorithms=["HS256"] ) assert decoded_payload["exp"] == timegm(current_datetime.utctimetuple()) assert decoded_payload["iat"] == timegm(current_datetime.utctimetuple()) assert decoded_payload["nbf"] == timegm(current_datetime.utctimetuple()) # payload is not mutated. assert payload == { "exp": current_datetime, "iat": current_datetime, "nbf": current_datetime, } # 'Control' Elliptic Curve JWT created by another library. # Used to test for regressions that could affect both # encoding / decoding operations equally (causing tests # to still pass). @crypto_required def test_decodes_valid_es256_jwt(self, jwt: PyJWT) -> None: example_payload = {"hello": "world"} with open(key_path("testkey_ec.pub")) as fp: example_pubkey = fp.read() example_jwt = ( b"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9." b"eyJoZWxsbyI6IndvcmxkIn0.TORyNQab_MoXM7DvNKaTwbrJr4UY" b"d2SsX8hhlnWelQFmPFSf_JzC2EbLnar92t-bXsDovzxp25ExazrVHkfPkQ" ) decoded_payload = jwt.decode(example_jwt, example_pubkey, algorithms=["ES256"]) assert decoded_payload == example_payload # 'Control' RSA JWT created by another library. # Used to test for regressions that could affect both # encoding / decoding operations equally (causing tests # to still pass). @crypto_required def test_decodes_valid_rs384_jwt(self, jwt: PyJWT) -> None: example_payload = {"hello": "world"} with open(key_path("testkey_rsa.pub")) as fp: example_pubkey = fp.read() example_jwt = ( b"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9" b".eyJoZWxsbyI6IndvcmxkIn0" b".yNQ3nI9vEDs7lEh-Cp81McPuiQ4ZRv6FL4evTYYAh1X" b"lRTTR3Cz8pPA9Stgso8Ra9xGB4X3rlra1c8Jz10nTUju" b"O06OMm7oXdrnxp1KIiAJDerWHkQ7l3dlizIk1bmMA457" b"W2fNzNfHViuED5ISM081dgf_a71qBwJ_yShMMrSOfxDx" b"mX9c4DjRogRJG8SM5PvpLqI_Cm9iQPGMvmYK7gzcq2cJ" b"urHRJDJHTqIdpLWXkY7zVikeen6FhuGyn060Dz9gYq9t" b"uwmrtSWCBUjiN8sqJ00CDgycxKqHfUndZbEAOjcCAhBr" b"qWW3mSVivUfubsYbwUdUG3fSRPjaUPcpe8A" ) decoded_payload = jwt.decode(example_jwt, example_pubkey, algorithms=["RS384"]) assert decoded_payload == example_payload def test_decode_with_expiration( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["exp"] = utc_timestamp() - 1 secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.raises(ExpiredSignatureError): jwt.decode(jwt_message, secret, algorithms=["HS256"]) def test_decode_with_notbefore( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["nbf"] = utc_timestamp() + 10 secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.raises(ImmatureSignatureError): jwt.decode(jwt_message, secret, algorithms=["HS256"]) def test_decode_skip_expiration_verification( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["exp"] = time.time() - 1 secret = "secret" jwt_message = jwt.encode(payload, secret) jwt.decode( jwt_message, secret, algorithms=["HS256"], options={"verify_exp": False}, ) def test_decode_skip_notbefore_verification( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["nbf"] = time.time() + 10 secret = "secret" jwt_message = jwt.encode(payload, secret) jwt.decode( jwt_message, secret, algorithms=["HS256"], options={"verify_nbf": False}, ) def test_decode_with_expiration_with_leeway( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["exp"] = utc_timestamp() - 2 secret = "secret" jwt_message = jwt.encode(payload, secret) # With 5 seconds leeway, should be ok for leeway in (5, timedelta(seconds=5)): decoded = jwt.decode( jwt_message, secret, leeway=leeway, algorithms=["HS256"] ) assert decoded == payload # With 1 seconds, should fail for leeway in (1, timedelta(seconds=1)): with pytest.raises(ExpiredSignatureError): jwt.decode(jwt_message, secret, leeway=leeway, algorithms=["HS256"]) def test_decode_with_notbefore_with_leeway( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["nbf"] = utc_timestamp() + 10 secret = "secret" jwt_message = jwt.encode(payload, secret) # With 13 seconds leeway, should be ok jwt.decode(jwt_message, secret, leeway=13, algorithms=["HS256"]) with pytest.raises(ImmatureSignatureError): jwt.decode(jwt_message, secret, leeway=1, algorithms=["HS256"]) def test_check_audience_when_valid(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": "urn:me"} token = jwt.encode(payload, "secret") jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) def test_check_audience_list_when_valid(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": "urn:me"} token = jwt.encode(payload, "secret") jwt.decode( token, "secret", audience=["urn:you", "urn:me"], algorithms=["HS256"], ) def test_check_audience_none_specified(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": "urn:me"} token = jwt.encode(payload, "secret") with pytest.raises(InvalidAudienceError): jwt.decode(token, "secret", algorithms=["HS256"]) def test_raise_exception_invalid_audience_list(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": "urn:me"} token = jwt.encode(payload, "secret") with pytest.raises(InvalidAudienceError): jwt.decode( token, "secret", audience=["urn:you", "urn:him"], algorithms=["HS256"], ) def test_check_audience_in_array_when_valid(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": ["urn:me", "urn:someone-else"]} token = jwt.encode(payload, "secret") jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) def test_raise_exception_invalid_audience(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": "urn:someone-else"} token = jwt.encode(payload, "secret") with pytest.raises(InvalidAudienceError): jwt.decode(token, "secret", audience="urn-me", algorithms=["HS256"]) def test_raise_exception_audience_as_bytes(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": ["urn:me", "urn:someone-else"]} token = jwt.encode(payload, "secret") with pytest.raises(InvalidAudienceError): jwt.decode( token, "secret", audience=b"urn:me", # type: ignore[arg-type] algorithms=["HS256"], ) def test_raise_exception_invalid_audience_in_array(self, jwt: PyJWT) -> None: payload = { "some": "payload", "aud": ["urn:someone", "urn:someone-else"], } token = jwt.encode(payload, "secret") with pytest.raises(InvalidAudienceError): jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) def test_raise_exception_token_without_issuer(self, jwt: PyJWT) -> None: issuer = "urn:wrong" payload = {"some": "payload"} token = jwt.encode(payload, "secret") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) assert exc.value.claim == "iss" def test_rasise_exception_on_partial_issuer_match(self, jwt: PyJWT) -> None: issuer = "urn:expected" payload = {"iss": "urn:"} token = jwt.encode(payload, "secret") with pytest.raises(InvalidIssuerError): jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) def test_raise_exception_token_without_audience(self, jwt: PyJWT) -> None: payload = {"some": "payload"} token = jwt.encode(payload, "secret") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) assert exc.value.claim == "aud" def test_raise_exception_token_with_aud_none_and_without_audience( self, jwt: PyJWT ) -> None: payload = {"some": "payload", "aud": None} token = jwt.encode(payload, "secret") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode(token, "secret", audience="urn:me", algorithms=["HS256"]) assert exc.value.claim == "aud" def test_check_issuer_when_valid(self, jwt: PyJWT) -> None: issuer = "urn:foo" payload = {"some": "payload", "iss": "urn:foo"} token = jwt.encode(payload, "secret") jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) def test_check_issuer_list_when_valid(self, jwt: PyJWT) -> None: issuer = ["urn:foo", "urn:bar"] payload = {"some": "payload", "iss": "urn:foo"} token = jwt.encode(payload, "secret") jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) def test_raise_exception_invalid_issuer(self, jwt: PyJWT) -> None: issuer = "urn:wrong" payload = {"some": "payload", "iss": "urn:foo"} token = jwt.encode(payload, "secret") with pytest.raises(InvalidIssuerError): jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) def test_raise_exception_invalid_issuer_list(self, jwt: PyJWT) -> None: issuer = ["urn:wrong", "urn:bar", "urn:baz"] payload = {"some": "payload", "iss": "urn:foo"} token = jwt.encode(payload, "secret") with pytest.raises(InvalidIssuerError): jwt.decode(token, "secret", issuer=issuer, algorithms=["HS256"]) def test_skip_check_audience(self, jwt: PyJWT) -> None: payload = {"some": "payload", "aud": "urn:me"} token = jwt.encode(payload, "secret") jwt.decode( token, "secret", options={"verify_aud": False}, algorithms=["HS256"], ) def test_skip_check_exp(self, jwt: PyJWT) -> None: payload = { "some": "payload", "exp": datetime.now(tz=timezone.utc) - timedelta(days=1), } token = jwt.encode(payload, "secret") jwt.decode( token, "secret", options={"verify_exp": False}, algorithms=["HS256"], ) def test_decode_should_raise_error_if_exp_required_but_not_present( self, jwt: PyJWT ) -> None: payload = { "some": "payload", # exp not present } token = jwt.encode(payload, "secret") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode( token, "secret", options={"require": ["exp"]}, algorithms=["HS256"], ) assert exc.value.claim == "exp" def test_decode_should_raise_error_if_iat_required_but_not_present( self, jwt: PyJWT ) -> None: payload = { "some": "payload", # iat not present } token = jwt.encode(payload, "secret") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode( token, "secret", options={"require": ["iat"]}, algorithms=["HS256"], ) assert exc.value.claim == "iat" def test_decode_should_raise_error_if_nbf_required_but_not_present( self, jwt: PyJWT ) -> None: payload = { "some": "payload", # nbf not present } token = jwt.encode(payload, "secret") with pytest.raises(MissingRequiredClaimError) as exc: jwt.decode( token, "secret", options={"require": ["nbf"]}, algorithms=["HS256"], ) assert exc.value.claim == "nbf" def test_skip_check_signature(self, jwt: PyJWT) -> None: token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJzb21lIjoicGF5bG9hZCJ9" ".4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZA" ) jwt.decode( token, "secret", options={"verify_signature": False}, algorithms=["HS256"], ) def test_skip_check_iat(self, jwt: PyJWT) -> None: payload = { "some": "payload", "iat": datetime.now(tz=timezone.utc) + timedelta(days=1), } token = jwt.encode(payload, "secret") jwt.decode( token, "secret", options={"verify_iat": False}, algorithms=["HS256"], ) def test_skip_check_nbf(self, jwt: PyJWT) -> None: payload = { "some": "payload", "nbf": datetime.now(tz=timezone.utc) + timedelta(days=1), } token = jwt.encode(payload, "secret") jwt.decode( token, "secret", options={"verify_nbf": False}, algorithms=["HS256"], ) def test_custom_json_encoder(self, jwt: PyJWT) -> None: class CustomJSONEncoder(json.JSONEncoder): def default(self, o: object) -> str: assert isinstance(o, Decimal) return "it worked" data = {"some_decimal": Decimal("2.2")} with pytest.raises(TypeError): jwt.encode(data, "secret", algorithm="HS256") token = jwt.encode(data, "secret", json_encoder=CustomJSONEncoder) payload = jwt.decode(token, "secret", algorithms=["HS256"]) assert payload == {"some_decimal": "it worked"} def test_decode_with_verify_exp_option( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["exp"] = utc_timestamp() - 1 secret = "secret" jwt_message = jwt.encode(payload, secret) jwt.decode( jwt_message, secret, algorithms=["HS256"], options={"verify_exp": False}, ) with pytest.raises(ExpiredSignatureError): jwt.decode( jwt_message, secret, algorithms=["HS256"], options={"verify_exp": True}, ) def test_decode_with_verify_exp_option_and_signature_off( self, jwt: PyJWT, payload: dict[str, object] ) -> None: payload["exp"] = utc_timestamp() - 1 secret = "secret" jwt_message = jwt.encode(payload, secret) jwt.decode( jwt_message, options={"verify_signature": False}, ) with pytest.raises(ExpiredSignatureError): jwt.decode( jwt_message, options={"verify_signature": False, "verify_exp": True}, ) def test_decode_with_optional_algorithms( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.raises(DecodeError) as exc: jwt.decode(jwt_message, secret) assert ( 'It is required that you pass in a value for the "algorithms" argument when calling decode().' in str(exc.value) ) def test_decode_no_algorithms_verify_signature_false( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" jwt_message = jwt.encode(payload, secret) jwt.decode(jwt_message, secret, options={"verify_signature": False}) def test_decode_legacy_verify_warning( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.deprecated_call(): # The implicit default for options.verify_signature is True, # but the user sets verify to False. jwt.decode(jwt_message, secret, verify=False, algorithms=["HS256"]) with pytest.deprecated_call(): # The user explicitly sets verify=True, # but contradicts it in verify_signature. jwt.decode( jwt_message, secret, verify=True, options={"verify_signature": False} ) def test_decode_no_options_mutation( self, jwt: PyJWT, payload: dict[str, object] ) -> None: options: Options = {"verify_signature": True} orig_options = options.copy() secret = "secret" jwt_message = jwt.encode(payload, secret) jwt.decode( jwt_message, secret, options=options, algorithms=["HS256"], ) assert options == orig_options def test_decode_warns_on_unsupported_kwarg( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.warns(RemovedInPyjwt3Warning) as record: jwt.decode(jwt_message, secret, algorithms=["HS256"], foo="bar") deprecation_warnings = [ w for w in record if issubclass(w.category, RemovedInPyjwt3Warning) ] assert len(deprecation_warnings) == 1 assert "foo" in str(deprecation_warnings[0].message) def test_decode_complete_warns_on_unsupported_kwarg( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.warns(RemovedInPyjwt3Warning) as record: jwt.decode_complete(jwt_message, secret, algorithms=["HS256"], foo="bar") deprecation_warnings = [ w for w in record if issubclass(w.category, RemovedInPyjwt3Warning) ] assert len(deprecation_warnings) == 1 assert "foo" in str(deprecation_warnings[0].message) def test_decode_strict_aud_forbids_list_audience( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" payload["aud"] = "urn:foo" jwt_message = jwt.encode(payload, secret) # Decodes without `strict_aud`. jwt.decode( jwt_message, secret, audience=["urn:foo", "urn:bar"], options={"strict_aud": False}, algorithms=["HS256"], ) # Fails with `strict_aud`. with pytest.raises(InvalidAudienceError, match=r"Invalid audience \(strict\)"): jwt.decode( jwt_message, secret, audience=["urn:foo", "urn:bar"], options={"strict_aud": True}, algorithms=["HS256"], ) def test_decode_strict_aud_forbids_list_claim( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" payload["aud"] = ["urn:foo", "urn:bar"] jwt_message = jwt.encode(payload, secret) # Decodes without `strict_aud`. jwt.decode( jwt_message, secret, audience="urn:foo", options={"strict_aud": False}, algorithms=["HS256"], ) # Fails with `strict_aud`. with pytest.raises( InvalidAudienceError, match=r"Invalid claim format in token \(strict\)" ): jwt.decode( jwt_message, secret, audience="urn:foo", options={"strict_aud": True}, algorithms=["HS256"], ) def test_decode_strict_aud_does_not_match( self, jwt: PyJWT, payload: dict[str, object] ) -> None: secret = "secret" payload["aud"] = "urn:foo" jwt_message = jwt.encode(payload, secret) with pytest.raises( InvalidAudienceError, match=r"Audience doesn't match \(strict\)" ): jwt.decode( jwt_message, secret, audience="urn:bar", options={"strict_aud": True}, algorithms=["HS256"], ) def test_decode_strict_ok(self, jwt: PyJWT, payload: dict[str, object]) -> None: secret = "secret" payload["aud"] = "urn:foo" jwt_message = jwt.encode(payload, secret) jwt.decode( jwt_message, secret, audience="urn:foo", options={"strict_aud": True}, algorithms=["HS256"], ) # -------------------- Sub Claim Tests -------------------- def test_encode_decode_sub_claim(self, jwt: PyJWT) -> None: payload = { "sub": "user123", } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) assert decoded["sub"] == "user123" def test_decode_without_and_not_required_sub_claim(self, jwt: PyJWT) -> None: secret = "your-256-bit-secret" token = jwt.encode({}, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) assert "sub" not in decoded def test_decode_missing_sub_but_required_claim(self, jwt: PyJWT) -> None: secret = "your-256-bit-secret" token = jwt.encode({}, secret, algorithm="HS256") with pytest.raises(MissingRequiredClaimError): jwt.decode( token, secret, algorithms=["HS256"], options={"require": ["sub"]} ) def test_decode_invalid_int_sub_claim(self, jwt: PyJWT) -> None: payload = { "sub": 1224344, } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(InvalidSubjectError): jwt.decode(token, secret, algorithms=["HS256"]) def test_decode_with_valid_sub_claim(self, jwt: PyJWT) -> None: payload = { "sub": "user123", } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"], subject="user123") assert decoded["sub"] == "user123" def test_decode_with_invalid_sub_claim(self, jwt: PyJWT) -> None: payload = { "sub": "user123", } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(InvalidSubjectError) as exc_info: jwt.decode(token, secret, algorithms=["HS256"], subject="user456") assert "Invalid subject" in str(exc_info.value) def test_decode_with_sub_claim_and_none_subject(self, jwt: PyJWT) -> None: payload = { "sub": "user789", } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"], subject=None) assert decoded["sub"] == "user789" # -------------------- JTI Claim Tests -------------------- def test_encode_decode_with_valid_jti_claim(self, jwt: PyJWT) -> None: payload = { "jti": "unique-id-456", } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) assert decoded["jti"] == "unique-id-456" def test_decode_missing_jti_when_required_claim(self, jwt: PyJWT) -> None: payload = {"name": "Bob", "admin": False} secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(MissingRequiredClaimError) as exc_info: jwt.decode( token, secret, algorithms=["HS256"], options={"require": ["jti"]} ) assert "jti" in str(exc_info.value) def test_decode_missing_jti_claim(self, jwt: PyJWT) -> None: secret = "your-256-bit-secret" token = jwt.encode({}, secret, algorithm="HS256") decoded = jwt.decode(token, secret, algorithms=["HS256"]) assert decoded.get("jti") is None def test_jti_claim_with_invalid_int_value(self, jwt: PyJWT) -> None: special_jti = 12223 payload = { "jti": special_jti, } secret = "your-256-bit-secret" token = jwt.encode(payload, secret, algorithm="HS256") with pytest.raises(InvalidJTIError): jwt.decode(token, secret, algorithms=["HS256"]) def test_validate_iss_with_container_of_str(self, jwt: PyJWT) -> None: """Check _validate_iss works with Container[str].""" payload = { "iss": "urn:expected", } # pytest.mark.parametrize triggers Untyped Decorator mypy issue, # so trying inline for now for issuer in ( ["urn:expected", "urn:other"], ("urn:expected", "urn:other"), {"urn:expected", "urn:other"}, ): jwt._validate_iss(payload, issuer=issuer) def test_validate_iss_with_non_str(self, jwt: PyJWT) -> None: """Regression test for #1039""" payload = { "iss": 123, } with pytest.raises(InvalidIssuerError): jwt._validate_iss(payload, issuer="123") def test_validate_iss_with_non_str_issuer(self, jwt: PyJWT) -> None: """Regression test for #1039""" payload = { "iss": "123", } with pytest.raises(InvalidIssuerError): jwt._validate_iss( payload, issuer=123, # type: ignore[arg-type] ) # -------------------- Crit Header Tests -------------------- def test_decode_rejects_token_with_unknown_crit_extension(self, jwt: PyJWT) -> None: """RFC 7515 §4.1.11: tokens with unsupported critical extensions MUST be rejected.""" from jwt.exceptions import InvalidTokenError secret = "secret" payload = {"sub": "attacker", "role": "admin"} token = jwt.encode( payload, secret, algorithm="HS256", headers={"crit": ["x-custom-policy"], "x-custom-policy": "require-mfa"}, ) with pytest.raises(InvalidTokenError, match="Unsupported critical extension"): jwt.decode(token, secret, algorithms=["HS256"]) jpadilla-pyjwt-a4e1a3d/tests/test_compressed_jwt.py000066400000000000000000000025071515505507500227500ustar00rootroot00000000000000import json import zlib from jwt import PyJWT class CompressedPyJWT(PyJWT): def _decode_payload(self, decoded: dict[str, bytes]) -> dict[str, object]: payload = json.loads( # wbits=-15 has zlib not worry about headers of crc's zlib.decompress(decoded["payload"], wbits=-15).decode("utf-8") ) assert isinstance(payload, dict) return payload def test_decodes_complete_valid_jwt_with_compressed_payload() -> None: # Test case from https://github.com/jpadilla/pyjwt/pull/753/files example_payload = {"hello": "world"} example_secret = "secret" # payload made with the pako (https://nodeca.github.io/pako/) library in Javascript: # Buffer.from(pako.deflateRaw('{"hello": "world"}')).toString('base64') example_jwt = ( b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" b".q1bKSM3JyVeyUlAqzy/KSVGqBQA=" b".08wHYeuh1rJXmcBcMrz6NxmbxAnCQp2rGTKfRNIkxiw=" ) decoded = CompressedPyJWT().decode_complete( example_jwt, example_secret, algorithms=["HS256"] ) assert decoded == { "header": {"alg": "HS256", "typ": "JWT"}, "payload": example_payload, "signature": ( b"\xd3\xcc\x07a\xeb\xa1\xd6\xb2W\x99\xc0\\2\xbc\xfa7" b"\x19\x9b\xc4\t\xc2B\x9d\xab\x192\x9fD\xd2$\xc6," ), } jpadilla-pyjwt-a4e1a3d/tests/test_exceptions.py000066400000000000000000000003351515505507500220760ustar00rootroot00000000000000from jwt.exceptions import MissingRequiredClaimError def test_missing_required_claim_error_has_proper_str() -> None: exc = MissingRequiredClaimError("abc") assert str(exc) == 'Token is missing the "abc" claim' jpadilla-pyjwt-a4e1a3d/tests/test_jwks_client.py000066400000000000000000000421071515505507500222340ustar00rootroot00000000000000import contextlib import io import json import ssl import time from collections.abc import Iterator from unittest import mock from urllib.error import HTTPError, URLError from email.message import Message import pytest import jwt from jwt import PyJWKClient from jwt.api_jwk import PyJWK from jwt.exceptions import PyJWKClientConnectionError, PyJWKClientError from .utils import crypto_required RESPONSE_DATA_WITH_MATCHING_KID = { "keys": [ { "alg": "RS256", "kty": "RSA", "use": "sig", "n": "0wtlJRY9-ru61LmOgieeI7_rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset_Obh8BwtO-Ww-UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6_GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEw", "e": "AQAB", "kid": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw", "x5t": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw", "x5c": [ "MIIDBzCCAe+gAwIBAgIJNtD9Ozi6j2jJMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi04N2V2eDlydS5hdXRoMC5jb20wHhcNMTkwNjIwMTU0NDU4WhcNMzMwMjI2MTU0NDU4WjAhMR8wHQYDVQQDExZkZXYtODdldng5cnUuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wtlJRY9+ru61LmOgieeI7/rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset/Obh8BwtO+Ww+UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6/GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQlGXpmYaXFB7Q3eG69Uhjd4cFp/jAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAIzQOF/h4T5WWAdjhcIwdNS7hS2Deq+UxxkRv+uavj6O9mHLuRG1q5onvSFShjECXaYT6OGibn7Ufw/JSm3+86ZouMYjBEqGh4OvWRkwARy1YTWUVDGpT2HAwtIq3lfYvhe8P4VfZByp1N4lfn6X2NcJflG+Q+mfXNmRFyyft3Oq51PCZyyAkU7bTun9FmMOyBtmJvQjZ8RXgBLvu9nUcZB8yTVoeUEg4cLczQlli/OkiFXhWgrhVr8uF0/9klslMFXtm78iYSgR8/oC+k1pSNd1+ESSt7n6+JiAQ2Co+ZNKta7LTDGAjGjNDymyoCrZpeuYQwwnHYEHu/0khjAxhXo=" ], } ] } RESPONSE_DATA_NO_MATCHING_KID = { "keys": [ { "alg": "RS256", "kty": "RSA", "use": "sig", "n": "39SJ39VgrQ0qMNK74CaueUBlyYsUyuA7yWlHYZ-jAj6tlFKugEVUTBUVbhGF44uOr99iL_cwmr-srqQDEi-jFHdkS6WFkYyZ03oyyx5dtBMtzrXPieFipSGfQ5EGUGloaKDjL-Ry9tiLnysH2VVWZ5WDDN-DGHxuCOWWjiBNcTmGfnj5_NvRHNUh2iTLuiJpHbGcPzWc5-lc4r-_ehw9EFfp2XsxE9xvtbMZ4SouJCiv9xnrnhe2bdpWuu34hXZCrQwE8DjRY3UR8LjyMxHHPLzX2LWNMHjfN3nAZMteS-Ok11VYDFI-4qCCVGo_WesBCAeqCjPLRyZoV27x1YGsUQ", "e": "AQAB", "kid": "MLYHNMMhwCNXw9roHIILFsK4nLs=", } ] } @contextlib.contextmanager def mocked_success_response(data: object) -> Iterator[mock.Mock]: with mock.patch("urllib.request.urlopen") as urlopen_mock: response = mock.Mock() response.__enter__ = mock.Mock(return_value=response) response.__exit__ = mock.Mock() response.read.side_effect = [json.dumps(data)] urlopen_mock.return_value = response yield urlopen_mock @contextlib.contextmanager def mocked_failed_response() -> Iterator[mock.Mock]: with mock.patch("urllib.request.urlopen") as urlopen_mock: urlopen_mock.side_effect = URLError("Fail to process the request.") yield urlopen_mock @contextlib.contextmanager def mocked_first_call_wrong_kid_second_call_correct_kid( response_data_one: object, response_data_two: object ) -> Iterator[mock.Mock]: with mock.patch("urllib.request.urlopen") as urlopen_mock: response = mock.Mock() response.__enter__ = mock.Mock(return_value=response) response.__exit__ = mock.Mock() response.read.side_effect = [ json.dumps(response_data_one), json.dumps(response_data_two), ] urlopen_mock.return_value = response yield urlopen_mock @contextlib.contextmanager def mocked_timeout() -> Iterator[mock.Mock]: with mock.patch("urllib.request.urlopen") as urlopen_mock: urlopen_mock.side_effect = TimeoutError("timed out") yield urlopen_mock @contextlib.contextmanager def mocked_http_error_response() -> Iterator[tuple[mock.Mock, HTTPError]]: with mock.patch("urllib.request.urlopen") as urlopen_mock: http_error = HTTPError( url="https://example.com", code=401, msg="Unauthorized", hdrs=Message(), fp=io.BytesIO(b""), ) urlopen_mock.side_effect = http_error yield urlopen_mock, http_error @crypto_required class TestPyJWKClient: def test_fetch_data_forwards_headers_to_correct_url(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as mock_request: custom_headers = {"User-agent": "my-custom-agent"} jwks_client = PyJWKClient(url, headers=custom_headers) jwk_set = jwks_client.get_jwk_set() request_params = mock_request.call_args[0][0] assert request_params.full_url == url assert request_params.headers == custom_headers assert len(jwk_set.keys) == 1 def test_get_jwk_set(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client = PyJWKClient(url) jwk_set = jwks_client.get_jwk_set() assert len(jwk_set.keys) == 1 def test_get_signing_keys(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client = PyJWKClient(url) signing_keys = jwks_client.get_signing_keys() assert len(signing_keys) == 1 assert isinstance(signing_keys[0], PyJWK) def test_get_signing_keys_if_no_use_provided(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" mocked_key = RESPONSE_DATA_WITH_MATCHING_KID["keys"][0].copy() del mocked_key["use"] response = {"keys": [mocked_key]} with mocked_success_response(response): jwks_client = PyJWKClient(url) signing_keys = jwks_client.get_signing_keys() assert len(signing_keys) == 1 assert isinstance(signing_keys[0], PyJWK) def test_get_signing_keys_raises_if_none_found(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" mocked_key = RESPONSE_DATA_WITH_MATCHING_KID["keys"][0].copy() mocked_key["use"] = "enc" response = {"keys": [mocked_key]} with mocked_success_response(response): jwks_client = PyJWKClient(url) with pytest.raises(PyJWKClientError) as exc: jwks_client.get_signing_keys() assert "The JWKS endpoint did not contain any signing keys" in str(exc.value) def test_get_signing_key(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client = PyJWKClient(url) signing_key = jwks_client.get_signing_key(kid) assert isinstance(signing_key, PyJWK) assert signing_key.key_type == "RSA" assert signing_key.key_id == kid assert signing_key.public_key_use == "sig" def test_get_signing_key_caches_result(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" jwks_client = PyJWKClient(url, cache_keys=True) with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client.get_signing_key(kid) # mocked_response does not allow urllib.request.urlopen to be called twice # so a second mock is needed with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as repeated_call: jwks_client.get_signing_key(kid) assert repeated_call.call_count == 0 def test_get_signing_key_does_not_cache_opt_out(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" jwks_client = PyJWKClient(url, cache_jwk_set=False) with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client.get_signing_key(kid) # mocked_response does not allow urllib.request.urlopen to be called twice # so a second mock is needed with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as repeated_call: jwks_client.get_signing_key(kid) assert repeated_call.call_count == 1 def test_get_signing_key_from_jwt(self) -> None: token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client = PyJWKClient(url) signing_key = jwks_client.get_signing_key_from_jwt(token) data = jwt.decode( token, signing_key.key, algorithms=["RS256"], audience="https://expenses-api", options={"verify_exp": False}, ) assert data == { "iss": "https://dev-87evx9ru.auth0.com/", "sub": "aW4Cca79xReLWUz0aE2H6kD0O3cXBVtC@clients", "aud": "https://expenses-api", "iat": 1572006954, "exp": 1572006964, "azp": "aW4Cca79xReLWUz0aE2H6kD0O3cXBVtC", "gty": "client-credentials", } def test_get_jwk_set_caches_result(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) assert jwks_client.jwk_set_cache is not None with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client.get_jwk_set() # mocked_response does not allow urllib.request.urlopen to be called twice # so a second mock is needed with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as repeated_call: jwks_client.get_jwk_set() assert repeated_call.call_count == 0 def test_get_jwt_set_cache_expired_result(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url, lifespan=1) with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client.get_jwk_set() time.sleep(2) # mocked_response does not allow urllib.request.urlopen to be called twice # so a second mock is needed with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as repeated_call: jwks_client.get_jwk_set() assert repeated_call.call_count == 1 def test_get_jwt_set_cache_disabled(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url, cache_jwk_set=False) assert jwks_client.jwk_set_cache is None with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client.get_jwk_set() assert jwks_client.jwk_set_cache is None time.sleep(2) # mocked_response does not allow urllib.request.urlopen to be called twice # so a second mock is needed with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as repeated_call: jwks_client.get_jwk_set() assert repeated_call.call_count == 1 def test_get_jwt_set_failed_request_should_clear_cache(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID): jwks_client.get_jwk_set() with pytest.raises(PyJWKClientError): with mocked_failed_response(): jwks_client.get_jwk_set(refresh=True) assert jwks_client.jwk_set_cache is None def test_failed_request_should_raise_connection_error(self) -> None: token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA" url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) with pytest.raises(PyJWKClientConnectionError): with mocked_failed_response(): jwks_client.get_signing_key_from_jwt(token) def test_get_jwt_set_refresh_cache(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" # The first call will return response with no matching kid, # the function should make another call to try to refresh the cache. with mocked_first_call_wrong_kid_second_call_correct_kid( RESPONSE_DATA_NO_MATCHING_KID, RESPONSE_DATA_WITH_MATCHING_KID ) as call_data: jwks_client.get_signing_key(kid) assert call_data.call_count == 2 def test_get_jwt_set_no_matching_kid_after_second_attempt(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw" with pytest.raises(PyJWKClientError): with mocked_first_call_wrong_kid_second_call_correct_kid( RESPONSE_DATA_NO_MATCHING_KID, RESPONSE_DATA_NO_MATCHING_KID ): jwks_client.get_signing_key(kid) def test_get_jwt_set_invalid_lifespan(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" with pytest.raises(PyJWKClientError): jwks_client = PyJWKClient(url, lifespan=-1) assert jwks_client is None def test_get_jwt_set_timeout(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url, timeout=5) with pytest.raises(PyJWKClientError) as exc: with mocked_timeout(): jwks_client.get_jwk_set() assert 'Fail to fetch data from the url, err: "timed out"' in str(exc.value) def test_get_jwt_set_sslcontext_default(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" ssl_ctx = ssl.create_default_context() jwks_client = PyJWKClient(url, ssl_context=ssl_ctx) with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as mock_request: jwk_set = jwks_client.get_jwk_set() request_call = mock_request.call_args assert request_call[1].get("context") is ssl_ctx assert jwk_set is not None def test_get_jwt_set_sslcontext_no_ca(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient( url, ssl_context=ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) ) with mock.patch("urllib.request.urlopen") as urlopen_mock: urlopen_mock.side_effect = URLError( ssl.SSLCertVerificationError("certificate verify failed") ) with pytest.raises(PyJWKClientError): jwks_client.get_jwk_set() def test_http_error_is_closed_on_connection_failure(self) -> None: url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url) with mocked_http_error_response() as (_, http_error): with pytest.raises(PyJWKClientConnectionError): jwks_client.get_jwk_set() assert http_error.closed jpadilla-pyjwt-a4e1a3d/tests/test_jwt.py000066400000000000000000000011711515505507500205200ustar00rootroot00000000000000import jwt from .utils import utc_timestamp def test_encode_decode() -> None: """ This test exists primarily to ensure that calls to jwt.encode and jwt.decode don't explode. Most functionality is tested by the PyJWT class tests. This is primarily a sanity check to make sure we don't break the public global functions. """ payload = {"iss": "jeff", "exp": utc_timestamp() + 15, "claim": "insanity"} secret = "secret" jwt_message = jwt.encode(payload, secret, algorithm="HS256") decoded_payload = jwt.decode(jwt_message, secret, algorithms=["HS256"]) assert decoded_payload == payload jpadilla-pyjwt-a4e1a3d/tests/test_utils.py000066400000000000000000000027631515505507500210640ustar00rootroot00000000000000from contextlib import nullcontext import pytest from contextlib import AbstractContextManager from jwt.utils import force_bytes, from_base64url_uint, is_ssh_key, to_base64url_uint @pytest.mark.parametrize( "inputval,expected", [ (0, nullcontext(b"AA")), (1, nullcontext(b"AQ")), (255, nullcontext(b"_w")), (65537, nullcontext(b"AQAB")), (123456789, nullcontext(b"B1vNFQ")), (-1, pytest.raises(ValueError)), ], ) def test_to_base64url_uint( inputval: int, expected: AbstractContextManager[bytes] ) -> None: with expected as e: actual = to_base64url_uint(inputval) assert actual == e @pytest.mark.parametrize( "inputval,expected", [ (b"AA", 0), (b"AQ", 1), (b"_w", 255), (b"AQAB", 65537), (b"B1vNFQ", 123456789), ], ) def test_from_base64url_uint(inputval: bytes, expected: int) -> None: actual = from_base64url_uint(inputval) assert actual == expected def test_force_bytes_raises_error_on_invalid_object() -> None: with pytest.raises(TypeError): force_bytes({}) # type: ignore[arg-type] @pytest.mark.parametrize( "key_format", ( b"ssh-ed25519", b"ssh-rsa", b"ssh-dss", b"ecdsa-sha2-nistp256", b"ecdsa-sha2-nistp384", b"ecdsa-sha2-nistp521", ), ) def test_is_ssh_key(key_format: bytes) -> None: assert is_ssh_key(key_format + b" any") is True assert is_ssh_key(b"not a ssh key") is False jpadilla-pyjwt-a4e1a3d/tests/utils.py000066400000000000000000000011121515505507500200100ustar00rootroot00000000000000import os from calendar import timegm from datetime import datetime, timezone import pytest from jwt.algorithms import has_crypto def utc_timestamp() -> int: return timegm(datetime.now(tz=timezone.utc).utctimetuple()) def key_path(key_name: str) -> str: return os.path.join(os.path.dirname(os.path.realpath(__file__)), "keys", key_name) no_crypto_required = pytest.mark.skipif( has_crypto, reason="Requires cryptography library not installed", ) crypto_required = pytest.mark.skipif( not has_crypto, reason="Requires cryptography library installed", ) jpadilla-pyjwt-a4e1a3d/tox.ini000066400000000000000000000036751515505507500164670ustar00rootroot00000000000000[flake8] min_python_version = 3.9 ignore= E501, E203, W503, E704 [gh-actions] python = 3.9: py39 3.10: py310 3.11: py311, docs 3.12: py312 3.13: py313 3.14: py314 pypy3.9: pypy39 pypy3.10: pypy310 pypy3.11: pypy311 [tox] envlist = lint typing py{39,310,311,312,313,314,py39,py310,py311}-{crypto,nocrypto} py{39,310,311,312,313,314}{,-crypto}-mypy docs pypi-description coverage-report isolated_build = True labels = mypy = py{39,310,311,312,313,314}{,-crypto}-mypy [testenv] # Build a shared wheel once, rather than building a .tar.gz file # and forcing each individual tox environment to convert it to a wheel. package = wheel wheel_build_env = build_wheel # Prevent random setuptools/pip breakages like # https://github.com/pypa/setuptools/issues/1042 from breaking our builds. setenv = VIRTUALENV_NO_DOWNLOAD=1 extras = tests crypto: crypto commands = {envpython} -b -m coverage run -m pytest {posargs} [testenv:docs] # The tox config must match the ReadTheDocs config. basepython = python3.11 extras = docs crypto commands = sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html python -m doctest README.rst docs/usage.rst [testenv:py{39,310,311,312,313,314}{,-crypto}-mypy] extras = tests crypto: crypto deps = mypy set_env = MYPY_FORCE_COLOR=1 commands = mypy [testenv:lint] basepython = python3.9 extras = dev passenv = HOMEPATH # needed on Windows commands = pre-commit run --all-files [testenv:pypi-description] basepython = python3.9 skip_install = true deps = twine pip >= 18.0.0 commands = pip wheel -w {envtmpdir}/build --no-deps . twine check {envtmpdir}/build/* [testenv:coverage-report] basepython = python3.9 skip_install = true deps = coverage[toml]==7.10.7 commands = coverage combine coverage report