mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-25 08:09:38 +02:00
fix: Plaid webhook verification (#1824)
* Fix Plaid webhook verification * Fix client creation in webhook controller
This commit is contained in:
parent
331de2f997
commit
5eb5ec7aef
5 changed files with 87 additions and 62 deletions
|
@ -4,65 +4,67 @@ class Provider::Plaid
|
|||
MAYBE_SUPPORTED_PLAID_PRODUCTS = %w[transactions investments liabilities].freeze
|
||||
MAX_HISTORY_DAYS = Rails.env.development? ? 90 : 730
|
||||
|
||||
class << self
|
||||
def process_webhook(webhook_body)
|
||||
parsed = JSON.parse(webhook_body)
|
||||
type = parsed["webhook_type"]
|
||||
code = parsed["webhook_code"]
|
||||
|
||||
item = PlaidItem.find_by(plaid_id: parsed["item_id"])
|
||||
|
||||
case [ type, code ]
|
||||
when [ "TRANSACTIONS", "SYNC_UPDATES_AVAILABLE" ]
|
||||
item.sync_later
|
||||
when [ "INVESTMENTS_TRANSACTIONS", "DEFAULT_UPDATE" ]
|
||||
item.sync_later
|
||||
when [ "HOLDINGS", "DEFAULT_UPDATE" ]
|
||||
item.sync_later
|
||||
else
|
||||
Rails.logger.warn("Unhandled Plaid webhook type: #{type}:#{code}")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_webhook!(verification_header, raw_body)
|
||||
jwks_loader = ->(options) do
|
||||
key_id = options[:kid]
|
||||
|
||||
jwk_response = client.webhook_verification_key_get(
|
||||
Plaid::WebhookVerificationKeyGetRequest.new(key_id: key_id)
|
||||
)
|
||||
|
||||
jwks = JWT::JWK::Set.new([ jwk_response.key.to_hash ])
|
||||
|
||||
jwks.filter! { |key| key[:use] == "sig" }
|
||||
jwks
|
||||
end
|
||||
|
||||
payload, _header = JWT.decode(
|
||||
verification_header, nil, true,
|
||||
{
|
||||
algorithms: [ "ES256" ],
|
||||
jwks: jwks_loader,
|
||||
verify_expiration: false
|
||||
}
|
||||
)
|
||||
|
||||
issued_at = Time.at(payload["iat"])
|
||||
raise JWT::VerificationError, "Webhook is too old" if Time.now - issued_at > 5.minutes
|
||||
|
||||
expected_hash = payload["request_body_sha256"]
|
||||
actual_hash = Digest::SHA256.hexdigest(raw_body)
|
||||
raise JWT::VerificationError, "Invalid webhook body hash" unless ActiveSupport::SecurityUtils.secure_compare(expected_hash, actual_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(config, region)
|
||||
def initialize(config, region: :us)
|
||||
@client = Plaid::PlaidApi.new(
|
||||
Plaid::ApiClient.new(config)
|
||||
)
|
||||
@region = region
|
||||
end
|
||||
|
||||
def process_webhook(webhook_body)
|
||||
parsed = JSON.parse(webhook_body)
|
||||
|
||||
type = parsed["webhook_type"]
|
||||
code = parsed["webhook_code"]
|
||||
|
||||
item = PlaidItem.find_by(plaid_id: parsed["item_id"])
|
||||
|
||||
case [ type, code ]
|
||||
when [ "TRANSACTIONS", "SYNC_UPDATES_AVAILABLE" ]
|
||||
item.sync_later
|
||||
when [ "INVESTMENTS_TRANSACTIONS", "DEFAULT_UPDATE" ]
|
||||
item.sync_later
|
||||
when [ "HOLDINGS", "DEFAULT_UPDATE" ]
|
||||
item.sync_later
|
||||
else
|
||||
Rails.logger.warn("Unhandled Plaid webhook type: #{type}:#{code}")
|
||||
end
|
||||
rescue => error
|
||||
# Processing errors shouldn't return a 400 to Plaid since they are internal, so capture silently
|
||||
Sentry.capture_exception(error)
|
||||
end
|
||||
|
||||
def validate_webhook!(verification_header, raw_body)
|
||||
jwks_loader = ->(options) do
|
||||
key_id = options[:kid]
|
||||
|
||||
jwk_response = client.webhook_verification_key_get(
|
||||
Plaid::WebhookVerificationKeyGetRequest.new(key_id: key_id)
|
||||
)
|
||||
|
||||
jwks = JWT::JWK::Set.new([ jwk_response.key.to_hash ])
|
||||
|
||||
jwks.filter! { |key| key[:use] == "sig" }
|
||||
jwks
|
||||
end
|
||||
|
||||
payload, _header = JWT.decode(
|
||||
verification_header, nil, true,
|
||||
{
|
||||
algorithms: [ "ES256" ],
|
||||
jwks: jwks_loader,
|
||||
verify_expiration: false
|
||||
}
|
||||
)
|
||||
|
||||
issued_at = Time.at(payload["iat"])
|
||||
raise JWT::VerificationError, "Webhook is too old" if Time.now - issued_at > 5.minutes
|
||||
|
||||
expected_hash = payload["request_body_sha256"]
|
||||
actual_hash = Digest::SHA256.hexdigest(raw_body)
|
||||
raise JWT::VerificationError, "Invalid webhook body hash" unless ActiveSupport::SecurityUtils.secure_compare(expected_hash, actual_hash)
|
||||
end
|
||||
|
||||
def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil)
|
||||
request = Plaid::LinkTokenCreateRequest.new({
|
||||
user: { client_user_id: user_id },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue