无法执行成功的 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 验证的主要内容,如果未能解决你的问题,请参考以下文章