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](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) , 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](https://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](https://plaid.com/docs/api/sandbox/index.html.md#sandboxitemfire_webhook) ). 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.

JWT header

```json
{
  "alg": "ES256",
  "kid": "bfbd5111-8e33-4643-8ced-b2e642a72f3c",
  "typ": "JWT"
}
```

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](https://plaid.com/docs/api/webhooks/webhook-verification/index.html.md#get-webhook-verification-key) 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](https://plaid.com/docs/api/webhooks/webhook-verification/index.html.md#get-webhook-verification-key) endpoint provides a JSON Web Key (JWK) that can be used to verify a JWT.

#### Request fields 

string

Your Plaid API `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.

string

Your Plaid API `secret`. The `secret` is required and may be provided either in the `PLAID-SECRET` header or as part of a request body.

required, string

The key ID ( `kid` ) from the JWT header.

```bash
curl -X POST https://production.plaid.com/webhook_verification_key/get \
  -H 'content-type: application/json' \
  -d '{
    "client_id": "${PLAID_CLIENT_ID}",
    "secret": "${PLAID_SECRET}",
    "key_id": String
  }'

```

```node
const request: WebhookVerificationKeyGetRequest = {
  key_id: keyID,
};
try {
  const response = await plaidClient.webhookVerificationKeyGet(request);
  const key = response.data.key;
} catch (error) {
  // handle error
}

```

```ruby
request = Plaid::WebhookVerificationKeyGetRequest.new({ key_id: key_id })
response = client.webhook_verification_key_get(request)
key = response[:key]

```

```go
webhookReq := plaid.NewWebhookVerificationKeyGetRequest("KEY_ID")
webhookResp, _, err := client.PlaidApi.WebhookVerificationKeyGet(ctx).WebhookVerificationKeyGetRequest(*webhookReq).Execute()
if err != nil {
  // handle error
}

key := webhookResp.GetKey()

```

```python
request = WebhookVerificationKeyGetRequest(key_id=key_id)

response = client.webhook_verification_key_get(request)
key = response['key']

```

```java
WebhookVerificationKeyGetRequest request = new WebhookVerificationKeyGetRequest()
  .keyId(keyId);

Response response = client()
  .webhookVerificationKeyGet(request)
  .execute();
WebhookVerificationKey key = response.body().getKey();

```

#### Response fields 

object

A JSON Web Key (JWK) that can be used in conjunction with [JWT libraries](https://jwt.io/#libraries-io) to verify Plaid webhooks

string

The alg member identifies the cryptographic algorithm family used with the key.

string

The crv member identifies the cryptographic curve used with the key.

string

The kid (Key ID) member can be used to match a specific key. This can be used, for instance, to choose among a set of keys within the JWK during key rollover.

string

The kty (key type) parameter identifies the cryptographic algorithm family used with the key, such as RSA or EC.

string

The use (public key use) parameter identifies the intended use of the public key.

string

The x member contains the x coordinate for the elliptic curve point, provided as a base64url-encoded string of the coordinate's big endian representation.

string

The y member contains the y coordinate for the elliptic curve point, provided as a base64url-encoded string of the coordinate's big endian representation.

integer

The timestamp when the key was created, in Unix time.

nullable, integer

The timestamp when the key expired, in Unix time.

string

A unique identifier for the request, which can be used for troubleshooting. This identifier, like all Plaid identifiers, is case sensitive.

Response Object

```json
{
  "key": {
    "alg": "ES256",
    "created_at": 1560466150,
    "crv": "P-256",
    "expired_at": null,
    "kid": "bfbd5111-8e33-4643-8ced-b2e642a72f3c",
    "kty": "EC",
    "use": "sig",
    "x": "hKXLGIjWvCBv-cP5euCTxl8g9GLG9zHo_3pO5NN1DwQ",
    "y": "shhexqPB7YffGn6fR6h2UhTSuCtPmfzQJ6ENVIoO4Ys"
  },
  "request_id": "RZ6Omi1bzzwDaLo"
}
```

##### 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.

JWT Payload

```json
{
  "iat": 1560211755,
  "request_body_sha256": "bbe8e9..."
}
```

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.

```python
import hashlib
import hmac
import time
import requests
from jose import jwt

# Plaid client credentials. Fetch these from your secrets manager or similar storage.
CLIENT_ID = 'PLAID_CLIENT_ID'
SECRET = 'PLAID_SECRET'

