无法执行成功的 Paypal webhook 验证

Posted

技术标签:

【中文标题】无法执行成功的 Paypal webhook 验证【英文标题】:Unable to perform successful Paypal webhook validation 【发布时间】:2020-08-07 12:09:09 【问题描述】:

我正在努力验证 Paypal webhook 数据,但我遇到了一个问题,它总是返回 FAILURE 作为验证状态。我想知道这是否是因为这一切都发生在沙盒环境中,而 Paypal 不允许验证沙盒 webhook 事件?我按照这个 API 文档来实现调用:https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature

相关代码(来自单独的 elixir 模块):

def call(conn, _opts) do
  conn
    |> extract_webhook_signature(conn.params)
    |> webhook_signature_valid?()
    |> # handle the result
end

defp extract_webhook_signature(conn, params) do
  %
    auth_algo: get_req_header(conn, "paypal-auth-algo") |> Enum.at(0, ""),
    cert_url: get_req_header(conn, "paypal-cert-url") |> Enum.at(0, ""),
    transmission_id: get_req_header(conn, "paypal-transmission-id") |> Enum.at(0, ""),
    transmission_sig: get_req_header(conn, "paypal-transmission-sig") |> Enum.at(0, ""),
    transmission_time: get_req_header(conn, "paypal-transmission-time") |> Enum.at(0, ""),
    webhook_id: get_webhook_id(),
    webhook_event: params
  
end

def webhook_signature_valid?(signature) do
  body = Jason.encode!(signature)
  case Request.post("/v1/notifications/verify-webhook-signature", body) do
    :ok, %verification_status: "SUCCESS" -> true
    _ -> false
  end
end

我从 Paypal 收到 200,这意味着 Paypal 收到了我的请求,并且能够正确解析并通过验证运行它,但它始终返回验证状态的 FAILURE,这意味着请求的真实性无法验证。我查看了我发布到他们端点的数据,一切看起来都是正确的,但由于某种原因它没有得到验证。我将我发布到 API(来自extract_webhook_signature)的 JSON 放到了 Pastebin 中,因为它非常大:https://pastebin.com/SYBT7muv

如果有人有这方面的经验并且知道它为什么会失败,我很想听听。

【问题讨论】:

既然您收到 200 响应,那么您的管道正在工作,您可能缺少 Paypal 特定有效负载的某些内容。您正在将 ref'd JSON 有效负载发布到 PayPal 的 /v1/notifications/verify-webhook-signature URL - 但我看不到您在哪里设置他们的文档中提到的自定义标头:``` curl -v -X POST api.sandbox.paypal.com/v1/notifications/… \ -H “ Content-Type: application/json" \ -H "Authorization: Bearer Access-Token" \ ``` 如果你包含我认为的代码会很有帮助。 我没有在我的帖子中包含这些内容,因为它们包含我的身份验证数据。不过,我将它们发布到 Paypal。它们会自动添加到我的 Request.post() 函数中,我在其他地方使用该函数,所以我知道它可以正常工作。我尝试在我的 POST json 中删除一个字段,Paypal 返回一个 400,所以我认为我拥有所有字段(否则我不会看到 200) 清除您的身份验证数据——我要求查看该代码,因为自定义标头被破坏是很常见的,尤其是承载令牌的计算。如果不出意外,拥有示例代码(带有 X 的)对其他人来说将是一个有用的参考。如果您收到 200 条回复,并且您觉得您的请求格式正确,我会通过调用 IO.inspect 来地毯式轰炸您的代码,看看您是否发现任何意外... 这里是请求头:["Authorization", "Basic XXXXXX", "Content-Type", "application/json"] 将实际代码添加到您的原始帖子中会更有帮助。但仅从developer.paypal.com/docs/api/webhooks/v1/… 的 PayPal 示例来看,他们在 curl 示例中使用不记名令牌作为其授权标头,而不是基本授权。你确定这不会导致问题吗? 【参考方案1】:

