mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 21:29: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
|
@ -53,7 +53,7 @@ module AccountableResource
|
||||||
private
|
private
|
||||||
def set_link_token
|
def set_link_token
|
||||||
@us_link_token = Current.family.get_link_token(
|
@us_link_token = Current.family.get_link_token(
|
||||||
webhooks_url: webhooks_url,
|
webhooks_url: plaid_us_webhooks_url,
|
||||||
redirect_url: accounts_url,
|
redirect_url: accounts_url,
|
||||||
accountable_type: accountable_type.name,
|
accountable_type: accountable_type.name,
|
||||||
region: :us
|
region: :us
|
||||||
|
@ -61,7 +61,7 @@ module AccountableResource
|
||||||
|
|
||||||
if Current.family.eu?
|
if Current.family.eu?
|
||||||
@eu_link_token = Current.family.get_link_token(
|
@eu_link_token = Current.family.get_link_token(
|
||||||
webhooks_url: webhooks_url,
|
webhooks_url: plaid_eu_webhooks_url,
|
||||||
redirect_url: accounts_url,
|
redirect_url: accounts_url,
|
||||||
accountable_type: accountable_type.name,
|
accountable_type: accountable_type.name,
|
||||||
region: :eu
|
region: :eu
|
||||||
|
@ -69,11 +69,16 @@ module AccountableResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def webhooks_url
|
def plaid_us_webhooks_url
|
||||||
return webhooks_plaid_url if Rails.env.production?
|
return webhooks_plaid_url if Rails.env.production?
|
||||||
|
|
||||||
base_url = ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/"))
|
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid"
|
||||||
base_url + "/webhooks/plaid"
|
end
|
||||||
|
|
||||||
|
def plaid_eu_webhooks_url
|
||||||
|
return webhooks_plaid_eu_url if Rails.env.production?
|
||||||
|
|
||||||
|
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid_eu"
|
||||||
end
|
end
|
||||||
|
|
||||||
def accountable_type
|
def accountable_type
|
||||||
|
|
|
@ -6,8 +6,25 @@ class WebhooksController < ApplicationController
|
||||||
webhook_body = request.body.read
|
webhook_body = request.body.read
|
||||||
plaid_verification_header = request.headers["Plaid-Verification"]
|
plaid_verification_header = request.headers["Plaid-Verification"]
|
||||||
|
|
||||||
Provider::Plaid.validate_webhook!(plaid_verification_header, webhook_body)
|
client = Provider::Plaid.new(Rails.application.config.plaid, region: :us)
|
||||||
Provider::Plaid.process_webhook(webhook_body)
|
|
||||||
|
client.validate_webhook!(plaid_verification_header, webhook_body)
|
||||||
|
client.process_webhook(webhook_body)
|
||||||
|
|
||||||
|
render json: { received: true }, status: :ok
|
||||||
|
rescue => error
|
||||||
|
Sentry.capture_exception(error)
|
||||||
|
render json: { error: "Invalid webhook: #{error.message}" }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
def plaid_eu
|
||||||
|
webhook_body = request.body.read
|
||||||
|
plaid_verification_header = request.headers["Plaid-Verification"]
|
||||||
|
|
||||||
|
client = Provider::Plaid.new(Rails.application.config.plaid_eu, region: :eu)
|
||||||
|
|
||||||
|
client.validate_webhook!(plaid_verification_header, webhook_body)
|
||||||
|
client.process_webhook(webhook_body)
|
||||||
|
|
||||||
render json: { received: true }, status: :ok
|
render json: { received: true }, status: :ok
|
||||||
rescue => error
|
rescue => error
|
||||||
|
|
|
@ -3,11 +3,11 @@ module Plaidable
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def plaid_us_provider
|
def plaid_us_provider
|
||||||
Provider::Plaid.new(Rails.application.config.plaid, :us) if Rails.application.config.plaid
|
Provider::Plaid.new(Rails.application.config.plaid, region: :us) if Rails.application.config.plaid
|
||||||
end
|
end
|
||||||
|
|
||||||
def plaid_eu_provider
|
def plaid_eu_provider
|
||||||
Provider::Plaid.new(Rails.application.config.plaid_eu, :eu) if Rails.application.config.plaid_eu
|
Provider::Plaid.new(Rails.application.config.plaid_eu, region: :eu) if Rails.application.config.plaid_eu
|
||||||
end
|
end
|
||||||
|
|
||||||
def plaid_provider_for_region(region)
|
def plaid_provider_for_region(region)
|
||||||
|
|
|
@ -4,9 +4,16 @@ class Provider::Plaid
|
||||||
MAYBE_SUPPORTED_PLAID_PRODUCTS = %w[transactions investments liabilities].freeze
|
MAYBE_SUPPORTED_PLAID_PRODUCTS = %w[transactions investments liabilities].freeze
|
||||||
MAX_HISTORY_DAYS = Rails.env.development? ? 90 : 730
|
MAX_HISTORY_DAYS = Rails.env.development? ? 90 : 730
|
||||||
|
|
||||||
class << self
|
def initialize(config, region: :us)
|
||||||
|
@client = Plaid::PlaidApi.new(
|
||||||
|
Plaid::ApiClient.new(config)
|
||||||
|
)
|
||||||
|
@region = region
|
||||||
|
end
|
||||||
|
|
||||||
def process_webhook(webhook_body)
|
def process_webhook(webhook_body)
|
||||||
parsed = JSON.parse(webhook_body)
|
parsed = JSON.parse(webhook_body)
|
||||||
|
|
||||||
type = parsed["webhook_type"]
|
type = parsed["webhook_type"]
|
||||||
code = parsed["webhook_code"]
|
code = parsed["webhook_code"]
|
||||||
|
|
||||||
|
@ -22,6 +29,9 @@ class Provider::Plaid
|
||||||
else
|
else
|
||||||
Rails.logger.warn("Unhandled Plaid webhook type: #{type}:#{code}")
|
Rails.logger.warn("Unhandled Plaid webhook type: #{type}:#{code}")
|
||||||
end
|
end
|
||||||
|
rescue => error
|
||||||
|
# Processing errors shouldn't return a 400 to Plaid since they are internal, so capture silently
|
||||||
|
Sentry.capture_exception(error)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_webhook!(verification_header, raw_body)
|
def validate_webhook!(verification_header, raw_body)
|
||||||
|
@ -54,14 +64,6 @@ class Provider::Plaid
|
||||||
actual_hash = Digest::SHA256.hexdigest(raw_body)
|
actual_hash = Digest::SHA256.hexdigest(raw_body)
|
||||||
raise JWT::VerificationError, "Invalid webhook body hash" unless ActiveSupport::SecurityUtils.secure_compare(expected_hash, actual_hash)
|
raise JWT::VerificationError, "Invalid webhook body hash" unless ActiveSupport::SecurityUtils.secure_compare(expected_hash, actual_hash)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(config, region)
|
|
||||||
@client = Plaid::PlaidApi.new(
|
|
||||||
Plaid::ApiClient.new(config)
|
|
||||||
)
|
|
||||||
@region = region
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil)
|
def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil)
|
||||||
request = Plaid::LinkTokenCreateRequest.new({
|
request = Plaid::LinkTokenCreateRequest.new({
|
||||||
|
|
|
@ -182,6 +182,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
namespace :webhooks do
|
namespace :webhooks do
|
||||||
post "plaid"
|
post "plaid"
|
||||||
|
post "plaid_eu"
|
||||||
post "stripe"
|
post "stripe"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue