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?
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");
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With