我有一个使用Kotlin1.5、JDK11、http4k v4.12的服务器,我还有Twilio Java SDKv8.19,它是使用Google Cloud Run托管的。
我已经使用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 provide实现的,如此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
发布于 2021-09-21 03:01:07
Twilio开发者的布道者在这里。
describes how the signature is created和文档可能会显示出测试方式的一些不同之处。在您的服务器上,检查签名的算法是:
您使用的是GET请求,因此可以放弃步骤2和3。
我可以从这个算法中看到一些东西,它们可能会导致您测试验证器的方式不同。
您在现实生活中测试的错误使用的是URLhttps://my-gcr-server.run.app/twilio,但是来自真实请求的测试脚本使用的是https://my-gcr-server.run.app/voicemail/transcript。URL在签名的生成过程中很重要。
您的测试还会向请求中添加查询参数,但是很难知道这些参数的顺序。URL中查询参数的顺序应该与Twilio向其发出请求的URL完全相同。
另一方面,如果最初的Twilio请求是一个POST请求,那么这些参数应该作为表单参数添加,因为算法采用表单参数,对它们进行排序,并将它们附加到POST中,而不使用分隔符。
你说过:
还有一个问题,如果一个请求是‘修改’的,例如,如果我设置了一个Twilio回调URL来包含一个查询参数,例如https://my-gcr-server.app.run/twilio/callback?type=recording,那么Twilio签名会忽略这个参数,但在验证身份验证时,不可能知道Twilio忽略了哪些参数。如果标头被更改,情况也是如此。
这不是真的,正如我上面所说的,查询参数是URL的一部分。Twilio不会忽略参数,它会根据上述算法处理参数。至于头部,除了用于测试签名的X-Twilio-Signature之外,它们不起作用。
说了这么多,我不确定为什么现实生活中的请求会让验证器失败,因为它应该处理我上面讨论的所有事情。您可以检查用于validate a request和get 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开发人员。我希望这能给你一个好主意,让你知道该调查什么。
发布于 2021-11-23 06:59:15
我遇到了麻烦,因为我正在测试ngrok,以便在开发时将请求路由到我的本地服务器。我所做的就是我正在运行算法(根据Twilio docs,参见上面philnash的答案)
然而,当我将回调设置到Twilio用来计算签名的https ngrok端点时,cam对我的实际请求是http端点,ngrok将https转发到免费帐户上的http。
所以我测试的是http端点,但是Twilio是针对https端点进行计算的。
当我告诉Twilio在http端点上回调时,没有ngrok误导,签名匹配!
另外,我从上面philnash答案中的“验证请求:和获取签名”链接中注意到,代码尝试在URL中使用端口(例如443或80)和不使用端口(例如443或80),并接受任何一个签名作为匹配。
https://stackoverflow.com/questions/69239062
复制相似问题