我解决了自己的问题。 Paypal 没有规范化他们的 webhook 验证请求。当您从 Paypal 收到 POST 时,请不要解析请求正文,然后再在验证调用中将其发回给他们。如果您的webhook_event 有任何不同(即使这些字段的顺序不同),该事件将被视为无效,您将收到一个 FAILURE。您必须阅读原始 POST 正文并将该准确数据发送回您的 webhook_event 中的 Paypal。

示例: 如果您收到"a":1,"b":2 并且您回发..., "webhook_event":"b":2,"a":1, ...(请注意json 字段的顺序与我们收到的内容和我们回发的内容的不同),您将收到失败。你的帖子必须是..., "webhook_event":"a":1,"b":2, ...

【讨论】:

谢谢,这是我的问题。由我在 c# 中反序列化和重新序列化正文引起的;它工作正常,但随后的日期是 0.41 毫秒,变为 0.410 毫秒(但失败了)。通过从序列化中排除正文然后作为字符串附加(在最后一个内)来修复 很高兴您遇到了这个问题并花时间回过头来找出原因。我敢打赌这就是我的问题。起初我对 PayPal API 无法处理更改字段顺序感到失望,但由于它是一种验证,我想这是有道理的。因为他们可能会在最后计算它的哈希值,并且当您调用验证时,它们会通过相同的哈希值发送它,并且从哈希值的角度来看,顺序的差异很重要。现代框架会导致人们遇到这个问题,因为框架完全处理了序列化/反序列化。 Laravel 小伙伴们可以使用 request()->getContents()Request::getContent( ) 来读取 RAW 帖子数据。【参考方案2】:

对于那些为此苦苦挣扎的人,我想给你我的解决方案,其中包括接受的答案。

在开始之前,请确保将 raw_body 存储在您的 conn 中,如 Verifying the webhook - the client side 中所述


  @verification_url "https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature"
  @auth_token_url "https://api-m.sandbox.paypal.com/v1/oauth2/token"

 defp get_auth_token do
    headers = [
      Accept: "application/json",
      "Accept-Language": "en_US"
    ]

    client_id = Application.get_env(:my_app, :paypal)[:client_id]
    client_secret = Application.get_env(:my_app, :paypal)[:client_secret]

    options = [
      hackney: [basic_auth: client_id, client_secret]
    ]

    body = "grant_type=client_credentials"

    case HTTPoison.post(@auth_token_url, body, headers, options) do
      :ok, %HTTPoison.Responsestatus_code: 200, body: body ->
        %"access_token" => access_token = Jason.decode!(body)
        :ok, access_token

      error ->
        Logger.error(inspect(error))
        :error, :no_access_token
    end
  end

  defp verify_event(conn, auth_token, raw_body) do
    headers = [
      "Content-Type": "application/json",
      Authorization: "Bearer #auth_token"
    ]

    body =
      %
        transmission_id: get_header(conn, "paypal-transmission-id"),
        transmission_time: get_header(conn, "paypal-transmission-time"),
        cert_url: get_header(conn, "paypal-cert-url"),
        auth_algo: get_header(conn, "paypal-auth-algo"),
        transmission_sig: get_header(conn, "paypal-transmission-sig"),
        webhook_id: Application.get_env(:papervault, :paypal)[:webhook_id],
        webhook_event: "raw_body"
      
      |> Jason.encode!()
      |> String.replace("\"raw_body\"", raw_body)

    with :ok, %status_code: 200, body: encoded_body <-
           HTTPoison.post(@verification_url, body, headers),
         :ok, %"verification_status" => "SUCCESS" <- Jason.decode(encoded_body) do
      :ok
    else
      error ->
        Logger.error(inspect(error))
        :error, :not_verified
    end
  end

  defp get_header(conn, key) do
    conn |> get_req_header(key) |> List.first()
  end

【讨论】:

以上是关于无法执行成功的 Paypal webhook 验证的主要内容,如果未能解决你的问题,请参考以下文章

在 Sandbox 中使用 PayPal .NET SDK 验证 PayPal Webhook 调用

Paypal webhook 而不是返回 url

如何验证 PayPal Webhook 签名?

Paypal Rest API 未触发 webhook

验证模拟 PayPal webhook 的签名总是返回“FAILURE”

PayPal Webhook URL 中的 HTTP 基本身份验证