Skip to content

Instantly share code, notes, and snippets.

@swain
Last active July 14, 2022 21:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save swain/046bffc200fd3ddee76b49cfdcb7a1dc to your computer and use it in GitHub Desktop.
Save swain/046bffc200fd3ddee76b49cfdcb7a1dc to your computer and use it in GitHub Desktop.
A request validation approach using JWTs

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.

Sending Requests (our platform does this)

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
    }
  }
)

Receiving Requests (the customer does this)

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!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment