我有一台使用 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
}
}
}
这可以使用Twilio 提供的示例来工作,正如 Kotest 单元测试所演示的那样。
(测试和示例代码不匹配 - 但OperatorAuth
它是一个应用twilioAuthPredicate
, 并ApplicationProperties
从 .env 文件中获取 Twilio auth 密钥的类。)
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