使用公密钥的方式登录mqtt

This commit is contained in:
External trust 2024-05-16 15:42:43 +08:00
parent e8d0b79d19
commit 6b276c8cfa
247 changed files with 26151 additions and 12 deletions

View File

@ -0,0 +1,42 @@
repos:
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
args: ["--target-version=py37"]
- repo: https://github.com/asottile/blacken-docs
rev: 1.15.0
hooks:
- id: blacken-docs
args: ["--target-version=py37"]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
language_version: python3.9
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: debug-statements
- repo: https://github.com/mgedmin/check-manifest
rev: "0.49"
hooks:
- id: check-manifest
args: [--no-build-isolation]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.4.1"
hooks:
- id: mypy
additional_dependencies: [cryptography>=3.4.0]

7
PyJWT/AUTHORS.rst Normal file
View File

@ -0,0 +1,7 @@
Authors
=======
``pyjwt`` is currently written and maintained by `Jose Padilla <https://github.com/jpadilla>`_.
Originally written and maintained by `Jeff Lindsay <https://github.com/progrium>`_.
A full list of contributors can be found on GitHubs `overview <https://github.com/jpadilla/pyjwt/graphs/contributors>`_.

802
PyJWT/CHANGELOG.rst Normal file
View File

@ -0,0 +1,802 @@
Changelog
=========
All notable changes to this project will be documented in this file.
This project adheres to `Semantic Versioning <https://semver.org/>`__.
`Unreleased <https://github.com/jpadilla/pyjwt/compare/2.7.0...HEAD>`__
-----------------------------------------------------------------------
Changed
~~~~~~~
Fixed
~~~~~
Added
~~~~~
`v2.8.0 <https://github.com/jpadilla/pyjwt/compare/2.7.0...2.8.0>`__
-----------------------------------------------------------------------
Changed
~~~~~~~
- Update python version test matrix by @auvipy in `#895 <https://github.com/jpadilla/pyjwt/pull/895>`__
Fixed
~~~~~
Added
~~~~~
- Add ``strict_aud`` as an option to ``jwt.decode`` by @woodruffw in `#902 <https://github.com/jpadilla/pyjwt/pull/902>`__
- Export PyJWKClientConnectionError class by @daviddavis in `#887 <https://github.com/jpadilla/pyjwt/pull/887>`__
- Allows passing of ssl.SSLContext to PyJWKClient by @juur in `#891 <https://github.com/jpadilla/pyjwt/pull/891>`__
`v2.7.0 <https://github.com/jpadilla/pyjwt/compare/2.6.0...2.7.0>`__
-----------------------------------------------------------------------
Changed
~~~~~~~
- Changed the error message when the token audience doesn't match the expected audience by @irdkwmnsb `#809 <https://github.com/jpadilla/pyjwt/pull/809>`__
- Improve error messages when cryptography isn't installed by @Viicos in `#846 <https://github.com/jpadilla/pyjwt/pull/846>`__
- Make `Algorithm` an abstract base class by @Viicos in `#845 <https://github.com/jpadilla/pyjwt/pull/845>`__
- ignore invalid keys in a jwks by @timw6n in `#863 <https://github.com/jpadilla/pyjwt/pull/863>`__
Fixed
~~~~~
- Add classifier for Python 3.11 by @eseifert in `#818 <https://github.com/jpadilla/pyjwt/pull/818>`__
- Fix ``_validate_iat`` validation by @Viicos in `#847 <https://github.com/jpadilla/pyjwt/pull/847>`__
- fix: use datetime.datetime.timestamp function to have a milliseconds by @daillouf `#821 <https://github.com/jpadilla/pyjwt/pull/821>`__
- docs: correct mistake in the changelog about verify param by @gbillig in `#866 <https://github.com/jpadilla/pyjwt/pull/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 <https://github.com/jpadilla/pyjwt/pull/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 <https://github.com/jpadilla/pyjwt/pull/823>`__
- Add PyJWT._{de,en}code_payload hooks by @akx in `#829 <https://github.com/jpadilla/pyjwt/pull/829>`__
- Add `sort_headers` parameter to `api_jwt.encode` by @evroon in `#832 <https://github.com/jpadilla/pyjwt/pull/832>`__
- Make mypy configuration stricter and improve typing by @akx in `#830 <https://github.com/jpadilla/pyjwt/pull/830>`__
- Add more types by @Viicos in `#843 <https://github.com/jpadilla/pyjwt/pull/843>`__
- Add a timeout for PyJWKClient requests by @daviddavis in `#875 <https://github.com/jpadilla/pyjwt/pull/875>`__
- Add client connection error exception by @daviddavis in `#876 <https://github.com/jpadilla/pyjwt/pull/876>`__
- Add complete types to take all allowed keys into account by @Viicos in `#873 <https://github.com/jpadilla/pyjwt/pull/873>`__
- Add `as_dict` option to `Algorithm.to_jwk` by @fluxth in `#881 <https://github.com/jpadilla/pyjwt/pull/881>`__
`v2.6.0 <https://github.com/jpadilla/pyjwt/compare/2.5.0...2.6.0>`__
-----------------------------------------------------------------------
Changed
~~~~~~~
- bump up cryptography >= 3.4.0 by @jpadilla in `#807 <https://github.com/jpadilla/pyjwt/pull/807>`_
- Remove `types-cryptography` from `crypto` extra by @lautat in `#805 <https://github.com/jpadilla/pyjwt/pull/805>`_
Fixed
~~~~~
- Invalidate token on the exact second the token expires `#797 <https://github.com/jpadilla/pyjwt/pull/797>`_
- fix: version 2.5.0 heading typo by @c0state in `#803 <https://github.com/jpadilla/pyjwt/pull/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 <https://github.com/jpadilla/pyjwt/compare/2.4.0...2.5.0>`__
-----------------------------------------------------------------------
Changed
~~~~~~~
- Skip keys with incompatible alg when loading JWKSet by @DaGuich in `#762 <https://github.com/jpadilla/pyjwt/pull/762>`__
- Remove support for python3.6 by @sirosen in `#777 <https://github.com/jpadilla/pyjwt/pull/777>`__
- Emit a deprecation warning for unsupported kwargs by @sirosen in `#776 <https://github.com/jpadilla/pyjwt/pull/776>`__
- Remove redundant wheel dep from pyproject.toml by @mgorny in `#765 <https://github.com/jpadilla/pyjwt/pull/765>`__
- Do not fail when an unusable key occurs by @DaGuich in `#762 <https://github.com/jpadilla/pyjwt/pull/762>`__
- Update audience typing by @JulianMaurin in `#782 <https://github.com/jpadilla/pyjwt/pull/782>`__
- Improve PyJWKSet error accuracy by @JulianMaurin in `#786 <https://github.com/jpadilla/pyjwt/pull/786>`__
- Mypy as pre-commit check + api_jws typing by @JulianMaurin in `#787 <https://github.com/jpadilla/pyjwt/pull/787>`__
Fixed
~~~~~
- Adjust expected exceptions in option merging tests for PyPy3 by @mgorny in `#763 <https://github.com/jpadilla/pyjwt/pull/763>`__
- Fixes for pyright on strict mode by @brandon-leapyear in `#747 <https://github.com/jpadilla/pyjwt/pull/747>`__
- docs: fix simple typo, iinstance -> isinstance by @timgates42 in `#774 <https://github.com/jpadilla/pyjwt/pull/774>`__
- Fix typo: priot -> prior by @jdufresne in `#780 <https://github.com/jpadilla/pyjwt/pull/780>`__
- Fix for headers disorder issue by @kadabusha in `#721 <https://github.com/jpadilla/pyjwt/pull/721>`__
Added
~~~~~
- Add to_jwk static method to ECAlgorithm by @leonsmith in `#732 <https://github.com/jpadilla/pyjwt/pull/732>`__
- Expose get_algorithm_by_name as new method by @sirosen in `#773 <https://github.com/jpadilla/pyjwt/pull/773>`__
- Add type hints to jwt/help.py and add missing types dependency by @kkirsche in `#784 <https://github.com/jpadilla/pyjwt/pull/784>`__
- Add cacheing functionality for JWK set by @wuhaoyujerry in `#781 <https://github.com/jpadilla/pyjwt/pull/781>`__
`v2.4.0 <https://github.com/jpadilla/pyjwt/compare/2.3.0...2.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 <https://github.com/jpadilla/pyjwt/compare/2.2.0...2.3.0>`__
-----------------------------------------------------------------------
Fixed
~~~~~
- Revert "Remove arbitrary kwargs." `#701 <https://github.com/jpadilla/pyjwt/pull/701>`__
Added
~~~~~
- Add exception chaining `#702 <https://github.com/jpadilla/pyjwt/pull/702>`__
`v2.2.0 <https://github.com/jpadilla/pyjwt/compare/2.1.0...2.2.0>`__
-----------------------------------------------------------------------
Changed
~~~~~~~
- Remove arbitrary kwargs. `#657 <https://github.com/jpadilla/pyjwt/pull/657>`__
- Use timezone package as Python 3.5+ is required. `#694 <https://github.com/jpadilla/pyjwt/pull/694>`__
Fixed
~~~~~
- Assume JWK without the "use" claim is valid for signing as per RFC7517 `#668 <https://github.com/jpadilla/pyjwt/pull/668>`__
- Prefer `headers["alg"]` to `algorithm` in `jwt.encode()`. `#673 <https://github.com/jpadilla/pyjwt/pull/673>`__
- Fix aud validation to support {'aud': null} case. `#670 <https://github.com/jpadilla/pyjwt/pull/670>`__
- Make `typ` optional in JWT to be compliant with RFC7519. `#644 <https://github.com/jpadilla/pyjwt/pull/644>`__
- Remove upper bound on cryptography version. `#693 <https://github.com/jpadilla/pyjwt/pull/693>`__
Added
~~~~~
- Add support for Ed448/EdDSA. `#675 <https://github.com/jpadilla/pyjwt/pull/675>`__
`v2.1.0 <https://github.com/jpadilla/pyjwt/compare/2.0.1...2.1.0>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Allow claims validation without making JWT signature validation mandatory. `#608 <https://github.com/jpadilla/pyjwt/pull/608>`__
Fixed
~~~~~
- Remove padding from JWK test data. `#628 <https://github.com/jpadilla/pyjwt/pull/628>`__
- Make `kty` mandatory in JWK to be compliant with RFC7517. `#624 <https://github.com/jpadilla/pyjwt/pull/624>`__
- Allow JWK without `alg` to be compliant with RFC7517. `#624 <https://github.com/jpadilla/pyjwt/pull/624>`__
- Allow to verify with private key on ECAlgorithm, as well as on Ed25519Algorithm. `#645 <https://github.com/jpadilla/pyjwt/pull/645>`__
Added
~~~~~
- Add caching by default to PyJWKClient `#611 <https://github.com/jpadilla/pyjwt/pull/611>`__
- Add missing exceptions.InvalidKeyError to jwt module __init__ imports `#620 <https://github.com/jpadilla/pyjwt/pull/620>`__
- Add support for ES256K algorithm `#629 <https://github.com/jpadilla/pyjwt/pull/629>`__
- Add `from_jwk()` to Ed25519Algorithm `#621 <https://github.com/jpadilla/pyjwt/pull/621>`__
- Add `to_jwk()` to Ed25519Algorithm `#643 <https://github.com/jpadilla/pyjwt/pull/643>`__
- Export `PyJWK` and `PyJWKSet` `#652 <https://github.com/jpadilla/pyjwt/pull/652>`__
`v2.0.1 <https://github.com/jpadilla/pyjwt/compare/2.0.0...2.0.1>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Rename CHANGELOG.md to CHANGELOG.rst and include in docs `#597 <https://github.com/jpadilla/pyjwt/pull/597>`__
Fixed
~~~~~
- Fix `from_jwk()` for all algorithms `#598 <https://github.com/jpadilla/pyjwt/pull/598>`__
Added
~~~~~
`v2.0.0 <https://github.com/jpadilla/pyjwt/compare/1.7.1...2.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 <https://github.com/jpadilla/pyjwt/compare/1.7.0...1.7.1>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Update test dependencies with pinned ranges
- Fix pytest deprecation warnings
`v1.7.0 <https://github.com/jpadilla/pyjwt/compare/1.6.4...1.7.0>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Remove CRLF line endings
`#353 <https://github.com/jpadilla/pyjwt/pull/353>`__
Fixed
~~~~~
- Update usage.rst
`#360 <https://github.com/jpadilla/pyjwt/pull/360>`__
Added
~~~~~
- Support for Python 3.7
`#375 <https://github.com/jpadilla/pyjwt/pull/375>`__
`#379 <https://github.com/jpadilla/pyjwt/pull/379>`__
`#384 <https://github.com/jpadilla/pyjwt/pull/384>`__
`v1.6.4 <https://github.com/jpadilla/pyjwt/compare/1.6.3...1.6.4>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Reverse an unintentional breaking API change to .decode()
`#352 <https://github.com/jpadilla/pyjwt/pull/352>`__
`v1.6.3 <https://github.com/jpadilla/pyjwt/compare/1.6.1...1.6.3>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- All exceptions inherit from PyJWTError
`#340 <https://github.com/jpadilla/pyjwt/pull/340>`__
Added
~~~~~
- Add type hints `#344 <https://github.com/jpadilla/pyjwt/pull/344>`__
- Add help module
`7ca41e <https://github.com/jpadilla/pyjwt/commit/7ca41e53b3d7d9f5cd31bdd8a2b832d192006239>`__
Docs
~~~~
- Added section to usage docs for jwt.get\_unverified\_header()
`#350 <https://github.com/jpadilla/pyjwt/pull/350>`__
- Update legacy instructions for using pycrypto
`#337 <https://github.com/jpadilla/pyjwt/pull/337>`__
`v1.6.1 <https://github.com/jpadilla/pyjwt/compare/1.6.0...1.6.1>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Audience parameter throws ``InvalidAudienceError`` when application
does not specify an audience, but the token does.
`#336 <https://github.com/jpadilla/pyjwt/pull/336>`__
`v1.6.0 <https://github.com/jpadilla/pyjwt/compare/1.5.3...1.6.0>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Dropped support for python 2.6 and 3.3
`#301 <https://github.com/jpadilla/pyjwt/pull/301>`__
- An invalid signature now raises an ``InvalidSignatureError`` instead
of ``DecodeError``
`#316 <https://github.com/jpadilla/pyjwt/pull/316>`__
Fixed
~~~~~
- Fix over-eager fallback to stdin
`#304 <https://github.com/jpadilla/pyjwt/pull/304>`__
Added
~~~~~
- Audience parameter now supports iterables
`#306 <https://github.com/jpadilla/pyjwt/pull/306>`__
`v1.5.3 <https://github.com/jpadilla/pyjwt/compare/1.5.2...1.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 <https://github.com/jpadilla/pyjwt/pull/281>`__
`v1.5.2 <https://github.com/jpadilla/pyjwt/compare/1.5.1...1.5.2>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Ensure correct arguments order in decode super call
`7c1e61d <https://github.com/jpadilla/pyjwt/commit/7c1e61dde27bafe16e7d1bb6e35199e778962742>`__
`v1.5.1 <https://github.com/jpadilla/pyjwt/compare/1.5.0...1.5.1>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Change optparse for argparse.
`#238 <https://github.com/jpadilla/pyjwt/pull/238>`__
Fixed
~~~~~
- Guard against PKCS1 PEM encoded public keys
`#277 <https://github.com/jpadilla/pyjwt/pull/277>`__
- Add deprecation warning when decoding without specifying
``algorithms`` `#277 <https://github.com/jpadilla/pyjwt/pull/277>`__
- Improve deprecation messages
`#270 <https://github.com/jpadilla/pyjwt/pull/270>`__
- PyJWT.decode: move verify param into options
`#271 <https://github.com/jpadilla/pyjwt/pull/271>`__
Added
~~~~~
- Support for Python 3.6
`#262 <https://github.com/jpadilla/pyjwt/pull/262>`__
- Expose jwt.InvalidAlgorithmError
`#264 <https://github.com/jpadilla/pyjwt/pull/264>`__
`v1.5.0 <https://github.com/jpadilla/pyjwt/compare/1.4.2...1.5.0>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Add support for ECDSA public keys in RFC 4253 (OpenSSH) format
`#244 <https://github.com/jpadilla/pyjwt/pull/244>`__
- Renamed commandline script ``jwt`` to ``jwt-cli`` to avoid issues
with the script clobbering the ``jwt`` module in some circumstances.
`#187 <https://github.com/jpadilla/pyjwt/pull/187>`__
- Better error messages when using an algorithm that requires the
cryptography package, but it isn't available
`#230 <https://github.com/jpadilla/pyjwt/pull/230>`__
- Tokens with future 'iat' values are no longer rejected
`#190 <https://github.com/jpadilla/pyjwt/pull/190>`__
- Non-numeric 'iat' values now raise InvalidIssuedAtError instead of
DecodeError
- Remove rejection of future 'iat' claims
`#252 <https://github.com/jpadilla/pyjwt/pull/252>`__
Fixed
~~~~~
- Add back 'ES512' for backward compatibility (for now)
`#225 <https://github.com/jpadilla/pyjwt/pull/225>`__
- Fix incorrectly named ECDSA algorithm
`#219 <https://github.com/jpadilla/pyjwt/pull/219>`__
- Fix rpm build `#196 <https://github.com/jpadilla/pyjwt/pull/196>`__
Added
~~~~~
- Add JWK support for HMAC and RSA keys
`#202 <https://github.com/jpadilla/pyjwt/pull/202>`__
`v1.4.2 <https://github.com/jpadilla/pyjwt/compare/1.4.1...1.4.2>`__
--------------------------------------------------------------------
Fixed
~~~~~
- A PEM-formatted key encoded as bytes could cause a ``TypeError`` to
be raised `#213 <https://github.com/jpadilla/pyjwt/pull/214>`__
`v1.4.1 <https://github.com/jpadilla/pyjwt/compare/1.4.0...1.4.1>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Newer versions of Pytest could not detect warnings properly
`#182 <https://github.com/jpadilla/pyjwt/pull/182>`__
- Non-string 'kid' value now raises ``InvalidTokenError``
`#174 <https://github.com/jpadilla/pyjwt/pull/174>`__
- ``jwt.decode(None)`` now gracefully fails with ``InvalidTokenError``
`#183 <https://github.com/jpadilla/pyjwt/pull/183>`__
`v1.4 <https://github.com/jpadilla/pyjwt/compare/1.3.0...1.4.0>`__
------------------------------------------------------------------
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 <https://github.com/jpadilla/pyjwt/compare/1.2.0...1.3.0>`__
------------------------------------------------------------------
Fixed
~~~~~
- ECDSA (ES256, ES384, ES512) signatures are now being properly
serialized `#158 <https://github.com/jpadilla/pyjwt/pull/158>`__
- RSA-PSS (PS256, PS384, PS512) signatures now use the proper salt
length for PSS padding.
`#163 <https://github.com/jpadilla/pyjwt/pull/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 <https://github.com/jpadilla/pyjwt/compare/1.1.0...1.2.0>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Added back ``verify_expiration=`` argument to ``jwt.decode()`` that
was erroneously removed in
`v1.1.0 <https://github.com/jpadilla/pyjwt/compare/1.0.1...1.1.0>`__.
Changed
~~~~~~~
- Refactored JWS-specific logic out of PyJWT and into PyJWS superclass.
`#141 <https://github.com/jpadilla/pyjwt/pull/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 <https://github.com/jpadilla/pyjwt/compare/1.0.1...1.1.0>`__
--------------------------------------------------------------------
Added
~~~~~
- Added support for PS256, PS384, and PS512 algorithms.
`#132 <https://github.com/jpadilla/pyjwt/pull/132>`__
- Added flexible and complete verification options during decode.
`#131 <https://github.com/jpadilla/pyjwt/pull/131>`__
- Added this CHANGELOG.md file.
Deprecated
~~~~~~~~~~
- Deprecated usage of the .decode(..., verify=False) parameter.
Fixed
~~~~~
- Fixed command line encoding.
`#128 <https://github.com/jpadilla/pyjwt/pull/128>`__
`v1.0.1 <https://github.com/jpadilla/pyjwt/compare/1.0.0...1.0.1>`__
--------------------------------------------------------------------
Fixed
~~~~~
- Include jwt/contrib' and jwt/contrib/algorithms\` in setup.py so that
they will actually be included when installing.
`882524d <https://github.com/jpadilla/pyjwt/commit/882524d>`__
- Fix bin/jwt after removing jwt.header().
`bd57b02 <https://github.com/jpadilla/pyjwt/commit/bd57b02>`__
`v1.0.0 <https://github.com/jpadilla/pyjwt/compare/0.4.3...1.0.0>`__
--------------------------------------------------------------------
Changed
~~~~~~~
- Moved ``jwt.api.header`` out of the public API.
`#85 <https://github.com/jpadilla/pyjwt/pull/85>`__
- Added README details how to extract public / private keys from an
x509 certificate.
`#100 <https://github.com/jpadilla/pyjwt/pull/100>`__
- Refactor api.py functions into an object (``PyJWT``).
`#101 <https://github.com/jpadilla/pyjwt/pull/101>`__
- Added support for PyCrypto and ecdsa when cryptography isn't
available. `#101 <https://github.com/jpadilla/pyjwt/pull/103>`__
Fixed
~~~~~
- Fixed a security vulnerability where ``alg=None`` header could bypass
signature verification.
`#109 <https://github.com/jpadilla/pyjwt/pull/109>`__
- Fixed a security vulnerability by adding support for a whitelist of
allowed ``alg`` values ``jwt.decode(algorithms=[])``.
`#110 <https://github.com/jpadilla/pyjwt/pull/110>`__

