frasertweedale/hs-jose

Better support JWT with custom headers (doc, mostly)

Closed this issue · 12 comments

There isn't an "obvious" way to create a SignedJWT with custom headers. (The API I'm working with requires a nonce header in the JWT, which I think is somewhat common.) That is, the signJWT function from Crypto.JWT explicitly takes a JWSHeader () value, so it is not possible to pass custom headers created as documented here.

It's possible to get around this by using the signJWS function directly, to create a JWS that meets the definition of a JWT (i.e., a JWS with a payload of JSON-encoded claims), but IMO it'd be kinder to users not to make them figure out how to do this. It looks to me like signJWT can be generalized to accept custom header types in a backwards-compatible way simply by generalizing its type signature:

signJWT
  :: ( MonadRandom m, MonadError e m, AsError e
     , HasJWSHeader header, HasParams header, ProtectionIndicator p
     , ToJSON payload )
  => JWK
  -> header p  --^ Header with protection indicator p
  -> payload
  -> m SignedJWT
signJWT k h c = signJWS (encode c) (Identity (h, k))

Originally posted by @ericpashman in #121 (comment)

@ericpashman what you want is possible now, if you encode the claims payload and use signJWS directly:

signJWS (encode claims) (Identity (header, key))

But it would be best to document this a bit better, and perhaps generalise the functions as you describe or provide more general variants.

Generalizing signJWT (or providing another function that accepts custom headers to produce a SignedJWT specifically) would be a meaningful convenience for other users as minimally knowledgeable as me, I think, just because the presence of the SignedJWT type would aid the sort of type-sleuthing Haskellers tend to do when encountering a new library.

That is to say, I didn't know that a JWT was a special case of a JWS when I came to the library, so to figure out how to do this, I had to read the RFCs and learn more about the domain than I would've preferred to. 😄

Oh, perhaps what would really help would be to generalize not just the signJWT function, but also theSignedJWT type to permit it to carry custom headers:

type SignedJWT h = CompactJWS h

Then signJWT could retain its SignedJWT output type, and users wanting to produce a JWT with custom headers would not need to touch anything outside the Crypto.JWT module. As is, the JWS type must be used in this case solely because SignedJWT is defined as CompactJWS JWSHeader, which disallows custom headers for no reason that is very apparent to me.

In my case, the JWT that I needed ended up as type JWS Identity () MyHeaders, which cannot be "simplified" into a type synonym. Changing the definition of Signed JWT would simplify this to SignedJWT MyHeaders, hiding a lot of the complexity.

However, this would change the kind of SignedJWT, so it would be a breaking change.

@ericpashman yes, I want to avoid a breaking change.

I might just add some documentation for this scenario, and possibly a new type synonym e.g. type SignedJWTWithHeader h = CompactJWS h.

Hm, generalising the verifyJWT signature will be a breaking change anyway, because it will result in type ambiguity when decoding. But this lends further weight to retaining SignedJWT as-is, for simpler type annotations.

Ah. Yes, probably best just to add documentation then and leave it at that.

@ericpashman could you please review #124 and provide feedback?

Ping @ericpashman could you please review #124 and provide feedback? I will most likely merge soon as-is, if no further feedback received. Thanks!

I was hoping to play around with this to see whether it does cover my use case before responding, but I haven't had the chance to do yet.

By eye, those changes look good. If you want to go ahead and commit, I'll report back later after trying to produce a working JWT with this.

By the way, I ended up rolling my own implementation to meet my immediate need, as I ran into problems with the approach of using the library's JWS functionality to produce a JWT. (I think the problems stemmed mostly from my own abuses of the library's functionality.) I'll try to summarize that experience for you at some point; I can also give you a super-simple, working JWT-with-custom-headers implementation to test against if you want it.

Happy to review whatever feedback or use cases you provide. Regarding the timeline, I intend to merge within the week, and cut a new release in the next few weeks.

I'm happy to report that I was able to use the new, type-generalized signJWT to make a working JWT. I successfully tested this against an API in the wild that requires a couple of custom headers, a couple of custom claims, and an ES256 signature.

Thanks again, Fraser, for your efforts to get this working.

Great! Thank you for the feedback, @ericpashman.