pact-foundation / pact-reference

Reference implementations for the pact specifications
https://pact.io
MIT License
91 stars 46 forks source link

Feature request: enhance PACT to understand signed JWT payloads. #356

Open DanCorderIPV opened 5 months ago

DanCorderIPV commented 5 months ago

We are using pact to test endpoints that return signed JWTs. As JWTs are not plain json we can't specify the structure of the data using newJsonBody() etc which makes our tests quite brittle.

Signed JWTs are strings of the form <header>.<body>.<signature> where each of the three parts is base64 encoded. It would be great if the pact framework could support something like this when specifying a pact (JVM syntax):

...
.body(newJwt((jwt) -> {
    jwt.headerAlgorithm(JwsSigningAlgorithm.ES256);
    jwt.signingKeys("publicKey", "privateKey");
    jwt.payload(newJsonBody((jb) -> {
        jb.stringValue("name", "Joe Bloggs");
    }).build());
}).build())
...

https://jwt.io/ is very useful for experimenting with and validating JWTs

mefellows commented 5 months ago

A few other questions:

  1. Do you want to be able to simply verify the JWT is valid, or verify that the payload of the JWT (e.g. name in your example) are present and match any given matchers?
  2. Presumably we would also want to support this in headers and not just the body
rholshausen commented 5 months ago

This could be implemented as a plugin

DanCorderIPV commented 5 months ago

Sorry for the delay replying, I didn't see a notification - probably gone in the spam.

Anyway: 1) I would like to be able to use the existing JSON matching code within the body of the JWT. This also seems more in keeping with the existing code. 2) I assume you mean the JWT header. I'm not a JWT expert, but I think that the header values are more limited in what they can be so allowing full matching may not be necessary. To fully validate/generate a valid JWT the framework would also need to understand the header block to use the correct signing algorithm. I'm not sure if you'd want to support this (at least not immediately) as it seems like quite a lot of work to allow all the various signing algorithms that JWTs can use.

DanCorderIPV commented 5 months ago

I've just looked at the plugin documentation that I could find and it looks like the pact would look more like this?

    return builder
      .usingPlugin("jwt")
      .expectsToReceive("request for a JWT", "core/interaction/http")
      .with(Map.of(
        "request.path", "/getCredential",
        "response.privateSigningKey", "privateKeyValue",
        "response.publicSigningKey", "publicKeyValue",
        "response.status", "200",
        "response.contents", Map.of(
          "content-type", "application/jwt",
          "$.header.alg", "matching(???, 'ES256')",    <-- Would need to have a list of valid values/enum
          "$.body.name", "matching(string,'Joe Bloggs')",
        )
      ))
      .toPact();

That would work, but seems less user friendly and type safe.

mefellows commented 5 months ago

Yes, the "raw" (my words) plugin interface is not as typesafe, but I believe there is nothing stopping anyone from creating a typesafe wrapper over the top of it.