Вебхук от платёжки:...
 
Уведомления
Очистить всё

Вебхук от платёжки: Rails 7.0 отвечает 422, InvalidAuthenticityToken

5 Сообщения
4 Пользователи
0 Реакции
423 Просмотры
Илья50
Сообщения: 6
Автор темы
Дружит с bundler фрилансер
Присоединился: 4 года назад
[#15]

Делаю заказчику приём уведомлений об оплате. Платёжка шлёт POST с JSON на /payments/callback, а Rails 7.0 отвечает 422, в логе ActionController::InvalidAuthenticityToken. Контроллер обычный, от ApplicationController. Нашёл совет убрать protect_from_forgery из ApplicationController, но это как-то совсем грубо. Как принято?


4 ответа
GalinaKorneva
Сообщения: 30
Читает чужие PR бэкенд-разработчица
Присоединился: 4 года назад

Отключать глобально не надо. Для вебхуков удобно завести отдельный контроллер от ActionController::API: там нет ни CSRF, ни сессий, ни куки. И раз CSRF нет, подпись проверять обязательно, иначе любой сможет прислать «оплачено».

class Webhooks::PaymentsController < ActionController::API
  before_action :verify_signature

  def create
    PaymentEventJob.perform_later(request.raw_post)
    head :ok
  end

  private

  def verify_signature
    secret = Rails.application.credentials.dig(:payments, :webhook_secret)
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
    given = request.headers["X-Signature"].to_s
    head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, given)
  end
end

Подпись считается от сырого тела, поэтому request.raw_post, а не params.to_json: после разбора JSON порядок ключей и пробелы другие, подпись не сойдётся. Заголовок у каждой платёжки свой, смотри их документацию. И отвечай 200 быстро, обработку в джобу: если держать соединение секунд десять, они решат, что не дошло, и пришлют ещё раз.


Ответ
Валерий40
Сообщения: 33
Читает чужие PR фулстек-разработчик
Присоединился: 4 года назад

В Laravel для этого был массив $except в мидлваре VerifyCsrfToken, и я по привычке тоже искал такой список. В Rails, как понял, ближайший аналог это skip_forgery_protection прямо в контроллере. Но вариант с ActionController::API чище, там и сессия не создаётся.


Ответ
kostya.sokurov
Сообщения: 73
Держит прод бэкенд-разработчик, senior
Присоединился: 4 года назад

И сохраняй сырое тело в таблицу до обработки. Когда придут разбираться, что именно прислала платёжка, это будет единственный источник правды.


Ответ
Илья50
Сообщения: 6
Автор темы
Дружит с bundler фрилансер
Присоединился: 4 года назад

Сделал через ActionController::API. Подпись сошлась с первого раза, как только перешёл на raw_post, до этого считал от params и получал unauthorized на каждом запросе. Тело кладу в таблицу payment_events с уникальным индексом по id события, чтобы повторная отправка не создавала второй платёж. Ещё словил забавное: в тестовом режиме платёжка присылала подпись маленькими буквами, а в боевом большими, и secure_compare, конечно, не сходился. Теперь привожу заголовок к нижнему регистру перед сравнением. Неделю ходит без ошибок, заказчик доволен.


Ответ