jpadilla / pyjwt

JSON Web Token implementation in Python
https://pyjwt.readthedocs.io
MIT License
5.1k stars 682 forks source link

Migration guide for python-jose users #942

Open lsmith77 opened 8 months ago

lsmith77 commented 8 months ago

It appears that python-jose is unmaintained, itself depends on unmaintaiend projects and now also suffers from dependencies with security vulnerabilities: https://github.com/mpdavis/python-jose/issues/341

As such I am looking to migrate to this package. Most of the methods appear to be 100% API compatible. There is no get_unverified_claims() but jwt.decode(token, options={"verify_signature": False}) is easy enough to use in its place though having a dedicated function with such a clear name might be a good idea to facilitate defensive coding practices.

Where I am struggeling is that with python-jose I can pass in the RSA key as a dict (containing the n, e etc. values) to jwt.decode() rather than a PEM-formatted public key as expected in this package. I didn't find a function to generate a PEM in this package. Or am I missing something?

lsmith77 commented 8 months ago

I have looked through the python-json code and I think I have a working version to create a PEM from n/e values:

# Copied from https://github.com/mpdavis/python-jose/blob/master/jose/utils.py - MIT License
def int_arr_to_long(arr):
    return int("".join(["%02x" % byte for byte in arr]), 16)

# Copied from https://github.com/mpdavis/python-jose/blob/master/jose/utils.py - MIT License
def base64_to_long(data):
    if isinstance(data, str):
        data = data.encode("ascii")

    # urlsafe_b64decode will happily convert b64encoded data
    _d = base64.urlsafe_b64decode(bytes(data) + b"==")
    return int_arr_to_long(struct.unpack("%sB" % len(_d), _d))

# Inspired by https://github.com/mpdavis/python-jose/blob/master/jose/backends/rsa_backend.py - MIT License
def convert_to_pem(n, e):
    rsa_key = pyrsa.PublicKey(e=base64_to_long(e), n=base64_to_long(n))
    der = rsa_key.save_pkcs1(format="DER")
    return pyrsa_pem.save_pem(der, pem_marker="RSA PUBLIC KEY")
jpmckinney commented 6 months ago

get_unverified_header

from jose import jwt
jwt.get_unverified_header(token)
import jwt
jwt.get_unverified_header(token)

base64url_decode

from jose.utils import base64url_decode

https://github.com/mpdavis/python-jose/blob/4b0701b46a8d00988afcc5168c2b3a1fd60d15d8/jose/utils.py#L72-L77

from jwt.utils import base64url_decode

https://github.com/jpadilla/pyjwt/blob/7b4bc844b9d4c38a8dbba1e727f963611124dd5b/jwt/utils.py#L28-L33

JWTError

jose's JWTError's subclasses are JWTClaimsError and ExpiredSignatureError.

from jose import JWTError

https://github.com/mpdavis/python-jose/blob/4b0701b46a8d00988afcc5168c2b3a1fd60d15d8/jose/exceptions.py#L17

from jwt import InvalidTokenError

https://github.com/jpadilla/pyjwt/blob/72ad55f6d7041ae698dc0790a690804118be50fc/jwt/exceptions.py#L9

pyjwt has ExpiredSignatureError, and multiple errors to cover JWTClaimsError.

jpmckinney commented 6 months ago

For my needs, I just haven't figured out the replacement for jwk.construct() (docs).

I probably need to do something with PyJWK?

from jose import jwk
key = jwk.construct(data)
key.verify(msg, sig)
import jwt
obj = jwt.PyJWK(data)
# ???

Edit: This seems to work:

import jwt
obj = jwt.PyJWK(data)
alg_obj = obj.Algorithm
prepared_key = alg_obj.prepare_key(obj.key)
alg_obj.verify(msg, prepared_key, sig)