{"url":"https://api.github.com/repos/jpadilla/pyjwt/compare/2.12.1...2.13.0","html_url":"https://github.com/jpadilla/pyjwt/compare/2.12.1...2.13.0","permalink_url":"https://github.com/jpadilla/pyjwt/compare/jpadilla:a4e1a3d...jpadilla:7144e45","diff_url":"https://github.com/jpadilla/pyjwt/compare/2.12.1...2.13.0.diff","patch_url":"https://github.com/jpadilla/pyjwt/compare/2.12.1...2.13.0.patch","base_commit":{"sha":"a4e1a3d1218b01c5806420b8f16d9308ac4adc30","node_id":"C_kwDOABV8otoAKGE0ZTFhM2QxMjE4YjAxYzU4MDY0MjBiOGYxNmQ5MzA4YWM0YWRjMzA","commit":{"author":{"name":"José Padilla","email":"jpadilla@webapplicate.com","date":"2026-03-13T18:41:01Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-13T18:41:01Z"},"message":"Add typing_extensions dependency for Python < 3.11 (#1151)\n\n* Add typing_extensions as dependency for Python < 3.11\n\nPyJWT imports typing_extensions.Never (Python < 3.11) and\ntyping_extensions.TypeAlias (Python < 3.10) at runtime in\njwt/algorithms.py, but typing_extensions was never declared as a\ndependency.  This causes a ModuleNotFoundError on Python 3.9/3.10\nwhen importing the library.\n\nFixes #1150\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n* Add minimal install CI job to catch undeclared dependencies\n\nThe existing install-dev job always installs with [dev] extras, which\nbundles cryptography and pulls in typing_extensions transitively via\ntest tooling. This masked the missing typing_extensions dependency\n(#1150).\n\nAdd an install-minimal job that tests bare `pip install .` and\n`pip install .[crypto]` on Python 3.9 and 3.13, ensuring undeclared\nruntime dependencies are caught immediately.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Merge install-minimal into install-dev and test all install modes\n\nConsolidate into a single job that tests progressively: bare install,\ncrypto extra, then dev extras. Tests Python 3.9 and 3.13 to cover\nboth the typing_extensions fallback and native typing paths.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Bump version to 2.12.1\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Add explicit permissions block to CI workflow\n\nRestrict GITHUB_TOKEN to read-only contents access, as flagged by\ngithub-advanced-security[bot].\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Fix RST underline length in CHANGELOG\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n---------\n\nCo-authored-by: Claude <noreply@anthropic.com>\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","tree":{"sha":"186372a542e33bf8232eefe4ad7c9e17dea0306b","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/186372a542e33bf8232eefe4ad7c9e17dea0306b"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJptFo9CRC1aQ7uu5UhlAAAa2oQAH8ct0cQ0mXEfVnEdedE2kZ3\npZL0ZGhORWJ5fogxkDUuBTBftZxGgGmMP2P34PDovIKv/DdBnCpyWl76Ozayu0QC\nSZTH0V0iyYQ/GzHKMTCS/qbYvd2186wnSCnYT6DsCTkfAEyjWVrFC+9rn0U2uOij\nGvLuEEf77JADixxuuF6jjTmuahNOnJHMVP6fJt6x+JY/MNlt5kniG+UM/4HjItgw\n8GH+U22vwzW3HPfRVELYrREg4GYzoOx/m+G1vlqBJF0PU6gfGhy0aSIEEfLZQLJj\neTZQ9lp/IbkHTh9d9qWfD2R7v++CuV+/9S9q7vWDEfutxI1Dnm97btWMVJmIQPpE\nZHtPCZlWZdyNx7w1BelYD8Eopv7zRxGW90VuO9fDvl/vwetW5b47XfZqKH7JFap4\nLOJNqHjxrVchUYHue6VH6bGAgnVXfyAkgiGMqRe9Izt++tgxeIA71Q2PFKLEz3uS\nsWZFvmXNcLxdPRZKXFSxQ3lSXXfIWKm+nQCoaJ3BbIVqIWTlD6deyhBvqFm48+Lh\nVxW+FDFZJoK1VO0SqXHp83XtYho703r+Qpn38ciuqOCqGK9PbW6k1+26GOj0uhDt\n+sBCIWXMYBP1U2/UQDKu6AWvMDPtCrwa/95yLBzzQSIB3mpus6ZwraEF/cmCgjJb\nM1iXkXGdqjLFZgevphfT\n=NoL8\n-----END PGP SIGNATURE-----\n","payload":"tree 186372a542e33bf8232eefe4ad7c9e17dea0306b\nparent bd9700cca7f9258fadcc429c1034e508025931f2\nauthor José Padilla <jpadilla@webapplicate.com> 1773427261 -0400\ncommitter GitHub <noreply@github.com> 1773427261 -0400\n\nAdd typing_extensions dependency for Python < 3.11 (#1151)\n\n* Add typing_extensions as dependency for Python < 3.11\n\nPyJWT imports typing_extensions.Never (Python < 3.11) and\ntyping_extensions.TypeAlias (Python < 3.10) at runtime in\njwt/algorithms.py, but typing_extensions was never declared as a\ndependency.  This causes a ModuleNotFoundError on Python 3.9/3.10\nwhen importing the library.\n\nFixes #1150\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n* Add minimal install CI job to catch undeclared dependencies\n\nThe existing install-dev job always installs with [dev] extras, which\nbundles cryptography and pulls in typing_extensions transitively via\ntest tooling. This masked the missing typing_extensions dependency\n(#1150).\n\nAdd an install-minimal job that tests bare `pip install .` and\n`pip install .[crypto]` on Python 3.9 and 3.13, ensuring undeclared\nruntime dependencies are caught immediately.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Merge install-minimal into install-dev and test all install modes\n\nConsolidate into a single job that tests progressively: bare install,\ncrypto extra, then dev extras. Tests Python 3.9 and 3.13 to cover\nboth the typing_extensions fallback and native typing paths.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Bump version to 2.12.1\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Add explicit permissions block to CI workflow\n\nRestrict GITHUB_TOKEN to read-only contents access, as flagged by\ngithub-advanced-security[bot].\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Fix RST underline length in CHANGELOG\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n---------\n\nCo-authored-by: Claude <noreply@anthropic.com>\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","verified_at":"2026-03-13T18:41:03Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","html_url":"https://github.com/jpadilla/pyjwt/commit/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30/comments","author":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"bd9700cca7f9258fadcc429c1034e508025931f2","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/bd9700cca7f9258fadcc429c1034e508025931f2","html_url":"https://github.com/jpadilla/pyjwt/commit/bd9700cca7f9258fadcc429c1034e508025931f2"}]},"merge_base_commit":{"sha":"a4e1a3d1218b01c5806420b8f16d9308ac4adc30","node_id":"C_kwDOABV8otoAKGE0ZTFhM2QxMjE4YjAxYzU4MDY0MjBiOGYxNmQ5MzA4YWM0YWRjMzA","commit":{"author":{"name":"José Padilla","email":"jpadilla@webapplicate.com","date":"2026-03-13T18:41:01Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-13T18:41:01Z"},"message":"Add typing_extensions dependency for Python < 3.11 (#1151)\n\n* Add typing_extensions as dependency for Python < 3.11\n\nPyJWT imports typing_extensions.Never (Python < 3.11) and\ntyping_extensions.TypeAlias (Python < 3.10) at runtime in\njwt/algorithms.py, but typing_extensions was never declared as a\ndependency.  This causes a ModuleNotFoundError on Python 3.9/3.10\nwhen importing the library.\n\nFixes #1150\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n* Add minimal install CI job to catch undeclared dependencies\n\nThe existing install-dev job always installs with [dev] extras, which\nbundles cryptography and pulls in typing_extensions transitively via\ntest tooling. This masked the missing typing_extensions dependency\n(#1150).\n\nAdd an install-minimal job that tests bare `pip install .` and\n`pip install .[crypto]` on Python 3.9 and 3.13, ensuring undeclared\nruntime dependencies are caught immediately.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Merge install-minimal into install-dev and test all install modes\n\nConsolidate into a single job that tests progressively: bare install,\ncrypto extra, then dev extras. Tests Python 3.9 and 3.13 to cover\nboth the typing_extensions fallback and native typing paths.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Bump version to 2.12.1\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Add explicit permissions block to CI workflow\n\nRestrict GITHUB_TOKEN to read-only contents access, as flagged by\ngithub-advanced-security[bot].\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Fix RST underline length in CHANGELOG\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n---------\n\nCo-authored-by: Claude <noreply@anthropic.com>\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","tree":{"sha":"186372a542e33bf8232eefe4ad7c9e17dea0306b","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/186372a542e33bf8232eefe4ad7c9e17dea0306b"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJptFo9CRC1aQ7uu5UhlAAAa2oQAH8ct0cQ0mXEfVnEdedE2kZ3\npZL0ZGhORWJ5fogxkDUuBTBftZxGgGmMP2P34PDovIKv/DdBnCpyWl76Ozayu0QC\nSZTH0V0iyYQ/GzHKMTCS/qbYvd2186wnSCnYT6DsCTkfAEyjWVrFC+9rn0U2uOij\nGvLuEEf77JADixxuuF6jjTmuahNOnJHMVP6fJt6x+JY/MNlt5kniG+UM/4HjItgw\n8GH+U22vwzW3HPfRVELYrREg4GYzoOx/m+G1vlqBJF0PU6gfGhy0aSIEEfLZQLJj\neTZQ9lp/IbkHTh9d9qWfD2R7v++CuV+/9S9q7vWDEfutxI1Dnm97btWMVJmIQPpE\nZHtPCZlWZdyNx7w1BelYD8Eopv7zRxGW90VuO9fDvl/vwetW5b47XfZqKH7JFap4\nLOJNqHjxrVchUYHue6VH6bGAgnVXfyAkgiGMqRe9Izt++tgxeIA71Q2PFKLEz3uS\nsWZFvmXNcLxdPRZKXFSxQ3lSXXfIWKm+nQCoaJ3BbIVqIWTlD6deyhBvqFm48+Lh\nVxW+FDFZJoK1VO0SqXHp83XtYho703r+Qpn38ciuqOCqGK9PbW6k1+26GOj0uhDt\n+sBCIWXMYBP1U2/UQDKu6AWvMDPtCrwa/95yLBzzQSIB3mpus6ZwraEF/cmCgjJb\nM1iXkXGdqjLFZgevphfT\n=NoL8\n-----END PGP SIGNATURE-----\n","payload":"tree 186372a542e33bf8232eefe4ad7c9e17dea0306b\nparent bd9700cca7f9258fadcc429c1034e508025931f2\nauthor José Padilla <jpadilla@webapplicate.com> 1773427261 -0400\ncommitter GitHub <noreply@github.com> 1773427261 -0400\n\nAdd typing_extensions dependency for Python < 3.11 (#1151)\n\n* Add typing_extensions as dependency for Python < 3.11\n\nPyJWT imports typing_extensions.Never (Python < 3.11) and\ntyping_extensions.TypeAlias (Python < 3.10) at runtime in\njwt/algorithms.py, but typing_extensions was never declared as a\ndependency.  This causes a ModuleNotFoundError on Python 3.9/3.10\nwhen importing the library.\n\nFixes #1150\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n* Add minimal install CI job to catch undeclared dependencies\n\nThe existing install-dev job always installs with [dev] extras, which\nbundles cryptography and pulls in typing_extensions transitively via\ntest tooling. This masked the missing typing_extensions dependency\n(#1150).\n\nAdd an install-minimal job that tests bare `pip install .` and\n`pip install .[crypto]` on Python 3.9 and 3.13, ensuring undeclared\nruntime dependencies are caught immediately.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Merge install-minimal into install-dev and test all install modes\n\nConsolidate into a single job that tests progressively: bare install,\ncrypto extra, then dev extras. Tests Python 3.9 and 3.13 to cover\nboth the typing_extensions fallback and native typing paths.\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Bump version to 2.12.1\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Add explicit permissions block to CI workflow\n\nRestrict GITHUB_TOKEN to read-only contents access, as flagged by\ngithub-advanced-security[bot].\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n* Fix RST underline length in CHANGELOG\n\nhttps://claude.ai/code/session_013ZvbKEyVkcUx8xHrqt8v2F\n\n---------\n\nCo-authored-by: Claude <noreply@anthropic.com>\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","verified_at":"2026-03-13T18:41:03Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","html_url":"https://github.com/jpadilla/pyjwt/commit/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30/comments","author":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"bd9700cca7f9258fadcc429c1034e508025931f2","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/bd9700cca7f9258fadcc429c1034e508025931f2","html_url":"https://github.com/jpadilla/pyjwt/commit/bd9700cca7f9258fadcc429c1034e508025931f2"}]},"status":"ahead","ahead_by":8,"behind_by":0,"total_commits":8,"commits":[{"sha":"40e3147eb5f790d8d041772e5fc00728a176c812","node_id":"C_kwDOABV8otoAKDQwZTMxNDdlYjVmNzkwZDhkMDQxNzcyZTVmYzAwNzI4YTE3NmM4MTI","commit":{"author":{"name":"Kurt McKee","email":"contactme@kurtmckee.org","date":"2026-03-16T13:30:51Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-16T13:30:51Z"},"message":"Migrate development extras to dependency groups (#1152)\n\nThis introduces the following changes:\n\n* The `dev` extra is moved to a dependency group.\n* The `tests` extra is moved to a dependency group.\n* The `docs` extra is moved to a dependency group.\n* `tox.ini` has been updated to use the dependency groups.\n* `main.yml` has been updated to install the `dev` dependency group.\n* The Read the Docs config is updated to install the `docs` dependency group.\n\n  This follows the how-to documentation published by Read the Docs.\n  https://docs.readthedocs.com/platform/latest/build-customization.html#install-dependencies-from-dependency-groups\n\nIn addition, when tox was run, the `lint` environment failed\nbecause pyupgrade doesn't support Python 3.9.\nTherefore, one additional change was made:\n\n* The `lint` tox environment's `basepython` is updated to Python 3.10.","tree":{"sha":"cab492757633c7ff517b1503796f1dfed45e4456","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/cab492757633c7ff517b1503796f1dfed45e4456"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/40e3147eb5f790d8d041772e5fc00728a176c812","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpuAYLCRC1aQ7uu5UhlAAA3JYQAElKVjlp/0580bfWBuxyFU9x\nbBm+/muG/4msRI6bSKzz5mjlZwhMBR/bJszrr8EFC34wd3kHJ83x3VIHDXmh3B6E\n44kTkfKNHs6oQ81sQUOX0v7RaC4xRuW27hLshxlmEFxumrk+GKUSJb8q6lp7b0MH\nrktIsrvU0XPDZsnAMjPJZSrE8t/aUEmzY67X7v2JoECk8r4WMRYYNxe/SDKqJVAE\nT5zCM93IwpnNs7Ek0gTKA63myzc8xWLd2yk3u86lccx+QlNxeSnTfj7HR9xVObcw\n212t2ZTVWFjP4/1Q+LpsCsM/BPsETR4ey1SnSIARhTqd9jRJWS2zEpHSV+jevpGB\naV37gpnG1h6dz2swg5T1K7g1PgYpWdY9Hi+hUrx8G6YkvrUv3+zk4QFpK9Lbf0Nu\ncQZUh6m/kUY0U1y8lUgPWT6BBDtzR03MvPOL6uwRPKxZW5A9uFBqaAjH7zvEN+0f\nCBRKKxrdNsmbmD1/xRf7FdZrclEcjAZqJQN6VPH6ozFfuiPQ+ulb90crVFaFQA8I\nT6ZhoW/vUhjlZrnxIQer5gAXxjwk0i8FmBTZNF5/M0mW+tGeKcm83HLB3JGUKIA4\n53xcHd7EvCYmK2T7gN7a7oZoWg4gU+zyIWg/x5z09Izc2VFo08URWBLwiOZOW6pW\nAzjoQ+URg4HLqcdQwXwy\n=Ghfc\n-----END PGP SIGNATURE-----\n","payload":"tree cab492757633c7ff517b1503796f1dfed45e4456\nparent a4e1a3d1218b01c5806420b8f16d9308ac4adc30\nauthor Kurt McKee <contactme@kurtmckee.org> 1773667851 -0500\ncommitter GitHub <noreply@github.com> 1773667851 -0400\n\nMigrate development extras to dependency groups (#1152)\n\nThis introduces the following changes:\n\n* The `dev` extra is moved to a dependency group.\n* The `tests` extra is moved to a dependency group.\n* The `docs` extra is moved to a dependency group.\n* `tox.ini` has been updated to use the dependency groups.\n* `main.yml` has been updated to install the `dev` dependency group.\n* The Read the Docs config is updated to install the `docs` dependency group.\n\n  This follows the how-to documentation published by Read the Docs.\n  https://docs.readthedocs.com/platform/latest/build-customization.html#install-dependencies-from-dependency-groups\n\nIn addition, when tox was run, the `lint` environment failed\nbecause pyupgrade doesn't support Python 3.9.\nTherefore, one additional change was made:\n\n* The `lint` tox environment's `basepython` is updated to Python 3.10.","verified_at":"2026-03-16T13:30:51Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/40e3147eb5f790d8d041772e5fc00728a176c812","html_url":"https://github.com/jpadilla/pyjwt/commit/40e3147eb5f790d8d041772e5fc00728a176c812","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/40e3147eb5f790d8d041772e5fc00728a176c812/comments","author":{"login":"kurtmckee","id":39996,"node_id":"MDQ6VXNlcjM5OTk2","avatar_url":"https://avatars.githubusercontent.com/u/39996?v=4","gravatar_id":"","url":"https://api.github.com/users/kurtmckee","html_url":"https://github.com/kurtmckee","followers_url":"https://api.github.com/users/kurtmckee/followers","following_url":"https://api.github.com/users/kurtmckee/following{/other_user}","gists_url":"https://api.github.com/users/kurtmckee/gists{/gist_id}","starred_url":"https://api.github.com/users/kurtmckee/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kurtmckee/subscriptions","organizations_url":"https://api.github.com/users/kurtmckee/orgs","repos_url":"https://api.github.com/users/kurtmckee/repos","events_url":"https://api.github.com/users/kurtmckee/events{/privacy}","received_events_url":"https://api.github.com/users/kurtmckee/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"a4e1a3d1218b01c5806420b8f16d9308ac4adc30","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/a4e1a3d1218b01c5806420b8f16d9308ac4adc30","html_url":"https://github.com/jpadilla/pyjwt/commit/a4e1a3d1218b01c5806420b8f16d9308ac4adc30"}]},{"sha":"b87c10014d4109f0214fea188d00faaaf8a80e64","node_id":"C_kwDOABV8otoAKGI4N2MxMDAxNGQ0MTA5ZjAyMTRmZWExODhkMDBmYWFhZjhhODBlNjQ","commit":{"author":{"name":"dependabot[bot]","email":"49699333+dependabot[bot]@users.noreply.github.com","date":"2026-03-29T13:03:15Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-29T13:03:15Z"},"message":"Bump codecov/codecov-action from 5 to 6 (#1154)\n\nBumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.\n- [Release notes](https://github.com/codecov/codecov-action/releases)\n- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)\n\n---\nupdated-dependencies:\n- dependency-name: codecov/codecov-action\n  dependency-version: '6'\n  dependency-type: direct:production\n  update-type: version-update:semver-major\n...\n\nSigned-off-by: dependabot[bot] <support@github.com>\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>","tree":{"sha":"7decd5663a44bd8939e2f5fd18b8fe6d8f8ba6b9","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/7decd5663a44bd8939e2f5fd18b8fe6d8f8ba6b9"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/b87c10014d4109f0214fea188d00faaaf8a80e64","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpySMTCRC1aQ7uu5UhlAAAbRkQAIP1mEkcD/nZp4Uwwh4hJver\ny1B+VuQGYt/kc2SlkMixZEj40hKozZxmgchVYOc2o30WJeSAbcmdtkypBFM3kZ1N\nxkPlbH75vBHX21z4g/qBpnpVypsyuWkzGgdkhGbsirh+p0xwHWUPWWy4f1DRoDtw\nsJWjmBBRaoOLkAUHtZBzQGuTmCmjX0hg/+qvcmwrXLxk57ZhNnD7fUPuLUCHkAno\nLcweqENOjzsRWgRHDTHl71MnklVsDajGtYOZNmsgLt/rmaup7K7gz9RbTyTJbP6p\nv2hsiEjG/3E4DBZw8eWbzbhWgDv7XBagb/a1xLK87tjnM6KXQmaupp8V79WA/p+G\nmac91rC97pjWL1dXQEHb73EhSr4BVNah8XclWZnyQRwFoeYl5Kfya2/qV59H68Jl\nPf3AvKlM3Kr1QK/FnGHBDaGrfFvmifXQleMkmv5+VhDtyJ2ZOSimVWlV6FY16jBq\nPLVKxUjVEO0MlluWNKs7dIl/5udTYbWZArOhoGagyrVwypxOOaGPWlO/EUyj5/DG\noO2Jv3c+4obq6di8R2Og07CukB3zvAmjhNcUtXJbgE2pAS5pmrVNWDRQWnRBK9FO\nLGsTYjnV4Xf3XkntpUeIvzh0XscZ81GU40uG8UBPwJ+J19yfALY8bT1PIxJtIlI/\nljXGHA70pjWr8pC/67ms\n=5/oz\n-----END PGP SIGNATURE-----\n","payload":"tree 7decd5663a44bd8939e2f5fd18b8fe6d8f8ba6b9\nparent 40e3147eb5f790d8d041772e5fc00728a176c812\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1774789395 +0600\ncommitter GitHub <noreply@github.com> 1774789395 +0600\n\nBump codecov/codecov-action from 5 to 6 (#1154)\n\nBumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.\n- [Release notes](https://github.com/codecov/codecov-action/releases)\n- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)\n- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)\n\n---\nupdated-dependencies:\n- dependency-name: codecov/codecov-action\n  dependency-version: '6'\n  dependency-type: direct:production\n  update-type: version-update:semver-major\n...\n\nSigned-off-by: dependabot[bot] <support@github.com>\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>","verified_at":"2026-03-29T13:03:15Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/b87c10014d4109f0214fea188d00faaaf8a80e64","html_url":"https://github.com/jpadilla/pyjwt/commit/b87c10014d4109f0214fea188d00faaaf8a80e64","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/b87c10014d4109f0214fea188d00faaaf8a80e64/comments","author":{"login":"dependabot[bot]","id":49699333,"node_id":"MDM6Qm90NDk2OTkzMzM=","avatar_url":"https://avatars.githubusercontent.com/in/29110?v=4","gravatar_id":"","url":"https://api.github.com/users/dependabot%5Bbot%5D","html_url":"https://github.com/apps/dependabot","followers_url":"https://api.github.com/users/dependabot%5Bbot%5D/followers","following_url":"https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dependabot%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/dependabot%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/dependabot%5Bbot%5D/repos","events_url":"https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/dependabot%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"40e3147eb5f790d8d041772e5fc00728a176c812","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/40e3147eb5f790d8d041772e5fc00728a176c812","html_url":"https://github.com/jpadilla/pyjwt/commit/40e3147eb5f790d8d041772e5fc00728a176c812"}]},{"sha":"9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a","node_id":"C_kwDOABV8otoAKDlkMDhhOWExODk2ODQ1ZWQ4ZWFmODhlNmY2YWM2MWU1ODAwYzNlN2E","commit":{"author":{"name":"pre-commit-ci[bot]","email":"66853113+pre-commit-ci[bot]@users.noreply.github.com","date":"2026-03-29T13:05:38Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-29T13:05:38Z"},"message":"[pre-commit.ci] pre-commit autoupdate (#1146)\n\nupdates:\n- [github.com/astral-sh/ruff-pre-commit: v0.15.4 → v0.15.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.4...v0.15.7)\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","tree":{"sha":"a858deb59e32675fb127dd4bd17fafb48be01b7b","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/a858deb59e32675fb127dd4bd17fafb48be01b7b"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpySOiCRC1aQ7uu5UhlAAANpoQACChGVCISx+zarY8QdtjnVup\n7lXM8xQQARgz8ntXSEh0+xak99cIGe8dIrI1IQ0krs3D13TBvlrSuWmFT+xZYcZB\nIZ6qwM2whEU60mwufmit7iIPP1TOVN5k8BecytZH0ZGNhrS32hNOmwejK0Wy2mDq\ncw67Ov5H5vNuqZQN66dAEsCPW8VuatS7RQaCgOnFlx/Jzho1H7Nk8SC3K+DHf4MO\njq78FCKyGvceXgpJTN/UzkLVchD/NdnEvLHzES6g6MjccnV5eQUcQCfl8vuHEa3H\nFwvSuBsqPAYKRnpZtMzveqWVE8po62FnrUrOC//TViaHy3c3RlpR1axTx0EPt4kp\nt9jh1uP6p9Zm/JZpxETzGKx6W4R72wsfyGF/SZcc8TUVt9WjDInfx1bLM4KjlHSC\nzpMMvr/oYgMvQQYKbWDsqimWa5Q8/v9lieOWhNXBNrxKVyzNDuRvV4Ve39F0h8kq\nodeXqfcutClKlzUdxY6LdMqVzedZSHb0EO66Ud9nWAkf4C4oKmslpPVgnQM46xFY\nXDmfo+SosFvLjUrWuzBJQclqm9kqo1ZXSoWSLJjjqweqfzIBzBjpaxH5noOnl2Te\njH6cv1FZUBaT+c9VIv3bMQGL0zYBrWxh5R9XVdY4dIlyHFDjVoq9XOPXPIRHLc06\nRSfhOSx0p4VjrmHTRca5\n=HOk5\n-----END PGP SIGNATURE-----\n","payload":"tree a858deb59e32675fb127dd4bd17fafb48be01b7b\nparent b87c10014d4109f0214fea188d00faaaf8a80e64\nauthor pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 1774789538 +0600\ncommitter GitHub <noreply@github.com> 1774789538 +0600\n\n[pre-commit.ci] pre-commit autoupdate (#1146)\n\nupdates:\n- [github.com/astral-sh/ruff-pre-commit: v0.15.4 → v0.15.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.4...v0.15.7)\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","verified_at":"2026-03-29T13:05:38Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a","html_url":"https://github.com/jpadilla/pyjwt/commit/9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a/comments","author":{"login":"pre-commit-ci[bot]","id":66853113,"node_id":"MDM6Qm90NjY4NTMxMTM=","avatar_url":"https://avatars.githubusercontent.com/in/68672?v=4","gravatar_id":"","url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D","html_url":"https://github.com/apps/pre-commit-ci","followers_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/followers","following_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/repos","events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"b87c10014d4109f0214fea188d00faaaf8a80e64","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/b87c10014d4109f0214fea188d00faaaf8a80e64","html_url":"https://github.com/jpadilla/pyjwt/commit/b87c10014d4109f0214fea188d00faaaf8a80e64"}]},{"sha":"dcc27a9d3182a2349c30b160758785c6ce7a6508","node_id":"C_kwDOABV8otoAKGRjYzI3YTlkMzE4MmEyMzQ5YzMwYjE2MDc1ODc4NWM2Y2U3YTY1MDg","commit":{"author":{"name":"pre-commit-ci[bot]","email":"66853113+pre-commit-ci[bot]@users.noreply.github.com","date":"2026-03-31T04:04:24Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-03-31T04:04:24Z"},"message":"[pre-commit.ci] pre-commit autoupdate (#1155)\n\nupdates:\n- [github.com/python-jsonschema/check-jsonschema: 0.37.0 → 0.37.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.0...0.37.1)\n- [github.com/astral-sh/ruff-pre-commit: v0.15.7 → v0.15.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.7...v0.15.8)\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","tree":{"sha":"e8049603d621fd4a3e0a2e5f8958b397ea848eb3","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/e8049603d621fd4a3e0a2e5f8958b397ea848eb3"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/dcc27a9d3182a2349c30b160758785c6ce7a6508","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpy0fICRC1aQ7uu5UhlAAAtDEQAKVxQ1Lzk3FFb7s6s7nxlQn+\n5r5dmM9uUhrP4neKnuz7xN9/wjwkfk+766O2sYgqlcFW5qn7vJQ6Gh6YqCQfKJ3S\n8+dOkaOYJiJdc64H2ib1s9ThHIkTFDAcgy8wKntKPAegSiCxQ0ZJvRemRHfnw3t3\nFELE5lXlD9V70Yg36SOig3ngYnU/aw5lxBhGJaiuhNMbijwsYnIg8ObMLBFjBLWc\n3CnTe7GyEUTA509UsvD5gbOb5U1MM5Z7+3qhvCtG2fovSYlheCWzwzpOmm6bXiQz\nESiGIvUBpodb4R6ibTiydhvIFRhz1PijoRNrbt+eNWq08Qa6+mnrrEdM5D82A58Q\nK68w3Q7RNU4qVBLlu5s80zGsAiHHBYC3nP6BemIP2kOXsqjjuxPjalBfTLt8agS2\n/AuuJ3NrJ/W+bFQ1n8FrK8lByb1CP1PzgLtvCL/yURWQMkgp83GUPG6+hI/CE9+2\niU2kjKelVnP6jJ+1nxd/juITG84LLlqVxBb2xWgTA+8Shz2fMCIKA2tAIvQSxwen\n9+2ExYAh83y1idbpr6Btf0deMR0N56cqg9fuWm8SHx9Y/dfeE1akB05j71P/cvjA\ni72YwVkMJ7xKcdKbNqwhXlcbaYLld5DuEO/ED3yUj6xvKmszDkYfH6nI8gURd4yT\nz6aLANeXe9Mrnc4/FXN8\n=Eqs7\n-----END PGP SIGNATURE-----\n","payload":"tree e8049603d621fd4a3e0a2e5f8958b397ea848eb3\nparent 9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a\nauthor pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> 1774929864 +0600\ncommitter GitHub <noreply@github.com> 1774929864 +0600\n\n[pre-commit.ci] pre-commit autoupdate (#1155)\n\nupdates:\n- [github.com/python-jsonschema/check-jsonschema: 0.37.0 → 0.37.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.0...0.37.1)\n- [github.com/astral-sh/ruff-pre-commit: v0.15.7 → v0.15.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.7...v0.15.8)\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","verified_at":"2026-03-31T04:04:24Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/dcc27a9d3182a2349c30b160758785c6ce7a6508","html_url":"https://github.com/jpadilla/pyjwt/commit/dcc27a9d3182a2349c30b160758785c6ce7a6508","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/dcc27a9d3182a2349c30b160758785c6ce7a6508/comments","author":{"login":"pre-commit-ci[bot]","id":66853113,"node_id":"MDM6Qm90NjY4NTMxMTM=","avatar_url":"https://avatars.githubusercontent.com/in/68672?v=4","gravatar_id":"","url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D","html_url":"https://github.com/apps/pre-commit-ci","followers_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/followers","following_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/repos","events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a","html_url":"https://github.com/jpadilla/pyjwt/commit/9d08a9a1896845ed8eaf88e6f6ac61e5800c3e7a"}]},{"sha":"95791b1759b8aa4f2203575d344d5c78564cdc81","node_id":"C_kwDOABV8otoAKDk1NzkxYjE3NTliOGFhNGYyMjAzNTc1ZDM0NGQ1Yzc4NTY0Y2RjODE","commit":{"author":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T18:11:10Z"},"committer":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T18:11:10Z"},"message":"Bundle security fixes and hardening into 2.13.0\n\nSecurity:\n- `HMACAlgorithm.prepare_key` rejects JWK JSON documents passed as raw\n  HMAC secrets to close an algorithm-confusion gap not covered by the\n  existing PEM/SSH guard. Reported by @aradona91 in GHSA-xgmm-8j9v-c9wx.\n- Bind the JWT header `alg` to `PyJWK.algorithm_name` during verification\n  so the caller's `algorithms` allow-list cannot be bypassed when decoding\n  with a `PyJWK` / `PyJWKClient` key. Reported by @sushi-gif in\n  GHSA-jq35-7prp-9v3f.\n- Skip the unconditional base64 decode of the compact-form payload\n  segment when `b64=false` is set, and require that segment to be empty\n  (RFC 7515 Appendix F detached form). Closes an unauthenticated DoS\n  amplifier. Reported by @thesmartshadow in GHSA-w7vc-732c-9m39.\n- `PyJWKClient` rejects any URI whose scheme is not `http` or `https` so\n  attacker-influenced URIs cannot read local files or reach unintended\n  schemes via urllib's default `file://` / `ftp://` / `data:` handlers.\n  Reported by @KEIJOT in GHSA-993g-76c3-p5m4.\n- Preserve the cached JWK Set on fetch errors in `PyJWKClient.fetch_data`.\n  The previous `finally`-block `put(None)` pattern cleared the cache on\n  any transient outage. Reported by @eddieran in GHSA-fhv5-28vv-h8m8.\n\nFixes:\n- Reject empty HMAC keys outright in `HMACAlgorithm.prepare_key` with\n  `InvalidKeyError` instead of accepting them with only a warning.\n  Hardening prompted by reports from @SnailSploit and @spartan8806.\n- Forward per-call `options` (including `enforce_minimum_key_length`)\n  from `PyJWT.decode` through to `PyJWS._verify_signature`. Thanks to\n  @WLUB.\n- RFC 7797 §3 compliance for `b64=false`: encoder auto-adds `\"b64\"` to\n  `crit`; decoder rejects tokens that set `b64=false` without listing\n  it in `crit`. Thanks to @MachineLearning-Nerd.","tree":{"sha":"36af6a43d309e458cf445336dffd41b07da814ec","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/36af6a43d309e458cf445336dffd41b07da814ec"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/95791b1759b8aa4f2203575d344d5c78564cdc81","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZgwU0wRQgyAGxgpBIuTRIaMY6B\nCpBA4HAr1vk/S6ZCsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQNVDRl9mC4ck0a9AwmCM0sRxCBXmz5h+Ea2TT7jwA45/Zl1hWOr/j5ZbgLqmvzbW1S\ngYD4yizdEH2VelFb9z+QA=\n-----END SSH SIGNATURE-----","payload":"tree 36af6a43d309e458cf445336dffd41b07da814ec\nparent dcc27a9d3182a2349c30b160758785c6ce7a6508\nauthor José Padilla <jpadilla@users.noreply.github.com> 1779387070 -0400\ncommitter José Padilla <jpadilla@users.noreply.github.com> 1779387070 -0400\n\nBundle security fixes and hardening into 2.13.0\n\nSecurity:\n- `HMACAlgorithm.prepare_key` rejects JWK JSON documents passed as raw\n  HMAC secrets to close an algorithm-confusion gap not covered by the\n  existing PEM/SSH guard. Reported by @aradona91 in GHSA-xgmm-8j9v-c9wx.\n- Bind the JWT header `alg` to `PyJWK.algorithm_name` during verification\n  so the caller's `algorithms` allow-list cannot be bypassed when decoding\n  with a `PyJWK` / `PyJWKClient` key. Reported by @sushi-gif in\n  GHSA-jq35-7prp-9v3f.\n- Skip the unconditional base64 decode of the compact-form payload\n  segment when `b64=false` is set, and require that segment to be empty\n  (RFC 7515 Appendix F detached form). Closes an unauthenticated DoS\n  amplifier. Reported by @thesmartshadow in GHSA-w7vc-732c-9m39.\n- `PyJWKClient` rejects any URI whose scheme is not `http` or `https` so\n  attacker-influenced URIs cannot read local files or reach unintended\n  schemes via urllib's default `file://` / `ftp://` / `data:` handlers.\n  Reported by @KEIJOT in GHSA-993g-76c3-p5m4.\n- Preserve the cached JWK Set on fetch errors in `PyJWKClient.fetch_data`.\n  The previous `finally`-block `put(None)` pattern cleared the cache on\n  any transient outage. Reported by @eddieran in GHSA-fhv5-28vv-h8m8.\n\nFixes:\n- Reject empty HMAC keys outright in `HMACAlgorithm.prepare_key` with\n  `InvalidKeyError` instead of accepting them with only a warning.\n  Hardening prompted by reports from @SnailSploit and @spartan8806.\n- Forward per-call `options` (including `enforce_minimum_key_length`)\n  from `PyJWT.decode` through to `PyJWS._verify_signature`. Thanks to\n  @WLUB.\n- RFC 7797 §3 compliance for `b64=false`: encoder auto-adds `\"b64\"` to\n  `crit`; decoder rejects tokens that set `b64=false` without listing\n  it in `crit`. Thanks to @MachineLearning-Nerd.\n","verified_at":"2026-05-21T18:14:04Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/95791b1759b8aa4f2203575d344d5c78564cdc81","html_url":"https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/95791b1759b8aa4f2203575d344d5c78564cdc81/comments","author":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"dcc27a9d3182a2349c30b160758785c6ce7a6508","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/dcc27a9d3182a2349c30b160758785c6ce7a6508","html_url":"https://github.com/jpadilla/pyjwt/commit/dcc27a9d3182a2349c30b160758785c6ce7a6508"}]},{"sha":"22f478cebddd8294259c30f037ecb92b0b348774","node_id":"C_kwDOABV8otoAKDIyZjQ3OGNlYmRkZDgyOTQyNTljMzBmMDM3ZWNiOTJiMGIzNDg3NzQ","commit":{"author":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T19:17:11Z"},"committer":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T19:17:11Z"},"message":"Remove redundant casts in `RSAAlgorithm.prepare_key` and `ECAlgorithm.prepare_key`\n\nmypy 2.1.0 narrows `key` correctly via `isinstance(key, self._crypto_key_types)`,\nso the explicit `cast()` calls are redundant. Surfaced by CI on the previous\ncommit.","tree":{"sha":"2c75605ffb99922a3d250b1617e4ce72687a1a34","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/2c75605ffb99922a3d250b1617e4ce72687a1a34"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/22f478cebddd8294259c30f037ecb92b0b348774","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZgwU0wRQgyAGxgpBIuTRIaMY6B\nCpBA4HAr1vk/S6ZCsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQOjytBJH77nU+X1GInGqrTJWll3ncG9mIDIGThllRxRtgBS2XY1F8tEVkRUT8D+GA6\nUi/Tx4BnK4M4vxuIScvgg=\n-----END SSH SIGNATURE-----","payload":"tree 2c75605ffb99922a3d250b1617e4ce72687a1a34\nparent 95791b1759b8aa4f2203575d344d5c78564cdc81\nauthor José Padilla <jpadilla@users.noreply.github.com> 1779391031 -0400\ncommitter José Padilla <jpadilla@users.noreply.github.com> 1779391031 -0400\n\nRemove redundant casts in `RSAAlgorithm.prepare_key` and `ECAlgorithm.prepare_key`\n\nmypy 2.1.0 narrows `key` correctly via `isinstance(key, self._crypto_key_types)`,\nso the explicit `cast()` calls are redundant. Surfaced by CI on the previous\ncommit.\n","verified_at":"2026-05-21T19:17:52Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/22f478cebddd8294259c30f037ecb92b0b348774","html_url":"https://github.com/jpadilla/pyjwt/commit/22f478cebddd8294259c30f037ecb92b0b348774","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/22f478cebddd8294259c30f037ecb92b0b348774/comments","author":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"95791b1759b8aa4f2203575d344d5c78564cdc81","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/95791b1759b8aa4f2203575d344d5c78564cdc81","html_url":"https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81"}]},{"sha":"d2f4bec4963897c0ef96ef64a875894f2c8542ab","node_id":"C_kwDOABV8otoAKGQyZjRiZWM0OTYzODk3YzBlZjk2ZWY2NGE4NzU4OTRmMmM4NTQyYWI","commit":{"author":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T19:32:10Z"},"committer":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T19:32:10Z"},"message":"Restore `cast()` calls with cross-version `type: ignore` for `prepare_key`\n\nThe previous commit removed the `cast()` calls because newer mypy\nconsidered them redundant. That broke Python 3.9's mypy, which still\nneeds the cast for narrowing through `isinstance(key, self._crypto_key_types)`.\n\nRestore the casts and suppress `redundant-cast` on newer mypy while\nallowing `unused-ignore` on older mypy where the directive isn't needed.","tree":{"sha":"fff3e26b0b33da1458deb84f522abbc784ad1e5a","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/fff3e26b0b33da1458deb84f522abbc784ad1e5a"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/d2f4bec4963897c0ef96ef64a875894f2c8542ab","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZgwU0wRQgyAGxgpBIuTRIaMY6B\nCpBA4HAr1vk/S6ZCsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQEwUk5KUu+jVGZci0JiP9M2xKNWI52F26s/Zd6yNUNRzfVZQQqR8CRljAUzilbrCC3\nkao0Z7wNdkCN4ABlqMQQU=\n-----END SSH SIGNATURE-----","payload":"tree fff3e26b0b33da1458deb84f522abbc784ad1e5a\nparent 22f478cebddd8294259c30f037ecb92b0b348774\nauthor José Padilla <jpadilla@users.noreply.github.com> 1779391930 -0400\ncommitter José Padilla <jpadilla@users.noreply.github.com> 1779391930 -0400\n\nRestore `cast()` calls with cross-version `type: ignore` for `prepare_key`\n\nThe previous commit removed the `cast()` calls because newer mypy\nconsidered them redundant. That broke Python 3.9's mypy, which still\nneeds the cast for narrowing through `isinstance(key, self._crypto_key_types)`.\n\nRestore the casts and suppress `redundant-cast` on newer mypy while\nallowing `unused-ignore` on older mypy where the directive isn't needed.\n","verified_at":"2026-05-21T19:32:23Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/d2f4bec4963897c0ef96ef64a875894f2c8542ab","html_url":"https://github.com/jpadilla/pyjwt/commit/d2f4bec4963897c0ef96ef64a875894f2c8542ab","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/d2f4bec4963897c0ef96ef64a875894f2c8542ab/comments","author":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"22f478cebddd8294259c30f037ecb92b0b348774","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/22f478cebddd8294259c30f037ecb92b0b348774","html_url":"https://github.com/jpadilla/pyjwt/commit/22f478cebddd8294259c30f037ecb92b0b348774"}]},{"sha":"7144e4534c34810f4525dc4578a32addd8212cff","node_id":"C_kwDOABV8otoAKDcxNDRlNDUzNGMzNDgxMGY0NTI1ZGM0NTc4YTMyYWRkZDgyMTJjZmY","commit":{"author":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T19:34:15Z"},"committer":{"name":"José Padilla","email":"jpadilla@users.noreply.github.com","date":"2026-05-21T19:34:15Z"},"message":"Apply ruff format","tree":{"sha":"0c184657e5dd479dc8f686a758af1bce56e01801","url":"https://api.github.com/repos/jpadilla/pyjwt/git/trees/0c184657e5dd479dc8f686a758af1bce56e01801"},"url":"https://api.github.com/repos/jpadilla/pyjwt/git/commits/7144e4534c34810f4525dc4578a32addd8212cff","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZgwU0wRQgyAGxgpBIuTRIaMY6B\nCpBA4HAr1vk/S6ZCsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCoK4BdWb6K59/tzD/I/eGTBhFyGfv6tGQTqvplk8iDuYHFxRvdCy1EliEol6I6+9N\nPgXOu/DCegBcYMVhAJTwA=\n-----END SSH SIGNATURE-----","payload":"tree 0c184657e5dd479dc8f686a758af1bce56e01801\nparent d2f4bec4963897c0ef96ef64a875894f2c8542ab\nauthor José Padilla <jpadilla@users.noreply.github.com> 1779392055 -0400\ncommitter José Padilla <jpadilla@users.noreply.github.com> 1779392055 -0400\n\nApply ruff format\n","verified_at":"2026-05-21T19:34:28Z"}},"url":"https://api.github.com/repos/jpadilla/pyjwt/commits/7144e4534c34810f4525dc4578a32addd8212cff","html_url":"https://github.com/jpadilla/pyjwt/commit/7144e4534c34810f4525dc4578a32addd8212cff","comments_url":"https://api.github.com/repos/jpadilla/pyjwt/commits/7144e4534c34810f4525dc4578a32addd8212cff/comments","author":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"jpadilla","id":83319,"node_id":"MDQ6VXNlcjgzMzE5","avatar_url":"https://avatars.githubusercontent.com/u/83319?v=4","gravatar_id":"","url":"https://api.github.com/users/jpadilla","html_url":"https://github.com/jpadilla","followers_url":"https://api.github.com/users/jpadilla/followers","following_url":"https://api.github.com/users/jpadilla/following{/other_user}","gists_url":"https://api.github.com/users/jpadilla/gists{/gist_id}","starred_url":"https://api.github.com/users/jpadilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpadilla/subscriptions","organizations_url":"https://api.github.com/users/jpadilla/orgs","repos_url":"https://api.github.com/users/jpadilla/repos","events_url":"https://api.github.com/users/jpadilla/events{/privacy}","received_events_url":"https://api.github.com/users/jpadilla/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"d2f4bec4963897c0ef96ef64a875894f2c8542ab","url":"https://api.github.com/repos/jpadilla/pyjwt/commits/d2f4bec4963897c0ef96ef64a875894f2c8542ab","html_url":"https://github.com/jpadilla/pyjwt/commit/d2f4bec4963897c0ef96ef64a875894f2c8542ab"}]}],"files":[{"sha":"3df6aac38a1108dc409d720af297b5f360769148","filename":".github/workflows/main.yml","status":"modified","additions":2,"deletions":2,"changes":4,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/.github%2Fworkflows%2Fmain.yml","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/.github%2Fworkflows%2Fmain.yml","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/.github%2Fworkflows%2Fmain.yml?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -67,7 +67,7 @@ jobs:\n \n       - name: \"Upload coverage to Codecov\"\n         if: \"contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest'\"\n-        uses: \"codecov/codecov-action@v5\"\n+        uses: \"codecov/codecov-action@v6\"\n         with:\n           fail_ci_if_error: true\n           token: ${{ secrets.CODECOV_TOKEN }}\n@@ -125,7 +125,7 @@ jobs:\n         run: \"python -c 'import jwt; print(jwt.__version__)'\"\n \n       - name: \"Install in dev mode\"\n-        run: \"python -m pip install -e .[dev]\"\n+        run: \"python -m pip install -e . --group dev\"\n \n       - name: \"Import package in dev mode\"\n         run: \"python -c 'import jwt; print(jwt.__version__)'\""},{"sha":"d6b3beea242cf5da5dec395f1f2c959acc29f5a4","filename":".pre-commit-config.yaml","status":"modified","additions":2,"deletions":2,"changes":4,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/.pre-commit-config.yaml","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/.pre-commit-config.yaml","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/.pre-commit-config.yaml?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -36,7 +36,7 @@ repos:\n       - id: pyprojectsort\n \n   - repo: https://github.com/python-jsonschema/check-jsonschema\n-    rev: \"0.37.0\"\n+    rev: \"0.37.1\"\n     hooks:\n       - id: check-github-workflows\n       - id: check-readthedocs\n@@ -48,7 +48,7 @@ repos:\n \n   - repo: https://github.com/astral-sh/ruff-pre-commit\n     # Ruff version.\n-    rev: v0.15.4\n+    rev: v0.15.8\n     hooks:\n       # Run the linter.\n       - id: ruff"},{"sha":"f4052d6cbd44563f44087d716e065cde139fffa6","filename":".readthedocs.yaml","status":"modified","additions":4,"deletions":8,"changes":12,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/.readthedocs.yaml","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/.readthedocs.yaml","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/.readthedocs.yaml?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -6,14 +6,10 @@ build:\n   os: \"ubuntu-lts-latest\"\n   tools:\n     python: \"3.11\"\n-\n-python:\n-  install:\n-    - method: \"pip\"\n-      path: \".\"\n-      extra_requirements:\n-        - \"docs\"\n-        - \"crypto\"\n+  jobs:\n+    install:\n+      - \"pip install --upgrade pip\"\n+      - \"pip install .[crypto] --group 'docs'\"\n \n sphinx:\n   configuration: \"docs/conf.py\""},{"sha":"d52f9537d7b7214c513db32732ee610d003fc51f","filename":"CHANGELOG.rst","status":"modified","additions":50,"deletions":1,"changes":51,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/CHANGELOG.rst","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/CHANGELOG.rst","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/CHANGELOG.rst?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -4,9 +4,58 @@ Changelog\n All notable changes to this project will be documented in this file.\n This project adheres to `Semantic Versioning <https://semver.org/>`__.\n \n-`Unreleased <https://github.com/jpadilla/pyjwt/compare/2.12.1...HEAD>`__\n+`Unreleased <https://github.com/jpadilla/pyjwt/compare/2.13.0...HEAD>`__\n ------------------------------------------------------------------------\n \n+`v2.13.0 <https://github.com/jpadilla/pyjwt/compare/2.12.1...2.13.0>`__\n+-----------------------------------------------------------------------\n+\n+Security\n+~~~~~~~~\n+\n+- Reject JWK JSON documents passed as raw HMAC secrets in\n+  ``HMACAlgorithm.prepare_key`` to close an algorithm-confusion gap that\n+  the existing PEM/SSH guard did not cover. Reported by @aradona91 in\n+  `GHSA-xgmm-8j9v-c9wx <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-xgmm-8j9v-c9wx>`__.\n+- Bind the JWT header ``alg`` to ``PyJWK.algorithm_name`` during\n+  verification so the caller's ``algorithms=[...]`` allow-list cannot be\n+  bypassed when decoding with a ``PyJWK`` / ``PyJWKClient`` key. Reported\n+  by @sushi-gif in `GHSA-jq35-7prp-9v3f <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-jq35-7prp-9v3f>`__.\n+- Reject non-``http(s)`` URI schemes in ``PyJWKClient`` so attacker-\n+  influenced URIs cannot read local files or reach unintended schemes via\n+  urllib's default ``file://`` / ``ftp://`` / ``data:`` handlers. Reported\n+  by @KEIJOT in `GHSA-993g-76c3-p5m4 <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-993g-76c3-p5m4>`__.\n+- Preserve the cached JWK Set on fetch errors in ``PyJWKClient.fetch_data``.\n+  The previous ``finally``-block ``put(None)`` pattern cleared the cache\n+  on any transient outage, turning one bad JWKS request into application-\n+  wide auth failure. Reported by @eddieran in `GHSA-fhv5-28vv-h8m8 <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-fhv5-28vv-h8m8>`__.\n+- Skip the unconditional base64 decode of the compact-form payload segment\n+  when ``b64=false`` is set in the protected header, and require that\n+  segment to be empty (RFC 7515 Appendix F detached form). Closes an\n+  unauthenticated DoS amplifier. Reported by @thesmartshadow in\n+  `GHSA-w7vc-732c-9m39 <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-w7vc-732c-9m39>`__.\n+\n+Fixed\n+~~~~~\n+\n+- Reject empty HMAC keys outright in ``HMACAlgorithm.prepare_key`` with\n+  ``InvalidKeyError`` instead of accepting them with only a warning.\n+  Thanks to @SnailSploit and @spartan8806 for independently flagging the\n+  footgun.\n+- Forward per-call ``options`` (including ``enforce_minimum_key_length``)\n+  from ``PyJWT.decode`` through to ``PyJWS._verify_signature`` so the\n+  option actually takes effect when set at the call site rather than only\n+  on the ``PyJWT`` instance. Thanks to @WLUB for the report.\n+- RFC 7797 §3 compliance for ``b64=false``: the encoder now auto-adds\n+  ``\"b64\"`` to the ``crit`` header parameter, and the decoder rejects\n+  tokens that set ``b64=false`` without listing it in ``crit``. Thanks to\n+  @MachineLearning-Nerd for the report.\n+\n+Changed\n+~~~~~~~\n+\n+- Migrate the ``dev``, ``docs``, and ``tests`` package extras to dependency groups by @kurtmckee in `#1152 <https://github.com/jpadilla/pyjwt/pull/1152>`__\n+\n `v2.12.1 <https://github.com/jpadilla/pyjwt/compare/2.12.0...2.12.1>`__\n ------------------------------------------------------------------------\n "},{"sha":"e85ab52d52af56f246a1c6abec9ad80193facb95","filename":"jwt/__init__.py","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2F__init__.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2F__init__.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/jwt%2F__init__.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -28,7 +28,7 @@\n from .jwks_client import PyJWKClient\n from .warnings import InsecureKeyLengthWarning\n \n-__version__ = \"2.12.1\"\n+__version__ = \"2.13.0\"\n \n __title__ = \"PyJWT\"\n __description__ = \"JSON Web Token implementation in Python\""},{"sha":"f848397b93b0cee4c1cc328266a6fe8c0f22a1b7","filename":"jwt/algorithms.py","status":"modified","additions":29,"deletions":2,"changes":31,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Falgorithms.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Falgorithms.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/jwt%2Falgorithms.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -325,12 +325,35 @@ def __init__(self, hash_alg: HashlibHash) -> None:\n     def prepare_key(self, key: str | bytes) -> bytes:\n         key_bytes = force_bytes(key)\n \n+        if len(key_bytes) == 0:\n+            raise InvalidKeyError(\"HMAC key must not be empty.\")\n+\n         if is_pem_format(key_bytes) or is_ssh_key(key_bytes):\n             raise InvalidKeyError(\n                 \"The specified key is an asymmetric key or x509 certificate and\"\n                 \" should not be used as an HMAC secret.\"\n             )\n \n+        # Defense against algorithm-confusion attacks: an attacker with\n+        # control over the token header can force this code path by setting\n+        # alg=HS*, and HMACAlgorithm is the only algorithm that accepts\n+        # arbitrary bytes as a valid secret. Other algorithms reject\n+        # non-key-shaped input naturally. Even a symmetric (kty=oct) JWK\n+        # should be loaded via PyJWK / from_jwk rather than fed as raw JSON\n+        # bytes (whose contents are not the secret material).\n+        stripped = key_bytes.lstrip()\n+        if stripped.startswith(b\"{\"):\n+            try:\n+                jwk_obj = json.loads(key_bytes)\n+            except ValueError:\n+                jwk_obj = None\n+            if isinstance(jwk_obj, dict) and \"kty\" in jwk_obj:\n+                raise InvalidKeyError(\n+                    \"The specified key looks like a JWK and should not be \"\n+                    \"used directly as an HMAC secret. Load it via \"\n+                    \"PyJWK / HMACAlgorithm.from_jwk first.\"\n+                )\n+\n         return key_bytes\n \n     @overload\n@@ -420,7 +443,10 @@ def check_key_length(self, key: AllowedRSAKeys) -> str | None:\n \n         def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys:\n             if isinstance(key, self._crypto_key_types):\n-                return cast(AllowedRSAKeys, key)\n+                # Cast is required for type narrowing on Python 3.9's mypy\n+                # but redundant on newer mypy versions; suppress both\n+                # diagnostics so the line works across all supported envs.\n+                return cast(AllowedRSAKeys, key)  # type: ignore[redundant-cast,unused-ignore]\n \n             if not isinstance(key, (bytes, str)):\n                 raise TypeError(\"Expecting a PEM-formatted key.\")\n@@ -614,7 +640,8 @@ def _validate_curve(self, key: AllowedECKeys) -> None:\n \n         def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys:\n             if isinstance(key, self._crypto_key_types):\n-                ec_key = cast(AllowedECKeys, key)\n+                # See note in RSAAlgorithm.prepare_key.\n+                ec_key = cast(AllowedECKeys, key)  # type: ignore[redundant-cast,unused-ignore]\n                 self._validate_curve(ec_key)\n                 return ec_key\n "},{"sha":"d8cee41c9052fe2c989fbf0140bd79f4da2fcbcd","filename":"jwt/api_jws.py","status":"modified","additions":55,"deletions":6,"changes":61,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Fapi_jws.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Fapi_jws.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/jwt%2Fapi_jws.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -165,6 +165,15 @@ def encode(\n \n         if is_payload_detached:\n             header[\"b64\"] = False\n+            # RFC 7797 §3: producers MUST list \"b64\" in \"crit\" whenever\n+            # \"b64\" appears in the protected header, so b64-unaware\n+            # verifiers don't silently treat an unencoded payload as\n+            # base64-encoded.\n+            existing_crit = header.get(\"crit\", [])\n+            if not isinstance(existing_crit, list):\n+                raise InvalidTokenError(\"Invalid 'crit' header: must be a list\")\n+            if \"b64\" not in existing_crit:\n+                header[\"crit\"] = [*existing_crit, \"b64\"]\n         elif \"b64\" in header:\n             # True is the standard value for b64, so no need for it\n             del header[\"b64\"]\n@@ -242,6 +251,14 @@ def decode_complete(\n         self._validate_headers(header)\n \n         if header.get(\"b64\", True) is False:\n+            # RFC 7797 §3: when \"b64\" is present in the protected header,\n+            # it MUST also appear in \"crit\". A token that sets b64=false\n+            # without declaring it critical is malformed.\n+            crit = header.get(\"crit\") or []\n+            if not isinstance(crit, list) or \"b64\" not in crit:\n+                raise InvalidTokenError(\n+                    \"The 'b64' header parameter requires 'b64' to be listed in 'crit'.\"\n+                )\n             if detached_payload is None:\n                 raise DecodeError(\n                     '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.'\n@@ -250,7 +267,14 @@ def decode_complete(\n             signing_input = b\".\".join([signing_input.rsplit(b\".\", 1)[0], payload])\n \n         if verify_signature:\n-            self._verify_signature(signing_input, header, signature, key, algorithms)\n+            self._verify_signature(\n+                signing_input,\n+                header,\n+                signature,\n+                key,\n+                algorithms,\n+                options=merged_options,\n+            )\n \n         return {\n             \"payload\": payload,\n@@ -317,10 +341,22 @@ def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict[str, Any], bytes]:\n         if not isinstance(header, dict):\n             raise DecodeError(\"Invalid header string: must be a json object\")\n \n-        try:\n-            payload = base64url_decode(payload_segment)\n-        except (TypeError, binascii.Error) as err:\n-            raise DecodeError(\"Invalid payload padding\") from err\n+        if header.get(\"b64\", True) is False:\n+            # Detached payload form (RFC 7515 Appendix F): the compact-form\n+            # payload segment must be empty; the caller supplies the actual\n+            # payload via the `detached_payload` argument in decode_complete.\n+            # Skipping the base64 decode here removes an unauthenticated work\n+            # amplifier — otherwise an attacker can inflate the unused\n+            # segment to force CPU + memory cost before the signature is\n+            # even checked.\n+            if payload_segment:\n+                raise DecodeError(\"Payload segment must be empty when 'b64' is false.\")\n+            payload = b\"\"\n+        else:\n+            try:\n+                payload = base64url_decode(payload_segment)\n+            except (TypeError, binascii.Error) as err:\n+                raise DecodeError(\"Invalid payload padding\") from err\n \n         try:\n             signature = base64url_decode(crypto_segment)\n@@ -336,7 +372,10 @@ def _verify_signature(\n         signature: bytes,\n         key: AllowedPublicKeys | PyJWK | str | bytes = \"\",\n         algorithms: Sequence[str] | None = None,\n+        options: SigOptions | None = None,\n     ) -> None:\n+        effective_options = options if options is not None else self.options\n+\n         if algorithms is None and isinstance(key, PyJWK):\n             algorithms = [key.algorithm_name]\n         try:\n@@ -348,6 +387,16 @@ def _verify_signature(\n             raise InvalidAlgorithmError(\"The specified alg value is not allowed\")\n \n         if isinstance(key, PyJWK):\n+            # The PyJWK has a fixed algorithm bound at construction time.\n+            # Verification must use that algorithm, not whatever the token\n+            # header advertises, otherwise the caller's allow-list check\n+            # above degenerates into a string compare with no behavioural\n+            # effect on which algorithm actually verifies the signature.\n+            if alg != key.algorithm_name:\n+                raise InvalidAlgorithmError(\n+                    f\"Token algorithm {alg!r} does not match the key's \"\n+                    f\"algorithm {key.algorithm_name!r}\"\n+                )\n             alg_obj = key.Algorithm\n             prepared_key = key.key\n         else:\n@@ -359,7 +408,7 @@ def _verify_signature(\n \n         key_length_msg = alg_obj.check_key_length(prepared_key)\n         if key_length_msg:\n-            if self.options.get(\"enforce_minimum_key_length\", False):\n+            if effective_options.get(\"enforce_minimum_key_length\", False):\n                 raise InvalidKeyError(key_length_msg)\n             else:\n                 warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=4)"},{"sha":"ad65cb8b4fd0d3742a2c5c7a0a69141a4b968ea7","filename":"jwt/api_jwt.py","status":"modified","additions":3,"deletions":0,"changes":3,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Fapi_jwt.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Fapi_jwt.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/jwt%2Fapi_jwt.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -258,6 +258,9 @@ def decode_complete(\n \n         sig_options: SigOptions = {\n             \"verify_signature\": verify_signature,\n+            \"enforce_minimum_key_length\": merged_options.get(\n+                \"enforce_minimum_key_length\", False\n+            ),\n         }\n         decoded = self._jws.decode_complete(\n             jwt,"},{"sha":"41f333a9cdfd61fd12efd1d38e33f9056d62da1b","filename":"jwt/jwks_client.py","status":"modified","additions":19,"deletions":6,"changes":25,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Fjwks_client.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/jwt%2Fjwks_client.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/jwt%2Fjwks_client.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -6,6 +6,7 @@\n from ssl import SSLContext\n from typing import Any\n from urllib.error import HTTPError, URLError\n+from urllib.parse import urlparse\n \n from .api_jwk import PyJWK, PyJWKSet\n from .api_jwt import decode_complete as decode_token\n@@ -69,6 +70,16 @@ def __init__(\n         \"\"\"\n         if headers is None:\n             headers = {}\n+        # urllib's default OpenerDirector also handles file://, ftp://, and\n+        # data: URIs. Reject anything that isn't http(s) eagerly so a caller\n+        # passing an attacker-influenced URL (e.g. taken from a `jku` token\n+        # header) can't read local files or reach other unintended schemes.\n+        scheme = urlparse(uri).scheme.lower()\n+        if scheme not in (\"http\", \"https\"):\n+            raise PyJWKClientError(\n+                f\"Invalid JWKS URI scheme {scheme!r}: only 'http' and 'https' \"\n+                f\"are supported.\"\n+            )\n         self.uri = uri\n         self.jwk_set_cache: JWKSetCache | None = None\n         self.headers = headers\n@@ -102,7 +113,6 @@ def fetch_data(self) -> Any:\n         :returns: The parsed JWK Set as a dictionary.\n         :raises PyJWKClientConnectionError: If the HTTP request fails.\n         \"\"\"\n-        jwk_set: Any = None\n         try:\n             r = urllib.request.Request(url=self.uri, headers=self.headers)\n             with urllib.request.urlopen(\n@@ -115,11 +125,14 @@ def fetch_data(self) -> Any:\n             raise PyJWKClientConnectionError(\n                 f'Fail to fetch data from the url, err: \"{e}\"'\n             ) from e\n-        else:\n-            return jwk_set\n-        finally:\n-            if self.jwk_set_cache is not None:\n-                self.jwk_set_cache.put(jwk_set)\n+\n+        # Only update the cache on a successful fetch. Writing in a\n+        # `finally` block with `jwk_set=None` on error clears any\n+        # previously-cached JWKS, turning a transient outage into a cache\n+        # wipe that breaks legitimate auth.\n+        if self.jwk_set_cache is not None:\n+            self.jwk_set_cache.put(jwk_set)\n+        return jwk_set\n \n     def get_jwk_set(self, refresh: bool = False) -> PyJWKSet:\n         \"\"\"Return the JWK Set, using the cache when available."},{"sha":"f449b42f84104c30077d92c66933c50c4001e573","filename":"pyproject.toml","status":"modified","additions":20,"deletions":18,"changes":38,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/pyproject.toml","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/pyproject.toml","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/pyproject.toml?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -4,6 +4,26 @@ requires = [\n     \"setuptools>=77.0.3\",\n ]\n \n+[dependency-groups]\n+dev = [\n+    \"coverage[toml]==7.10.7\",\n+    \"cryptography>=3.4.0\",\n+    \"pre-commit\",\n+    \"pytest>=8.4.2,<9.0.0\",\n+    \"sphinx\",\n+    \"sphinx-rtd-theme\",\n+    \"zope.interface\",\n+]\n+docs = [\n+    \"sphinx\",\n+    \"sphinx-rtd-theme\",\n+    \"zope.interface\",\n+]\n+tests = [\n+    \"coverage[toml]==7.10.7\",\n+    \"pytest>=8.4.2,<9.0.0\",\n+]\n+\n [project]\n authors = [\n     { email = \"hello@jpadilla.com\", name = \"Jose Padilla\" },\n@@ -46,24 +66,6 @@ requires-python = \">=3.9\"\n crypto = [\n     \"cryptography>=3.4.0\",\n ]\n-dev = [\n-    \"coverage[toml]==7.10.7\",\n-    \"cryptography>=3.4.0\",\n-    \"pre-commit\",\n-    \"pytest>=8.4.2,<9.0.0\",\n-    \"sphinx\",\n-    \"sphinx-rtd-theme\",\n-    \"zope.interface\",\n-]\n-docs = [\n-    \"sphinx\",\n-    \"sphinx-rtd-theme\",\n-    \"zope.interface\",\n-]\n-tests = [\n-    \"coverage[toml]==7.10.7\",\n-    \"pytest>=8.4.2,<9.0.0\",\n-]\n \n [project.readme]\n content-type = \"text/x-rst\""},{"sha":"a1c902b0f8f9d1f966dbdf920eac9a340e2f27ea","filename":"tests/test_algorithms.py","status":"modified","additions":64,"deletions":4,"changes":68,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/tests%2Ftest_algorithms.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/tests%2Ftest_algorithms.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/tests%2Ftest_algorithms.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -122,6 +122,38 @@ def test_hmac_from_jwk_should_raise_exception_if_empty_json(self) -> None:\n             with pytest.raises(InvalidKeyError):\n                 algo.from_jwk(keyfile.read())\n \n+    @pytest.mark.parametrize(\"empty_key\", [\"\", b\"\"])\n+    def test_hmac_prepare_key_rejects_empty_key(\n+        self, empty_key: Union[str, bytes]\n+    ) -> None:\n+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)\n+\n+        with pytest.raises(InvalidKeyError, match=\"must not be empty\"):\n+            algo.prepare_key(empty_key)\n+\n+    @pytest.mark.parametrize(\n+        \"jwk_file\",\n+        [\n+            \"jwk_rsa_pub.json\",\n+            \"jwk_ec_pub_P-256.json\",\n+            \"jwk_okp_pub_Ed25519.json\",\n+            \"jwk_hmac.json\",\n+        ],\n+    )\n+    def test_hmac_prepare_key_rejects_jwk_json(self, jwk_file: str) -> None:\n+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)\n+\n+        with open(key_path(jwk_file)) as keyfile:\n+            with pytest.raises(InvalidKeyError, match=\"looks like a JWK\"):\n+                algo.prepare_key(keyfile.read())\n+\n+    def test_hmac_prepare_key_accepts_json_without_kty(self) -> None:\n+        # JSON that doesn't look like a JWK (no \"kty\") should not be misclassified.\n+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)\n+\n+        key = algo.prepare_key('{\"this\": \"is just a json-shaped secret\"}')\n+        assert key == b'{\"this\": \"is just a json-shaped secret\"}'\n+\n     @crypto_required\n     def test_rsa_should_parse_pem_public_key(self) -> None:\n         algo = RSAAlgorithm(RSAAlgorithm.SHA256)\n@@ -1452,11 +1484,10 @@ def test_hmac_short_key_warns_by_default_hs512(self) -> None:\n         assert msg is not None\n         assert \"64\" in msg\n \n-    def test_hmac_empty_key_returns_warning_message(self) -> None:\n+    def test_hmac_empty_key_rejected_outright(self) -> None:\n         algo = HMACAlgorithm(HMACAlgorithm.SHA256)\n-        key = algo.prepare_key(b\"\")\n-        msg = algo.check_key_length(key)\n-        assert msg is not None\n+        with pytest.raises(InvalidKeyError, match=\"must not be empty\"):\n+            algo.prepare_key(b\"\")\n \n     def test_hmac_exact_minimum_no_warning(self) -> None:\n         algo = HMACAlgorithm(HMACAlgorithm.SHA256)\n@@ -1603,6 +1634,35 @@ def test_pyjwt_decode_enforces_short_hmac_key(self) -> None:\n         with pytest.raises(InvalidKeyError):\n             pyjwt_enforce.decode(token, \"short\", algorithms=[\"HS256\"])\n \n+    def test_pyjwt_decode_honors_per_call_enforce_minimum_key_length(self) -> None:\n+        # Regression: per-call options passed to PyJWT.decode() must be\n+        # forwarded to the JWS verification layer. enforce_minimum_key_length\n+        # was previously dropped between PyJWT.decode_complete and\n+        # PyJWS._verify_signature.\n+        import jwt\n+\n+        adequate_key = \"a\" * 32\n+        token = jwt.encode({\"hello\": \"world\"}, adequate_key, algorithm=\"HS256\")\n+\n+        # Module-level singleton path with per-call enforcement.\n+        with pytest.raises(InvalidKeyError, match=\"below\"):\n+            jwt.decode(\n+                token,\n+                \"short\",\n+                algorithms=[\"HS256\"],\n+                options={\"enforce_minimum_key_length\": True},\n+            )\n+\n+        # PyJWT() instance path (instance default off, per-call on).\n+        pyjwt = jwt.PyJWT()\n+        with pytest.raises(InvalidKeyError, match=\"below\"):\n+            pyjwt.decode(\n+                token,\n+                \"short\",\n+                algorithms=[\"HS256\"],\n+                options={\"enforce_minimum_key_length\": True},\n+            )\n+\n     def test_pyjwt_encode_no_warning_adequate_key(self) -> None:\n         import warnings\n "},{"sha":"d1b453ebf9529f5ea58c4244c362958d6cfa643b","filename":"tests/test_api_jws.py","status":"modified","additions":120,"deletions":10,"changes":130,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/tests%2Ftest_api_jws.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/tests%2Ftest_api_jws.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/tests%2Ftest_api_jws.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -9,10 +9,11 @@\n from jwt.exceptions import (\n     DecodeError,\n     InvalidAlgorithmError,\n+    InvalidKeyError,\n     InvalidSignatureError,\n     InvalidTokenError,\n )\n-from jwt.utils import base64url_decode\n+from jwt.utils import base64url_decode, base64url_encode\n from jwt.warnings import RemovedInPyjwt3Warning\n \n from .utils import crypto_required, key_path, no_crypto_required\n@@ -397,6 +398,33 @@ def test_decodes_with_jwk_and_mismatched_algorithm(\n         with pytest.raises(InvalidAlgorithmError):\n             jws.decode(example_jws, jwk)\n \n+    def test_decodes_with_jwk_rejects_header_alg_outside_jwk_alg(\n+        self, jws: PyJWS\n+    ) -> None:\n+        # Token header says HS256 and the caller's allow-list also accepts\n+        # HS256, but the PyJWK is bound to HS512. Even though the allow-list\n+        # would pass, verification must be locked to the PyJWK's algorithm\n+        # rather than the header's — otherwise an attacker who controls a\n+        # registered key can advertise a disallowed algorithm in the header\n+        # and have it accepted.\n+        jwk = PyJWK(\n+            {\n+                \"kty\": \"oct\",\n+                \"alg\": \"HS512\",\n+                \"k\": \"c2VjcmV0\",  # \"secret\"\n+            }\n+        )\n+        example_jws = (\n+            b\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.\"\n+            b\"aGVsbG8gd29ybGQ.\"\n+            b\"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM\"\n+        )\n+\n+        with pytest.raises(\n+            InvalidAlgorithmError, match=\"does not match the key's algorithm\"\n+        ):\n+            jws.decode(example_jws, jwk, algorithms=[\"HS256\", \"HS512\"])\n+\n     # 'Control' Elliptic Curve jws created by another library.\n     # Used to test for regressions that could affect both\n     # encoding / decoding operations equally (causing tests\n@@ -511,18 +539,16 @@ def test_no_secret(self, jws: PyJWS, payload: bytes) -> None:\n         right_secret = \"foo\"\n         jws_message = jws.encode(payload, right_secret)\n \n-        with pytest.raises(DecodeError):\n+        with pytest.raises(InvalidKeyError, match=\"must not be empty\"):\n             jws.decode(jws_message, algorithms=[\"HS256\"])\n \n     def test_verify_signature_with_no_secret(self, jws: PyJWS, payload: bytes) -> None:\n         right_secret = \"foo\"\n         jws_message = jws.encode(payload, right_secret)\n \n-        with pytest.raises(DecodeError) as exc:\n+        with pytest.raises(InvalidKeyError, match=\"must not be empty\"):\n             jws.decode(jws_message, algorithms=[\"HS256\"])\n \n-        assert \"Signature verification\" in str(exc.value)\n-\n     def test_verify_signature_with_no_algo_header_throws_exception(\n         self, jws: PyJWS, payload: bytes\n     ) -> None:\n@@ -957,13 +983,97 @@ def test_encode_detached_content_with_b64_header(\n         assert \"b64\" not in msg_header_obj\n         assert msg_payload\n \n-    def test_decode_detached_content_without_proper_argument(self, jws: PyJWS) -> None:\n-        example_jws = (\n-            \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2V9\"\n-            \".\"\n-            \".65yNkX_ZH4A_6pHaTL_eI84OXOHtfl4K0k5UnlXZ8f4\"\n+    def test_encode_b64_false_auto_adds_b64_to_crit(\n+        self, jws: PyJWS, payload: bytes\n+    ) -> None:\n+        # RFC 7797 §3: producers MUST list \"b64\" in \"crit\" whenever \"b64\"\n+        # appears in the protected header.\n+        secret = \"secret\"\n+        token = jws.encode(payload, secret, algorithm=\"HS256\", is_payload_detached=True)\n+\n+        msg_header, _, _ = token.split(\".\")\n+        header_obj = json.loads(base64url_decode(msg_header.encode()))\n+\n+        assert header_obj[\"b64\"] is False\n+        assert \"b64\" in header_obj.get(\"crit\", [])\n+\n+    def test_encode_b64_false_preserves_existing_crit_entries(\n+        self, jws: PyJWS, payload: bytes\n+    ) -> None:\n+        secret = \"secret\"\n+        # Caller-supplied crit (containing a hypothetical extension that\n+        # PyJWT does support) should be preserved alongside the auto-added\n+        # \"b64\" marker.\n+        token = jws.encode(\n+            payload,\n+            secret,\n+            algorithm=\"HS256\",\n+            headers={\"b64\": False, \"crit\": [\"b64\"]},\n         )\n+\n+        header_obj = json.loads(base64url_decode(token.split(\".\")[0].encode()))\n+        assert header_obj[\"crit\"] == [\"b64\"]\n+\n+    def test_decode_b64_false_rejects_non_empty_payload_segment(\n+        self, jws: PyJWS, payload: bytes\n+    ) -> None:\n+        # RFC 7515 Appendix F detached form: when b64=false, the compact-\n+        # serialization payload segment must be empty. PyJWT must reject a\n+        # non-empty middle segment without doing any base64-decoding work\n+        # on it — that decode used to be the unauthenticated DoS amplifier.\n+        secret = \"secret\"\n+        import hmac as _hmac\n+        import hashlib as _hashlib\n+\n+        header_obj = {\n+            \"typ\": \"JWT\",\n+            \"alg\": \"HS256\",\n+            \"b64\": False,\n+            \"crit\": [\"b64\"],\n+        }\n+        header_b64 = base64url_encode(\n+            json.dumps(header_obj, separators=(\",\", \":\")).encode()\n+        )\n+        # Stuff the middle segment with arbitrary attacker-controlled bytes.\n+        # This should be rejected without being base64-decoded.\n+        attacker_segment = b\"A\" * 1024\n+        signing_input = b\".\".join([header_b64, payload])\n+        sig = _hmac.new(secret.encode(), signing_input, _hashlib.sha256).digest()\n+        token = b\".\".join(\n+            [header_b64, attacker_segment, base64url_encode(sig)]\n+        ).decode()\n+\n+        with pytest.raises(DecodeError, match=\"Payload segment must be empty\"):\n+            jws.decode(token, secret, algorithms=[\"HS256\"], detached_payload=payload)\n+\n+    def test_decode_b64_false_without_crit_b64_is_rejected(\n+        self, jws: PyJWS, payload: bytes\n+    ) -> None:\n+        # Hand-craft a non-compliant token: header has b64=false but no\n+        # crit:[\"b64\"]. Per RFC 7797 §3, such tokens are malformed and must\n+        # be rejected even though PyJWT understands b64.\n+        secret = \"secret\"\n+        import hmac as _hmac\n+        import hashlib as _hashlib\n+\n+        header_obj = {\"typ\": \"JWT\", \"alg\": \"HS256\", \"b64\": False}\n+        header_b64 = base64url_encode(\n+            json.dumps(header_obj, separators=(\",\", \":\")).encode()\n+        )\n+        signing_input = b\".\".join([header_b64, payload])\n+        sig = _hmac.new(secret.encode(), signing_input, _hashlib.sha256).digest()\n+        token = b\".\".join([header_b64, b\"\", base64url_encode(sig)]).decode()\n+\n+        with pytest.raises(InvalidTokenError, match=\"b64.*crit\"):\n+            jws.decode(token, secret, algorithms=[\"HS256\"], detached_payload=payload)\n+\n+    def test_decode_detached_content_without_proper_argument(\n+        self, jws: PyJWS, payload: bytes\n+    ) -> None:\n         example_secret = \"secret\"\n+        example_jws = jws.encode(\n+            payload, example_secret, algorithm=\"HS256\", is_payload_detached=True\n+        )\n \n         with pytest.raises(DecodeError) as exc:\n             jws.decode(example_jws, example_secret, algorithms=[\"HS256\"])"},{"sha":"da686645e14fbb028e6ad43e6550bb5848eda83f","filename":"tests/test_jwks_client.py","status":"modified","additions":44,"deletions":2,"changes":46,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/tests%2Ftest_jwks_client.py","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/tests%2Ftest_jwks_client.py","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/tests%2Ftest_jwks_client.py?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -288,18 +288,30 @@ def test_get_jwt_set_cache_disabled(self) -> None:\n \n         assert repeated_call.call_count == 1\n \n-    def test_get_jwt_set_failed_request_should_clear_cache(self) -> None:\n+    def test_get_jwt_set_failed_refresh_preserves_cached_jwks(self) -> None:\n+        # Regression: a transient fetch failure used to clear the cache via\n+        # the previous `finally: put(jwk_set=None)` pattern, turning one bad\n+        # request from the JWKS endpoint into application-wide auth failure.\n+        # The cache must survive.\n         url = \"https://dev-87evx9ru.auth0.com/.well-known/jwks.json\"\n \n         jwks_client = PyJWKClient(url)\n         with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID):\n             jwks_client.get_jwk_set()\n \n+        assert jwks_client.jwk_set_cache is not None\n+        assert jwks_client.jwk_set_cache.get() is not None\n+\n         with pytest.raises(PyJWKClientError):\n             with mocked_failed_response():\n                 jwks_client.get_jwk_set(refresh=True)\n \n-            assert jwks_client.jwk_set_cache is None\n+        cached = jwks_client.jwk_set_cache.get()\n+        assert cached is not None\n+        # Subsequent reads still serve from cache without another fetch.\n+        with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as call:\n+            jwks_client.get_jwk_set()\n+        assert call.call_count == 0\n \n     def test_failed_request_should_raise_connection_error(self) -> None:\n         token = \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA\"\n@@ -344,6 +356,36 @@ def test_get_jwt_set_invalid_lifespan(self) -> None:\n             jwks_client = PyJWKClient(url, lifespan=-1)\n             assert jwks_client is None\n \n+    @pytest.mark.parametrize(\n+        \"uri\",\n+        [\n+            \"file:///etc/passwd\",\n+            \"ftp://example.org/keys.json\",\n+            'data:application/json,{\"keys\":[]}',\n+            \"/etc/passwd\",  # urlparse gives scheme=\"\" — also rejected\n+            \"ldap://internal.test/jwks\",\n+        ],\n+    )\n+    def test_pyjwkclient_rejects_non_http_schemes(self, uri: str) -> None:\n+        # urllib's default OpenerDirector handles file://, ftp://, and data:\n+        # URIs. PyJWKClient must reject these so callers can't be tricked\n+        # into reading attacker-controlled local files or other unintended\n+        # schemes via a manipulated URI.\n+        with pytest.raises(PyJWKClientError, match=\"Invalid JWKS URI scheme\"):\n+            PyJWKClient(uri)\n+\n+    @pytest.mark.parametrize(\n+        \"uri\",\n+        [\n+            \"http://localhost/jwks.json\",\n+            \"https://example.test/jwks.json\",\n+            \"HTTPS://Example.Test/jwks.json\",  # case-insensitive\n+        ],\n+    )\n+    def test_pyjwkclient_accepts_http_https_schemes(self, uri: str) -> None:\n+        # Construction succeeds; no fetch is made until get_jwk_set().\n+        PyJWKClient(uri)\n+\n     def test_get_jwt_set_timeout(self) -> None:\n         url = \"https://dev-87evx9ru.auth0.com/.well-known/jwks.json\"\n         jwks_client = PyJWKClient(url, timeout=5)"},{"sha":"fa2c454182d5fc853846fdbb04db1a303bf57d3b","filename":"tox.ini","status":"modified","additions":8,"deletions":5,"changes":13,"blob_url":"https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/tox.ini","raw_url":"https://github.com/jpadilla/pyjwt/raw/7144e4534c34810f4525dc4578a32addd8212cff/tox.ini","contents_url":"https://api.github.com/repos/jpadilla/pyjwt/contents/tox.ini?ref=7144e4534c34810f4525dc4578a32addd8212cff","patch":"@@ -39,17 +39,19 @@ wheel_build_env = build_wheel\n # https://github.com/pypa/setuptools/issues/1042 from breaking our builds.\n setenv =\n     VIRTUALENV_NO_DOWNLOAD=1\n-extras =\n+dependency_groups =\n     tests\n+extras =\n     crypto: crypto\n commands = {envpython} -b -m coverage run -m pytest {posargs}\n \n \n [testenv:docs]\n # The tox config must match the ReadTheDocs config.\n basepython = python3.11\n-extras =\n+dependency_groups =\n     docs\n+extras =\n     crypto\n commands =\n     sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html\n@@ -58,8 +60,9 @@ commands =\n \n \n [testenv:py{39,310,311,312,313,314}{,-crypto}-mypy]\n-extras =\n+dependency_groups =\n     tests\n+extras =\n     crypto: crypto\n deps =\n     mypy\n@@ -69,8 +72,8 @@ commands =\n     mypy\n \n [testenv:lint]\n-basepython = python3.9\n-extras = dev\n+basepython = python3.10\n+dependency_groups = dev\n passenv = HOMEPATH  # needed on Windows\n commands = pre-commit run --all-files\n "}]}