Check webhook signatures

Use this guide to learn how to confirm that webhook events were sent by Moov.

Every event Moov sends to a webhook endpoint includes a signature generated through a SHA-512 hash-based message authentication code (HMAC). This allows you to verify that Moov (and not a third party) sent these events to your service.

Check the signature

To check the signature for a particular webhook, use the signing secret to create a new hash through the steps outlined below. If the hash you created matches the value of the X-Signature header, you know that the event came from Moov. Otherwise, your service can discard the event.

All of the data needed to create the hash, except for the signing secret, is sent in HTTP headers in the POST to the configured webhook endpoint. You can obtain the signing secret for each webhook from the Moov Dashboard.

The headers with values needed to create the hash are:

  • X-Timestamp
  • X-Nonce
  • X-Webhook-ID
  • X-Signature

Using your favorite programming language, perform the following steps to construct your hash and compare against the event signature:

  1. Get the signing secret from the Moov Dashboard.
  2. Get the header values from the received POST.
  3. Prepare the signed payload. timeStamp + "|" + nonce + "|" + webhookID
  4. Determine the expected signature using the signing secret and the payload from step 3.
  5. Check both signatures for equality.

See the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const hmacSHA512 = require("crypto-js/hmac-sha512");

// Get your signing secret from dashboard.moov.io
const webhookSecret = process.env.WEBHOOK_SECRET;

// Check if the hash of headers match the signature
const isSigned = (timestamp, nonce, webhookId, signature) => {
  const concatHeaders = `${timestamp}|${nonce}|${webhookId}`;
  const checkHash = hmacSHA512(concatHeaders, webhookSecret);

  return signature === checkHash.toString();
}

// Serverless function 
exports.handler = async (event, context, callback) => {
  if (!event.body) {
    console.log("Invalid request");
    callback(null, {
      statusCode: 400,
      body: "Invalid request"
    });
  }

  // Headers are lowercased
  if (!isSigned(
    event.headers["x-timestamp"], 
    event.headers["x-nonce"], 
    event.headers["x-webhook-id"], 
    event.headers["x-signature"])
  ) {
    console.log("Signature is invalid");
    callback(null, {
      statusCode: 400,
      body: "Signature is invalid"
    });
  }

  let webhook;
  try {
    webhook = JSON.parse(event.body);
  } catch (err) {
    console.log("Invalid JSON");
    callback(null, {
      statusCode: 400,
      body: "Invalid JSON"
    });
  }

  // Logs the event message payload
  console.log(event.body);
  callback(null, {
    statusCode: 200
  });
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
 "crypto/hmac"
 "crypto/sha512"
 "encoding/hex"
 "fmt"
 "os"

 "github.com/aws/aws-lambda-go/events"
 "github.com/aws/aws-lambda-go/lambda"
)

func main() {
 lambda.Start(handler)
}

func handler(r events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
 var (
  webhookSecret = os.Getenv("WEBHOOK_SECRET")

  timestamp = r.Headers["x-timestamp"]
  nonce     = r.Headers["x-nonce"]
  webhookID = r.Headers["x-webhook-id"]
  gotHash   = r.Headers["x-signature"]
 )

 concatHeaders := timestamp + "|" + nonce + "|" + webhookID
 wantHash, err := hash([]byte(concatHeaders), []byte(webhookSecret))
 if err != nil {
  return nil, err
 }

 if *wantHash == gotHash {
  fmt.Println("Webhook received!")

  return &events.APIGatewayProxyResponse{
   StatusCode: 200,
  }, nil
 } else {
  msg := "Signature is invalid"
  fmt.Println(msg)

  return &events.APIGatewayProxyResponse{
   StatusCode: 400,
   Body:       msg,
  }, nil
 }
}

// hash generates a SHA512 HMAC hash of p using the secret provided.
func hash(p []byte, secret []byte) (*string, error) {
 h := hmac.New(sha512.New, secret)
 _, err := h.Write(p)
 if err != nil {
  return nil, err
 }
 hash := hex.EncodeToString(h.Sum(nil))
 return &hash, nil
}
Summary Beta