# Endpoint for getting public verification keys.
ENDPOINT = 'https://production.plaid.com/webhook_verification_key/get'

# Single cached key instead of a dictionary
CACHED_KEY = None


def verify(body, headers):
    global CACHED_KEY

    signed_jwt = headers.get('plaid-verification')
    current_key_id = jwt.get_unverified_header(signed_jwt)['kid']

    # Fetch key if not already cached
    if CACHED_KEY is None:
        response = requests.post(ENDPOINT, json={
            'client_id': CLIENT_ID,
            'secret': SECRET,
            'key_id': current_key_id
        })

        if response.status_code != 200:
            return False

        CACHED_KEY = response.json()['key']

    # If key is still not set, verification fails
    if CACHED_KEY is None:
        return False

    # Validate the signature and extract the claims.
    try:
        claims = jwt.decode(signed_jwt, CACHED_KEY, algorithms=['ES256'])
    except jwt.JWTError:
        return False

    # Ensure that the token is not expired.
    if claims["iat"] < time.time() - 5 * 60:
        return False

    # Compute the hash of the body.
    m = hashlib.sha256()
    m.update(body.encode())
    body_hash = m.hexdigest()

    # Ensure that the hash of the body matches the claim.
    return hmac.compare_digest(body_hash, claims['request_body_sha256'])

```

```ruby
require 'digest'
require 'jwt'
require 'time'
require 'base64'
require 'openssl'
require 'json'
require 'jose'
require 'active_support/security_utils'
require 'plaid'

module PlaidWebhookVerifier
  def self.verify(body, headers)
    # Cache for webhook validation key.
    @cached_key ||= nil

    client = Plaid::ApiClient.new(env: PLAID_ENV,
      client_id: PLAID_CLIENT_ID,
      secret: PLAID_SECRET)

    signed_jwt = headers['plaid-verification']
    decoded_token = JWT.decode signed_jwt, nil, false
    current_key_id = decoded_token[1]['kid']

    # Fetch key if not already cached
    unless @cached_key
      request = Plaid::WebhookVerificationKeyGetRequest.new({ key_id: current_key_id })
      response = client.webhook_verification_key_get(request)
      @cached_key = response.key
    end

    # If key is still not set, verification fails
    return false unless @cached_key

    # Validate the signature
    begin
      return false unless JOSE::JWT.verify(@cached_key, signed_jwt)[0]
    rescue
      return false
    end

    # Compare hashes
    body_hash = Digest::SHA256.hexdigest(body)
    claimed_body_hash = decoded_token[0]["request_body_sha256"]
    return false unless ActiveSupport::SecurityUtils.secure_compare(body_hash, claimed_body_hash)

    # Validate that token is not expired
    iat = decoded_token[0]["iat"]
    return false if Time.now.to_i - iat > 60 * 5

    true
  end
end

```

```bash

# Not applicable


```

```java
import com.plaid.client.model.WebhookVerificationKeyGetRequest;
import com.plaid.client.model.WebhookVerificationKeyGetResponse;
import com.plaid.client.request.PlaidApi;
import com.plaid.client.ApiClient;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.nimbusds.jose.jwk.ECKey;
import com.google.common.hash.Hashing;
import retrofit2.Response;
import org.json.JSONObject;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECPublicKey;
import java.util.HashMap;
import java.util.Map;

private static PlaidApi plaidClient;
private static JSONObject cachedKey = null;

public static boolean verifyWebhook(String body, HashMap headers) {

    // Instantiate Plaid client (usually you'd do this elsewhere, shown for context)
    HashMap apiKeys = new HashMap<>();
    apiKeys.put("clientId", "YOUR_CLIENT_ID");
    apiKeys.put("secret", "YOUR_CLIENT_SECRET");
    ApiClient apiClient = new ApiClient(apiKeys);
    apiClient.setPlaidAdapter(ApiClient.Sandbox);
    plaidClient = apiClient.createService(PlaidApi.class);

    // Extract the signed JWT from the webhook header
    String signedToken = headers.get("plaid-verification");

    // Decode the signed JWT returned by Plaid
    DecodedJWT decodedToken = JWT.decode(signedToken);
    String decodedTokenHeader = decodedToken.getHeader();

    // Convert String token header to JSON Object
    JSONObject decodedTokenHeaderJson = new JSONObject(decodedTokenHeader);

    // Return false if alg in header is not "ES256"
    if (!decodedTokenHeaderJson.getString("alg").equals("ES256")) {
        return false;
    }

    // Extract the key id, "kid", from the decoded token header
    String currentKid = decodedTokenHeaderJson.getString("kid");

    // Fetch key if not already cached
    if (cachedKey == null) {
        try {
            WebhookVerificationKeyGetRequest request = new WebhookVerificationKeyGetRequest()
                    .keyId(currentKid);

            Response response = plaidClient
                    .webhookVerificationKeyGet(request)
                    .execute();

            if (response.body() == null) {
                return false;
            }

            // Read verification key as JSON
            cachedKey = new JSONObject(response.body().getKey().toString());
        } catch (Exception e) {
            return false;
        }
    }

    // If key is still not set, verification fails
    if (cachedKey == null) {
        return false;
    }

    // Create ECPublicKey from JSON string, needed for Algorithm instance
    ECPublicKey ecpkVerificationKey;
    try {
        ecpkVerificationKey = ECKey.parse(cachedKey.toString()).toECPublicKey();
    } catch (Exception e) {
        return false;
    }

    try {
        // Set up the algorithm needed to verify token
        Algorithm algorithm = Algorithm.ECDSA256(ecpkVerificationKey, null);

        // Verify the token
        JWTVerifier verifier = JWT.require(algorithm)
                .acceptLeeway(300) // 5 minutes
                .build(); // Reusable verifier instance

        DecodedJWT verifiedJwt = verifier.verify(signedToken);

        // Extract payload of verified token
        String payload = verifiedJwt.getPayload();

        // Decode Base64 encoded payload to String
        String payloadDecoded = new String(Base64.getDecoder().decode(payload), StandardCharsets.UTF_8);

        // Convert String payload to JSON Object
        JSONObject payloadJson = new JSONObject(payloadDecoded);

        // Extract value of 'request_body_sha256' field in payload
        String sha256InPayload = payloadJson.getString("request_body_sha256");

        // Compute sha256 of webhook body using Google's Guava library
        String sha256OfWebhookBody = Hashing.sha256()
                .hashString(body, StandardCharsets.UTF_8) // Compute sha256 of body
                .toString();

        return sha256OfWebhookBody.equals(sha256InPayload);

    } catch (JWTVerificationException exception) {
        return false;
    }
}

```

```node
const compare = require('secure-compare');
const { jwtDecode } = require('jwt-decode'); // syntax for jwtDecode 4.0 or later
const JWT = require('jose');
const sha256 = require('js-sha256');
const { Configuration, PlaidApi, PlaidEnvironments } = require('plaid');

// Single cached key instead of a Map
let cachedKey = null;

const configuration = new Configuration({
  basePath: PlaidEnvironments.sandbox,
  baseOptions: {
    headers: {
      'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
      'PLAID-SECRET': process.env.PLAID_SECRET,
    },
  },
});

const plaidClient = new PlaidApi(configuration);

const verify = async (body, headers) => {
  const signedJwt = headers['plaid-verification'];
  const decodedToken = jwtDecode(signedJwt);
  const decodedTokenHeader = jwtDecode(signedJwt, { header: true });
  const currentKeyID = decodedTokenHeader.kid;

  // Fetch key if not already cached
  if (!cachedKey) {
    try {
      const response = await plaidClient.webhookVerificationKeyGet({
        key_id: currentKeyID,
      });
      cachedKey = response.data.key;
    } catch (error) {
      return false;
    }
  }

  // If key is still not set, verification fails
  if (!cachedKey) {
    return false;
  }

  // Validate the signature and iat
  try {
    const keyLike = await JWT.importJWK(cachedKey);
    // This will throw an error if verification fails
    await JWT.jwtVerify(signedJwt, keyLike, {
      maxTokenAge: '5 min',
    });
  } catch (error) {
    return false;
  }

  // Compare hashes
  const bodyHash = sha256(body);
  const claimedBodyHash = decodedToken.request_body_sha256;
  return compare(bodyHash, claimedBodyHash);
};

```

```go
package main

import (
    "context"
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/base64"
    "encoding/hex"
    "errors"
    "fmt"
    "math/big"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v4"
    "github.com/plaid/plaid-go/v42/plaid"
)

// helper method
func jwkToECDSAPublicKey(jwk *plaid.JWKPublicKey) (*ecdsa.PublicKey, error) {
    if jwk == nil || jwk.X == "" || jwk.Y == "" ||
        jwk.Kty == nil || *jwk.Kty != "EC" ||
        jwk.Crv == nil || *jwk.Crv != "P-256" {
        return nil, errors.New("invalid/unsupported JWK")
    }
    xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X)
    if err != nil {
        return nil, fmt.Errorf("decode x: %w", err)
    }
    yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y)
    if err != nil {
        return nil, fmt.Errorf("decode y: %w", err)
    }
    return &ecdsa.PublicKey{
        Curve: elliptic.P256(),
        X:     new(big.Int).SetBytes(xBytes),
        Y:     new(big.Int).SetBytes(yBytes),
    }, nil
}

var (
    jwkCache = map[string]*plaid.JWKPublicKey{} 
    maxAge   = 5 * time.Minute                  
)

func verifyWebhook(ctx context.Context, client *plaid.APIClient, webhookBody []byte, headers map[string]string) (bool, error) {
    tokenString := getHeaderCI(headers, "Plaid-Verification")
    if tokenString == "" {
        return false, errors.New("missing Plaid-Verification header")
    }

    // Decode JWT header (unverified) to extract alg and kid
    parser := jwt.Parser{}
    unverified, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
    if err != nil {
        return false, fmt.Errorf("parse unverified token: %w", err)
    }
    if unverified.Method.Alg() != jwt.SigningMethodES256.Alg() {
        return false, fmt.Errorf("unexpected alg %q (want ES256)", unverified.Method.Alg())
    }
    kid, _ := unverified.Header["kid"].(string)
    if kid == "" {
        return false, errors.New("missing kid in JWT header")
    }

    // Get verification key for kid via /webhook_verification_key/get
    jwk, err := getJWK(ctx, client, kid)
    if err != nil {
        return false, fmt.Errorf("get JWK: %w", err)
    }
    pubKey, err := jwkToECDSAPublicKey(jwk)
    if err != nil {
        return false, fmt.Errorf("jwk->ecdsa: %w", err)
    }

    // Verify JWT signature
    claims := jwt.MapClaims{}
    token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
        if t.Method != jwt.SigningMethodES256 {
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        return pubKey, nil
    })
    if err != nil || !token.Valid {
        return false, fmt.Errorf("invalid token: %w", err)
    }

    // Verify that the webhook is not more than 5 minutes old
    iatVal, ok := claims["iat"]
    if !ok {
        return false, errors.New("missing iat")
    }
    var iat time.Time
    switch v := iatVal.(type) {
    case float64:
        iat = time.Unix(int64(v), 0)
    case int64:
        iat = time.Unix(v, 0)
    default:
        return false, errors.New("invalid iat type")
    }
    if time.Since(iat) > maxAge {
        return false, errors.New("token too old (>5m)")
    }

    // Verify body hash integrity
    wantHash, ok := claims["request_body_sha256"].(string)
    if !ok || wantHash == "" {
        return false, errors.New("missing request_body_sha256")
    }
    sum := sha256.Sum256(webhookBody)
    gotHex := strings.ToLower(hex.EncodeToString(sum[:]))
    if subtle.ConstantTimeCompare([]byte(gotHex), []byte(strings.ToLower(wantHash))) != 1 {
        return false, errors.New("body hash mismatch")
    }

    return true, nil
}

func getHeaderCI(h map[string]string, name string) string {
    lname := strings.ToLower(name)
    for k, v := range h {
        if strings.ToLower(k) == lname {
            return v
        }
    }
    return ""
}

func getJWK(ctx context.Context, client *plaid.APIClient, kid string) (*plaid.JWKPublicKey, error) {
    if key, ok := jwkCache[kid]; ok && key != nil {
        return key, nil
    }
    req := *plaid.NewWebhookVerificationKeyGetRequest(kid)
    resp, _, err := client.PlaidApi.WebhookVerificationKeyGet(ctx).
        WebhookVerificationKeyGetRequest(req).
        Execute()
    if err != nil {
        return nil, err
    }
    key := resp.GetKey()
    if key.Kid != nil && *key.Kid == kid {
        jwkCache[kid] = &key
    }
    return &key, nil
}

```