【发布时间】: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