Verify webhooks
API reference for verifying webhooks
Plaid signs all outgoing webhooks so that you can verify the authenticity of any incoming webhooks to your application. A message signature is included in the Plaid-Verification
header (Note: this is the canonical representation of the header field, but HTTP 1.x headers should be handled as case-insensitive, HTTP 2 headers are always lowercase). The verification process is optional and is not required for your application to handle Plaid webhooks.
The verification process requires understanding JSON Web Tokens (JWTs) and JSON Web Keys (JWKs). More information about these specifications can be found at jwt.io.
Libraries for interpreting and verifying JWKs and JWTs most likely exist in your preferred language. It is highly recommended that you utilize well-tested libraries rather than trying to implement these specifications from scratch.
Steps to verify webhooks
Extract the JWT header
Extract the Plaid-Verification HTTP header from any Plaid webhook (to get a webhook, see firing webhooks in Sandbox). The value of the Plaid-Verification header is a JWT, and will be referred to as "the JWT" in following steps.
Using your preferred JWT library, decode the JWT and extract the header without validating the signature. This functionality most likely exists in your preferred JWT library. An example JWT header is shown below.
1{2 "alg": "ES256",3 "kid": "bfbd5111-8e33-4643-8ced-b2e642a72f3c",4 "typ": "JWT"5}
Ensure that the value of the alg
(algorithm) field in the header is "ES256"
. Reject the webhook if this is not the case.
Extract the value corresponding to the kid
(key ID) field. This will be used to retrieve the JWK public key corresponding to the private key that was used to sign this request.
Get the verification key
Use the /webhook_verification_key/get
endpoint to get the webhook verification key.
/webhook_verification_key/get
Get webhook verification key
Plaid signs all outgoing webhooks and provides JSON Web Tokens (JWTs) so that you can verify the authenticity of any incoming webhooks to your application. A message signature is included in the Plaid-Verification
header.
The /webhook_verification_key/get
endpoint provides a JSON Web Key (JWK) that can be used to verify a JWT.
client_id
client_id
. The client_id
is required and may be provided either in the PLAID-CLIENT-ID
header or as part of a request body.secret
secret
. The secret
is required and may be provided either in the PLAID-SECRET
header or as part of a request body.key_id
kid
) from the JWT header.1const request: WebhookVerificationKeyGetRequest = {2 key_id: keyID,3};4try {5 const response = await plaidClient.webhookVerificationKeyGet(request);6 const key = response.data.key;7} catch (error) {8 // handle error9}
Response fields and example
key
alg
crv
kid
kty
use
x
y
created_at
expired_at
request_id
1{2 "key": {3 "alg": "ES256",4 "created_at": 1560466150,5 "crv": "P-256",6 "expired_at": null,7 "kid": "bfbd5111-8e33-4643-8ced-b2e642a72f3c",8 "kty": "EC",9 "use": "sig",10 "x": "hKXLGIjWvCBv-cP5euCTxl8g9GLG9zHo_3pO5NN1DwQ",11 "y": "shhexqPB7YffGn6fR6h2UhTSuCtPmfzQJ6ENVIoO4Ys"12 },13 "request_id": "RZ6Omi1bzzwDaLo"14}
Validate the webhook
Interpret the returned key
as a JWK public key. Using your preferred JWT library, verify the JWT using the JWK. If the signature is not valid, reject the webhook. Otherwise, extract the payload of the JWT. It will look something like the JSON below.
1{2 "iat": 1560211755,3 "request_body_sha256": "bbe8e9..."4}
Use the issued at time denoted by the iat
field to verify that the webhook is not more than 5 minutes old. Rejecting outdated webhooks can help prevent replay attacks.
Extract the value of the request_body_sha256
field. This will be used to check the integrity and authenticity of the webhook body.
Compute the SHA-256 of the webhook body and ensure that it matches what is specified in the request_body_sha256
field of the validated JWT. If not, reject the webhook. It is best practice to use a constant time string/hash comparison method in your preferred language to prevent timing attacks.
Note that the request_body_sha256
sent in the JWT payload is sensitive to the whitespace in the webhook body and uses a tab-spacing of 2. If the webhook body is stored with a tab-spacing of 4, the hash will not match.
Example implementation
The following code shows one example method that can be used to verify webhooks sent by Plaid and cache public keys.
1const compare = require('secure-compare');2const { jwtDecode } = require('jwt-decode'); // syntax for jwtDecode 4.0 or later3const JWT = require('jose');4const sha256 = require('js-sha256');5const plaid = require('plaid');6
7// Single cached key instead of a Map8let cachedKey = null;9
10const plaidClient = new plaid.Client({11 clientID: process.env.CLIENT_ID,12 secret: process.env.SECRET,13 env: plaid.environments.sandbox,14});15
16const verify = async (body, headers) => {17 const signedJwt = JSON.parse(headers)['plaid-verification'];18 const decodedToken = jwtDecode(signedJwt);19 const decodedTokenHeader = jwtDecode(signedJwt, { header: true });20 const currentKeyID = decodedTokenHeader.kid;21
22 // Fetch key if not already cached23 if (!cachedKey) {24 try {25 const response = await plaidClient.webhookVerificationKeyGet({26 key_id: currentKeyID,27 });28 cachedKey = response.data.key;29 } catch (error) {30 return false;31 }32 }33
34 // If key is still not set, verification fails35 if (!cachedKey) {36 return false;37 }38
39 // Validate the signature and iat40 try {41 const keyLike = await JWT.importJWK(cachedKey);42 // This will throw an error if verification fails43 await JWT.jwtVerify(signedJwt, keyLike, {44 maxTokenAge: '5 min',45 });46 } catch (error) {47 return false;48 }49
50 // Compare hashes51 const bodyHash = sha256(body);52 const claimedBodyHash = decodedToken.request_body_sha256;53 return compare(bodyHash, claimedBodyHash);54};