How do I validate a JWS with a public key

514 views Asked by At

I'm using Saleor and have set up a webhook to my django server. According to their documentation there is in the header a JWS signature using RS256 with payload detached, to verify the signature you can use a public key, which can be fetched from http(s)://<your-backend-domain>/.well-known/jwks.json

The JWS is in the format xxx..yyy, so there is no payload, instead the payload is directly in the request.body. The way I have understood JWTs and public keys is that you can verify that the payload is legit by using the token together with the public key to see that the payload was signed with someone using the private key.

The problem I have is that I keep getting the exception jwt.exceptions.InvalidSignatureError: Signature verification failed.

This is my code:

@csrf_exempt
@require_http_methods(["POST"])
def saleor_webhook(request):
    print("Saleor webhook received")
    jws_token = request.headers.get('Saleor-Signature')
    response = requests.get(JWKS_URL)
    jwks = response.json()
    jwk_dict = jwks['keys'][0]
    # Convert JWK to PEM public key
    public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk_dict))
    pem_public_key = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    pem_public_key = pem_public_key.decode("utf-8")
    # Here is where it fails...
    decoded = jwt.decode(jws_token, pem_public_key, algorithms=['RS256'])
    print(decoded)
    
    return HttpResponse(status=200)

Not sure if this is the right way or not, but I tried to also base64 encode the request.body and put it in the token (between the dots) but I got the same error. This code is using PyJWT (or from import jwt), but I tried an alternative with python-jose which also failed. I'm new to JWTs, any help is greatly appreciated.

Thanks!

1

There are 1 answers

3
Henry On

Translating the example from the Saleor docs using this PyJWT usage example gives something like this:

def saleor_webhook():
    jws_token = request.headers.get('Saleor-Signature')
    body = request.body
    jwks_client = jwt.PyJWKClient(JWKS_URL)
    unverified = decode_complete(jws_token, options={"verify_signature": False}, detached_payload=body)
    header = unverified["header"]
    signing_key = jwks_client.get_signing_key(header.get("kid"))


    print(jwt.decode(
        jws_token,
        signing_key.key,
        algorithms=["RS256"],
        detached_payload=body
    ))

But this still gives jwt.exceptions.InvalidSignatureError: Signature verification failed, so I recreated the example from the docs

const jose = require('jose')
const getRawBody = require('raw-body')
const express = require('express')

const app = express();
const host = "0.0.0.0";
const port = 3000;

const JWKS = jose.createRemoteJWKSet(
  new URL("https://<my saleor url>/.well-known/jwks.json")
);

app.get("/", (req, res) => {
  res.send("working")
})

app.post("/webhook", (req, res) => {
  const jws = req.headers["saleor-signature"];
  const buffer = getRawBody(req, {
    length: req.headers["content-length"],
    limit: "1mb",
  });
  const [header, _, signature] = jws.split(".");
  jose.flattenedVerify({
    protected: header,
    payload: buffer.toString("utf-8"),
    signature
  }, JWKS);
  res.send("success");
})

app.listen(port, host, () => {
  console.log(`Example app listening on htpps://${host}:${port}`)
})

and I still get JWSSignatureVerificationFailed: signature verification failed, so it looks like there's a problem with Saleor, not with how you're verifying the JWT.