使用公密钥的方式登录mqtt
This commit is contained in:
parent
e8d0b79d19
commit
6b276c8cfa
42
PyJWT/.pre-commit-config.yaml
Normal file
42
PyJWT/.pre-commit-config.yaml
Normal 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
7
PyJWT/AUTHORS.rst
Normal 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 GitHub’s `overview <https://github.com/jpadilla/pyjwt/graphs/contributors>`_.
|
||||
802
PyJWT/CHANGELOG.rst
Normal file
802
PyJWT/CHANGELOG.rst
Normal 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
46
PyJWT/CODE_OF_CONDUCT.md
Normal 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
21
PyJWT/LICENSE
Normal 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
14
PyJWT/MANIFEST.in
Normal 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
93
PyJWT/PKG-INFO
Normal 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
|
||||
93
PyJWT/PyJWT.egg-info/PKG-INFO
Normal file
93
PyJWT/PyJWT.egg-info/PKG-INFO
Normal 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
|
||||
82
PyJWT/PyJWT.egg-info/SOURCES.txt
Normal file
82
PyJWT/PyJWT.egg-info/SOURCES.txt
Normal 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
|
||||
1
PyJWT/PyJWT.egg-info/dependency_links.txt
Normal file
1
PyJWT/PyJWT.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
||||
|
||||
1
PyJWT/PyJWT.egg-info/not-zip-safe
Normal file
1
PyJWT/PyJWT.egg-info/not-zip-safe
Normal file
@ -0,0 +1 @@
|
||||
|
||||
24
PyJWT/PyJWT.egg-info/requires.txt
Normal file
24
PyJWT/PyJWT.egg-info/requires.txt
Normal 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
|
||||
1
PyJWT/PyJWT.egg-info/top_level.txt
Normal file
1
PyJWT/PyJWT.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
||||
jwt
|
||||
62
PyJWT/README.rst
Normal file
62
PyJWT/README.rst
Normal 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
192
PyJWT/docs/Makefile
Normal 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
15
PyJWT/docs/_static/theme_overrides.css
vendored
Normal 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
71
PyJWT/docs/algorithms.rst
Normal 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
178
PyJWT/docs/api.rst
Normal 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
1
PyJWT/docs/changelog.rst
Normal file
@ -0,0 +1 @@
|
||||
.. include:: ../CHANGELOG.rst
|
||||
134
PyJWT/docs/conf.py
Normal file
134
PyJWT/docs/conf.py
Normal 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
17
PyJWT/docs/faq.rst
Normal 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
54
PyJWT/docs/index.rst
Normal 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
|
||||
30
PyJWT/docs/installation.rst
Normal file
30
PyJWT/docs/installation.rst
Normal 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
|
||||
2
PyJWT/docs/requirements-docs.txt
Normal file
2
PyJWT/docs/requirements-docs.txt
Normal file
@ -0,0 +1,2 @@
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
371
PyJWT/docs/usage.rst
Normal file
371
PyJWT/docs/usage.rst
Normal 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
74
PyJWT/jwt/__init__.py
Normal 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",
|
||||
]
|
||||
BIN
PyJWT/jwt/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/algorithms.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/algorithms.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/api_jwk.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/api_jwk.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/api_jws.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/api_jws.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/api_jwt.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/api_jwt.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/exceptions.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/exceptions.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/jwk_set_cache.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/jwk_set_cache.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/jwks_client.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/jwks_client.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/types.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/types.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/utils.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/utils.cpython-38.pyc
Normal file
Binary file not shown.
BIN
PyJWT/jwt/__pycache__/warnings.cpython-38.pyc
Normal file
BIN
PyJWT/jwt/__pycache__/warnings.cpython-38.pyc
Normal file
Binary file not shown.
862
PyJWT/jwt/algorithms.py
Normal file
862
PyJWT/jwt/algorithms.py
Normal 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
132
PyJWT/jwt/api_jwk.py
Normal 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
328
PyJWT/jwt/api_jws.py
Normal 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
372
PyJWT/jwt/api_jwt.py
Normal 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
70
PyJWT/jwt/exceptions.py
Normal 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
64
PyJWT/jwt/help.py
Normal 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()
|
||||
31
PyJWT/jwt/jwk_set_cache.py
Normal file
31
PyJWT/jwt/jwk_set_cache.py
Normal 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
124
PyJWT/jwt/jwks_client.py
Normal 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
0
PyJWT/jwt/py.typed
Normal file
5
PyJWT/jwt/types.py
Normal file
5
PyJWT/jwt/types.py
Normal 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
156
PyJWT/jwt/utils.py
Normal 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
2
PyJWT/jwt/warnings.py
Normal file
@ -0,0 +1,2 @@
|
||||
class RemovedInPyjwt3Warning(DeprecationWarning):
|
||||
pass
|
||||
34
PyJWT/pyproject.toml
Normal file
34
PyJWT/pyproject.toml
Normal 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
74
PyJWT/setup.cfg
Normal 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
5
PyJWT/setup.py
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
0
PyJWT/tests/__init__.py
Normal file
0
PyJWT/tests/__init__.py
Normal file
58
PyJWT/tests/keys/__init__.py
Normal file
58
PyJWT/tests/keys/__init__.py
Normal 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()
|
||||
8
PyJWT/tests/keys/jwk_ec_key_P-256.json
Normal file
8
PyJWT/tests/keys/jwk_ec_key_P-256.json
Normal 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"
|
||||
}
|
||||
8
PyJWT/tests/keys/jwk_ec_key_P-384.json
Normal file
8
PyJWT/tests/keys/jwk_ec_key_P-384.json
Normal 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"
|
||||
}
|
||||
8
PyJWT/tests/keys/jwk_ec_key_P-521.json
Normal file
8
PyJWT/tests/keys/jwk_ec_key_P-521.json
Normal 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"
|
||||
}
|
||||
8
PyJWT/tests/keys/jwk_ec_key_secp256k1.json
Normal file
8
PyJWT/tests/keys/jwk_ec_key_secp256k1.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"kty": "EC",
|
||||
"kid": "bilbo.baggins.256k@hobbiton.example",
|
||||
"crv": "secp256k1",
|
||||
"x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs",
|
||||
"y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI",
|
||||
"d": "XV7LOlEOANIaSxyil8yE8NPDT5jmVw_HQeCwNDzochQ"
|
||||
}
|
||||
7
PyJWT/tests/keys/jwk_ec_pub_P-256.json
Normal file
7
PyJWT/tests/keys/jwk_ec_pub_P-256.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"kty": "EC",
|
||||
"kid": "bilbo.baggins.256@hobbiton.example",
|
||||
"crv": "P-256",
|
||||
"x": "PTTjIY84aLtaZCxLTrG_d8I0G6YKCV7lg8M4xkKfwQ4",
|
||||
"y": "ank6KA34vv24HZLXlChVs85NEGlpg2sbqNmR_BcgyJU"
|
||||
}
|
||||
7
PyJWT/tests/keys/jwk_ec_pub_P-384.json
Normal file
7
PyJWT/tests/keys/jwk_ec_pub_P-384.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"kty": "EC",
|
||||
"kid": "bilbo.baggins.384@hobbiton.example",
|
||||
"crv": "P-384",
|
||||
"x": "IDC-5s6FERlbC4Nc_4JhKW8sd51AhixtMdNUtPxhRFP323QY6cwWeIA3leyZhz-J",
|
||||
"y": "eovmN9ocANS8IJxDAGSuC1FehTq5ZFLJU7XSPg36zHpv4H2byKGEcCBiwT4sFJsy"
|
||||
}
|
||||
7
PyJWT/tests/keys/jwk_ec_pub_P-521.json
Normal file
7
PyJWT/tests/keys/jwk_ec_pub_P-521.json
Normal 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"
|
||||
}
|
||||
7
PyJWT/tests/keys/jwk_ec_pub_secp256k1.json
Normal file
7
PyJWT/tests/keys/jwk_ec_pub_secp256k1.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"kty": "EC",
|
||||
"kid": "bilbo.baggins.256k@hobbiton.example",
|
||||
"crv": "secp256k1",
|
||||
"x": "MLnVyPDPQpNm0KaaO4iEh0i8JItHXJE0NcIe8GK1SYs",
|
||||
"y": "7r8d-xF7QAgT5kSRdly6M8xeg4Jz83Gs_CQPQRH65QI"
|
||||
}
|
||||
7
PyJWT/tests/keys/jwk_hmac.json
Normal file
7
PyJWT/tests/keys/jwk_hmac.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"kty": "oct",
|
||||
"kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037",
|
||||
"use": "sig",
|
||||
"alg": "HS256",
|
||||
"k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
|
||||
}
|
||||
1
PyJWT/tests/keys/jwk_keyset_only_unknown_alg.json
Normal file
1
PyJWT/tests/keys/jwk_keyset_only_unknown_alg.json
Normal 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"}]}
|
||||
1
PyJWT/tests/keys/jwk_keyset_with_unknown_alg.json
Normal file
1
PyJWT/tests/keys/jwk_keyset_with_unknown_alg.json
Normal 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"}]}
|
||||
6
PyJWT/tests/keys/jwk_okp_key_Ed25519.json
Normal file
6
PyJWT/tests/keys/jwk_okp_key_Ed25519.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"kty":"OKP",
|
||||
"crv":"Ed25519",
|
||||
"d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
|
||||
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
|
||||
}
|
||||
9
PyJWT/tests/keys/jwk_okp_key_Ed448.json
Normal file
9
PyJWT/tests/keys/jwk_okp_key_Ed448.json
Normal 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"
|
||||
}
|
||||
5
PyJWT/tests/keys/jwk_okp_pub_Ed25519.json
Normal file
5
PyJWT/tests/keys/jwk_okp_pub_Ed25519.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"kty":"OKP",
|
||||
"crv":"Ed25519",
|
||||
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
|
||||
}
|
||||
8
PyJWT/tests/keys/jwk_okp_pub_Ed448.json
Normal file
8
PyJWT/tests/keys/jwk_okp_pub_Ed448.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"kty": "OKP",
|
||||
"kid": "sig_ed448_01",
|
||||
"crv": "Ed448",
|
||||
"use": "sig",
|
||||
"x": "kvqP7TzMosCQCpNcW8qY2HmVmpPYUEIGn-sQWQgoWlAZbWpnXpXqAT6yMoYA08pkJm7P_HKZoHwA",
|
||||
"alg": "EdDSA"
|
||||
}
|
||||
13
PyJWT/tests/keys/jwk_rsa_key.json
Normal file
13
PyJWT/tests/keys/jwk_rsa_key.json
Normal 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"
|
||||
}
|
||||
7
PyJWT/tests/keys/jwk_rsa_pub.json
Normal file
7
PyJWT/tests/keys/jwk_rsa_pub.json
Normal 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"
|
||||
}
|
||||
9
PyJWT/tests/keys/testkey2_rsa.pub.pem
Normal file
9
PyJWT/tests/keys/testkey2_rsa.pub.pem
Normal 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-----
|
||||
5
PyJWT/tests/keys/testkey_ec.priv
Normal file
5
PyJWT/tests/keys/testkey_ec.priv
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2nninfu2jMHDwAbn
|
||||
9oERUhRADS6duQaJEadybLaa0YShRANCAAQfMBxRZKUYEdy5/fLdGI2tYj6kTr50
|
||||
PZPt8jOD23rAR7dhtNpG1ojqopmH0AH5wEXadgk8nLCT4cAPK59Qp9Ek
|
||||
-----END PRIVATE KEY-----
|
||||
4
PyJWT/tests/keys/testkey_ec.pub
Normal file
4
PyJWT/tests/keys/testkey_ec.pub
Normal file
@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHzAcUWSlGBHcuf3y3RiNrWI+pE6+
|
||||
dD2T7fIzg9t6wEe3YbTaRtaI6qKZh9AB+cBF2nYJPJywk+HADyufUKfRJA==
|
||||
-----END PUBLIC KEY-----
|
||||
5
PyJWT/tests/keys/testkey_ec_secp192r1.priv
Normal file
5
PyJWT/tests/keys/testkey_ec_secp192r1.priv
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBiON6kYcPu8ZUDRTu8W
|
||||
eXJ2FmX7e9yq0hahNAMyAARHecLjkXWDUJfZ4wiFH61JpmonCYH1GpinVlqw68Sf
|
||||
wtDHg2F6SifQEFC6VKj1ZXw=
|
||||
-----END PRIVATE KEY-----
|
||||
1
PyJWT/tests/keys/testkey_ec_ssh.pub
Normal file
1
PyJWT/tests/keys/testkey_ec_ssh.pub
Normal file
@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB8wHFFkpRgR3Ln98t0Yja1iPqROvnQ9k+3yM4PbesBHt2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ=
|
||||
3
PyJWT/tests/keys/testkey_ed25519
Normal file
3
PyJWT/tests/keys/testkey_ed25519
Normal file
@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIBy9N4xfv/9qOiKrxwRKeGfO5ab6lSukKHbuC5vaJ1Mg
|
||||
-----END PRIVATE KEY-----
|
||||
1
PyJWT/tests/keys/testkey_ed25519.pub
Normal file
1
PyJWT/tests/keys/testkey_ed25519.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC4pK2dePGgctIAsh0H/tmUrLzx2Vc4Ltc8TN9nfuChG
|
||||
5
PyJWT/tests/keys/testkey_pkcs1.pub.pem
Normal file
5
PyJWT/tests/keys/testkey_pkcs1.pub.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN RSA PUBLIC KEY-----
|
||||
MIGHAoGBAOV/0Vl/5VdHcYpnILYzBGWo5JQVzo9wBkbxzjAStcAnTwvv1ZJTMXs6
|
||||
fjz91f9hiMM4Z/5qNTE/EHlDWxVdj1pyRaQulZPUs0r9qJ02ogRRGLG3jjrzzbzF
|
||||
yj/pdNBwym0UJYC/Jmn/kMLwGiWI2nfa9vM5SovqZiAy2FD7eOtVAgED
|
||||
-----END RSA PUBLIC KEY-----
|
||||
21
PyJWT/tests/keys/testkey_rsa.cer
Normal file
21
PyJWT/tests/keys/testkey_rsa.cer
Normal 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-----
|
||||
27
PyJWT/tests/keys/testkey_rsa.priv
Normal file
27
PyJWT/tests/keys/testkey_rsa.priv
Normal 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-----
|
||||
1
PyJWT/tests/keys/testkey_rsa.pub
Normal file
1
PyJWT/tests/keys/testkey_rsa.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUeDMF8m/Zw6NCvILB7w0R6WuI5M0VYplSK969StGGWpyZkl0K+4IlGv7EI346Y7yrCAklH3x9426PFWD4n0uzf9Q4DgdwX2dgfpqpVcQsATweT8Au/GL3qazUMO4C9AZphn6UA3GW/F5AU5lJuOTP/2aHxot4YrCokunen46FQFCnC8BQA+Dzb45S2BNbedKWON4WvXgHyoCd4ZJz/CXSeL77VJFlRcVKi/NypA6SVrdbbuust+sPoteCP9OwOzi8PU+cibXNxV6cpPtN9gjXtuwb879Z5zDPY2XX2kvuAOiw2u4XeiAlY2rUvKf+KM9CeQ8ulkfKG+6LJxhwjcOJ aasmundo@mair.local
|
||||
119
PyJWT/tests/test_advisory.py
Normal file
119
PyJWT/tests/test_advisory.py
Normal 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,
|
||||
)
|
||||
1102
PyJWT/tests/test_algorithms.py
Normal file
1102
PyJWT/tests/test_algorithms.py
Normal file
File diff suppressed because it is too large
Load Diff
298
PyJWT/tests/test_api_jwk.py
Normal file
298
PyJWT/tests/test_api_jwk.py
Normal 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
817
PyJWT/tests/test_api_jws.py
Normal 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
804
PyJWT/tests/test_api_jwt.py
Normal 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"],
|
||||
)
|
||||
37
PyJWT/tests/test_compressed_jwt.py
Normal file
37
PyJWT/tests/test_compressed_jwt.py
Normal 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,"
|
||||
),
|
||||
}
|
||||
7
PyJWT/tests/test_exceptions.py
Normal file
7
PyJWT/tests/test_exceptions.py
Normal 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'
|
||||
357
PyJWT/tests/test_jwks_client.py
Normal file
357
PyJWT/tests/test_jwks_client.py
Normal 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
19
PyJWT/tests/test_jwt.py
Normal 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
39
PyJWT/tests/test_utils.py
Normal 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
30
PyJWT/tests/utils.py
Normal 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
75
PyJWT/tox.ini
Normal 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
66
paho_mqtt/.gitignore
vendored
Normal 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
114
paho_mqtt/CONTRIBUTING.md
Normal 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
3
paho_mqtt/LICENSE.txt
Normal 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
635
paho_mqtt/PKG-INFO
Normal 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
606
paho_mqtt/README.rst
Normal 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
41
paho_mqtt/about.html
Normal 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
Loading…
Reference in New Issue
Block a user