【问题标题】:How to verify request is coming from Twilio on Google Cloud Platform with http4k?如何使用 http4k 验证请求来自 Google Cloud Platform 上的 Twilio?
【发布时间】:2021-09-18 22:55:46
【问题描述】:

我有一台使用 Kotlin 1.5、JDK 11、http4k v4.12 的服务器,还有使用 Google Cloud Run 托管的 Twilio Java SDK v8.19。

我使用 Twilio 的 Java SDK RequestValidator 创建了一个谓词。

import com.twilio.security.RequestValidator
import mu.KotlinLogging
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.body.form
import org.http4k.core.queries
import org.http4k.core.then
import org.http4k.core.toParametersMap
import org.http4k.filter.RequestPredicate
import org.http4k.filter.ServerFilters
import org.http4k.lens.Header

private val twilioAuthHeaderLens = Header.optional("X-Twilio-Signature")
/** Twilio's helper [RequestValidator]. */
private val twilioValidator = RequestValidator("my-auth-token")

/**
 * Use the Twilio helper validator, [RequestValidator]
 */
val twilioAuthPredicate: RequestPredicate = { request ->

  when (val requestSignature: String? = twilioAuthHeaderLens(request)) {
    null -> {
      logger.debug { "Request has no Twilio request header valid" }
      false
    }
    else -> {
      val uri: String = request.uri.toString()
      val paramMap: Map<String, String?> = request.form().toMap()

      logger.info { "Validating request with uri: $uri, paramMap: $paramMap, signature: $requestSignature" }
      val isTwilioSignatureValid = twilioValidator.validate(uri, paramMap, requestSignature)
      logger.info { "Request Twilio valid: $isTwilioSignatureValid" }
      isTwilioSignatureValid
    }
  }

}

这可以使用the example Twilio 提供,如 Kotest 单元测试所示。

(测试和示例代码不匹配 - 但OperatorAuth 是一个应用twilioAuthPredicate 的类,ApplicationProperties 从 .env 文件中获取 Twilio 身份验证密钥。)

test("demo https://www.twilio.com/docs/usage/security") {

  val twilioApiKey = "12345"
  val appProps = ApplicationProperties(
    TWILIO_API_AUTH_TOKEN(twilioApiKey, TEST_ENV)
  )

  // system-under-test
  val handler: HttpHandler = OperatorAuth(appProps).then { Response(OK) }

  // construct a GET request: https://mycompany.com/myapp.php?foo=1&bar=2
  val urlProto = "https"
  val urlBase = "mycompany.com"

  val requestSignature = "0/KCTR6DLpKmkAf8muzZqo1nDgQ="

  val request = Request(Method.GET, "$urlProto://$urlBase/myapp.php")

    .query("foo", "1")
    .query("bar", "2")

    .form("CallSid", "CA1234567890ABCDE")
    .form("Caller", "+12349013030")
    .form("Digits", "1234")
    .form("From", "+12349013030")
    .form("To", "+18005551212")

    .header("X-Twilio-Signature", requestSignature)
    .header("X-Forwarded-Proto", urlProto)
    .header("Host", urlBase)

  val response = handler(request)
  response shouldHaveStatus OK
}

但是,除了这个简单的示例之外,其他请求都不起作用,无论是在创建单元测试时还是在运行时。所有 Twilio 请求均未通过验证,我的服务器返回 401。Twilio 网站中的信息完全不透明。这令人难以置信的沮丧。它没有告诉我它是如何计算哈希的,所以我不知道出了什么问题。

Warning  15003
Message  Got HTTP 401 response to https://my-gcr-server.run.app/twilio

这是一个使用从日志中收集的真实值的示例测试(尽管我已经编辑了标识符)。

test("real request") {

  val appProps = ApplicationProperties() // this loads the Twilio Auth Key from my environment variables

  val handler: HttpHandler = OperatorAuth(appProps).then { Response(OK) }

  // construct a GET request
  val urlProto = "https"
  val urlBase = "my-gcr-server.run.app"

  val requestSignature = "GATG2313LSuCYRbPASD4axJ26XyTk="

  val request = Request(Method.GET, "$urlProto://$urlBase/voicemail/transcript")

    .query("ApplicationSid", "AP1234567890abcdefg")
    .query("ApiVersion", "2010-04-01")
    .query("Called", "")
    .query("Caller", "client:Anonymous")
    .query("CallStatus", "ringing")
    .query("CallSid", "CA1234567890abcdefg")
    .query("From", "client:Anonymous")
    .query("To", "")
    .query("Direction", "inbound")
    .query("AccountSid", "AC1234567890abcdefg")
    // note, changing these variables to be form parameters doesn't affect the result, Twilio's validator still says the request is invalid.

    .header("X-Twilio-Signature", requestSignature)
    .header("I-Twilio-Idempotency-Token", "337aaaa-1111-2222-3333-ffffb5333")
    .header("Content-Type", "text/html")
    .header("User-Agent: ", "TwilioProxy/1.1")
    .header("X-Forwarded-Proto", urlProto)
    .header("Host", urlBase)

  val response = handler(request)
  response shouldHaveStatus OK // this fails, Status: expected:<200 OK> but was:<401 Unauthorized>
}

