Skip to content

PyJWT v2.11.0 algorithm of passed PyJWT is no longer preferred #1147

@beje2k15

Description

@beje2k15

After upgrading to 2.11.0 my unittests for JWT auth no longer worked. After debugging I found that the algorithm is no longer extracted from the supplied PyJWK, but uses the default "HS256," which does not work as I provide an RS256 key.

Expected Result

Using jwt.encode with key= the algorithm should be derived from it, as stated in the documentation:

:param algorithm: algorithm to sign the token with, e.g. "ES256".
If headers includes alg, it will be preferred to this parameter.
If key is a :class:PyJWK object, by default the key algorithm will be used.
:type algorithm: str or None

Actual Result

The parameter is has a default value of "HS256" which prevents the algorithm being set from the PyJWK.

In jwt/api_jwt.py: PyJWT.encode the algorithm defaults to "HS256" and without further inspection it is passed to jwt/api_jws.py: PyJWS.encode (here it would default again to HS256 if not set) From line 130 we have this code:

# declare a new var to narrow the type for type checkers
if algorithm is None:
    if isinstance(key, PyJWK):
        algorithm_ = key.algorithm_name
    else:
        algorithm_ = "none"
else:
    algorithm_ = algorithm

The default value will not trigger this inspection so the PyJWT never gets checked, unless algorithm is explicitly set to None.

I consider this a bug, as the documentation also indicates another behavior.

Reproduction Steps

