pax_global_header00006660000000000000000000000064147217617250014526gustar00rootroot0000000000000052 comment=3ebbb22f30f2b1b41727b269a08b427e9a85d6bb pyjwt-2.10.1/000077500000000000000000000000001472176172500127645ustar00rootroot00000000000000pyjwt-2.10.1/.github/000077500000000000000000000000001472176172500143245ustar00rootroot00000000000000pyjwt-2.10.1/.github/FUNDING.yml000066400000000000000000000001021472176172500161320ustar00rootroot00000000000000# These are supported funding model platforms github: [jpadilla] pyjwt-2.10.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000005241472176172500170320ustar00rootroot00000000000000Summary. ## 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. pyjwt-2.10.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001472176172500165075ustar00rootroot00000000000000pyjwt-2.10.1/.github/ISSUE_TEMPLATE/Bug.md000066400000000000000000000006301472176172500175450ustar00rootroot00000000000000--- 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. pyjwt-2.10.1/.github/ISSUE_TEMPLATE/Custom.md000066400000000000000000000002501472176172500203000ustar00rootroot00000000000000--- name: Request for Help about: Guidance on using PyJWT. --- Please refer to our [StackOverflow tag](https://stackoverflow.com/questions/tagged/pyjwt) for guidance. pyjwt-2.10.1/.github/ISSUE_TEMPLATE/Feature.md000066400000000000000000000001511472176172500204210ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- Suggest an idea for this project. pyjwt-2.10.1/.github/dependabot.yml000066400000000000000000000001651472176172500171560ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" pyjwt-2.10.1/.github/workflows/000077500000000000000000000000001472176172500163615ustar00rootroot00000000000000pyjwt-2.10.1/.github/workflows/enforce-changelog-entry.yml000066400000000000000000000011461472176172500236130ustar00rootroot00000000000000name: "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 pyjwt-2.10.1/.github/workflows/main.yml000066400000000000000000000054151472176172500200350ustar00rootroot00000000000000--- name: CI on: push: branches: ["master"] pull_request: branches: ["master"] workflow_dispatch: 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", "pypy3.9"] steps: - uses: "actions/checkout@v4" - uses: "actions/setup-python@v5" 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@v5" 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@v4" - uses: "actions/setup-python@v5" with: python-version: "3.9" - name: "Install pep517 and twine" run: "python -m pip install pep517 twine" - name: "Build package" run: "python -m pep517.build --source --binary ." - 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"] name: "Verify dev env" runs-on: "${{ matrix.os }}" steps: - uses: "actions/checkout@v4" - uses: "actions/setup-python@v5" with: python-version: "3.9" - name: "Install in dev mode" run: "python -m pip install -e .[dev]" - name: "Import package" run: "python -c 'import jwt; print(jwt.__version__)'" pyjwt-2.10.1/.github/workflows/pypi-package.yml000066400000000000000000000033541472176172500214630ustar00rootroot00000000000000--- 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@v4 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@v4 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@v4 with: name: Packages path: dist - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 pyjwt-2.10.1/.github/workflows/stale.yml000066400000000000000000000012751472176172500202210ustar00rootroot00000000000000name: '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@v8 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: 60 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.*, ',') }} pyjwt-2.10.1/.gitignore000066400000000000000000000014611472176172500147560ustar00rootroot00000000000000# 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 pyjwt-2.10.1/.pre-commit-config.yaml000066400000000000000000000031521472176172500172460ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black args: ["--target-version=py39"] - repo: https://github.com/asottile/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs args: ["--target-version=py39"] - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/mgedmin/check-manifest rev: "0.50" hooks: - id: check-manifest args: [--no-build-isolation] - repo: https://github.com/pre-commit/mirrors-mypy rev: "v1.13.0" hooks: - id: mypy additional_dependencies: [cryptography>=3.4.0] - repo: https://github.com/abravalheri/validate-pyproject rev: "v0.23" hooks: - id: validate-pyproject # conflict with the backend dependencies: tomli-w==1.1.0 is incompatible with tomli-w==1.0.0. # - repo: https://github.com/kieran-ryan/pyprojectsort # rev: "v0.3.0" # hooks: # - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema rev: "0.29.4" hooks: - id: check-github-workflows - id: check-readthedocs - repo: https://github.com/regebro/pyroma rev: "4.2" hooks: - id: pyroma - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.7.3 hooks: # Run the linter. - id: ruff args: [ --fix ] # Run the formatter. - id: ruff-format pyjwt-2.10.1/.readthedocs.yaml000066400000000000000000000004521472176172500162140ustar00rootroot00000000000000# 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" sphinx: configuration: "docs/conf.py" fail_on_warning: true pyjwt-2.10.1/AUTHORS.rst000066400000000000000000000005021472176172500146400ustar00rootroot00000000000000Authors ======= ``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 `_. pyjwt-2.10.1/CHANGELOG.rst000066400000000000000000001077211472176172500150150ustar00rootroot00000000000000Changelog ========= All notable changes to this project will be documented in this file. This project adheres to `Semantic Versioning `__. `Unreleased `__ ------------------------------------------------------------------------ `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 https://github.com/jpadilla/pyjwt/pull/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 https://github.com/jpadilla/pyjwt/pull/713 - Raise DeprecationWarning for jwt.decode(verify=...) by @akx in https://github.com/jpadilla/pyjwt/pull/742 Fixed ~~~~~ - Don't use implicit optionals by @rekyungmin in https://github.com/jpadilla/pyjwt/pull/705 - documentation fix: show correct scope for decode_complete() by @sseering in https://github.com/jpadilla/pyjwt/pull/661 - fix: Update copyright information by @kkirsche in https://github.com/jpadilla/pyjwt/pull/729 - Don't mutate options dictionary in .decode_complete() by @akx in https://github.com/jpadilla/pyjwt/pull/743 Added ~~~~~ - Add support for Python 3.10 by @hugovk in https://github.com/jpadilla/pyjwt/pull/699 - api_jwk: Add PyJWKSet.__getitem__ by @woodruffw in https://github.com/jpadilla/pyjwt/pull/725 - Update usage.rst by @guneybilen in https://github.com/jpadilla/pyjwt/pull/727 - Docs: mention performance reasons for reusing RSAPrivateKey when encoding by @dmahr1 in https://github.com/jpadilla/pyjwt/pull/734 - Fixed typo in usage.rst by @israelabraham in https://github.com/jpadilla/pyjwt/pull/738 - Add detached payload support for JWS encoding and decoding by @fviard in https://github.com/jpadilla/pyjwt/pull/723 - Replace various string interpolations with f-strings by @akx in https://github.com/jpadilla/pyjwt/pull/744 - Update CHANGELOG.rst by @hipertracker in https://github.com/jpadilla/pyjwt/pull/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 `__ pyjwt-2.10.1/CODE_OF_CONDUCT.md000066400000000000000000000063101472176172500155630ustar00rootroot00000000000000# 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 pyjwt-2.10.1/LICENSE000066400000000000000000000020751472176172500137750ustar00rootroot00000000000000The 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. pyjwt-2.10.1/MANIFEST.in000066400000000000000000000005651472176172500145300ustar00rootroot00000000000000include .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__ pyjwt-2.10.1/README.rst000066400000000000000000000043511472176172500144560ustar00rootroot00000000000000PyJWT ===== .. 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 pyjwt-2.10.1/SECURITY.md000066400000000000000000000014111472176172500145520ustar00rootroot00000000000000# 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). pyjwt-2.10.1/codecov.yml000066400000000000000000000003141472176172500151270ustar00rootroot00000000000000comment: false coverage: status: patch: default: target: "100" project: default: target: "100" threshold: "10%" pyjwt-2.10.1/docs/000077500000000000000000000000001472176172500137145ustar00rootroot00000000000000pyjwt-2.10.1/docs/Makefile000066400000000000000000000127131472176172500153600ustar00rootroot00000000000000# 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." pyjwt-2.10.1/docs/_static/000077500000000000000000000000001472176172500153425ustar00rootroot00000000000000pyjwt-2.10.1/docs/_static/theme_overrides.css000066400000000000000000000005531472176172500212430ustar00rootroot00000000000000img.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; } } pyjwt-2.10.1/docs/algorithms.rst000066400000000000000000000067221472176172500166260ustar00rootroot00000000000000Digital 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. 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\*). pyjwt-2.10.1/docs/api.rst000066400000000000000000000217251472176172500152260ustar00rootroot00000000000000API Reference ============= .. module:: jwt .. function:: encode(payload, key, algorithm="HS256", headers=None, json_encoder=None) Encode the ``payload`` as JSON Web Token. :param dict payload: JWT claims, e.g. ``dict(iss=..., aud=..., sub=...)`` :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 jwt.PyJWK :param str 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:`jwt.PyJWK` object, by default the key algorithm will be used. :param dict headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")``. :param json.JSONEncoder json_encoder: custom JSON encoder for ``payload`` and ``headers`` :rtype: str :returns: a JSON Web Token .. function:: decode(jwt, key="", algorithms=None, options=None, audience=None, issuer=None, leeway=0) Verify the ``jwt`` token signature and return the token claims. :param str jwt: the token to be decoded :param key: the key suitable for the allowed algorithm :type key: str or bytes or jwt.PyJWK :param list algorithms: allowed algorithms, e.g. ``["ES256"]`` If ``key`` is a :class:`jwt.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\*). :param dict options: extended decoding and validation options * ``verify_signature=True`` verify the JWT cryptographic signature * ``require=[]`` 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. * ``verify_aud=verify_signature`` check that ``aud`` (audience) claim matches ``audience`` * ``verify_iss=verify_signature`` check that ``iss`` (issuer) claim matches ``issuer`` * ``verify_exp=verify_signature`` check that ``exp`` (expiration) claim value is in the future * ``verify_iat=verify_signature`` check that ``iat`` (issued at) claim value is an integer * ``verify_nbf=verify_signature`` check that ``nbf`` (not before) claim value is in the past * ``strict_aud=False`` check that the ``aud`` claim is a single value (not a list), and matches ``audience`` exactly .. warning:: ``exp``, ``iat`` and ``nbf`` will only be verified if present. Please pass respective value to ``require`` if you want to make sure that they are always present (and therefore always verified if ``verify_exp``, ``verify_iat``, and ``verify_nbf`` respectively is set to ``True``). :param audience: optional, the value for ``verify_aud`` check :type audience: Union[str, Iterable] :param str issuer: optional, the value for ``verify_iss`` check :param float leeway: a time margin in seconds for the expiration check :rtype: dict :returns: the JWT claims .. class:: PyJWK A class that represents a `JSON Web Key `_. .. method:: __init__(self, jwk_data, algorithm=None) :param dict data: The decoded JWK data. :param algorithm: The key algorithm. If not specific, the key's ``alg`` will be used. :type algorithm: str or None .. staticmethod:: from_json(data, algorithm=None) :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 :returntype: jwt.PyJWK Create a :class:`jwt.PyJWK` object from a JSON string. .. property:: algorithm_name :type: str The name of the algorithm used by the key. .. property:: Algorithm The ``Algorithm`` class associated with the key. .. property:: key_type :type: str or None The ``kty`` property from the JWK. .. property:: key_id :type: str or None The ``kid`` property from the JWK. .. property:: public_key_use :type: str or None The ``use`` property from the JWK. .. module:: jwt.api_jwt .. function:: decode_complete(jwt, key="", algorithms=None, options=None, audience=None, issuer=None, leeway=0) 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 str jwt: the token to be decoded :param str key: the key suitable for the allowed algorithm :param list 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\*). :param dict options: extended decoding and validation options * ``verify_signature=True`` verify the JWT cryptographic signature * ``require=[]`` 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. * ``verify_aud=verify_signature`` check that ``aud`` (audience) claim matches ``audience`` * ``verify_iss=verify_signature`` check that ``iss`` (issuer) claim matches ``issuer`` * ``verify_exp=verify_signature`` check that ``exp`` (expiration) claim value is in the future * ``verify_iat=verify_signature`` check that ``iat`` (issued at) claim value is an integer * ``verify_nbf=verify_signature`` check that ``nbf`` (not before) claim value is in the past * ``strict_aud=False`` check that the ``aud`` claim is a single value (not a list), and matches ``audience`` exactly .. warning:: ``exp``, ``iat`` and ``nbf`` will only be verified if present. Please pass respective value to ``require`` if you want to make sure that they are always present (and therefore always verified if ``verify_exp``, ``verify_iat``, and ``verify_nbf`` respectively is set to ``True``). :param Iterable audience: optional, the value for ``verify_aud`` check :param str issuer: optional, the value for ``verify_iss`` check :param float leeway: a time margin in seconds for the expiration check :rtype: dict :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``. .. note:: TODO: Document PyJWS class Exceptions ---------- .. currentmodule:: jwt.exceptions .. class:: InvalidTokenError Base exception when ``decode()`` fails on a token .. class:: DecodeError Raised when a token cannot be decoded because it failed validation .. class:: InvalidSignatureError Raised when a token's signature doesn't match the one provided as part of the token. .. class:: ExpiredSignatureError Raised when a token's ``exp`` claim indicates that it has expired .. class:: InvalidAudienceError Raised when a token's ``aud`` claim does not match one of the expected audience values .. class:: InvalidIssuerError Raised when a token's ``iss`` claim does not match the expected issuer .. class:: InvalidIssuedAtError Raised when a token's ``iat`` claim is non-numeric .. class:: ImmatureSignatureError Raised when a token's ``nbf`` or ``iat`` claims represent a time in the future .. class:: InvalidKeyError Raised when the specified key is not in the proper format .. class:: InvalidAlgorithmError Raised when the specified algorithm is not recognized by PyJWT .. class:: MissingRequiredClaimError Raised when a claim that is required to be present is not contained in the claimset pyjwt-2.10.1/docs/changelog.rst000066400000000000000000000000361472176172500163740ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst pyjwt-2.10.1/docs/conf.py000066400000000000000000000074541472176172500152250ustar00rootroot00000000000000import 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 # Intersphinx extension. intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), } # -- 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", ) ] pyjwt-2.10.1/docs/faq.rst000066400000000000000000000011551472176172500152170ustar00rootroot00000000000000Frequently 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() pyjwt-2.10.1/docs/index.rst000066400000000000000000000034411472176172500155570ustar00rootroot00000000000000Welcome 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 pyjwt-2.10.1/docs/installation.rst000066400000000000000000000014271472176172500171530ustar00rootroot00000000000000Installation ============ 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 pyjwt-2.10.1/docs/requirements-docs.txt000066400000000000000000000000301472176172500201170ustar00rootroot00000000000000sphinx sphinx_rtd_theme pyjwt-2.10.1/docs/usage.rst000066400000000000000000000546601472176172500155650ustar00rootroot00000000000000Usage 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' 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 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 10 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=10), 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") 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") 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 ---------------------------------------------- .. 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'} 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"] pyjwt-2.10.1/jwt/000077500000000000000000000000001472176172500135705ustar00rootroot00000000000000pyjwt-2.10.1/jwt/__init__.py000066400000000000000000000032571472176172500157100ustar00rootroot00000000000000from .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 __version__ = "2.10.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-2022 José Padilla" __all__ = [ "PyJWS", "PyJWT", "PyJWKClient", "PyJWK", "PyJWKSet", "decode", "decode_complete", "encode", "get_unverified_header", "register_algorithm", "unregister_algorithm", "get_algorithm_by_name", # Exceptions "DecodeError", "ExpiredSignatureError", "ImmatureSignatureError", "InvalidAlgorithmError", "InvalidAudienceError", "InvalidIssuedAtError", "InvalidIssuerError", "InvalidKeyError", "InvalidSignatureError", "InvalidTokenError", "MissingRequiredClaimError", "PyJWKClientConnectionError", "PyJWKClientError", "PyJWKError", "PyJWKSetError", "PyJWTError", ] pyjwt-2.10.1/jwt/algorithms.py000066400000000000000000000733111472176172500163200ustar00rootroot00000000000000from __future__ import annotations import hashlib import hmac import json from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, cast, 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, ) has_crypto = True except ModuleNotFoundError: has_crypto = False if TYPE_CHECKING: # Type aliases for convenience in algorithms method signatures AllowedRSAKeys = RSAPrivateKey | RSAPublicKey AllowedECKeys = EllipticCurvePrivateKey | EllipticCurvePublicKey AllowedOKPKeys = ( Ed25519PrivateKey | Ed25519PublicKey | Ed448PrivateKey | Ed448PublicKey ) AllowedKeys = AllowedRSAKeys | AllowedECKeys | AllowedOKPKeys AllowedPrivateKeys = ( RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey | Ed448PrivateKey ) AllowedPublicKeys = ( RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey | Ed448PublicKey ) 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 = { "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), "ES256K": ECAlgorithm(ECAlgorithm.SHA256), "ES384": ECAlgorithm(ECAlgorithm.SHA384), "ES521": ECAlgorithm(ECAlgorithm.SHA512), "ES512": ECAlgorithm( ECAlgorithm.SHA512 ), # 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. """ 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()) @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, as_dict: Literal[True]) -> JWKDict: ... # pragma: no cover @overload @staticmethod @abstractmethod def to_jwk(key_obj, as_dict: Literal[False] = False) -> str: ... # pragma: no cover @staticmethod @abstractmethod def to_jwk(key_obj, 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 """ 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: ... # pragma: no cover @overload @staticmethod def to_jwk( key_obj: str | bytes, as_dict: Literal[False] = False ) -> str: ... # pragma: no cover @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 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 def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: self.hash_alg = hash_alg def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: if isinstance(key, (RSAPrivateKey, RSAPublicKey)): return 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"): return cast(RSAPublicKey, load_ssh_public_key(key_bytes)) else: return cast( RSAPrivateKey, load_pem_private_key(key_bytes, password=None) ) except ValueError: try: return cast(RSAPublicKey, load_pem_public_key(key_bytes)) 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: ... # pragma: no cover @overload @staticmethod def to_jwk( key_obj: AllowedRSAKeys, as_dict: Literal[False] = False ) -> str: ... # pragma: no cover @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: return key.sign(msg, padding.PKCS1v15(), self.hash_alg()) 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 def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: self.hash_alg = hash_alg def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys: if isinstance(key, (EllipticCurvePrivateKey, EllipticCurvePublicKey)): return 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-"): crypto_key = load_ssh_public_key(key_bytes) else: crypto_key = load_pem_public_key(key_bytes) # type: ignore[assignment] except ValueError: crypto_key = load_pem_private_key(key_bytes, password=None) # type: ignore[assignment] # Explicit check the key to prevent confusing errors from cryptography if not isinstance( crypto_key, (EllipticCurvePrivateKey, EllipticCurvePublicKey) ): raise InvalidKeyError( "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for ECDSA algorithms" ) from None return crypto_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: ... # pragma: no cover @overload @staticmethod def to_jwk( key_obj: AllowedECKeys, as_dict: Literal[False] = False ) -> str: ... # pragma: no cover @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: return key.sign( msg, padding.PSS( mgf=padding.MGF1(self.hash_alg()), salt_length=self.hash_alg().digest_size, ), self.hash_alg(), ) 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. """ def __init__(self, **kwargs: Any) -> None: pass def prepare_key(self, key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys: if isinstance(key, (bytes, str)): key_str = key.decode("utf-8") if isinstance(key, bytes) else key key_bytes = key.encode("utf-8") if isinstance(key, str) else key if "-----BEGIN PUBLIC" in key_str: key = load_pem_public_key(key_bytes) # type: ignore[assignment] elif "-----BEGIN PRIVATE" in key_str: key = load_pem_private_key(key_bytes, password=None) # type: ignore[assignment] elif key_str[0:4] == "ssh-": key = load_ssh_public_key(key_bytes) # type: ignore[assignment] # Explicit check the key to prevent confusing errors from cryptography if not isinstance( key, (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), ): raise InvalidKeyError( "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for EdDSA algorithms" ) return 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 return key.sign(msg_bytes) 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: ... # pragma: no cover @overload @staticmethod def to_jwk( key: AllowedOKPKeys, as_dict: Literal[False] = False ) -> str: ... # pragma: no cover @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 pyjwt-2.10.1/jwt/api_jwk.py000066400000000000000000000105431472176172500155710ustar00rootroot00000000000000from __future__ import annotations import json import time 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: self._algorithms = get_default_algorithms() 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 if algorithm in self._algorithms: self.Algorithm = self._algorithms[algorithm] else: raise PyJWKError(f"Unable to find an algorithm for key: {self._jwk_data}") self.key = self.Algorithm.from_jwk(self._jwk_data) @staticmethod def from_dict(obj: JWKDict, algorithm: str | None = None) -> PyJWK: return PyJWK(obj, algorithm) @staticmethod def from_json(data: str, algorithm: None = None) -> PyJWK: obj = json.loads(data) return PyJWK.from_dict(obj, algorithm) @property def key_type(self) -> str | None: return self._jwk_data.get("kty", None) @property def key_id(self) -> str | None: return self._jwk_data.get("kid", None) @property def public_key_use(self) -> str | None: return self._jwk_data.get("use", None) class PyJWKSet: def __init__(self, keys: list[JWKDict]) -> None: self.keys = [] 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}") 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 pyjwt-2.10.1/jwt/api_jws.py000066400000000000000000000267621472176172500156130ustar00rootroot00000000000000from __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, InvalidSignatureError, InvalidTokenError, ) from .utils import base64url_decode, base64url_encode from .warnings import RemovedInPyjwt3Warning if TYPE_CHECKING: from .algorithms import AllowedPrivateKeys, AllowedPublicKeys class PyJWS: header_typ = "JWT" def __init__( self, algorithms: Sequence[str] | None = None, options: dict[str, Any] | 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] if options is None: options = {} self.options = {**self._get_default_options(), **options} @staticmethod def _get_default_options() -> dict[str, bool]: return {"verify_signature": True} def register_algorithm(self, alg_id: str, alg_obj: Algorithm) -> None: """ Registers a new Algorithm for use when creating and verifying tokens. """ 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 Throws 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. """ 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.get_algorithm_by_name("RS256") """ 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 = None, headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, is_payload_detached: bool = False, sort_headers: bool = True, ) -> str: segments = [] # declare a new var to narrow the type for type checkers if algorithm is None: if isinstance(key, PyJWK): algorithm_ = key.algorithm_name else: algorithm_ = "HS256" 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) 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) 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: dict[str, Any] | None = None, detached_payload: bytes | None = None, **kwargs, ) -> 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, ) if options is None: options = {} 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) 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: dict[str, Any] | None = None, detached_payload: bytes | None = None, **kwargs, ) -> 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 = 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) if not alg_obj.verify(signing_input, prepared_key, signature): raise InvalidSignatureError("Signature verification failed") def _validate_headers(self, headers: dict[str, Any]) -> None: if "kid" in headers: self._validate_kid(headers["kid"]) def _validate_kid(self, kid: Any) -> None: if not isinstance(kid, str): raise InvalidTokenError("Key ID header parameter must be a string") _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 pyjwt-2.10.1/jwt/api_jwt.py000066400000000000000000000342601472176172500156040ustar00rootroot00000000000000from __future__ import annotations import json import warnings from calendar import timegm from collections.abc import Iterable, Sequence from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any from . import api_jws from .exceptions import ( DecodeError, ExpiredSignatureError, ImmatureSignatureError, InvalidAudienceError, InvalidIssuedAtError, InvalidIssuerError, InvalidJTIError, InvalidSubjectError, MissingRequiredClaimError, ) from .warnings import RemovedInPyjwt3Warning if TYPE_CHECKING: from .algorithms import AllowedPrivateKeys, AllowedPublicKeys from .api_jwk import PyJWK class PyJWT: def __init__(self, options: dict[str, Any] | None = None) -> None: if options is None: options = {} self.options: dict[str, Any] = {**self._get_default_options(), **options} @staticmethod def _get_default_options() -> dict[str, bool | list[str]]: 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": [], } def encode( self, payload: dict[str, Any], key: AllowedPrivateKeys | PyJWK | str | bytes, algorithm: str | None = None, headers: dict[str, Any] | None = None, json_encoder: type[json.JSONEncoder] | None = None, sort_headers: bool = True, ) -> str: # 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()) json_payload = self._encode_payload( payload, headers=headers, json_encoder=json_encoder, ) return api_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: AllowedPublicKeys | PyJWK | str | bytes = "", algorithms: Sequence[str] | None = None, options: dict[str, Any] | 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 | Sequence[str] | None = None, subject: str | None = None, leeway: float | timedelta = 0, # kwargs **kwargs: 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, ) options = dict(options or {}) # shallow-copy or initialize an empty dict options.setdefault("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 != options["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, ) if not options["verify_signature"]: options.setdefault("verify_exp", False) options.setdefault("verify_nbf", False) options.setdefault("verify_iat", False) options.setdefault("verify_aud", False) options.setdefault("verify_iss", False) options.setdefault("verify_sub", False) options.setdefault("verify_jti", False) decoded = api_jws.decode_complete( jwt, key=key, algorithms=algorithms, options=options, detached_payload=detached_payload, ) payload = self._decode_payload(decoded) merged_options = {**self.options, **options} 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]) -> 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 = 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: dict[str, Any] | 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 | Sequence[str] | None = None, leeway: float | timedelta = 0, # kwargs **kwargs: 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, verify=verify, detached_payload=detached_payload, audience=audience, subject=subject, issuer=issuer, leeway=leeway, ) return decoded["payload"] def _validate_claims( self, payload: dict[str, Any], options: dict[str, Any], audience=None, issuer=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) 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], options: dict[str, Any], ) -> None: for claim in options["require"]: if payload.get(claim) is None: raise MissingRequiredClaimError(claim) def _validate_sub(self, payload: dict[str, Any], subject=None) -> None: """ Checks whether "sub" if in the payload is valid ot 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 ot 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: Any) -> None: if issuer is None: return if "iss" not in payload: raise MissingRequiredClaimError("iss") if isinstance(issuer, str): if payload["iss"] != issuer: raise InvalidIssuerError("Invalid issuer") else: if payload["iss"] not in issuer: raise InvalidIssuerError("Invalid issuer") _jwt_global_obj = PyJWT() encode = _jwt_global_obj.encode decode_complete = _jwt_global_obj.decode_complete decode = _jwt_global_obj.decode pyjwt-2.10.1/jwt/exceptions.py000066400000000000000000000022731472176172500163270ustar00rootroot00000000000000class PyJWTError(Exception): """ Base class for all exceptions """ pass class InvalidTokenError(PyJWTError): pass class DecodeError(InvalidTokenError): pass class InvalidSignatureError(DecodeError): pass class ExpiredSignatureError(InvalidTokenError): pass class InvalidAudienceError(InvalidTokenError): pass class InvalidIssuerError(InvalidTokenError): pass class InvalidIssuedAtError(InvalidTokenError): pass class ImmatureSignatureError(InvalidTokenError): pass class InvalidKeyError(PyJWTError): pass class InvalidAlgorithmError(InvalidTokenError): pass class MissingRequiredClaimError(InvalidTokenError): 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): pass class PyJWKSetError(PyJWTError): pass class PyJWKClientError(PyJWTError): pass class PyJWKClientConnectionError(PyJWKClientError): pass class InvalidSubjectError(InvalidTokenError): pass class InvalidJTIError(InvalidTokenError): pass pyjwt-2.10.1/jwt/help.py000066400000000000000000000034201472176172500150710ustar00rootroot00000000000000import json import platform import sys from typing import Dict 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() pyjwt-2.10.1/jwt/jwk_set_cache.py000066400000000000000000000016771472176172500167460ustar00rootroot00000000000000import time from typing import Optional from .api_jwk import PyJWKSet, PyJWTSetWithTimestamp class JWKSetCache: def __init__(self, lifespan: int) -> 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 ) pyjwt-2.10.1/jwt/jwks_client.py000066400000000000000000000102431472176172500164560ustar00rootroot00000000000000import json import urllib.request from functools import lru_cache from ssl import SSLContext from typing import Any, Dict, List, Optional from urllib.error import 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: int = 300, headers: Optional[Dict[str, Any]] = None, timeout: int = 30, ssl_context: Optional[SSLContext] = None, ): if headers is None: headers = {} self.uri = uri self.jwk_set_cache: Optional[JWKSetCache] = 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 # Ignore mypy (https://github.com/python/mypy/issues/2427) self.get_signing_key = lru_cache(maxsize=max_cached_keys)( self.get_signing_key ) # type: ignore def fetch_data(self) -> Any: 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: 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: 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]: 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: 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) -> 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) -> Optional[PyJWK]: signing_key = None for key in signing_keys: if key.key_id == kid: signing_key = key break return signing_key pyjwt-2.10.1/jwt/py.typed000066400000000000000000000000001472176172500152550ustar00rootroot00000000000000pyjwt-2.10.1/jwt/types.py000066400000000000000000000001431472176172500153040ustar00rootroot00000000000000from typing import Any, Callable, Dict JWKDict = Dict[str, Any] HashlibHash = Callable[..., Any] pyjwt-2.10.1/jwt/utils.py000066400000000000000000000070701472176172500153060ustar00rootroot00000000000000import 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) pyjwt-2.10.1/jwt/warnings.py000066400000000000000000000000731472176172500157720ustar00rootroot00000000000000class RemovedInPyjwt3Warning(DeprecationWarning): pass pyjwt-2.10.1/pyproject.toml000066400000000000000000000046621472176172500157100ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools", ] [project] authors = [ { email = "hello@jpadilla.com", name = "Jose Padilla" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "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", "Topic :: Utilities", ] description = "JSON Web Token implementation in Python" dynamic = [ "version", ] keywords = [ "json", "jwt", "security", "signing", "token", "web", ] name = "PyJWT" requires-python = ">=3.9" [project.license] text = "MIT" [project.optional-dependencies] crypto = [ "cryptography>=3.4.0", ] dev = [ "coverage[toml]==5.0.4", "cryptography>=3.4.0", "pre-commit", "pytest>=6.0.0,<7.0.0", "sphinx", "sphinx-rtd-theme", "zope.interface", ] docs = [ "sphinx", "sphinx-rtd-theme", "zope.interface", ] tests = [ "coverage[toml]==5.0.4", "pytest>=6.0.0,<7.0.0", ] [project.readme] content-type = "text/x-rst" file = "README.rst" [project.urls] Homepage = "https://github.com/jpadilla/pyjwt" [tool.coverage.paths] source = [ ".tox/*/site-packages", "jwt", ] [tool.coverage.report] exclude_lines = [ "if TYPE_CHECKING:", "pragma: no cover", ] show_missing = true [tool.coverage.run] branch = true parallel = true source = [ "jwt", ] [tool.isort] atomic = true combine_as_imports = true profile = "black" [tool.mypy] allow_incomplete_defs = true allow_untyped_defs = true disable_error_code = [ "method-assign", "unused-ignore", ] ignore_missing_imports = true no_implicit_optional = true overrides = [ { disallow_untyped_calls = false, module = "tests.*" }, ] python_version = 3.11 strict = true warn_return_any = false warn_unused_ignores = true [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 pyjwt-2.10.1/ruff.toml000066400000000000000000000034421472176172500146260ustar00rootroot00000000000000# 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" pyjwt-2.10.1/tests/000077500000000000000000000000001472176172500141265ustar00rootroot00000000000000pyjwt-2.10.1/tests/__init__.py000066400000000000000000000000001472176172500162250ustar00rootroot00000000000000pyjwt-2.10.1/tests/keys/000077500000000000000000000000001472176172500151015ustar00rootroot00000000000000pyjwt-2.10.1/tests/keys/__init__.py000066400000000000000000000027201472176172500172130ustar00rootroot00000000000000import json import os from jwt.algorithms import has_crypto from jwt.utils import base64url_decode try: from cryptography.hazmat.primitives.asymmetric import ec except ModuleNotFoundError: pass if has_crypto: from jwt.algorithms import RSAAlgorithm BASE_PATH = os.path.dirname(os.path.abspath(__file__)) def decode_value(val): decoded = base64url_decode(val) return int.from_bytes(decoded, byteorder="big") def load_hmac_key(): with open(os.path.join(BASE_PATH, "jwk_hmac.json")) as infile: keyobj = json.load(infile) return base64url_decode(keyobj["k"]) def load_rsa_key(): with open(os.path.join(BASE_PATH, "jwk_rsa_key.json")) as infile: return RSAAlgorithm.from_jwk(infile.read()) def load_rsa_pub_key(): with open(os.path.join(BASE_PATH, "jwk_rsa_pub.json")) as infile: return RSAAlgorithm.from_jwk(infile.read()) def load_ec_key(): with open(os.path.join(BASE_PATH, "jwk_ec_key.json")) as infile: keyobj = json.load(infile) return ec.EllipticCurvePrivateNumbers( private_value=decode_value(keyobj["d"]), public_numbers=load_ec_pub_key_p_521().public_numbers(), ) def load_ec_pub_key_p_521(): 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() pyjwt-2.10.1/tests/keys/jwk_ec_key_P-256.json000066400000000000000000000003651472176172500207030ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256@hobbiton.example", "crv": "P-256", "x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4", "y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU", "d": "9GJquUJf57a9sev-u8-PoYlIezIPqI_vGpIaiu4zyZk" } pyjwt-2.10.1/tests/keys/jwk_ec_key_P-384.json000066400000000000000000000004641472176172500207050ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.384@hobbiton.example", "crv": "P-384", "x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J", "y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy", "d": "xKPj5IXjiHpQpLOgyMGo6lg_DUp738SuXkiugCFMxbGNKTyTprYPfJz42wTOXbtd" } pyjwt-2.10.1/tests/keys/jwk_ec_key_P-521.json000066400000000000000000000005741472176172500207000ustar00rootroot00000000000000{ "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" } pyjwt-2.10.1/tests/keys/jwk_ec_key_secp256k1.json000066400000000000000000000003721472176172500216130ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256k@hobbiton.example", "crv": "secp256k1", "x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs", "y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI", "d": "XV7LOlEOANIaSxyil8yE8NPDT5jmVw_HQeCwNDzochQ" } pyjwt-2.10.1/tests/keys/jwk_ec_pub_P-256.json000066400000000000000000000002771472176172500207030ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256@hobbiton.example", "crv": "P-256", "x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4", "y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU" } pyjwt-2.10.1/tests/keys/jwk_ec_pub_P-384.json000066400000000000000000000003511472176172500206760ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.384@hobbiton.example", "crv": "P-384", "x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J", "y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy" } pyjwt-2.10.1/tests/keys/jwk_ec_pub_P-521.json000066400000000000000000000004311472176172500206660ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.521@hobbiton.example", "crv": "P-521", "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1" } pyjwt-2.10.1/tests/keys/jwk_ec_pub_secp256k1.json000066400000000000000000000003041472176172500216040ustar00rootroot00000000000000{ "kty": "EC", "kid": "bilbo.baggins.256k@hobbiton.example", "crv": "secp256k1", "x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs", "y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI" } pyjwt-2.10.1/tests/keys/jwk_empty.json000066400000000000000000000000001472176172500177730ustar00rootroot00000000000000pyjwt-2.10.1/tests/keys/jwk_hmac.json000066400000000000000000000002531472176172500175570ustar00rootroot00000000000000{ "kty": "oct", "kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037", "use": "sig", "alg": "HS256", "k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg" } pyjwt-2.10.1/tests/keys/jwk_keyset_only_unknown_alg.json000066400000000000000000000026761472176172500236310ustar00rootroot00000000000000{"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"}]} pyjwt-2.10.1/tests/keys/jwk_keyset_with_unknown_alg.json000066400000000000000000000055561472176172500236230ustar00rootroot00000000000000{"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"}]} pyjwt-2.10.1/tests/keys/jwk_okp_key_Ed25519.json000066400000000000000000000002171472176172500213260ustar00rootroot00000000000000{ "kty":"OKP", "crv":"Ed25519", "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" } pyjwt-2.10.1/tests/keys/jwk_okp_key_Ed448.json000066400000000000000000000004161472176172500211610ustar00rootroot00000000000000{ "kty": "OKP", "kid": "sig_ed448_01", "crv": "Ed448", "use": "sig", "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", "d": "Zh5xx0r_0tq39xj-8jGuCwAA6wsDim2ME7cX_iXzqDRgPN8lsZZHu60AO7m31Fa4NtHO07eU63q8", "alg": "EdDSA" } pyjwt-2.10.1/tests/keys/jwk_okp_pub_Ed25519.json000066400000000000000000000001321472176172500213200ustar00rootroot00000000000000{ "kty":"OKP", "crv":"Ed25519", "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" } pyjwt-2.10.1/tests/keys/jwk_okp_pub_Ed448.json000066400000000000000000000002671472176172500211630ustar00rootroot00000000000000{ "kty": "OKP", "kid": "sig_ed448_01", "crv": "Ed448", "use": "sig", "x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA", "alg": "EdDSA" } pyjwt-2.10.1/tests/keys/jwk_rsa_key.json000066400000000000000000000033211472176172500203030ustar00rootroot00000000000000{ "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" } pyjwt-2.10.1/tests/keys/jwk_rsa_pub.json000066400000000000000000000007151472176172500203050ustar00rootroot00000000000000{ "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" } pyjwt-2.10.1/tests/keys/testkey2_rsa.pub.pem000066400000000000000000000007031472176172500210100ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1tUH3/0v8fvLensHO1g2 6+U4r7jBg43DVOgqmXAWQa8ArAb4NfTrsYX8YkVhZZYwuLmKczRj0GhXUVY9iDbT sIGmgG+ySj6eiREz5VLqofFkAvRZ6y7yNv8PIGgXEhQTiDDNIkHGaFNMvn/eZ54H is70pdTjR5Ko+/y/wg71df1nb/5KwttSvy0YsTu/XpkduonPruYfAVRG3HK+3GZd xTygLcdamwe9jj+kjxtXRlrXVMQiXGFSU8U6bjafWnQiQ9XzjxvygBt0ZD0kRorr p74XGyQY5ThkN8DlpJbTTFsxOnBUAQz4zhohjobIGBRimi5yVlyLOwTlpaKGFC7O 7wIDAQAB -----END PUBLIC KEY----- pyjwt-2.10.1/tests/keys/testkey_ec.priv000066400000000000000000000003611472176172500201420ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2nninfu2jMHDwAbn 9oERUhRADS6duQaJEadybLaa0YShRANCAAQfMBxRZKUYEdy5/fLdGI2tYj6kTr50 PZPt8jOD23rAR7dhtNpG1ojqopmH0AH5wEXadgk8nLCT4cAPK59Qp9Ek -----END PRIVATE KEY----- pyjwt-2.10.1/tests/keys/testkey_ec.pub000066400000000000000000000002621472176172500177500ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHzAcUWSlGBHcuf3y3RiNrWI+pE6+ dD2T7fIzg9t6wEe3YbTaRtaI6qKZh9AB+cBF2nYJPJywk+HADyufUKfRJA== -----END PUBLIC KEY----- pyjwt-2.10.1/tests/keys/testkey_ec_secp192r1.priv000066400000000000000000000003211472176172500216470ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBiON6kYcPu8ZUDRTu8W eXJ2FmX7e9yq0hahNAMyAARHecLjkXWDUJfZ4wiFH61JpmonCYH1GpinVlqw68Sf wtDHg2F6SifQEFC6VKj1ZXw= -----END PRIVATE KEY----- pyjwt-2.10.1/tests/keys/testkey_ec_ssh.pub000066400000000000000000000002411472176172500206220ustar00rootroot00000000000000ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB8wHFFkpRgR3Ln98t0Yja1iPqROvnQ9k+3yM4PbesBHt2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ= pyjwt-2.10.1/tests/keys/testkey_ed25519000066400000000000000000000001671472176172500175760ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIBy9N4xfv/9qOiKrxwRKeGfO5ab6lSukKHbuC5vaJ1Mg -----END PRIVATE KEY----- pyjwt-2.10.1/tests/keys/testkey_ed25519.pub000066400000000000000000000001211472176172500203510ustar00rootroot00000000000000ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC4pK2dePGgctIAsh0H/tmUrLzx2Vc4Ltc8TN9nfuChG pyjwt-2.10.1/tests/keys/testkey_pkcs1.pub.pem000066400000000000000000000003671472176172500211700ustar00rootroot00000000000000-----BEGIN RSA PUBLIC KEY----- MIGHAoGBAOV/0Vl/5VdHcYpnILYzBGWo5JQVzo9wBkbxzjAStcAnTwvv1ZJTMXs6 fjz91f9hiMM4Z/5qNTE/EHlDWxVdj1pyRaQulZPUs0r9qJ02ogRRGLG3jjrzzbzF yj/pdNBwym0UJYC/Jmn/kMLwGiWI2nfa9vM5SovqZiAy2FD7eOtVAgED -----END RSA PUBLIC KEY----- pyjwt-2.10.1/tests/keys/testkey_rsa.cer000066400000000000000000000024011472176172500201260ustar00rootroot00000000000000-----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----- pyjwt-2.10.1/tests/keys/testkey_rsa.priv000066400000000000000000000032171472176172500203430ustar00rootroot00000000000000-----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----- pyjwt-2.10.1/tests/keys/testkey_rsa.pub000066400000000000000000000006211472176172500201450ustar00rootroot00000000000000ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUeDMF8m/Zw6NCvILB7w0R6WuI5M0VYplSK969StGGWpyZkl0K+4IlGv7EI346Y7yrCAklH3x9426PFWD4n0uzf9Q4DgdwX2dgfpqpVcQsATweT8Au/GL3qazUMO4C9AZphn6UA3GW/F5AU5lJuOTP/2aHxot4YrCokunen46FQFCnC8BQA+Dzb45S2BNbedKWON4WvXgHyoCd4ZJz/CXSeL77VJFlRcVKi/NypA6SVrdbbuust+sPoteCP9OwOzi8PU+cibXNxV6cpPtN9gjXtuwb879Z5zDPY2XX2kvuAOiw2u4XeiAlY2rUvKf+KM9CeQ8ulkfKG+6LJxhwjcOJ aasmundo@mair.local pyjwt-2.10.1/tests/test_advisory.py000066400000000000000000000117431472176172500174050ustar00rootroot00000000000000import 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): # 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, ) pyjwt-2.10.1/tests/test_algorithms.py000066400000000000000000001214421472176172500177140ustar00rootroot00000000000000import base64 import json from typing import Any, 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_none_algorithm_should_throw_exception_if_key_is_not_none(self): algo = NoneAlgorithm() with pytest.raises(InvalidKeyError): algo.prepare_key("123") def test_none_algorithm_should_throw_exception_on_to_jwk(self): 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): 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): 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): 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): 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): 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): algo = HMACAlgorithm(HMACAlgorithm.SHA256) key: Any = algo.to_jwk("secret", as_dict=as_dict) if not as_dict: key = json.loads(key) assert key == {"kty": "oct", "k": "c2VjcmV0"} def test_hmac_from_jwk_should_raise_exception_if_not_hmac_key(self): 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): 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): 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): 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): 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): algo = RSAAlgorithm(RSAAlgorithm.SHA256) with pytest.raises(TypeError): algo.prepare_key(None) # type: ignore[arg-type] @crypto_required def test_rsa_verify_should_return_false_if_signature_invalid(self): 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): 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): 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): 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): 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): algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.pub")) as keyfile: pub_key = algo.prepare_key(keyfile.read()) key: Any = algo.to_jwk(pub_key, as_dict=as_dict) if not as_dict: key = json.loads(key) 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): algo = ECAlgorithm(ECAlgorithm.SHA256) with open(key_path("testkey_ec.priv")) as keyfile: priv_key = algo.prepare_key(keyfile.read()) key: Any = algo.to_jwk(priv_key, as_dict=as_dict) if not as_dict: key = json.loads(key) 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): algo = ECAlgorithm(ECAlgorithm.SHA256) with pytest.raises(InvalidKeyError): algo.to_jwk({"not": "a valid key"}) # type: ignore[call-overload] @crypto_required @pytest.mark.parametrize("as_dict", (False, True)) def test_ec_to_jwk_with_valid_curves(self, as_dict): 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()) jwk: Any = algo.to_jwk(pub_key, as_dict=as_dict) if not as_dict: jwk = json.loads(jwk) assert jwk["crv"] == curve with open(key_path(f"jwk_ec_key_{curve}.json")) as keyfile: priv_key = algo.from_jwk(keyfile.read()) jwk = algo.to_jwk(priv_key, as_dict=as_dict) if not as_dict: jwk = json.loads(jwk) assert jwk["crv"] == curve @crypto_required def test_ec_to_jwk_with_invalid_curve(self): 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): 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): 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): 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): 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): 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): 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): 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): 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): algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.pub")) as keyfile: pub_key = algo.prepare_key(keyfile.read()) key: Any = algo.to_jwk(pub_key, as_dict=as_dict) if not as_dict: key = json.loads(key) 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): algo = RSAAlgorithm(RSAAlgorithm.SHA256) with open(key_path("testkey_rsa.priv")) as keyfile: priv_key = algo.prepare_key(keyfile.read()) key: Any = algo.to_jwk(priv_key, as_dict=as_dict) if not as_dict: key = json.loads(key) 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): algo = RSAAlgorithm(RSAAlgorithm.SHA256) with pytest.raises(InvalidKeyError): algo.to_jwk({"not": "a valid key"}) # type: ignore[call-overload] @crypto_required def test_rsa_from_jwk_raises_exception_on_invalid_key(self): 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): algo = ECAlgorithm(ECAlgorithm.SHA256) with pytest.raises(TypeError): algo.prepare_key(None) # type: ignore[arg-type] @crypto_required def test_ec_should_accept_pem_private_key_bytes(self): 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): 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): 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): 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): 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): 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): 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): """ 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): """ 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): """ 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): """ 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 = b"Hello World!" def test_okp_ed25519_should_reject_non_string_key(self): algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): algo.prepare_key(None) # type: ignore[arg-type] 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()) def test_okp_ed25519_sign_should_generate_correct_signature_value(self): algo = OKPAlgorithm() jwt_message = self.hello_world expected_sig = base64.b64decode(self.hello_world_sig) with open(key_path("testkey_ed25519")) as keyfile: jwt_key = cast(Ed25519PrivateKey, algo.prepare_key(keyfile.read())) with open(key_path("testkey_ed25519.pub")) 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 def test_okp_ed25519_verify_should_return_false_if_signature_invalid(self): algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(self.hello_world_sig) jwt_sig += b"123" # Signature is now invalid with open(key_path("testkey_ed25519.pub")) as keyfile: jwt_pub_key = algo.prepare_key(keyfile.read()) result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert not result def test_okp_ed25519_verify_should_return_true_if_signature_valid(self): algo = OKPAlgorithm() jwt_message = self.hello_world jwt_sig = base64.b64decode(self.hello_world_sig) with open(key_path("testkey_ed25519.pub")) as keyfile: jwt_pub_key = algo.prepare_key(keyfile.read()) result = algo.verify(jwt_message, jwt_pub_key, jwt_sig) assert result def test_okp_ed25519_prepare_key_should_be_idempotent(self): algo = OKPAlgorithm() with open(key_path("testkey_ed25519.pub")) 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_jwk_private_key_should_parse_and_verify(self): 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, ): 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): 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): 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): 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 = algo.to_jwk(pub_key_1, as_dict=as_dict) pub_key_2 = algo.from_jwk(pub) pri = algo.to_jwk(priv_key_1, as_dict=as_dict) priv_key_2 = cast(Ed25519PrivateKey, algo.from_jwk(pri)) 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): algo = OKPAlgorithm() with pytest.raises(InvalidKeyError): algo.to_jwk({"not": "a valid key"}) # type: ignore[call-overload] def test_okp_ed448_jwk_private_key_should_parse_and_verify(self): 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, ): 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): 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): 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): 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 = algo.to_jwk(pub_key_1, as_dict=as_dict) pub_key_2 = algo.from_jwk(pub) pri = algo.to_jwk(priv_key_1, as_dict=as_dict) priv_key_2 = cast(Ed448PrivateKey, algo.from_jwk(pri)) 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): # 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): # 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): 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) pyjwt-2.10.1/tests/test_api_jwk.py000066400000000000000000000252561472176172500171750ustar00rootroot00000000000000import 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): with pytest.raises(MissingCryptographyError): PyJWK({"kty": "dummy"}, algorithm="RS256") class TestPyJWKSet: @crypto_required def test_should_load_keys_from_jwk_data_dict(self): 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): 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): 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_with_unknown_alg(self): # 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): with pytest.raises(PyJWKSetError) as err: PyJWKSet(keys="string") # type: ignore assert str(err.value) == "Invalid JWK Set value" @crypto_required def test_empty_keys_list(self): 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): with pytest.raises(MissingCryptographyError): PyJWKSet(keys=[{"kty": "RSA"}]) pyjwt-2.10.1/tests/test_api_jws.py000066400000000000000000000773201472176172500172040ustar00rootroot00000000000000import 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, ) except ModuleNotFoundError: pass @pytest.fixture def jws(): return PyJWS() @pytest.fixture def payload(): """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): jws.register_algorithm("AAA", NoneAlgorithm()) with pytest.raises(ValueError): jws.register_algorithm("AAA", NoneAlgorithm()) def test_register_algo_rejects_non_algorithm_obj(self, jws): with pytest.raises(TypeError): jws.register_algorithm("AAA123", {}) def test_unregister_algo_removes_algorithm(self, jws): 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): with pytest.raises(KeyError): jws.unregister_algorithm("AAA") def test_algo_parameter_removes_alg_from_algorithms_list(self, jws): 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): jws = PyJWS(options={"verify_signature": False}) assert not jws.options["verify_signature"] def test_non_object_options_dont_persist(self, jws, payload): 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): pytest.raises(TypeError, PyJWS, options=object()) pytest.raises((TypeError, ValueError), PyJWS, options=("something")) def test_encode_decode(self, jws, payload): 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, payload ): 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): secret = "secret" unicode_jws = ( "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) jws.decode(unicode_jws, secret, algorithms=["HS256"]) def test_decode_missing_segments_throws_exception(self, jws): 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): example_jws = None example_secret = "secret" with pytest.raises(DecodeError) as context: jws.decode(example_jws, example_secret, algorithms=["HS256"]) exception = context.value assert "Invalid token type" in str(exception) def test_decode_invalid_token_type_is_int(self, jws): example_jws = 123 example_secret = "secret" with pytest.raises(DecodeError) as context: jws.decode(example_jws, 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): 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, payload): 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, payload): 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, payload): msg = jws.encode(payload, key=None, 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, payload): 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()) 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, payload): 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()) 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, payload): 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_decode_algorithm_param_should_be_case_sensitive(self, jws): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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): 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): 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, payload): 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, payload): 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): 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): 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, payload): right_secret = "foo" jws_message = jws.encode(payload, right_secret) decoded_payload = jws.decode( jws_message, key=None, algorithms=["HS256"], options={"verify_signature": False}, ) assert decoded_payload == payload def test_no_secret(self, jws, payload): 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, payload): 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, payload): 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, payload): with pytest.raises(NotImplementedError): jws.encode(payload, "secret", algorithm="HS1024") @no_crypto_required def test_missing_crypto_library_better_error_messages(self, jws, payload): with pytest.raises(NotImplementedError) as excinfo: jws.encode(payload, "secret", algorithm="RS256") assert "cryptography" in str(excinfo.value) def test_unicode_secret(self, jws, payload): 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, payload): 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, payload): 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, payload, sort_headers): 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): 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): 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): 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): 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, payload): jws_message = jws.encode(payload, key=None, 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, payload): jws_message = jws.encode(payload, key=None, algorithm="none") jws.decode(jws_message, options={"verify_signature": False}) def test_get_unverified_header_returns_header_values(self, jws, payload): 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, payload): # 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, payload, algo): # 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) 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()) jws.decode(jws_message, pub_rsakey, algorithms=[algo]) # string-formatted key with open(key_path("testkey_rsa.priv")) as rsa_priv_file: priv_rsakey = rsa_priv_file.read() # type: ignore[assignment] jws_message = jws.encode(payload, priv_rsakey, algorithm=algo) with open(key_path("testkey_rsa.pub")) as rsa_pub_file: pub_rsakey = rsa_pub_file.read() # type: ignore[assignment] jws.decode(jws_message, pub_rsakey, algorithms=[algo]) def test_rsa_related_algorithms(self, jws): 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", [ "ES256", "ES256K", "ES384", "ES512", ], ) @crypto_required def test_encode_decode_ecdsa_related_algorithms(self, jws, payload, algo): # PEM-formatted EC key with open(key_path("testkey_ec.priv"), "rb") as ec_priv_file: priv_eckey = load_pem_private_key(ec_priv_file.read(), password=None) jws_message = jws.encode(payload, priv_eckey, algorithm=algo) with open(key_path("testkey_ec.pub"), "rb") as ec_pub_file: pub_eckey = load_pem_public_key(ec_pub_file.read()) jws.decode(jws_message, pub_eckey, algorithms=[algo]) # string-formatted key with open(key_path("testkey_ec.priv")) as ec_priv_file: priv_eckey = ec_priv_file.read() # type: ignore[assignment] jws_message = jws.encode(payload, priv_eckey, algorithm=algo) with open(key_path("testkey_ec.pub")) as ec_pub_file: pub_eckey = ec_pub_file.read() # type: ignore[assignment] jws.decode(jws_message, pub_eckey, algorithms=[algo]) def test_ecdsa_related_algorithms(self, jws): 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): token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJzb21lIjoicGF5bG9hZCJ9" ".4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZA" ) jws.decode(token, "secret", options={"verify_signature": False}) def test_decode_options_must_be_dict(self, jws, payload): token = jws.encode(payload, "secret") with pytest.raises(TypeError): jws.decode(token, "secret", options=object()) with pytest.raises((TypeError, ValueError)): jws.decode(token, "secret", options="something") def test_custom_json_encoder(self, jws, payload): class CustomJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Decimal): return "it worked" return super().default(o) 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, payload): headers = {"testheader": True} token = jws.encode(payload, "secret", headers=headers) if not isinstance(token, str): token = token.decode() header = token[0 : token.index(".")].encode() header = base64url_decode(header) if not isinstance(header, str): header = 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): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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 = base64url_decode(msg_header.encode()) msg_header_obj = json.loads(msg_header) 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 = base64url_decode(msg_header.encode()) msg_header_obj = json.loads(msg_header) assert "b64" not in msg_header_obj assert msg_payload def test_decode_detached_content_without_proper_argument(self, jws): 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, payload): 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", ) assert len(record) == 1 assert "foo" in str(record[0].message) def test_decode_complete_warns_on_unuspported_kwarg(self, jws, payload): 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", ) assert len(record) == 1 assert "foo" in str(record[0].message) pyjwt-2.10.1/tests/test_api_jwt.py000066400000000000000000000776151472176172500172140ustar00rootroot00000000000000import json import time from calendar import timegm from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest 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(): return PyJWT() @pytest.fixture def payload(): """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_decodes_valid_jwt(self, jwt): 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): 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): 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): 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): 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): secret = "secret" example_jwt = ( "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9" ".eyJoZWxsbyI6ICJ3b3JsZCJ9" ".tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8" ) with pytest.raises(TypeError) as context: jwt.decode(example_jwt, secret, audience=1, 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): 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): 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): types = ["string", tuple(), list(), 42, set()] for t in types: pytest.raises( TypeError, lambda t=t: jwt.encode(t, "secret", algorithms=["HS256"]), ) def test_encode_with_typ(self, jwt): 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): # >>> 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): # >>> 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, payload): 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, payload): 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): # >>> 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_raises_exception_if_aud_is_none(self, jwt): # >>> 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): 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): 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): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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): 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): 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): 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): 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): 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): 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): 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", algorithms=["HS256"]) def test_raise_exception_invalid_audience_in_array(self, jwt): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJzb21lIjoicGF5bG9hZCJ9" ".4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZA" ) jwt.decode( token, "secret", options={"verify_signature": False}, algorithms=["HS256"], ) def test_skip_check_iat(self, jwt): 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): 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): class CustomJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Decimal): return "it worked" return super().default(o) data = {"some_decimal": Decimal("2.2")} with pytest.raises(TypeError): jwt.encode(data, "secret", algorithms=["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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.warns(RemovedInPyjwt3Warning) as record: jwt.decode(jwt_message, secret, algorithms=["HS256"], foo="bar") assert len(record) == 1 assert "foo" in str(record[0].message) def test_decode_complete_warns_on_unsupported_kwarg(self, jwt, payload): secret = "secret" jwt_message = jwt.encode(payload, secret) with pytest.warns(RemovedInPyjwt3Warning) as record: jwt.decode_complete(jwt_message, secret, algorithms=["HS256"], foo="bar") assert len(record) == 1 assert "foo" in str(record[0].message) def test_decode_strict_aud_forbids_list_audience(self, jwt, payload): 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, payload): 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, payload): 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, payload): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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"]) pyjwt-2.10.1/tests/test_compressed_jwt.py000066400000000000000000000023251472176172500205710ustar00rootroot00000000000000import json import zlib from jwt import PyJWT class CompressedPyJWT(PyJWT): def _decode_payload(self, decoded): return json.loads( # wbits=-15 has zlib not worry about headers of crc's zlib.decompress(decoded["payload"], wbits=-15).decode("utf-8") ) def test_decodes_complete_valid_jwt_with_compressed_payload(): # 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," ), } pyjwt-2.10.1/tests/test_exceptions.py000066400000000000000000000003251472176172500177200ustar00rootroot00000000000000from jwt.exceptions import MissingRequiredClaimError def test_missing_required_claim_error_has_proper_str(): exc = MissingRequiredClaimError("abc") assert str(exc) == 'Token is missing the "abc" claim' pyjwt-2.10.1/tests/test_jwks_client.py000066400000000000000000000370231472176172500200600ustar00rootroot00000000000000import contextlib import json import ssl import time from unittest import mock from urllib.error import URLError 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): 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(): 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, response_data_two ): 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(): with mock.patch("urllib.request.urlopen") as urlopen_mock: urlopen_mock.side_effect = TimeoutError("timed out") yield urlopen_mock @crypto_required class TestPyJWKClient: def test_fetch_data_forwards_headers_to_correct_url(self): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): 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): url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url, ssl_context=ssl.create_default_context()) jwk_set = jwks_client.get_jwk_set() assert jwk_set is not None def test_get_jwt_set_sslcontext_no_ca(self): url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient( url, ssl_context=ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) ) with pytest.raises(PyJWKClientError): jwks_client.get_jwk_set() assert "Failed to get an expected error" pyjwt-2.10.1/tests/test_jwt.py000066400000000000000000000011611472176172500163420ustar00rootroot00000000000000import jwt from .utils import utc_timestamp def test_encode_decode(): """ 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 pyjwt-2.10.1/tests/test_utils.py000066400000000000000000000025471472176172500167070ustar00rootroot00000000000000from contextlib import nullcontext import pytest 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, expected): 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, expected): actual = from_base64url_uint(inputval) assert actual == expected def test_force_bytes_raises_error_on_invalid_object(): 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): assert is_ssh_key(key_format + b" any") is True assert is_ssh_key(b"not a ssh key") is False pyjwt-2.10.1/tests/utils.py000066400000000000000000000013221472176172500156360ustar00rootroot00000000000000import os from calendar import timegm from datetime import datetime, timezone import pytest from jwt.algorithms import has_crypto def utc_timestamp(): return timegm(datetime.now(tz=timezone.utc).utctimetuple()) def key_path(key_name): return os.path.join(os.path.dirname(os.path.realpath(__file__)), "keys", key_name) def no_crypto_required(class_or_func): decorator = pytest.mark.skipif( has_crypto, reason="Requires cryptography library not installed", ) return decorator(class_or_func) def crypto_required(class_or_func): decorator = pytest.mark.skipif( not has_crypto, reason="Requires cryptography library installed" ) return decorator(class_or_func) pyjwt-2.10.1/tox.ini000066400000000000000000000030211472176172500142730ustar00rootroot00000000000000[flake8] min_python_version = 3.9 ignore= E501, E203, W503, E704 [pytest] addopts = -ra testpaths = tests filterwarnings = once::Warning ignore:::pympler[.*] [gh-actions] python = 3.9: py39 3.10: py310 3.11: py311, docs 3.12: py312 3.13: py313 pypy3.9: pypy3 [tox] envlist = lint typing py{39,310,311,312,313,py3}-{crypto,nocrypto} docs pypi-description coverage-report isolated_build = True [testenv] # 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: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]==5.0.4 commands = coverage combine coverage report