46
PyJWT/CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# 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

21
PyJWT/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The 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.

14
PyJWT/MANIFEST.in Normal file
View File

@ -0,0 +1,14 @@
include .pre-commit-config.yaml
include CODE_OF_CONDUCT.md
include AUTHORS.rst
include CHANGELOG.rst
include LICENSE
include README.rst
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__

93
PyJWT/PKG-INFO Normal file
View File

@ -0,0 +1,93 @@
Metadata-Version: 2.1
Name: PyJWT
Version: 2.8.0
Summary: JSON Web Token implementation in Python
Home-page: https://github.com/jpadilla/pyjwt
Author: Jose Padilla
Author-email: hello@jpadilla.com
License: MIT
Keywords: json,jwt,security,signing,token,web
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Utilities
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
Provides-Extra: docs
Provides-Extra: crypto
Provides-Extra: tests
Provides-Extra: dev
License-File: LICENSE
License-File: AUTHORS.rst
PyJWT
=====
.. 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 <https://tools.ietf.org/html/rfc7519>`_. Original implementation was written by `@progrium <https://github.com/progrium>`_.
Sponsor
-------
+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |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/developers <https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=pyjwt&utm_content=auth>`_. |
+--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. |auth0-logo| image:: https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png
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

View File

@ -0,0 +1,93 @@
Metadata-Version: 2.1
Name: PyJWT
Version: 2.8.0
Summary: JSON Web Token implementation in Python
Home-page: https://github.com/jpadilla/pyjwt
Author: Jose Padilla
Author-email: hello@jpadilla.com
License: MIT
Keywords: json,jwt,security,signing,token,web
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Utilities
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
Provides-Extra: docs
Provides-Extra: crypto
Provides-Extra: tests
Provides-Extra: dev
License-File: LICENSE
License-File: AUTHORS.rst
PyJWT
=====
.. 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 <https://tools.ietf.org/html/rfc7519>`_. Original implementation was written by `@progrium <https://github.com/progrium>`_.
Sponsor
-------
+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |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/developers <https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=pyjwt&utm_content=auth>`_. |
+--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. |auth0-logo| image:: https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png
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

View File

@ -0,0 +1,82 @@
.pre-commit-config.yaml
AUTHORS.rst
CHANGELOG.rst
CODE_OF_CONDUCT.md
LICENSE
MANIFEST.in
README.rst
pyproject.toml
setup.cfg
setup.py
tox.ini
PyJWT.egg-info/PKG-INFO
PyJWT.egg-info/SOURCES.txt
PyJWT.egg-info/dependency_links.txt
PyJWT.egg-info/not-zip-safe
PyJWT.egg-info/requires.txt
PyJWT.egg-info/top_level.txt
docs/Makefile
docs/algorithms.rst
docs/api.rst
docs/changelog.rst
docs/conf.py
docs/faq.rst
docs/index.rst
docs/installation.rst
docs/requirements-docs.txt
docs/usage.rst
docs/_static/theme_overrides.css
jwt/__init__.py
jwt/algorithms.py
jwt/api_jwk.py
jwt/api_jws.py
jwt/api_jwt.py
jwt/exceptions.py
jwt/help.py
jwt/jwk_set_cache.py
jwt/jwks_client.py
jwt/py.typed
jwt/types.py
jwt/utils.py
jwt/warnings.py
tests/__init__.py
tests/test_advisory.py
tests/test_algorithms.py
tests/test_api_jwk.py
tests/test_api_jws.py
tests/test_api_jwt.py
tests/test_compressed_jwt.py
tests/test_exceptions.py
tests/test_jwks_client.py
tests/test_jwt.py
tests/test_utils.py
tests/utils.py
tests/keys/__init__.py
tests/keys/jwk_ec_key_P-256.json
tests/keys/jwk_ec_key_P-384.json
tests/keys/jwk_ec_key_P-521.json
tests/keys/jwk_ec_key_secp256k1.json
tests/keys/jwk_ec_pub_P-256.json
tests/keys/jwk_ec_pub_P-384.json
tests/keys/jwk_ec_pub_P-521.json
tests/keys/jwk_ec_pub_secp256k1.json
tests/keys/jwk_hmac.json
tests/keys/jwk_keyset_only_unknown_alg.json
tests/keys/jwk_keyset_with_unknown_alg.json
tests/keys/jwk_okp_key_Ed25519.json
tests/keys/jwk_okp_key_Ed448.json
tests/keys/jwk_okp_pub_Ed25519.json
tests/keys/jwk_okp_pub_Ed448.json
tests/keys/jwk_rsa_key.json
tests/keys/jwk_rsa_pub.json
tests/keys/testkey2_rsa.pub.pem
tests/keys/testkey_ec.priv
tests/keys/testkey_ec.pub
tests/keys/testkey_ec_secp192r1.priv
tests/keys/testkey_ec_ssh.pub
tests/keys/testkey_ed25519
tests/keys/testkey_ed25519.pub
tests/keys/testkey_pkcs1.pub.pem
tests/keys/testkey_rsa.cer
tests/keys/testkey_rsa.priv
tests/keys/testkey_rsa.pub

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,24 @@
[:python_version <= "3.7"]
typing_extensions
[crypto]
cryptography>=3.4.0
[dev]
sphinx<5.0.0,>=4.5.0
sphinx-rtd-theme
zope.interface
cryptography>=3.4.0
pytest<7.0.0,>=6.0.0
coverage[toml]==5.0.4
pre-commit
[docs]
sphinx<5.0.0,>=4.5.0
sphinx-rtd-theme
zope.interface
[tests]
pytest<7.0.0,>=6.0.0
coverage[toml]==5.0.4

View File

@ -0,0 +1 @@
jwt

62
PyJWT/README.rst Normal file
View File

@ -0,0 +1,62 @@
PyJWT
=====
.. 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 <https://tools.ietf.org/html/rfc7519>`_. Original implementation was written by `@progrium <https://github.com/progrium>`_.
Sponsor
-------
+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |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/developers <https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=pyjwt&utm_content=auth>`_. |
+--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. |auth0-logo| image:: https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png
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

192
PyJWT/docs/Makefile Normal file
View File

@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# 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 coverage gettext
help:
@echo "Please use \`make <target>' where <target> 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 " applehelp to make an Apple Help Book"
@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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@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 " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -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/PyJWT.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyJWT.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/PyJWT"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyJWT"
@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."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@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."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

15
PyJWT/docs/_static/theme_overrides.css vendored Normal file
View File

@ -0,0 +1,15 @@
img.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;
}
}

71
PyJWT/docs/algorithms.rst Normal file
View File

@ -0,0 +1,71 @@
Digital 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 <https://en.wikipedia.org/wiki/Public-key_cryptography>`_.
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
<https://www.rfc-editor.org/rfc/rfc8725.html#section-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\*).