>>> import jwt
>>>
>>> my_key = jwt.PyJWK.from_dict({
...     "alg": "RS256",
...     "d": "Jmtb4kCoDJdHb1ph-QQIWXXLpqe94U2SzwuyrnYZV8NZE296kzc2cKhje4b5ocpOtOeGSDKYzC_5Zdm0e7_dx-qTSChKxVNzNGmqIyGXctHxQQM_tD4wq3Fnxi55eCdy3EdP6bkPTLAMqQlnKlfFXutnfpxSQt4us7tneN8XD94zr0S8_1uYfaayZr2VivxzRuhzx_FBEt_PIQC4CxmHO4RbYopNbBK_Cri0hiMSSwTOpqFf53o1iJesiGlYl2Aqtm-yW3aGSdGyMTmjk9kAqoT4OqbfVlb8ba6vxVOwHnjqpD11xtXEDG2CEBsOkx-VDfOG8_TCwDrVFF1zY86h",
...     "dp": "dpuIVlc-S-5y9hWUQWUUsJacCMZNlyS6wJPPhatxKkjrmSq0HMmL74eN4U1m7T2zUFQ5lIykqZoWD8vmKJQLC8dMLYmsR8t3fz3Grs6O4q_vWWrbjYjn0s5UlkzvE3rzRbBL8JzY9mSZWHcRWQ09nYhE6trBVYR4SdWHumqqHPk",
...     "dq": "Kqx5thwL_Jlwlfgs_vozKnPXKHe10kkAba2FMy1AB2Wznse9gxZ6vD8Mn7PA5s7QEaU0L_7erDrvr99zu2KPxEjon4fX0Go1Ql6qm8ASxOyxpyH3tblEjdGwYuxlGA9p54lKlGwZJytvBXzvCFHe7LLWXorByAYGOQsPFy0qEqU",
...     "e": "AQAB",
...     "kid": "b6161f4b30444e29912617a83fa45289",
...     "kty": "RSA",
...     "n": "6MVwn2a2DCxmGGjfcwb02GICHah2Qrkt4FyBtofm9rOYmvYW86qpq72p6fLPDa1QIzsaCXjrS4gKwii5tC8gKP9F9gOSzHKfxwGc0RU-wIXUmIoRCWw4sNet8AifcpgfV8iw9MSw_VDgWnCEA3X_a0aqzbK4J7XAzwNWJ14wc0BhZ-X3Nnd8lZvqDiHroQ3IeB_PxrK6jvM0HYy7Rsc_Tj_HoOTqNEkOyHBgs_zxdy2IFQ7-sQyC2kpZVlaBt9etqLa5-JuanGjuo1s3c3y8QTmIZpLVWDBXJhDlFaAbPwFtjx4QNjYBF1LvSzNWmPEPQSrWBBwVQMSF9UUGZGGPLw",
...     "p": "9vKxr-MmSo1sKgC37QMqFc6KZqw5yb1fzu42z8mpoEre6GiqM6ABPrV0-5gg0D2N8bLVhy3a6nZEJybNQzhh6z9hJ5DBo_toQv48Cr7cZWRFZq4JQipA4dHp9wRxkk2oxX8S6pvSxleW5hVv1jfDxhviIPfcSEzSbwd4ePjnfhk",
...     "q": "8U220DI68fB56ansGiJqkSPzX1ncQvHhIs8_wuodhhzH84c6rx0QXHSk1lmwzgfWqU-DXJ3EbkyC0rNYjBawD2NfNYHoPS8sO6LPamQR9R7vm1vQ_WpLKtspaA1A0Pa45ckuHQni0-GRI3jr1tkQcjCSseCkZWyAcXjppvJgkIc",
...     "qi": "f8bc_-gJvIq4O7iKPlWiEhKHqzus5RsFwdTAUntuV8PKgO1-bKgp1uOtrDsJycaeio9sMbXIWyAyj6dUrxlqfFlERTkTGQqaFMo_w-2NFlFj_zJDNrlT9rcrbKtOkAf-P8yD7-I2CfqgSHAK-UQ1si2oeyS-dxFal0cGFdmBfYo",
...     "use": "sig"
... })
>>>
>>> jwt.encode({"test": True}, my_key, headers={"kid": my_key.key_id})  # worked until PyJWT==2.10.1
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    jwt.encode({"test": True}, my_key, headers={"kid": my_key.key_id})
    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/beje/dev/beje_dtsp/.venv/lib/python3.13/site-packages/jwt/api_jwt.py", line 153, in encode
    return self._jws.encode(
           ~~~~~~~~~~~~~~~~^
        json_payload,
        ^^^^^^^^^^^^^
    ...<4 lines>...
        sort_headers=sort_headers,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/beje/dev/beje_dtsp/.venv/lib/python3.13/site-packages/jwt/api_jws.py", line 183, in encode
    key = alg_obj.prepare_key(key)
  File "/home/beje/dev/beje_dtsp/.venv/lib/python3.13/site-packages/jwt/algorithms.py", line 343, in prepare_key
    key_bytes = force_bytes(key)
  File "/home/beje/dev/beje_dtsp/.venv/lib/python3.13/site-packages/jwt/utils.py", line 22, in force_bytes
    raise TypeError("Expected a string value")
TypeError: Expected a string value
>>> jwt.encode({"test": True}, my_key, algorithm=None, headers={"kid": my_key.key_id})  # For PyJWT==2.11.0
eyJhbGciOiJSUzI1NiIsImtpZCI6ImI2MTYxZjRiMzA0NDRlMjk5MTI2MTdhODNmYTQ1Mjg5IiwidHlwIjoiSldUIn0.eyJ0ZXN0Ijp0cnVlfQ.etsMaJB-1QaI7hshsi-YmtRmQYDujoUXwKXC7i6SyCoQnbh2e5-g4_T0G_t1cpHEZbWxQClEbYCPgONmg8kJKBZQZrXAjAhRCJ2tDP363I3YPgWkBRBN4luoOo333fImt2HbbrrzkHpVHlQ-Em6k23uvYpegp-YG313np883y6yzl1bqX8NYsKLYQhPEjLLtqSWYtyCarNPTdDRqvPESjaPuFUlQIhlRSyqelOfTTWjIMNr9xObwUkPw01h0Rfi3n2v6XXkaTASEZextK3YAINb25RWQ-9yUi4nBcuUe6FyMo1MP9WXMvUU5Goyy
CbpyQLmCDwCv081Xbvf9I7i9pg
>>>

System Information

$ python -m jwt.help
{
  "cryptography": {
    "version": "46.0.5"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.13.9"
  },
  "platform": {
    "release": "6.8.0-100-generic",
    "system": "Linux"
  },
  "pyjwt": {
    "version": "2.11.0"
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions