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.
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:
consthmacSHA512=require("crypto-js/hmac-sha512");// Get your signing secret from dashboard.moov.io
constwebhookSecret=process.env.WEBHOOK_SECRET;// Check if the hash of headers match the signature
constisSigned=(timestamp,nonce,webhookId,signature)=>{constconcatHeaders=`${timestamp}|${nonce}|${webhookId}`;constcheckHash=hmacSHA512(concatHeaders,webhookSecret);returnsignature===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"});}letwebhook;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});};
packagemainimport("crypto/hmac""crypto/sha512""encoding/hex""fmt""os""github.com/aws/aws-lambda-go/events""github.com/aws/aws-lambda-go/lambda")funcmain(){lambda.Start(handler)}funchandler(revents.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+"|"+webhookIDwantHash,err:=hash([]byte(concatHeaders),[]byte(webhookSecret))iferr!=nil{returnnil,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.
funchash(p[]byte,secret[]byte)(*string,error){h:=hmac.New(sha512.New,secret)_,err:=h.Write(p)iferr!=nil{returnnil,err}hash:=hex.EncodeToString(h.Sum(nil))return&hash,nil}