178
PyJWT/docs/api.rst Normal file
View File

@ -0,0 +1,178 @@
API 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 str 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
:param str algorithm: algorithm to sign the token with, e.g. ``"ES256"``.
If ``headers`` includes ``alg``, it will be preferred to this parameter.
: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 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
<https://www.rfc-editor.org/rfc/rfc8725.html#section-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 Union[str, 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: the JWT claims
.. 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
<https://www.rfc-editor.org/rfc/rfc8725.html#section-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
.. 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 in the future
.. class:: ImmatureSignatureError
Raised when a token's ``nbf`` claim represents 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

1
PyJWT/docs/changelog.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../CHANGELOG.rst

134
PyJWT/docs/conf.py Normal file
View File

@ -0,0 +1,134 @@
import os
import re
import sphinx_rtd_theme
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",
]
# 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"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# 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"]
html_context = {
"extra_css_files": [
# override wide tables in RTD theme
"_static/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",
)
]

17
PyJWT/docs/faq.rst Normal file
View File

@ -0,0 +1,17 @@
Frequently 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()

54
PyJWT/docs/index.rst Normal file
View File

@ -0,0 +1,54 @@
Welcome 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| | 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/developers <https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=pyjwt&utm_content=auth>`_. |
+--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. |auth0-logo| image:: https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png
Installation
------------
You can install ``pyjwt`` with ``pip``:
.. code-block:: console
$ pip install pyjwt
See :doc:`Installation <installation>` for more information.
Example Usage
-------------
.. doctest::
>>> import jwt
>>> encoded_jwt = jwt.encode({"some": "payload"}, "secret", algorithm="HS256")
>>> print(encoded_jwt)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg
>>> jwt.decode(encoded_jwt, "secret", algorithms=["HS256"])
{'some': 'payload'}
See :doc:`Usage Examples <usage>` for more examples.
Index
-----
.. toctree::
:maxdepth: 2
installation
usage
faq
algorithms
api
changelog
.. _`RFC 7519`: https://tools.ietf.org/html/rfc7519

View File

@ -0,0 +1,30 @@
Installation
============
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

View File

@ -0,0 +1,2 @@
sphinx
sphinx_rtd_theme

371
PyJWT/docs/usage.rst Normal file
View File

@ -0,0 +1,371 @@
Usage Examples
==============
Encoding & Decoding Tokens with HS256
-------------------------------------
.. code-block:: pycon
>>> import jwt
>>> key = "secret"
>>> encoded = jwt.encode({"some": "payload"}, key, algorithm="HS256")
>>> print(encoded)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg
>>> 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 PRIVATE KEY-----\nMIGEAgEAMBAGByqGSM49AgEGBS..."
>>> public_key = b"-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEAC..."
>>> encoded = jwt.encode({"some": "payload"}, private_key, algorithm="RS256")
>>> print(encoded)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg
>>> decoded = 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 PRIVATE KEY-----\nMIGEAgEAMBAGByqGSM49AgEGBS..."
passphrase = b"your password"
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.
Specifying Additional Headers
-----------------------------
.. code-block:: pycon
>>> jwt.encode(
... {"some": "payload"},
... "secret",
... algorithm="HS256",
... headers={"kid": "230498151c214b788dd97f22b85410a5"},
... )
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJzb21lIjoicGF5bG9hZCJ9.DogbDGmMHgA_bU05TAB-R6geQ2nMU2BRM-LnYEtefwg'
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
>>> jwt.get_unverified_header(encoded)
{'alg': 'RS256', 'typ': 'JWT', 'kid': 'key-id-12345...'}
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:: python
jwt.encode({"exp": 1371720939}, "secret")
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:: python
try:
jwt.decode("JWT_STRING", "secret", algorithms=["HS256"])
except jwt.ExpiredSignatureError:
# Signature has 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:: python
jwt_payload = jwt.encode(
{"exp": datetime.datetime.now(tz=timezone.utc) + datetime.timedelta(seconds=30)},
"secret",
)
time.sleep(32)
# JWT payload is now expired
# But with some leeway, it will still validate
jwt.decode(jwt_payload, "secret", leeway=10, 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:: python
jwt.decode(
jwt_payload, "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:: python
jwt.encode({"nbf": 1371720939}, "secret")
jwt.encode({"nbf": 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:: python
payload = {"some": "payload", "iss": "urn:foo"}
token = jwt.encode(payload, "secret")
decoded = jwt.decode(token, "secret", issuer="urn:foo", algorithms=["HS256"])
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:: python
payload = {"some": "payload", "aud": ["urn:foo", "urn:bar"]}
token = jwt.encode(payload, "secret")
decoded = jwt.decode(token, "secret", audience="urn:foo", 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:: python
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:: python
payload = {"some": "payload", "aud": "urn:foo"}
token = jwt.encode(payload, "secret")
decoded = jwt.decode(
token, "secret", audience=["urn:foo", "urn:bar"], algorithms=["HS256"]
)
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:: python
jwt.encode({"iat": 1371720939}, "secret")
jwt.encode({"iat": 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
>>> jwt.decode(encoded, options={"require": ["exp", "iss", "sub"]})
{'exp': 1371720939, 'iss': 'urn:foo', 'sub': '25c37522-f148-4cbf-8ee6-c4a9718dd0af'}
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"
>>> kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw"
>>> 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)
>>> data = jwt.decode(
... token,
... signing_key.key,
... algorithms=["RS256"],
... audience="https://expenses-api",
... options={"verify_exp": False},
... )
>>> print(data)
{'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.key,
algorithms=signing_algos,
audience=client_id,
)
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"]

74
PyJWT/jwt/__init__.py Normal file
View File

@ -0,0 +1,74 @@
from .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, 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.8.0"
__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",
"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",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

862
PyJWT/jwt/algorithms.py Normal file
View File

@ -0,0 +1,862 @@
from __future__ import annotations
import hashlib
import hmac
import json
import sys
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn, Union, 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,
)
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
try:
from cryptography.exceptions import InvalidSignature
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) -> Union[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) -> Union[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")
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:
return cast(RSAPublicKey, load_pem_public_key(key_bytes))
@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
) -> Union[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")
if obj.get("kty") != "RSA":
raise InvalidKeyError("Not an RSA key")
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"
)
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"
)
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
) -> Union[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).decode(),
"y": to_base64url_uint(public_numbers.y).decode(),
}
if isinstance(key_obj, EllipticCurvePrivateKey):
obj["d"] = to_base64url_uint(
key_obj.private_numbers().private_value
).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")
if obj.get("kty") != "EC":
raise InvalidKeyError("Not an Elliptic curve key")
if "x" not in obj or "y" not in obj:
raise InvalidKeyError("Not an Elliptic curve key")
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")
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")
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")
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) -> Union[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")
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

132
PyJWT/jwt/api_jwk.py Normal file
View File

@ -0,0 +1,132 @@
from __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, 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 PyJWKError(f"{algorithm} requires 'cryptography' to be installed.")
self.Algorithm = self._algorithms.get(algorithm)
if not self.Algorithm:
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:
# 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

328
PyJWT/jwt/api_jws.py Normal file
View File

@ -0,0 +1,328 @@
from __future__ import annotations
import binascii
import json
import warnings
from typing import TYPE_CHECKING, Any
from .algorithms import (
Algorithm,
get_default_algorithms,
has_crypto,
requires_cryptography,
)
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: list[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 | str | bytes,
algorithm: str | None = "HS256",
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
algorithm_: str = algorithm if algorithm is not None else "none"
# 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_)
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 | str | bytes = "",
algorithms: list[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,
)
if options is None:
options = {}
merged_options = {**self.options, **options}
verify_signature = merged_options["verify_signature"]
if verify_signature and not algorithms:
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 | str | bytes = "",
algorithms: list[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,
)
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 | str | bytes = "",
algorithms: list[str] | None = None,
) -> None:
try:
alg = header["alg"]
except KeyError:
raise InvalidAlgorithmError("Algorithm not specified")
if not alg or (algorithms is not None and alg not in algorithms):
raise InvalidAlgorithmError("The specified alg value is not allowed")
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

372
PyJWT/jwt/api_jwt.py Normal file
View File

@ -0,0 +1,372 @@
from __future__ import annotations
import json
import warnings
from calendar import timegm
from collections.abc import Iterable
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,
MissingRequiredClaimError,
)
from .warnings import RemovedInPyjwt3Warning
if TYPE_CHECKING:
from .algorithms import AllowedPrivateKeys, AllowedPublicKeys
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,
"require": [],
}
def encode(
self,
payload: dict[str, Any],
key: AllowedPrivateKeys | str | bytes,
algorithm: str | None = "HS256",
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 | str | bytes = "",
algorithms: list[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 | 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,
)
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,
)
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)
if options["verify_signature"] and not algorithms:
raise DecodeError(
'It is required that you pass in a value for the "algorithms" argument when calling decode().'
)
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
)
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}")
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 | str | bytes = "",
algorithms: list[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 | 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,
)
decoded = self.decode_complete(
jwt,
key,
algorithms,
options,
verify=verify,
detached_payload=detached_payload,
audience=audience,
issuer=issuer,
leeway=leeway,
)
return decoded["payload"]
def _validate_claims(
self,
payload: dict[str, Any],
options: dict[str, Any],
audience=None,
issuer=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)
)
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_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.")
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.")
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.")
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 payload["iss"] != 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

70
PyJWT/jwt/exceptions.py Normal file
View File

@ -0,0 +1,70 @@
class 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 PyJWKSetError(PyJWTError):
pass
class PyJWKClientError(PyJWTError):
pass
class PyJWKClientConnectionError(PyJWKClientError):
pass

64
PyJWT/jwt/help.py Normal file
View File

@ -0,0 +1,64 @@
import 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()

View File

@ -0,0 +1,31 @@
import 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
)

124
PyJWT/jwt/jwks_client.py Normal file
View File

@ -0,0 +1,124 @@
import 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}"'
)
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

0
PyJWT/jwt/py.typed Normal file
View File

5
PyJWT/jwt/types.py Normal file
View File

@ -0,0 +1,5 @@
from typing import Any, Callable, Dict
JWKDict = Dict[str, Any]
HashlibHash = Callable[..., Any]

156
PyJWT/jwt/utils.py Normal file
View File

@ -0,0 +1,156 @@
import base64
import binascii
import re
from typing import 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) -> bytes:
if val < 0:
raise ValueError("Must be a positive integer")
int_bytes = bytes_from_int(val)
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) -> bytes:
remaining = val
byte_length = 0
while remaining != 0:
remaining >>= 8
byte_length += 1
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
_CERT_SUFFIX = b"-cert-v01@openssh.com"
_SSH_PUBKEY_RC = re.compile(rb"\A(\S+)[ \t]+(\S+)")
_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:
if any(string_value in key for string_value in _SSH_KEY_FORMATS):
return True
ssh_pubkey_match = _SSH_PUBKEY_RC.match(key)
if ssh_pubkey_match:
key_type = ssh_pubkey_match.group(1)
if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]:
return True
return False

2
PyJWT/jwt/warnings.py Normal file
View File

@ -0,0 +1,2 @@
class RemovedInPyjwt3Warning(DeprecationWarning):
pass

34
PyJWT/pyproject.toml Normal file
View File

@ -0,0 +1,34 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.coverage.run]
parallel = true
branch = true
source = ["jwt"]
[tool.coverage.paths]
source = ["jwt", ".tox/*/site-packages"]
[tool.coverage.report]
show_missing = true
exclude_lines = ["if TYPE_CHECKING:", "pragma: no cover"]
[tool.isort]
profile = "black"
atomic = true
combine_as_imports = true
[tool.mypy]
python_version = 3.11
ignore_missing_imports = true
warn_unused_ignores = true
no_implicit_optional = true
strict = true
# TODO: remove these strict loosenings when possible
allow_incomplete_defs = true
allow_untyped_defs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_calls = false

74
PyJWT/setup.cfg Normal file
View File

@ -0,0 +1,74 @@
[metadata]
name = PyJWT
version = attr: jwt.__version__
author = Jose Padilla
author_email = hello@jpadilla.com
description = JSON Web Token implementation in Python
license = MIT
keywords =
json
jwt
security
signing
token
web
url = https://github.com/jpadilla/pyjwt
long_description = file: README.rst
long_description_content_type = text/x-rst
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
Natural Language :: English
License :: OSI Approved :: MIT License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Utilities
[options]
zip_safe = false
include_package_data = true
python_requires = >=3.7
packages = find:
install_requires =
typing_extensions; python_version<="3.7"
[options.package_data]
* = py.typed
[options.extras_require]
docs =
sphinx>=4.5.0,<5.0.0
sphinx-rtd-theme
zope.interface
crypto =
cryptography>=3.4.0
tests =
pytest>=6.0.0,<7.0.0
coverage[toml]==5.0.4
dev =
sphinx>=4.5.0,<5.0.0
sphinx-rtd-theme
zope.interface
cryptography>=3.4.0
pytest>=6.0.0,<7.0.0
coverage[toml]==5.0.4
pre-commit
[options.packages.find]
exclude =
tests
tests.*
[flake8]
extend-ignore = E203, E501
[egg_info]
tag_build =
tag_date = 0

5
PyJWT/setup.py Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env python3
from setuptools import setup
setup()

0
PyJWT/tests/__init__.py Normal file
View File

View File

@ -0,0 +1,58 @@
import 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()

View File

@ -0,0 +1,8 @@
{
"kty": "EC",
"kid": "bilbo.baggins.256@hobbiton.example",
"crv": "P-256",
"x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4",
"y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU",
"d": "9GJquUJf57a9sev-u8-PoYlIezIPqI_vGpIaiu4zyZk"
}

View File

@ -0,0 +1,8 @@
{
"kty": "EC",
"kid": "bilbo.baggins.384@hobbiton.example",
"crv": "P-384",
"x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J",
"y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy",
"d": "xKPj5IXjiHpQpLOgyMGo6lg_DUp738SuXkiugCFMxbGNKTyTprYPfJz42wTOXbtd"
}

View File

@ -0,0 +1,8 @@
{
"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"
}

View File

@ -0,0 +1,8 @@
{
"kty": "EC",
"kid": "bilbo.baggins.256k@hobbiton.example",
"crv": "secp256k1",
"x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs",
"y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI",
"d": "XV7LOlEOANIaSxyil8yE8NPDT5jmVw_HQeCwNDzochQ"
}

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"kid": "bilbo.baggins.256@hobbiton.example",
"crv": "P-256",
"x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4",
"y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU"
}

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"kid": "bilbo.baggins.384@hobbiton.example",
"crv": "P-384",
"x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J",
"y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy"
}

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"kid": "bilbo.baggins.521@hobbiton.example",
"crv": "P-521",
"x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
"y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
}

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"kid": "bilbo.baggins.256k@hobbiton.example",
"crv": "secp256k1",
"x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs",
"y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI"
}

View File

@ -0,0 +1,7 @@
{
"kty": "oct",
"kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037",
"use": "sig",
"alg": "HS256",
"k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
}

View File

@ -0,0 +1 @@
{"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"}]}

View File

@ -0,0 +1 @@
{"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"}]}

View File

@ -0,0 +1,6 @@
{
"kty":"OKP",
"crv":"Ed25519",
"d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}

View File

@ -0,0 +1,9 @@
{
"kty": "OKP",
"kid": "sig_ed448_01",
"crv": "Ed448",
"use": "sig",
"x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA",
"d": "Zh5xx0r_0tq39xj-8jGuCwAA6wsDim2ME7cX_iXzqDRgPN8lsZZHu60AO7m31Fa4NtHO07eU63q8",
"alg": "EdDSA"
}

View File

@ -0,0 +1,5 @@
{
"kty":"OKP",
"crv":"Ed25519",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}

View File

@ -0,0 +1,8 @@
{
"kty": "OKP",
"kid": "sig_ed448_01",
"crv": "Ed448",
"use": "sig",
"x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA",
"alg": "EdDSA"
}

View File

@ -0,0 +1,13 @@
{
"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"
}

View File

@ -0,0 +1,7 @@
{
"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"
}

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1tUH3/0v8fvLensHO1g2
6+U4r7jBg43DVOgqmXAWQa8ArAb4NfTrsYX8YkVhZZYwuLmKczRj0GhXUVY9iDbT
sIGmgG+ySj6eiREz5VLqofFkAvRZ6y7yNv8PIGgXEhQTiDDNIkHGaFNMvn/eZ54H
is70pdTjR5Ko+/y/wg71df1nb/5KwttSvy0YsTu/XpkduonPruYfAVRG3HK+3GZd
xTygLcdamwe9jj+kjxtXRlrXVMQiXGFSU8U6bjafWnQiQ9XzjxvygBt0ZD0kRorr
p74XGyQY5ThkN8DlpJbTTFsxOnBUAQz4zhohjobIGBRimi5yVlyLOwTlpaKGFC7O
7wIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2nninfu2jMHDwAbn
9oERUhRADS6duQaJEadybLaa0YShRANCAAQfMBxRZKUYEdy5/fLdGI2tYj6kTr50
PZPt8jOD23rAR7dhtNpG1ojqopmH0AH5wEXadgk8nLCT4cAPK59Qp9Ek
-----END PRIVATE KEY-----

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHzAcUWSlGBHcuf3y3RiNrWI+pE6+
dD2T7fIzg9t6wEe3YbTaRtaI6qKZh9AB+cBF2nYJPJywk+HADyufUKfRJA==
-----END PUBLIC KEY-----

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBiON6kYcPu8ZUDRTu8W
eXJ2FmX7e9yq0hahNAMyAARHecLjkXWDUJfZ4wiFH61JpmonCYH1GpinVlqw68Sf
wtDHg2F6SifQEFC6VKj1ZXw=
-----END PRIVATE KEY-----

View File

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB8wHFFkpRgR3Ln98t0Yja1iPqROvnQ9k+3yM4PbesBHt2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ=

View File

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIBy9N4xfv/9qOiKrxwRKeGfO5ab6lSukKHbuC5vaJ1Mg
-----END PRIVATE KEY-----

View File

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC4pK2dePGgctIAsh0H/tmUrLzx2Vc4Ltc8TN9nfuChG

View File

@ -0,0 +1,5 @@
-----BEGIN RSA PUBLIC KEY-----
MIGHAoGBAOV/0Vl/5VdHcYpnILYzBGWo5JQVzo9wBkbxzjAStcAnTwvv1ZJTMXs6
fjz91f9hiMM4Z/5qNTE/EHlDWxVdj1pyRaQulZPUs0r9qJ02ogRRGLG3jjrzzbzF
yj/pdNBwym0UJYC/Jmn/kMLwGiWI2nfa9vM5SovqZiAy2FD7eOtVAgED
-----END RSA PUBLIC KEY-----

View File

@ -0,0 +1,21 @@
-----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-----

View File

@ -0,0 +1,27 @@
-----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-----

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUeDMF8m/Zw6NCvILB7w0R6WuI5M0VYplSK969StGGWpyZkl0K+4IlGv7EI346Y7yrCAklH3x9426PFWD4n0uzf9Q4DgdwX2dgfpqpVcQsATweT8Au/GL3qazUMO4C9AZphn6UA3GW/F5AU5lJuOTP/2aHxot4YrCokunen46FQFCnC8BQA+Dzb45S2BNbedKWON4WvXgHyoCd4ZJz/CXSeL77VJFlRcVKi/NypA6SVrdbbuust+sPoteCP9OwOzi8PU+cibXNxV6cpPtN9gjXtuwb879Z5zDPY2XX2kvuAOiw2u4XeiAlY2rUvKf+KM9CeQ8ulkfKG+6LJxhwjcOJ aasmundo@mair.local

View File

@ -0,0 +1,119 @@
import 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,
)

File diff suppressed because it is too large Load Diff

298
PyJWT/tests/test_api_jwk.py Normal file
View File

@ -0,0 +1,298 @@
import json
import pytest
from jwt.algorithms import has_crypto
from jwt.api_jwk import PyJWK, PyJWKSet
from jwt.exceptions import InvalidKeyError, 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
@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
@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
@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
@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
@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
@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
@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
@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)
@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)
@crypto_required
class TestPyJWKSet:
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"
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"
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"]
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)
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"
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"

817
PyJWT/tests/test_api_jws.py Normal file
View File

@ -0,0 +1,817 @@
import json
from decimal import Decimal
import pytest
from jwt.algorithms import NoneAlgorithm, has_crypto
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_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_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"
),
}
# '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)

804
PyJWT/tests/test_api_jwt.py Normal file
View File

@ -0,0 +1,804 @@
import 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,
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: 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="urn:me".encode(), 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_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_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_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"],
)

View File

@ -0,0 +1,37 @@
import 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,"
),
}

View File

@ -0,0 +1,7 @@
from 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'

View File

@ -0,0 +1,357 @@
import 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"

19
PyJWT/tests/test_jwt.py Normal file
View File

@ -0,0 +1,19 @@
import 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

39
PyJWT/tests/test_utils.py Normal file
View File

@ -0,0 +1,39 @@
import pytest
from jwt.utils import force_bytes, from_base64url_uint, to_base64url_uint
@pytest.mark.parametrize(
"inputval,expected",
[
(0, b"AA"),
(1, b"AQ"),
(255, b"_w"),
(65537, b"AQAB"),
(123456789, b"B1vNFQ"),
pytest.param(-1, "", marks=pytest.mark.xfail(raises=ValueError)),
],
)
def test_to_base64url_uint(inputval, expected):
actual = to_base64url_uint(inputval)
assert actual == expected
@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]

30
PyJWT/tests/utils.py Normal file
View File

@ -0,0 +1,30 @@
import 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)

75
PyJWT/tox.ini Normal file
View File

@ -0,0 +1,75 @@
[pytest]
addopts = -ra
testpaths = tests
filterwarnings =
once::Warning
ignore:::pympler[.*]
[gh-actions]
python =
3.7: py37, docs
3.8: py38, typing
3.9: py39
3.10: py310
3.11: py311
pypy-3.8: pypy3
pypy-3.9: pypy3
[tox]
envlist =
lint
typing
py{37,38,39,310,311,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]
basepython = python3.7
extras = docs
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
[testenv:lint]
basepython = python3.8
extras = dev
passenv = HOMEPATH # needed on Windows
commands = pre-commit run --all-files
[testenv:pypi-description]
basepython = python3.8
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.8
skip_install = true
deps = coverage[toml]==5.0.4
commands =
coverage combine
coverage report

66
paho_mqtt/.gitignore vendored Normal file
View File

@ -0,0 +1,66 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
/env/
/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
*.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
*,cover
.hypothesis/
tests/ssl/demoCA
tests/ssl/rootCA
tests/ssl/signingCA
*.csr
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
paho.mqtt.testing

114
paho_mqtt/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,114 @@
# Contributing to Paho
Thanks for your interest in this project!
You can contribute bugfixes and new features by sending pull requests through GitHub.
## Legal
In order for your contribution to be accepted, it must comply with the Eclipse Foundation IP policy.
Please read the [Eclipse Foundation policy on accepting contributions via Git](http://wiki.eclipse.org/Development_Resources/Contributing_via_Git).
1. Sign the [Eclipse CLA](http://www.eclipse.org/legal/CLA.php)
1. Register for an Eclipse Foundation User ID. You can register [here](https://dev.eclipse.org/site_login/createaccount.php).
2. Log into the [Projects Portal](https://projects.eclipse.org/), and click on the '[Eclipse CLA](https://projects.eclipse.org/user/sign/cla)' link.
2. Go to your [account settings](https://dev.eclipse.org/site_login/myaccount.php#open_tab_accountsettings) and add your GitHub username to your account.
3. Make sure that you _sign-off_ your Git commits in the following format:
``` Signed-off-by: John Smith <johnsmith@nowhere.com> ``` This is usually at the bottom of the commit message. You can automate this by adding the '-s' flag when you make the commits. e.g. ```git commit -s -m "Adding a cool feature"```
4. Ensure that the email address that you make your commits with is the same one you used to sign up to the Eclipse Foundation website with.
## Contributing a change
1. [Fork the repository on GitHub](https://github.com/eclipse/paho.mqtt.python/fork)
2. Clone the forked repository onto your computer: ``` git clone
https://github.com/<your username>/paho.mqtt.python.git ```
3. Most changes will go to branch ``master``. This include both bug fixes and
new features. Bug fixes are committed to ``master`` and if required,
cherry-picked to the release branch.
The only changes that goes directly to the release branch (``1.4``,
``1.5``, ...) are bug fixes that does not apply to ``master`` (e.g. because
there are fixed on master by a refactoring, or any other huge change we do
not want to cherry-pick to the release branch).
4. Create a new branch from the latest ```master``` branch
with ```git checkout -b YOUR_BRANCH_NAME origin/master```
5. Make your changes
6. Ensure that all new and existing tests pass by running ```tox```
7. Commit the changes into the branch: ``` git commit -s ``` Make sure that
your commit message is meaningful and describes your changes correctly.
8. If you have a lot of commits for the change, squash them into a single / few
commits.
9. Push the changes in your branch to your forked repository.
10. Finally, go to
[https://github.com/eclipse/paho.mqtt.python](https://github.com/eclipse/paho.mqtt.python)
and create a pull request from your "YOUR_BRANCH_NAME" branch to the
``master`` (or release branch if applicable) to request review and
merge of the commits in your pushed branch.
What happens next depends on the content of the patch. If it is 100% authored
by the contributor and is less than 1000 lines (and meets the needs of the
project), then it can be pulled into the main repository. If not, more steps
are required. These are detailed in the
[legal process poster](http://www.eclipse.org/legal/EclipseLegalProcessPoster.pdf).
## Developer resources:
Information regarding source code management, builds, coding standards, and
more.
- [https://projects.eclipse.org/projects/iot.paho/developer](https://projects.eclipse.org/projects/iot.paho/developer)
Contact:
--------
Contact the project developers via the project's development
[mailing list](https://dev.eclipse.org/mailman/listinfo/paho-dev).
Search for bugs:
----------------
This project uses [Github](https://github.com/eclipse/paho.mqtt.python/issues)
to track ongoing development and issues.
Create a new bug:
-----------------
Be sure to search for existing bugs before you create another one. Remember
that contributions are always welcome!
- [Create new Paho bug](https://github.com/eclipse/paho.mqtt.python/issues)
## Committer resources:
Making a release
----------------
The process to make a release is the following:
* Update the Changelog with the release version and date. Ensure it's up-to-date with latest fixes & PRs merged.
* Make sure test pass, check that Github actions are green.
* Check that documentation build (`cd docs; make html`)
* Bump the version number in ``paho/mqtt/__init__.py``, commit the change.
* Make a dry-run of build:
* Build using hatch: ``python -m hatch build``
* Check with twine for common errors: ``python -m twine check dist/*``
* Try uploading it to testpypi: ``python3 -m twine upload --repository testpypi dist/*``
* Do a GPG signed tag (assuming your GPG is correctly configured, it's ``git tag -s -m "Version 1.2.3" v1.2.3``)
* Push the commit and it's tag to Github
* Make sure your git is clean, especially the ``dist/`` folder.
* Build a release: ``python -m hatch build``
* You can also get the latest build from Github action. It should be identical to your local build:
https://github.com/eclipse/paho.mqtt.python/actions/workflows/build.yml?query=branch%3Amaster
* Then upload the dist file, you can follow instruction on https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives
It should mostly be ``python -m twine upload dist/*``
* Create a release on Github, copy-pasting the release note from Changelog.
* Build and publish the documentation
* To build the documentation, run `make clean html` in `docs` folder
* Copy `_build/html/` to https://github.com/eclipse/paho-website/tree/master/files/paho.mqtt.python/html
* Announce the release on the Mailing list.
* To allow installing from a git clone, update the version in ``paho/mqtt/__init__.py`` to next number WITH .dev0 (example ``1.2.3.dev0``)

3
paho_mqtt/LICENSE.txt Normal file
View File

@ -0,0 +1,3 @@
This project is dual licensed under the Eclipse Public License 2.0 and the
Eclipse Distribution License 1.0 as described in the epl-v20 and edl-v10 files.

635
paho_mqtt/PKG-INFO Normal file
View File

@ -0,0 +1,635 @@
Metadata-Version: 2.3
Name: paho-mqtt
Version: 2.1.0
Summary: MQTT version 5.0/3.1.1 client class
Project-URL: Homepage, http://eclipse.org/paho
Author-email: Roger Light <roger@atchoo.org>
License: EPL-2.0 OR BSD-3-Clause
License-File: LICENSE.txt
Keywords: paho
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved
Classifier: Natural Language :: English
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Topic :: Communications
Classifier: Topic :: Internet
Requires-Python: >=3.7
Provides-Extra: proxy
Requires-Dist: pysocks; extra == 'proxy'
Description-Content-Type: text/x-rst
Eclipse Paho™ MQTT Python Client
================================
The `full documentation is available here <documentation_>`_.
**Warning breaking change** - Release 2.0 contains a breaking change; see the `release notes <https://github.com/eclipse/paho.mqtt.python/releases/tag/v2.0.0>`_ and `migration details <https://eclipse.dev/paho/files/paho.mqtt.python/html/migrations.html>`_.
This document describes the source code for the `Eclipse Paho <http://eclipse.org/paho/>`_ MQTT Python client library, which implements versions 5.0, 3.1.1, and 3.1 of the MQTT protocol.
This code provides a client class which enables applications to connect to an `MQTT <http://mqtt.org/>`_ broker to publish messages, and to subscribe to topics and receive published messages. It also provides some helper functions to make publishing one off messages to an MQTT server very straightforward.
It supports Python 3.7+.
The MQTT protocol is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. Designed as an extremely lightweight publish/subscribe messaging transport, it is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium.
Paho is an `Eclipse Foundation <https://www.eclipse.org/org/foundation/>`_ project.
Contents
--------
* Installation_
* `Known limitations`_
* `Usage and API`_
* `Getting Started`_
* `Client`_
* `Network loop`_
* `Callbacks`_
* `Logger`_
* `External event loop support`_
* `Global helper functions`_
* `Publish`_
* `Single`_
* `Multiple`_
* `Subscribe`_
* `Simple`_
* `Using Callback`_
* `Reporting bugs`_
* `More information`_
Installation
------------
The latest stable version is available in the Python Package Index (PyPi) and can be installed using
::
pip install paho-mqtt
Or with ``virtualenv``:
::
virtualenv paho-mqtt
source paho-mqtt/bin/activate
pip install paho-mqtt
To obtain the full code, including examples and tests, you can clone the git repository:
::
git clone https://github.com/eclipse/paho.mqtt.python
Once you have the code, it can be installed from your repository as well:
::
cd paho.mqtt.python
pip install -e .
To perform all tests (including MQTT v5 tests), you also need to clone paho.mqtt.testing in paho.mqtt.python folder::
git clone https://github.com/eclipse/paho.mqtt.testing.git
cd paho.mqtt.testing
git checkout a4dc694010217b291ee78ee13a6d1db812f9babd
Known limitations
-----------------
The following are the known unimplemented MQTT features.
When ``clean_session`` is False, the session is only stored in memory and not persisted. This means that
when the client is restarted (not just reconnected, the object is recreated usually because the
program was restarted) the session is lost. This results in a possible message loss.
The following part of the client session is lost:
* QoS 2 messages which have been received from the server, but have not been completely acknowledged.
Since the client will blindly acknowledge any PUBCOMP (last message of a QoS 2 transaction), it
won't hang but will lose this QoS 2 message.
* QoS 1 and QoS 2 messages which have been sent to the server, but have not been completely acknowledged.
This means that messages passed to ``publish()`` may be lost. This could be mitigated by taking care
that all messages passed to ``publish()`` have a corresponding ``on_publish()`` call or use `wait_for_publish`.
It also means that the broker may have the QoS2 message in the session. Since the client starts
with an empty session it don't know it and will reuse the mid. This is not yet fixed.
Also, when ``clean_session`` is True, this library will republish QoS > 0 message across network
reconnection. This means that QoS > 0 message won't be lost. But the standard says that
we should discard any message for which the publish packet was sent. Our choice means that
we are not compliant with the standard and it's possible for QoS 2 to be received twice.
You should set ``clean_session = False`` if you need the QoS 2 guarantee of only one delivery.
Usage and API
-------------
Detailed API documentation `is available online <documentation_>`_ or could be built from ``docs/`` and samples are available in the `examples`_ directory.
The package provides two modules, a full `Client` and few `helpers` for simple publishing or subscribing.
Getting Started
***************
Here is a very simple example that subscribes to the broker $SYS topic tree and prints out the resulting messages:
.. code:: python
import paho.mqtt.client as mqtt
# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, reason_code, properties):
print(f"Connected with result code {reason_code}")
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe("$SYS/#")
# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
print(msg.topic+" "+str(msg.payload))
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_connect = on_connect
mqttc.on_message = on_message
mqttc.connect("mqtt.eclipseprojects.io", 1883, 60)
# Blocking call that processes network traffic, dispatches callbacks and
# handles reconnecting.
# Other loop*() functions are available that give a threaded interface and a
# manual interface.
mqttc.loop_forever()
Client
******
You can use the client class as an instance, within a class or by subclassing. The general usage flow is as follows:
* Create a client instance
* Connect to a broker using one of the ``connect*()`` functions
* Call one of the ``loop*()`` functions to maintain network traffic flow with the broker
* Use ``subscribe()`` to subscribe to a topic and receive messages
* Use ``publish()`` to publish messages to the broker
* Use ``disconnect()`` to disconnect from the broker
Callbacks will be called to allow the application to process events as necessary. These callbacks are described below.
Network loop
````````````
These functions are the driving force behind the client. If they are not
called, incoming network data will not be processed and outgoing network data
will not be sent. There are four options for managing the
network loop. Three are described here, the fourth in "External event loop
support" below. Do not mix the different loop functions.
loop_start() / loop_stop()
''''''''''''''''''''''''''
.. code:: python
mqttc.loop_start()
while True:
temperature = sensor.blocking_read()
mqttc.publish("paho/temperature", temperature)
mqttc.loop_stop()
These functions implement a threaded interface to the network loop. Calling
`loop_start()` once, before or after ``connect*()``, runs a thread in the
background to call `loop()` automatically. This frees up the main thread for
other work that may be blocking. This call also handles reconnecting to the
broker. Call `loop_stop()` to stop the background thread.
The loop is also stopped if you call `disconnect()`.
loop_forever()
''''''''''''''
.. code:: python
mqttc.loop_forever(retry_first_connection=False)
This is a blocking form of the network loop and will not return until the
client calls `disconnect()`. It automatically handles reconnecting.
Except for the first connection attempt when using `connect_async`, use
``retry_first_connection=True`` to make it retry the first connection.
*Warning*: This might lead to situations where the client keeps connecting to an
non existing host without failing.
loop()
''''''
.. code:: python
run = True
while run:
rc = mqttc.loop(timeout=1.0)
if rc != 0:
# need to handle error, possible reconnecting or stopping the application
Call regularly to process network events. This call waits in ``select()`` until
the network socket is available for reading or writing, if appropriate, then
handles the incoming/outgoing data. This function blocks for up to ``timeout``
seconds. ``timeout`` must not exceed the ``keepalive`` value for the client or
your client will be regularly disconnected by the broker.
Using this kind of loop, require you to handle reconnection strategie.
Callbacks
`````````
The interface to interact with paho-mqtt include various callback that are called by
the library when some events occur.
The callbacks are functions defined in your code, to implement the require action on those events. This could
be simply printing received message or much more complex behaviour.
Callbacks API is versioned, and the selected version is the `CallbackAPIVersion` you provided to `Client`
constructor. Currently two version are supported:
* ``CallbackAPIVersion.VERSION1``: it's the historical version used in paho-mqtt before version 2.0.
It's the API used before the introduction of `CallbackAPIVersion`.
This version is deprecated and will be removed in paho-mqtt version 3.0.
* ``CallbackAPIVersion.VERSION2``: This version is more consistent between protocol MQTT 3.x and MQTT 5.x. It's also
much more usable with MQTT 5.x since reason code and properties are always provided when available.
It's recommended for all user to upgrade to this version. It's highly recommended for MQTT 5.x user.
The following callbacks exists:
* `on_connect()`: called when the CONNACK from the broker is received. The call could be for a refused connection,
check the reason_code to see if the connection is successful or rejected.
* `on_connect_fail()`: called by `loop_forever()` and `loop_start()` when the TCP connection failed to establish.
This callback is not called when using `connect()` or `reconnect()` directly. It's only called following
an automatic (re)connection made by `loop_start()` and `loop_forever()`
* `on_disconnect()`: called when the connection is closed.
* `on_message()`: called when a MQTT message is received from the broker.
* `on_publish()`: called when an MQTT message was sent to the broker. Depending on QoS level the callback is called
at different moment:
* For QoS == 0, it's called as soon as the message is sent over the network. This could be before the corresponding ``publish()`` return.
* For QoS == 1, it's called when the corresponding PUBACK is received from the broker
* For QoS == 2, it's called when the corresponding PUBCOMP is received from the broker
* `on_subscribe()`: called when the SUBACK is received from the broker
* `on_unsubscribe()`: called when the UNSUBACK is received from the broker
* `on_log()`: called when the library log a message
* `on_socket_open`, `on_socket_close`, `on_socket_register_write`, `on_socket_unregister_write`: callbacks used for external loop support. See below for details.
For the signature of each callback, see the `online documentation <documentation_>`_.
Subscriber example
''''''''''''''''''
.. code:: python
import paho.mqtt.client as mqtt
def on_subscribe(client, userdata, mid, reason_code_list, properties):
# Since we subscribed only for a single channel, reason_code_list contains
# a single entry
if reason_code_list[0].is_failure:
print(f"Broker rejected you subscription: {reason_code_list[0]}")
else:
print(f"Broker granted the following QoS: {reason_code_list[0].value}")
def on_unsubscribe(client, userdata, mid, reason_code_list, properties):
# Be careful, the reason_code_list is only present in MQTTv5.
# In MQTTv3 it will always be empty
if len(reason_code_list) == 0 or not reason_code_list[0].is_failure:
print("unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)")
else:
print(f"Broker replied with failure: {reason_code_list[0]}")
client.disconnect()
def on_message(client, userdata, message):
# userdata is the structure we choose to provide, here it's a list()
userdata.append(message.payload)
# We only want to process 10 messages
if len(userdata) >= 10:
client.unsubscribe("$SYS/#")
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Failed to connect: {reason_code}. loop_forever() will retry connection")
else:
# we should always subscribe from on_connect callback to be sure
# our subscribed is persisted across reconnections.
client.subscribe("$SYS/#")
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_connect = on_connect
mqttc.on_message = on_message
mqttc.on_subscribe = on_subscribe
mqttc.on_unsubscribe = on_unsubscribe
mqttc.user_data_set([])
mqttc.connect("mqtt.eclipseprojects.io")
mqttc.loop_forever()
print(f"Received the following message: {mqttc.user_data_get()}")
publisher example
'''''''''''''''''
.. code:: python
import time
import paho.mqtt.client as mqtt
def on_publish(client, userdata, mid, reason_code, properties):
# reason_code and properties will only be present in MQTTv5. It's always unset in MQTTv3
try:
userdata.remove(mid)
except KeyError:
print("on_publish() is called with a mid not present in unacked_publish")
print("This is due to an unavoidable race-condition:")
print("* publish() return the mid of the message sent.")
print("* mid from publish() is added to unacked_publish by the main thread")
print("* on_publish() is called by the loop_start thread")
print("While unlikely (because on_publish() will be called after a network round-trip),")
print(" this is a race-condition that COULD happen")
print("")
print("The best solution to avoid race-condition is using the msg_info from publish()")
print("We could also try using a list of acknowledged mid rather than removing from pending list,")
print("but remember that mid could be re-used !")
unacked_publish = set()
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_publish = on_publish
mqttc.user_data_set(unacked_publish)
mqttc.connect("mqtt.eclipseprojects.io")
mqttc.loop_start()
# Our application produce some messages
msg_info = mqttc.publish("paho/test/topic", "my message", qos=1)
unacked_publish.add(msg_info.mid)
msg_info2 = mqttc.publish("paho/test/topic", "my message2", qos=1)
unacked_publish.add(msg_info2.mid)
# Wait for all message to be published
while len(unacked_publish):
time.sleep(0.1)
# Due to race-condition described above, the following way to wait for all publish is safer
msg_info.wait_for_publish()
msg_info2.wait_for_publish()
mqttc.disconnect()
mqttc.loop_stop()
Logger
``````
The Client emit some log message that could be useful during troubleshooting. The easiest way to
enable logs is the call `enable_logger()`. It's possible to provide a custom logger or let the
default logger being used.
Example:
.. code:: python
import logging
import paho.mqtt.client as mqtt
logging.basicConfig(level=logging.DEBUG)
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.enable_logger()
mqttc.connect("mqtt.eclipseprojects.io", 1883, 60)
mqttc.loop_start()
# Do additional action needed, publish, subscribe, ...
[...]
It's also possible to define a on_log callback that will receive a copy of all log messages. Example:
.. code:: python
import paho.mqtt.client as mqtt
def on_log(client, userdata, paho_log_level, messages):
if paho_log_level == mqtt.LogLevel.MQTT_LOG_ERR:
print(message)
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_log = on_log
mqttc.connect("mqtt.eclipseprojects.io", 1883, 60)
mqttc.loop_start()
# Do additional action needed, publish, subscribe, ...
[...]
The correspondence with Paho logging levels and standard ones is the following:
==================== ===============
Paho logging
==================== ===============
``MQTT_LOG_ERR`` ``logging.ERROR``
``MQTT_LOG_WARNING`` ``logging.WARNING``
``MQTT_LOG_NOTICE`` ``logging.INFO`` *(no direct equivalent)*
``MQTT_LOG_INFO`` ``logging.INFO``
``MQTT_LOG_DEBUG`` ``logging.DEBUG``
==================== ===============
External event loop support
```````````````````````````
To support other network loop like asyncio (see examples_), the library expose some
method and callback to support those use-case.
The following loop method exists:
* `loop_read`: should be called when the socket is ready for reading.
* `loop_write`: should be called when the socket is ready for writing AND the library want to write data.
* `loop_misc`: should be called every few seconds to handle message retrying and pings.
In pseudo code, it give the following:
.. code:: python
while run:
if need_read:
mqttc.loop_read()
if need_write:
mqttc.loop_write()
mqttc.loop_misc()
if not need_read and not need_write:
# But don't wait more than few seconds, loop_misc() need to be called regularly
wait_for_change_in_need_read_or_write()
updated_need_read_and_write()
The tricky part is implementing the update of need_read / need_write and wait for condition change. To support
this, the following method exists:
* `socket()`: which return the socket object when the TCP connection is open.
This call is particularly useful for select_ based loops. See ``examples/loop_select.py``.
* `want_write()`: return true if there is data waiting to be written. This is close to the
``need_writew`` of above pseudo-code, but you should also check whether the socket is ready for writing.
* callbacks ``on_socket_*``:
* `on_socket_open`: called when the socket is opened.
* `on_socket_close`: called when the socket is about to be closed.
* `on_socket_register_write`: called when there is data the client want to write on the socket
* `on_socket_unregister_write`: called when there is no more data to write on the socket.
Callbacks are particularly useful for event loops where you register or unregister a socket
for reading+writing. See ``examples/loop_asyncio.py`` for an example.
.. _select: https://docs.python.org/3/library/select.html#select.select
The callbacks are always called in this order:
- `on_socket_open`
- Zero or more times:
- `on_socket_register_write`
- `on_socket_unregister_write`
- `on_socket_close`
Global helper functions
```````````````````````
The client module also offers some global helper functions.
``topic_matches_sub(sub, topic)`` can be used to check whether a ``topic``
matches a ``subscription``.
For example:
the topic ``foo/bar`` would match the subscription ``foo/#`` or ``+/bar``
the topic ``non/matching`` would not match the subscription ``non/+/+``
Publish
*******
This module provides some helper functions to allow straightforward publishing
of messages in a one-shot manner. In other words, they are useful for the
situation where you have a single/multiple messages you want to publish to a
broker, then disconnect with nothing else required.
The two functions provided are `single()` and `multiple()`.
Both functions include support for MQTT v5.0, but do not currently let you
set any properties on connection or when sending messages.
Single
``````
Publish a single message to a broker, then disconnect cleanly.
Example:
.. code:: python
import paho.mqtt.publish as publish
publish.single("paho/test/topic", "payload", hostname="mqtt.eclipseprojects.io")
Multiple
````````
Publish multiple messages to a broker, then disconnect cleanly.
Example:
.. code:: python
from paho.mqtt.enums import MQTTProtocolVersion
import paho.mqtt.publish as publish
msgs = [{'topic':"paho/test/topic", 'payload':"multiple 1"},
("paho/test/topic", "multiple 2", 0, False)]
publish.multiple(msgs, hostname="mqtt.eclipseprojects.io", protocol=MQTTProtocolVersion.MQTTv5)
Subscribe
*********
This module provides some helper functions to allow straightforward subscribing
and processing of messages.
The two functions provided are `simple()` and `callback()`.
Both functions include support for MQTT v5.0, but do not currently let you
set any properties on connection or when subscribing.
Simple
``````
Subscribe to a set of topics and return the messages received. This is a
blocking function.
Example:
.. code:: python
import paho.mqtt.subscribe as subscribe
msg = subscribe.simple("paho/test/topic", hostname="mqtt.eclipseprojects.io")
print("%s %s" % (msg.topic, msg.payload))
Using Callback
``````````````
Subscribe to a set of topics and process the messages received using a user
provided callback.
Example:
.. code:: python
import paho.mqtt.subscribe as subscribe
def on_message_print(client, userdata, message):
print("%s %s" % (message.topic, message.payload))
userdata["message_count"] += 1
if userdata["message_count"] >= 5:
# it's possible to stop the program by disconnecting
client.disconnect()
subscribe.callback(on_message_print, "paho/test/topic", hostname="mqtt.eclipseprojects.io", userdata={"message_count": 0})
Reporting bugs
--------------
Please report bugs in the issues tracker at https://github.com/eclipse/paho.mqtt.python/issues.
More information
----------------
Discussion of the Paho clients takes place on the `Eclipse paho-dev mailing list <https://dev.eclipse.org/mailman/listinfo/paho-dev>`_.
General questions about the MQTT protocol itself (not this library) are discussed in the `MQTT Google Group <https://groups.google.com/forum/?fromgroups#!forum/mqtt>`_.
There is much more information available via the `MQTT community site <http://mqtt.org/>`_.
.. _examples: https://github.com/eclipse/paho.mqtt.python/tree/master/examples
.. _documentation: https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html

606
paho_mqtt/README.rst Normal file
View File

@ -0,0 +1,606 @@
Eclipse Paho™ MQTT Python Client
================================
The `full documentation is available here <documentation_>`_.
**Warning breaking change** - Release 2.0 contains a breaking change; see the `release notes <https://github.com/eclipse/paho.mqtt.python/releases/tag/v2.0.0>`_ and `migration details <https://eclipse.dev/paho/files/paho.mqtt.python/html/migrations.html>`_.
This document describes the source code for the `Eclipse Paho <http://eclipse.org/paho/>`_ MQTT Python client library, which implements versions 5.0, 3.1.1, and 3.1 of the MQTT protocol.
This code provides a client class which enables applications to connect to an `MQTT <http://mqtt.org/>`_ broker to publish messages, and to subscribe to topics and receive published messages. It also provides some helper functions to make publishing one off messages to an MQTT server very straightforward.
It supports Python 3.7+.
The MQTT protocol is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. Designed as an extremely lightweight publish/subscribe messaging transport, it is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium.
Paho is an `Eclipse Foundation <https://www.eclipse.org/org/foundation/>`_ project.
Contents
--------
* Installation_
* `Known limitations`_
* `Usage and API`_
* `Getting Started`_
* `Client`_
* `Network loop`_
* `Callbacks`_
* `Logger`_
* `External event loop support`_
* `Global helper functions`_
* `Publish`_
* `Single`_
* `Multiple`_
* `Subscribe`_
* `Simple`_
* `Using Callback`_
* `Reporting bugs`_
* `More information`_
Installation
------------
The latest stable version is available in the Python Package Index (PyPi) and can be installed using
::
pip install paho-mqtt
Or with ``virtualenv``:
::
virtualenv paho-mqtt
source paho-mqtt/bin/activate
pip install paho-mqtt
To obtain the full code, including examples and tests, you can clone the git repository:
::
git clone https://github.com/eclipse/paho.mqtt.python
Once you have the code, it can be installed from your repository as well:
::
cd paho.mqtt.python
pip install -e .
To perform all tests (including MQTT v5 tests), you also need to clone paho.mqtt.testing in paho.mqtt.python folder::
git clone https://github.com/eclipse/paho.mqtt.testing.git
cd paho.mqtt.testing
git checkout a4dc694010217b291ee78ee13a6d1db812f9babd
Known limitations
-----------------
The following are the known unimplemented MQTT features.
When ``clean_session`` is False, the session is only stored in memory and not persisted. This means that
when the client is restarted (not just reconnected, the object is recreated usually because the
program was restarted) the session is lost. This results in a possible message loss.
The following part of the client session is lost:
* QoS 2 messages which have been received from the server, but have not been completely acknowledged.
Since the client will blindly acknowledge any PUBCOMP (last message of a QoS 2 transaction), it
won't hang but will lose this QoS 2 message.
* QoS 1 and QoS 2 messages which have been sent to the server, but have not been completely acknowledged.
This means that messages passed to ``publish()`` may be lost. This could be mitigated by taking care
that all messages passed to ``publish()`` have a corresponding ``on_publish()`` call or use `wait_for_publish`.
It also means that the broker may have the QoS2 message in the session. Since the client starts
with an empty session it don't know it and will reuse the mid. This is not yet fixed.
Also, when ``clean_session`` is True, this library will republish QoS > 0 message across network
reconnection. This means that QoS > 0 message won't be lost. But the standard says that
we should discard any message for which the publish packet was sent. Our choice means that
we are not compliant with the standard and it's possible for QoS 2 to be received twice.
You should set ``clean_session = False`` if you need the QoS 2 guarantee of only one delivery.
Usage and API
-------------
Detailed API documentation `is available online <documentation_>`_ or could be built from ``docs/`` and samples are available in the `examples`_ directory.
The package provides two modules, a full `Client` and few `helpers` for simple publishing or subscribing.
Getting Started
***************
Here is a very simple example that subscribes to the broker $SYS topic tree and prints out the resulting messages:
.. code:: python
import paho.mqtt.client as mqtt
# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, reason_code, properties):
print(f"Connected with result code {reason_code}")
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe("$SYS/#")
# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
print(msg.topic+" "+str(msg.payload))
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_connect = on_connect
mqttc.on_message = on_message
mqttc.connect("mqtt.eclipseprojects.io", 1883, 60)
# Blocking call that processes network traffic, dispatches callbacks and
# handles reconnecting.
# Other loop*() functions are available that give a threaded interface and a
# manual interface.
mqttc.loop_forever()
Client
******
You can use the client class as an instance, within a class or by subclassing. The general usage flow is as follows:
* Create a client instance
* Connect to a broker using one of the ``connect*()`` functions
* Call one of the ``loop*()`` functions to maintain network traffic flow with the broker
* Use ``subscribe()`` to subscribe to a topic and receive messages
* Use ``publish()`` to publish messages to the broker
* Use ``disconnect()`` to disconnect from the broker
Callbacks will be called to allow the application to process events as necessary. These callbacks are described below.
Network loop
````````````
These functions are the driving force behind the client. If they are not
called, incoming network data will not be processed and outgoing network data
will not be sent. There are four options for managing the
network loop. Three are described here, the fourth in "External event loop
support" below. Do not mix the different loop functions.
loop_start() / loop_stop()
''''''''''''''''''''''''''
.. code:: python
mqttc.loop_start()
while True:
temperature = sensor.blocking_read()
mqttc.publish("paho/temperature", temperature)
mqttc.loop_stop()
These functions implement a threaded interface to the network loop. Calling
`loop_start()` once, before or after ``connect*()``, runs a thread in the
background to call `loop()` automatically. This frees up the main thread for
other work that may be blocking. This call also handles reconnecting to the
broker. Call `loop_stop()` to stop the background thread.
The loop is also stopped if you call `disconnect()`.
loop_forever()
''''''''''''''
.. code:: python
mqttc.loop_forever(retry_first_connection=False)
This is a blocking form of the network loop and will not return until the
client calls `disconnect()`. It automatically handles reconnecting.
Except for the first connection attempt when using `connect_async`, use
``retry_first_connection=True`` to make it retry the first connection.
*Warning*: This might lead to situations where the client keeps connecting to an
non existing host without failing.
loop()
''''''
.. code:: python
run = True
while run:
rc = mqttc.loop(timeout=1.0)
if rc != 0:
# need to handle error, possible reconnecting or stopping the application
Call regularly to process network events. This call waits in ``select()`` until
the network socket is available for reading or writing, if appropriate, then
handles the incoming/outgoing data. This function blocks for up to ``timeout``
seconds. ``timeout`` must not exceed the ``keepalive`` value for the client or
your client will be regularly disconnected by the broker.
Using this kind of loop, require you to handle reconnection strategie.
Callbacks
`````````
The interface to interact with paho-mqtt include various callback that are called by
the library when some events occur.
The callbacks are functions defined in your code, to implement the require action on those events. This could
be simply printing received message or much more complex behaviour.
Callbacks API is versioned, and the selected version is the `CallbackAPIVersion` you provided to `Client`
constructor. Currently two version are supported:
* ``CallbackAPIVersion.VERSION1``: it's the historical version used in paho-mqtt before version 2.0.
It's the API used before the introduction of `CallbackAPIVersion`.
This version is deprecated and will be removed in paho-mqtt version 3.0.
* ``CallbackAPIVersion.VERSION2``: This version is more consistent between protocol MQTT 3.x and MQTT 5.x. It's also
much more usable with MQTT 5.x since reason code and properties are always provided when available.
It's recommended for all user to upgrade to this version. It's highly recommended for MQTT 5.x user.
The following callbacks exists:
* `on_connect()`: called when the CONNACK from the broker is received. The call could be for a refused connection,
check the reason_code to see if the connection is successful or rejected.
* `on_connect_fail()`: called by `loop_forever()` and `loop_start()` when the TCP connection failed to establish.
This callback is not called when using `connect()` or `reconnect()` directly. It's only called following
an automatic (re)connection made by `loop_start()` and `loop_forever()`
* `on_disconnect()`: called when the connection is closed.
* `on_message()`: called when a MQTT message is received from the broker.
* `on_publish()`: called when an MQTT message was sent to the broker. Depending on QoS level the callback is called
at different moment:
* For QoS == 0, it's called as soon as the message is sent over the network. This could be before the corresponding ``publish()`` return.
* For QoS == 1, it's called when the corresponding PUBACK is received from the broker
* For QoS == 2, it's called when the corresponding PUBCOMP is received from the broker
* `on_subscribe()`: called when the SUBACK is received from the broker
* `on_unsubscribe()`: called when the UNSUBACK is received from the broker
* `on_log()`: called when the library log a message
* `on_socket_open`, `on_socket_close`, `on_socket_register_write`, `on_socket_unregister_write`: callbacks used for external loop support. See below for details.
For the signature of each callback, see the `online documentation <documentation_>`_.
Subscriber example
''''''''''''''''''
.. code:: python
import paho.mqtt.client as mqtt
def on_subscribe(client, userdata, mid, reason_code_list, properties):
# Since we subscribed only for a single channel, reason_code_list contains
# a single entry
if reason_code_list[0].is_failure:
print(f"Broker rejected you subscription: {reason_code_list[0]}")
else:
print(f"Broker granted the following QoS: {reason_code_list[0].value}")
def on_unsubscribe(client, userdata, mid, reason_code_list, properties):
# Be careful, the reason_code_list is only present in MQTTv5.
# In MQTTv3 it will always be empty
if len(reason_code_list) == 0 or not reason_code_list[0].is_failure:
print("unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)")
else:
print(f"Broker replied with failure: {reason_code_list[0]}")
client.disconnect()
def on_message(client, userdata, message):
# userdata is the structure we choose to provide, here it's a list()
userdata.append(message.payload)
# We only want to process 10 messages
if len(userdata) >= 10:
client.unsubscribe("$SYS/#")
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Failed to connect: {reason_code}. loop_forever() will retry connection")
else:
# we should always subscribe from on_connect callback to be sure
# our subscribed is persisted across reconnections.
client.subscribe("$SYS/#")
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_connect = on_connect
mqttc.on_message = on_message
mqttc.on_subscribe = on_subscribe
mqttc.on_unsubscribe = on_unsubscribe
mqttc.user_data_set([])
mqttc.connect("mqtt.eclipseprojects.io")
mqttc.loop_forever()
print(f"Received the following message: {mqttc.user_data_get()}")
publisher example
'''''''''''''''''
.. code:: python
import time
import paho.mqtt.client as mqtt
def on_publish(client, userdata, mid, reason_code, properties):
# reason_code and properties will only be present in MQTTv5. It's always unset in MQTTv3
try:
userdata.remove(mid)
except KeyError:
print("on_publish() is called with a mid not present in unacked_publish")
print("This is due to an unavoidable race-condition:")
print("* publish() return the mid of the message sent.")
print("* mid from publish() is added to unacked_publish by the main thread")
print("* on_publish() is called by the loop_start thread")
print("While unlikely (because on_publish() will be called after a network round-trip),")
print(" this is a race-condition that COULD happen")
print("")
print("The best solution to avoid race-condition is using the msg_info from publish()")
print("We could also try using a list of acknowledged mid rather than removing from pending list,")
print("but remember that mid could be re-used !")
unacked_publish = set()
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_publish = on_publish
mqttc.user_data_set(unacked_publish)
mqttc.connect("mqtt.eclipseprojects.io")
mqttc.loop_start()
# Our application produce some messages
msg_info = mqttc.publish("paho/test/topic", "my message", qos=1)
unacked_publish.add(msg_info.mid)
msg_info2 = mqttc.publish("paho/test/topic", "my message2", qos=1)
unacked_publish.add(msg_info2.mid)
# Wait for all message to be published
while len(unacked_publish):
time.sleep(0.1)
# Due to race-condition described above, the following way to wait for all publish is safer
msg_info.wait_for_publish()
msg_info2.wait_for_publish()
mqttc.disconnect()
mqttc.loop_stop()
Logger
``````
The Client emit some log message that could be useful during troubleshooting. The easiest way to
enable logs is the call `enable_logger()`. It's possible to provide a custom logger or let the
default logger being used.
Example:
.. code:: python
import logging
import paho.mqtt.client as mqtt
logging.basicConfig(level=logging.DEBUG)
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.enable_logger()
mqttc.connect("mqtt.eclipseprojects.io", 1883, 60)
mqttc.loop_start()
# Do additional action needed, publish, subscribe, ...
[...]
It's also possible to define a on_log callback that will receive a copy of all log messages. Example:
.. code:: python
import paho.mqtt.client as mqtt
def on_log(client, userdata, paho_log_level, messages):
if paho_log_level == mqtt.LogLevel.MQTT_LOG_ERR:
print(message)
mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqttc.on_log = on_log
mqttc.connect("mqtt.eclipseprojects.io", 1883, 60)
mqttc.loop_start()
# Do additional action needed, publish, subscribe, ...
[...]
The correspondence with Paho logging levels and standard ones is the following:
==================== ===============
Paho logging
==================== ===============
``MQTT_LOG_ERR`` ``logging.ERROR``
``MQTT_LOG_WARNING`` ``logging.WARNING``
``MQTT_LOG_NOTICE`` ``logging.INFO`` *(no direct equivalent)*
``MQTT_LOG_INFO`` ``logging.INFO``
``MQTT_LOG_DEBUG`` ``logging.DEBUG``
==================== ===============
External event loop support
```````````````````````````
To support other network loop like asyncio (see examples_), the library expose some
method and callback to support those use-case.
The following loop method exists:
* `loop_read`: should be called when the socket is ready for reading.
* `loop_write`: should be called when the socket is ready for writing AND the library want to write data.
* `loop_misc`: should be called every few seconds to handle message retrying and pings.
In pseudo code, it give the following:
.. code:: python
while run:
if need_read:
mqttc.loop_read()
if need_write:
mqttc.loop_write()
mqttc.loop_misc()
if not need_read and not need_write:
# But don't wait more than few seconds, loop_misc() need to be called regularly
wait_for_change_in_need_read_or_write()
updated_need_read_and_write()
The tricky part is implementing the update of need_read / need_write and wait for condition change. To support
this, the following method exists:
* `socket()`: which return the socket object when the TCP connection is open.
This call is particularly useful for select_ based loops. See ``examples/loop_select.py``.
* `want_write()`: return true if there is data waiting to be written. This is close to the
``need_writew`` of above pseudo-code, but you should also check whether the socket is ready for writing.
* callbacks ``on_socket_*``:
* `on_socket_open`: called when the socket is opened.
* `on_socket_close`: called when the socket is about to be closed.
* `on_socket_register_write`: called when there is data the client want to write on the socket
* `on_socket_unregister_write`: called when there is no more data to write on the socket.
Callbacks are particularly useful for event loops where you register or unregister a socket
for reading+writing. See ``examples/loop_asyncio.py`` for an example.
.. _select: https://docs.python.org/3/library/select.html#select.select
The callbacks are always called in this order:
- `on_socket_open`
- Zero or more times:
- `on_socket_register_write`
- `on_socket_unregister_write`
- `on_socket_close`
Global helper functions
```````````````````````
The client module also offers some global helper functions.
``topic_matches_sub(sub, topic)`` can be used to check whether a ``topic``
matches a ``subscription``.
For example:
the topic ``foo/bar`` would match the subscription ``foo/#`` or ``+/bar``
the topic ``non/matching`` would not match the subscription ``non/+/+``
Publish
*******
This module provides some helper functions to allow straightforward publishing
of messages in a one-shot manner. In other words, they are useful for the
situation where you have a single/multiple messages you want to publish to a
broker, then disconnect with nothing else required.
The two functions provided are `single()` and `multiple()`.
Both functions include support for MQTT v5.0, but do not currently let you
set any properties on connection or when sending messages.
Single
``````
Publish a single message to a broker, then disconnect cleanly.
Example:
.. code:: python
import paho.mqtt.publish as publish
publish.single("paho/test/topic", "payload", hostname="mqtt.eclipseprojects.io")
Multiple
````````
Publish multiple messages to a broker, then disconnect cleanly.
Example:
.. code:: python
from paho.mqtt.enums import MQTTProtocolVersion
import paho.mqtt.publish as publish
msgs = [{'topic':"paho/test/topic", 'payload':"multiple 1"},
("paho/test/topic", "multiple 2", 0, False)]
publish.multiple(msgs, hostname="mqtt.eclipseprojects.io", protocol=MQTTProtocolVersion.MQTTv5)
Subscribe
*********
This module provides some helper functions to allow straightforward subscribing
and processing of messages.
The two functions provided are `simple()` and `callback()`.
Both functions include support for MQTT v5.0, but do not currently let you
set any properties on connection or when subscribing.
Simple
``````
Subscribe to a set of topics and return the messages received. This is a
blocking function.
Example:
.. code:: python
import paho.mqtt.subscribe as subscribe
msg = subscribe.simple("paho/test/topic", hostname="mqtt.eclipseprojects.io")
print("%s %s" % (msg.topic, msg.payload))
Using Callback
``````````````
Subscribe to a set of topics and process the messages received using a user
provided callback.
Example:
.. code:: python
import paho.mqtt.subscribe as subscribe
def on_message_print(client, userdata, message):
print("%s %s" % (message.topic, message.payload))
userdata["message_count"] += 1
if userdata["message_count"] >= 5:
# it's possible to stop the program by disconnecting
client.disconnect()
subscribe.callback(on_message_print, "paho/test/topic", hostname="mqtt.eclipseprojects.io", userdata={"message_count": 0})
Reporting bugs
--------------
Please report bugs in the issues tracker at https://github.com/eclipse/paho.mqtt.python/issues.
More information
----------------
Discussion of the Paho clients takes place on the `Eclipse paho-dev mailing list <https://dev.eclipse.org/mailman/listinfo/paho-dev>`_.
General questions about the MQTT protocol itself (not this library) are discussed in the `MQTT Google Group <https://groups.google.com/forum/?fromgroups#!forum/mqtt>`_.
There is much more information available via the `MQTT community site <http://mqtt.org/>`_.
.. _examples: https://github.com/eclipse/paho.mqtt.python/tree/master/examples
.. _documentation: https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html

41
paho_mqtt/about.html Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>About</title>
</head>
<body lang="EN-US">
<h2>About This Content</h2>
<p><em>December 9, 2013</em></p>
<h3>License</h3>
<p>The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise
indicated below, the Content is provided to you under the terms and conditions of the
Eclipse Public License Version 2.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL").
A copy of the EPL is available at
<a href="http://www.eclipse.org/legal/epl-v20.html">http://www.eclipse.org/legal/epl-v20.html</a>
and a copy of the EDL is available at
<a href="http://www.eclipse.org/org/documents/edl-v10.php">http://www.eclipse.org/org/documents/edl-v10.php</a>.
For purposes of the EPL, "Program" will mean the Content.</p>
<p>If you did not receive this Content directly from the Eclipse Foundation, the Content is
being redistributed by another party ("Redistributor") and different terms and conditions may
apply to your use of any object code in the Content. Check the Redistributor's license that was
provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
indicated below, the terms and conditions of the EPL still apply to any source code in the Content
and such source code may be obtained at <a href="http://www.eclipse.org/">http://www.eclipse.org</a>.</p>
<h3>Third Party Content</h3>
<p>The Content includes items that have been sourced from third parties as set out below. If you
did not receive this Content directly from the Eclipse Foundation, the following is provided
for informational purposes only, and you should look to the Redistributor's license for
terms and conditions of use.</p>
<p><em>
<strong>None</strong> <br><br>
<br><br>
</em></p>
</body></html>

Some files were not shown because too many files have changed in this diff Show More