2

I am writing the Webhook to handle automatic micro deposits for Plaid in C#. I don't entirely understand how it is supposed to work, mainly because the examples are in other languages I don't know.

My first problem is will Plaid send me a string? I'm guessing the Jwt is a string?

My code:

            var token = "[someJwtstring]";
            var handler = new JwtSecurityTokenHandler();
            var jsonToken = handler.ReadJwtToken(token);

            //Get the Json Web Key from the API using the key id
            var verifyJwt = await _plaidRepo.VerifyWebHook(jsonToken.Header.Kid);
            var webkey = new JsonWebKey()
            {
                Alg = verifyJwt.Data.alg,
                Crv = verifyJwt.Data.crv,
                Kty = verifyJwt.Data.kty,
                Use = verifyJwt.Data.use,
                X = verifyJwt.Data.x,
                Y = verifyJwt.Data.y
            };

So up to here I understand...but now what? What do I do with the web key so that I can get the request body?

yithril
  • 57
  • 7
  • I figured it out! JSON Web Keys can be put into the Token validation parameters. So I could do handler.ValidateToken(token, new TokenValidationParameters() { TokenDecryptionLey = webkey }, out var validatedToken) – yithril Sep 25 '20 at 18:56
  • Can you please share your approach? Thanks – Mustafa Mohammadi Nov 07 '20 at 06:22

2 Answers2

1

I'm currently trying this out as well.

Yeah, I think the Jwt will be a string.

What I think could work is if you put the webKey into the token validation parameters

var validationParameters = new TokenValidationParameters
{
  IssuerSigningKey = webKey
  //Some more parameters may be needed here for extra stuff like ClockSkew
};

Validating the token with the security token handler will return a ClaimsPrinciple with the claims mentioned in the plaid doc like iat and request_body_sha256

var handler = new JwtSecurityTokenHandler();
var claimsPrinciple = handler.ValidateToken("someJwtString", validationParameters, out var validatedToken);

If validation fails there will be an exception

rw_coder
  • 11
  • 1
1

This is the complete solution I ended up using to validate Plaid webhooks, which includes decoding. I'm using Going.Plaid library to handle getting the JWK. My solution doesn't do any caching (something recommended by Plaid).

I hope this helps someone else, because it took me a little bit to get it configured correctly as my familiarity with JWT is limited.

public async Task<JsonWebKey> GetVerificationKeyAsync(string kid)
{
    var plaidClient = new PlaidClient(Settings.Environment, Settings.ClientId, Settings.Secret);
    var result = await plaidClient.WebhookVerificationKeyGetAsync(new Going.Plaid.WebhookVerificationKey.WebhookVerificationKeyGetRequest { KeyId = kid });

    return new JsonWebKey {
        Kid = kid,
        Alg = result.Key.Alg,
        Crv = result.Key.Crv,
        Kty = result.Key.Kty,
        Use = result.Key.Use,
        X = result.Key.X,
        Y = result.Key.Y
    };
}

public async Task ValidateWebhookAsync(IDictionary<string, StringValues> headers, string body)
{
    var signedJwt = headers.TryGetValue("plaid-verification", out var token) ? token.FirstOrDefault().ToString() : (string)null;

    if (string.IsNullOrWhiteSpace(signedJwt))
    {
        throw new UnauthorizedWebhookRequest("Missing Plaid Verification Header");
    }
    Logger.LogInformation("Signed JWT: " + signedJwt);

    var handler = new JwtSecurityTokenHandler();
    var decodedToken = handler.ReadJwtToken(signedJwt);

    // validate algorithm
    if(decodedToken.Header.Alg != "ES256")
    {
        throw new UnauthorizedWebhookRequest("Invalid algorithm: " + decodedToken.Header.Alg);
    }

    // validate issued time
    if(decodedToken.IssuedAt.AddMinutes(5) < DateTime.UtcNow)
    {
        throw new UnauthorizedWebhookRequest("Expired token: " + decodedToken.IssuedAt);
    }

    // Get the decoder
    JsonWebKey decoderKey;
    try
    {
        decoderKey = await GetVerificationKeyAsync(decodedToken.Header.Kid);
    }
    catch (Exception ex)
    {
        throw new UnauthorizedWebhookRequest("Unable to get plaid verification key for kid: " + decodedToken.Header.Kid, ex);
    }

    // Validate the token using decoder
    ClaimsPrincipal principal;
    try
    {
        principal = handler.ValidateToken(signedJwt, new TokenValidationParameters
        {
            IssuerSigningKey = decoderKey,
            ValidateLifetime = false,
            ValidateAudience = false,
            ValidateIssuer = false,
            ValidateActor = false,
        }, out var validToken);
    }
    catch (Exception ex)
    {
        throw new UnauthorizedWebhookRequest("Unable to validate token", ex);
    }

    // Compare Hash
    var requestBodySha256 = principal.FindFirstValue("request_body_sha256");
    var bodySha256 = ComputeSha256Hash(body);

    if(requestBodySha256 != bodySha256)
    {
        Logger.LogInformation("Request Sha256: \n" + requestBodySha256);
        Logger.LogInformation("Computed Sha256: \n" + bodySha256);
        throw new UnauthorizedWebhookRequest("Hash does not match");
    }     
}