lestrrat-go / jwx

Implementation of various JWx (Javascript Object Signing and Encryption/JOSE) technologies
MIT License
1.95k stars 164 forks source link

How am I supposed to parse tokens created with: jwt.NewSerializer. #1133

Closed advdv closed 5 months ago

advdv commented 5 months ago

Thank you for this great library! I'm trying to store a refresh token in an encrypted JWT. Thinking I don't want to implement such critical security functionality from scratch I figured I used the high-level jwt.NewSerializer. It seems to allow for signing and encrypting a payload. But I don't understand how I'm supposed to parse it. I always seem to get "unsupported format (#2)"

I great the token like this

serialized, err := jwt.NewSerializer().
Encrypt(jwt.WithKey(encryptKey.Algorithm(), encryptKey)).
Sign(jwt.WithKey(signKey.Algorithm(), signKey)).
Serialize(tok)
if err != nil {
return "", fmt.Errorf("failed to serialize session token: %w", err)
}

The keys come from two key sets. One for encryption, one of signing (as I understand that we require different key for this). I then go ahead an try to parse it like this:

session, err := jwt.ParseString(cookie.Value,
   jwt.WithClock(e.clock),
   jwt.WithKeySet(e.keys.encrypt.private),
   jwt.WithKeySet(e.keys.signing.public))
if err != nil {
    return "", fmt.Errorf("failed to parse session cookie: %w", err)
}

The e.keys, looks like this:

// Keys hold our own private keys, and the WorkOS public keys.
type Keys struct {
    cfg    Config
    workos struct {
        public jwk.Set
    }
    signing struct {
        private jwk.Set
        public  jwk.Set
    }
    encrypt struct {
        private jwk.Set
        public  jwk.Set
    }
}

This is an example token: eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.ZXlKaGJHY2lPaUpTVTBFdFQwRkZVQ0lzSW1WdVl5STZJa0V5TlRaSFEwMGlMQ0owZVhBaU9pSktWMVFpZlEubFVCR1NhcmFJOG9Sd29vRTBKblhnb3BFNndyS3gzZWxVUWdYczdSQ1RtUlpIR0NSVi1hdlpDdjQzUmltbUJ6RC1pa1JSWDJXNnVEcGVnbV9ZbVlxU3JKV2h2RW92SlRoSWpfQXJDaXN1OGR0M1NHRWJvR1RrWmU4TUE1VkN4RTFxNWthQTJ0bHhzeU5xWDN6MGcyd0daWXZSZ3JHSDJQZk5PVkxiQ2RxX0hmZmE5TER4U2pKNlRiZ0ZWZi1FSjJyMkdTS09HbHFFU2xPRGtoalp1b25RMXRkaWZvcDlDRXdsclp3S0ZPOUdRclBITkQxMkQwNmJvaVhnemhWWGUxTldsRklya3VLZ2xndXNoWGI1aDBHLWUzNnh4WWZEZUpOMTBJNXFtSTg1Y0tKV05vaHREWEtWa2JTaV94dnIwMWJCTGMzWHVSQmNabVJFSDdKQjdTYllBLklOUEY0YmlHbFlTTkpSdUkuWnNIQVVaeEtzb0tGTV84OUFVTDY2YU1GX21sUGFwYUNUSjEtLkdqNXNpRElRUWZvZ2YzR2pIY1JLOXc.w6o725mvW4bnZJ1XlzvnqAIQxIg13imuJwE6pLR1uyc

The testing keys I'm using look like this (well-known, just used for testing):

{
    "keys": [
        {
            "kty": "EC",
            "d": "Qj0DkYfqE4jJqB7iPhCnsQJet2po3014OXAXzOhZSds",
            "use": "sig",
            "crv": "P-256",
            "kid": "key1",
            "x": "6WoYjtPv1EbyPpqzdhn5sTcyxHnDS6hgoy1aJ6iZVAc",
            "y": "fQGoUnduRNTPzC3KnRlv8wcrghf9c1BH7BdDm5EEWG8",
            "alg": "ES256"
        }
    ]
}

And

{
    "keys": [
        {
            "kty": "EC",
            "d": "4DWqDmifdqsu3AJX_kcZYtDwA1ypD_XY24svDAqvV4k",
            "use": "enc",
            "crv": "P-256",
            "kid": "key1",
            "x": "LaQF_NldYtMMRTZ9tBc9HPwJDIA51VCMDGbQyUxTL-8",
            "y": "73PK1Y6VKBK_9_Ym1WYPyoff0Js5t7TiLISVDWCEjro",
            "alg": "ECDH-ES+A128KW"
        }
    ]
}
lestrrat commented 5 months ago

You are ENCRYPTING and then SIGNING. jwt.ParseXXXX only does signature verification/unwrapping. You need to use jwe.Decrypt yourself to decrypt your token before calling jwt.ParseXXX (or jws.Verify)

advdv commented 5 months ago

Ah, ok. Thank you for the super fast reply. I did try that (see below) but I got the error compact JWE format must have five parts (3) so I figured I should leave it to a higher level abstraction to do it. I wil do more research and learn about what "compact" means.

    decrypted, err := jwe.Decrypt([]byte(cookie.Value),
        jwe.WithKeySet(e.keys.encrypt.public))
    if err != nil {
        return "", fmt.Errorf("failed to decrypt session cookie: %w", err)
    }
lestrrat commented 5 months ago

An, sorry, in your case you need to jws.Verify, then jwe.Decrypt, and then finally jwt.Parse.

That is, you are doing signed = Sign(Encrypt(JWT_payload)), so in order to get back the JWT_payload, you need to do ParseJWT(Decrypt(Verify(signed))) I highly suggest you look into how these messages are constructed from the RFCs or similar.

advdv commented 5 months ago

I'm now doing the following:

    verified, err := jws.Verify([]byte(cookie.Value), jws.WithKeySet(e.keys.signing.public))
    if err != nil {
        return "", fmt.Errorf("failed to verify session token: %w", err)
    }

    decrypted, err := jwe.Decrypt(verified, jwe.WithKeySet(e.keys.encrypt.private))
    if err != nil {
        return "", fmt.Errorf("failed to decrypt session token: %w", err)
    }

    fmt.Println(string(decrypted)) // {"rt":"some.refresh.token"}

    parsed, err := jwt.Parse(decrypted,
        jwt.WithClock(e.clock),
        jwt.WithKeySet(e.keys.signing.public))
    if err != nil {
        return "", fmt.Errorf("failed to parse session token: %w", err)
    }

But it will fail on the final parse because "decrypted" now just looks like this: {"rt":"some.refresh.token"}. I will look into how this exactly works but I just want to report it here in case it's unexpected. The error is : failed to unmarshal jws message: required field "signatures" not present

Ofcourse, I now have the data I was looking for so it is fine. Just reporting here in case it's unexpected

lestrrat commented 5 months ago

The last error is because you're trying to jwt.Parse with the key set -- that is, you're verifying the message signature. If the payload you end up with is a JSON message, you could either simply use json.Unmarshal, or use jwt.Parse with the jwt.WithVerify(false) option. I'm not claiming I have the best documentation, but these are all documented, so please take a bit of time looking at the documentation or the examples directory.

advdv commented 5 months ago

I will, thank you again for the quick responses (and the great library). I will close this, maybe others in the future have use for the information in this thread.

lestrrat commented 5 months ago

no prob. Thanks for the kind words