有时验证会因 Google Cloud 而失败。我之前在 Google Cloud Functions 上托管了我的服务器,直到我发现 GCF 会默默地忽略部分 URI https://github.com/GoogleCloudPlatform/functions-framework-java/issues/90

还有一个问题,如果请求被“修改”,例如,如果我设置 Twilio 回调 URL 以包含查询参数,例如https://my-gcr-server.app.run/twilio/callback?type=recording,则 Twilio 签名会忽略此参数,但在验证身份验证时,无法知道 Twilio 忽略了哪些参数。如果标题被更改,也是如此。

是否有验证请求来自 Twilio 的工作方法?还是替代验证解决方案?

更新

我刚刚发现 Twilio 的 RequestValidator 确实测试不足,只有一个例子 RequestValidatorTest

【问题讨论】:

    标签: java kotlin google-cloud-platform twilio http4k


    【解决方案1】:

    这里是 Twilio 开发者宣传员。

    文档describes how the signature is created 可能会显示您测试方式的一些差异。在您的服务器上,检查签名的算法是:

    1. 获取您为电话号码或应用程序指定的请求 URL 的完整 URL,从协议 (https...) 到查询字符串的末尾(? 之后的所有内容)。
    2. 如果请求是 POST,请按字母顺序对所有 POST 参数进行排序(使用 Unix 风格区分大小写的排序顺序)。
    3. 遍历已排序的 POST 参数列表,并将变量名称和值(不带分隔符)附加到 URL 字符串的末尾。
    4. 使用您的 AuthToken 作为密钥,使用 HMAC-SHA1 对结果字符串进行签名(请记住,您的 AuthToken 的大小写很重要!)。
    5. Base64 编码生成的哈希值。
    6. 将您的哈希值与我们在 X-Twilio-Signature 标头中提交的哈希值进行比较。如果它们匹配,那么您就可以开始了。

    您正在使用GET 请求,因此您可以放弃第 2 步和第 3 步。

    我可以从这个算法中看到一些可能会导致您测试验证器的方式不同的东西。

    您在现实生活中的测试错误使用了 URL https://my-gcr-server.run.app/twilio,但您在真实请求中的测试脚本使用了https://my-gcr-server.run.app/voicemail/transcript。 URL 对签名的生成很重要。

    您的测试还将查询参数添加到请求中,但很难知道这些参数的顺序是什么。 URL 中查询参数的顺序应与 Twilio 发出请求的 URL 完全相同。

    另一方面,如果原始 Twilio 请求是 POST 请求,则应将这些参数作为表单参数添加,因为算法采用表单参数,对它们进行排序并将它们附加到 URL,没有分隔符.

    你说:

    还有一个问题,如果请求被“修改”,例如,如果我设置 Twilio 回调 URL 以包含查询参数,例如https://my-gcr-server.app.run/twilio/callback?type=recording,然后 Twilio 签名会忽略此参数,但是在验证身份验证时,无法知道 Twilio 忽略了哪些参数。如果标题被更改,也是如此。

    这不是真的,查询参数是 URL 的一部分,正如我上面所说的。 Twilio 不会忽略参数,它会根据上述算法处理它们。至于标头,除了用于测试签名的X-Twilio-Signature 之外,它们不会发挥作用。

    说了这么多,我不确定为什么现实生活中的请求会使验证器失败,因为它应该处理我上面讨论的所有事情。您可以检查用于validate a requestget a signature 的代码。

    在您的代码中:

          val uri: String = request.uri.toString()
          val paramMap: Map<String, String?> = request.form().toMap()
    
          logger.info { "Validating request with uri: $uri, paramMap: $paramMap, signature: $requestSignature" }
          val isTwilioSignatureValid = twilioValidator.validate(uri, paramMap, requestSignature)
          logger.info { "Request Twilio valid: $isTwilioSignatureValid" }
          isTwilioSignatureValid
    

    您能否保证uri 确实是 Twilio 发出请求的原始 URL,而不是已被解析为多个部分并以不同顺序与查询参数组合在一起的 URL?在GET 请求中,request.form().toMap() 是否返回空的Map

    抱歉,这不是一个完整的答案,我不是 Java/Kotlin 开发人员。我希望这能让您对要研究的内容有一个很好的了解。

    【讨论】:

      【解决方案2】:

      我遇到了麻烦,因为我正在使用 ngrok 进行测试,以便在开发时将请求路由到我的本地服务器。我有的是我正在运行算法(根据Twilio docs,请参见上面的 philnash 的回答)

      但是,当我将回调设置为 Twilio 用于计算签名的 https ngrok 端点时,向我发送的实际请求是 http 端点,ngrok 将 https 转发到免费帐户上的 http。

      所以我正在测试一个 http 端点,但 Twilio 正在根据 https 端点进行计算。

      当我告诉 Twilio 在 http 端点上回调时,没有 ngrok 误导并且签名匹配!

      我还从 philnash 的回答中的“验证请求:和“获取签名”链接中注意到,代码在 URL 中尝试了使用端口(例如 443 或 80),并且没有并接受任一签名作为匹配项.

      【讨论】:

        猜你喜欢
        • 2019-04-10
        • 2020-09-09
        • 1970-01-01
        • 1970-01-01
        • 2020-02-11
        • 2017-09-23
        • 2020-04-07
        • 1970-01-01
        • 2019-11-06
        相关资源
        最近更新 更多