This approach is largely based on the webhook signing approach used by Plaid. See their docs for context: https://plaid.com/docs/api/webhooks/webhook-verification/.
It's a simple approach that works using JWTs.
Assume a generated key pair.
import axios from 'axios';
import * as JWT from 'jsonwebtoken';
const kid = 'some-kid';
const privateKeyPem = '-----BEGIN PRIVATE KEY-----...'
const request = {
url: 'https://api.somecustomer.com',
method: 'POST',
body: {
some: 'payload'
}
}
const jwtSignature = JWT.sign(
{
url,
method,
body: createHash('sha256')
.update(Buffer.from(JSON.stringify(request.body)))
.digest('base64')
},
privateKeyPem,
{
keyid: keyId,
algorithm: 'RS256'
}
)
axios.post(
request.url,
request.body,
{
headers: {
'LifeOmic-Signature': jwtSignature
}
}
)
import * as JWT from 'jsonwebtoken'
const handleRequestFromLifeOmic = async (request) => {
// 1. Decode the token, to get the kid.
const { header, payload } = JWT.decode(signatureHeader, { complete: true });
// 2. Fetch the public key with the key id, from a LifeOmic-owned JWKS url.
const publicKeyPEM = await getPublicKeyFromSomeLifeOmicJWKSUrl(header.kid);
// 3. Verify the signature of the JWT.
const signatureHeader = request.headers['LifeOmic-Signature'];
JWT.verify(signatureHeader, publicKeyPEM, { algorithms: ['RS256'] });
// 3. Verify that the url, method, and body match the signed payload.
if (payload.method !== request.httpMethod) {
throw new Error('method does not match');
}
if (payload.url !== request.url) {
throw new Error('url does not match');
}
// This check won't be necessary/applicable for GET requests
if (request.jsonBody) {
const receivedBodySHA256 = createHash('sha256')
.update(Buffer.from(JSON.stringify(request.jsonBody)))
.digest('base64');
if (payload.body_sha256 !== receivedBodySHA256) {
throw new Error('body does not match')
}
}
// Maybe check timestamp, using `payload.iat`
// Request is verified -- continue!
}