authlib / joserfc

Implementations of JOSE RFCs in Python
https://jose.authlib.org
BSD 3-Clause "New" or "Revised" License
84 stars 8 forks source link

Migrating from python-jose #25

Open maciejstromich opened 2 months ago

maciejstromich commented 2 months ago

Currently doing migration from python-jose to joserfc and found myself in a bit of a pickle.

python-jose implementation allows validation of the token against the public_key (using verify method from RSAKey https://github.com/mpdavis/python-jose/blob/master/jose/backends/rsa_backend.py#L206).

As our codebase does that I was looking for a way to achieve the same with joserfc but the only reference to RSAKey verification I found in the docs was in https://jose.authlib.org/en/guide/jwk/#options where I can pass additional parameters to the RSAKey.import_key but it does not explain exactly how does the verification works. Could you shed some additional light on how to approach this issue?

here's a pseudo code

def validate(public_key: jwk.RSAKey, token: bytes) -> bool:
    """Validate a token against a public key"""
    message, encoded_signature = token.rsplit(b".", 1)
    decoded_signature = base64url_decode(encoded_signature)
    return public_key.verify(message, decoded_signature)
swails commented 2 months ago

I've used jwt.decode() to do this:

def validate(public_key: jwk.RSAKey, token: str) -> bool:
    try:
        jwt.decode(token, public_key)
    except BadSignatureError:
        return False
    except JoseError:
        raise  # This will hit if, for instance, the token is an invalid token
    else:
        return True

This will just validate the signature. I combine this with a jwt.JWTClaimsRegistry in order to do further validation of the token. This will catch things like token expiry, and you can configure it to check for the existence, and value, of certain claims like the issuer or audience.

lepture commented 2 months ago

@swails is correct. It would be great if you can show me how are you using python-jose, I'll add a documentation about migration from python-jose, just like https://jose.authlib.org/en/migrations/pyjwt/

swails commented 2 months ago

The title of this issue is sufficient for me to share some of my migration experience from python-jose, so I'll just drop them here.

jose.jwt.get_unverified_header

The unverified header gives the signing key ID, so I grab the expected signing key by fetching them from the issuer and grabbing the one whose id matches what's in the header (but obviously you can't verify the token before you know which key to use).

# Equivalent to `jose.jwt.get_unverified_header`
from typing import Any
from joserfc import jws

def get_unverified_header(token: str) -> dict[str, Any]:
    token_content = jws.extract_compact(token.encode())
    return token_content.protected

jose.jwt.get_unverified_claims

I created an additional identity provider with the capability to create and sign tokens (storing a signing key in AWS KMS in a hardware security module). The signing key was either an Azure signing key minted by Microsoft or it was the signing key I created in AWS. In order to identify which key I should use to verify, I needed to check the issuer (it would either be Microsoft or my own). Then I would know which issuer to fetch public keys from so I could verify the signature.

# Equivalent to `jose.jwt.get_unverified_claims`
from typing import Any
from joserfc import jws

def get_unverified_claims(token: str) -> dict[str, Any]:
    token_content = jws.extract_compact(token.encode())
    return json.loads(token_content.payload)
maciejstromich commented 2 months ago

thanks @swails. That helped a lot to straighten my spaghetti approach :-)

I will add also

jose.jwk.construct

from joserfc import jwk

def return_jwks_keys():
    parameters = {"use": "sig", "alg": "RS256", "key_ops": ["verify"]}
    return {item["kid"]: jwk.RSAKey.import_key(item, parameters=parameters) for item in jwks_json}