Play Framework [2.4.x] OpenID 和 Oauth——对 oauth_signature_method 使用 PLAINTEXT
Play Framework [2.4.x] OpenID and Oauth -- using PLAINTEXT for oauth_signature_method
基于此处的文档:https://www.playframework.com/documentation/2.4.x/ScalaOAuth
我的 Twitter "reader" 运行没有问题。我可以授权 "my app" 并按预期通过 API 拨打电话。
我正在尝试对 FreshBooks 进行类似的处理,我似乎遇到了一个问题,即 oauth_signature_method
被预设为 HMAC-SHA1。
由于供应商对所有 API 调用强制执行 HTTPS,他们(仅)似乎支持 oauth_signature_method
的 PLAINTEXT
正在与供应商合作确认这一点。
搜索文档和代码以查看签名方法是否可以某种方式选择,如果是这样 when/where 我会将该更改注入代码吗?
如果其他人被这个绊倒并找到了解决方案,我们将不胜感激!
看来2/3s的解决方案是破解代码如下:
val serviceInfo = ServiceInfo(
"https://sample.freshbooks.com/oauth/oauth_request.php",
"https://sample.freshbooks.com/oauth/oauth_access.php",
"https://sample.freshbooks.com/oauth/oauth_authorize.php", KEY)
val provider = new CommonsHttpOAuthProvider(serviceInfo.requestTokenURL, serviceInfo.accessTokenURL, serviceInfo.authorizationURL)
val consumer = new DefaultOAuthConsumer(serviceInfo.key.key, serviceInfo.key.secret)
consumer.setMessageSigner(new PlainTextMessageSigner())
def retrieveAccessToken(token: RequestToken, verifier: String): Either[OAuthException, RequestToken] = {
consumer.setTokenWithSecret(token.token, token.secret)
try {
provider.retrieveAccessToken(consumer, verifier);
Right(new RequestToken(consumer.getToken(), consumer.getTokenSecret()))
} catch {
case ex: OAuthException => Left(ex)
}
}
def retrieveRequestToken(callbackURL: String): Either[OAuthException, RequestToken] = {
try {
provider.retrieveRequestToken(consumer, callbackURL)
Right(new RequestToken(consumer.getToken(), consumer.getTokenSecret()))
} catch {
case ex: OAuthException => Left(ex)
}
}
我说 2/3s 因为拼图的最后一块使用 OAuthCalculator
WS.url("https://sample.freshbooks.com/api/2.1/xml-in")
.sign(OAuthCalculator(KEY, credentials))
.post(data)
.map(result => Ok(result.body))
}
而且我找不到改变它的方法——我可能不得不重写整个 class 或创建一个新的。奇怪的是,我们有两种方法以一种方式做基本相同的事情,而另一种方法以完全不同的方式做这件事。
这完全是黑客攻击,因为我们应该能够
consumer.setMessageSigner(new PlainTextMessageSigner())
直接进入初始代码
我在 github 中发布了一个可行的解决方案:https://github.com/techmag/OAuthPlainTextCalculator
为方便起见,我将在此处重复代码:
class OAuthPlainTextCalculator(consumerKey: ConsumerKey, requestToken: RequestToken) extends WSSignatureCalculator with com.ning.http.client.SignatureCalculator {
import com.ning.http.client.{ Request, RequestBuilderBase }
import com.ning.http.util.UTF8UrlEncoder
import com.ning.http.util.Base64
val HEADER_AUTHORIZATION = "Authorization"
val KEY_OAUTH_CONSUMER_KEY = "oauth_consumer_key"
val KEY_OAUTH_NONCE = "oauth_nonce"
val KEY_OAUTH_SIGNATURE = "oauth_signature"
val KEY_OAUTH_SIGNATURE_METHOD = "oauth_signature_method"
val KEY_OAUTH_TIMESTAMP = "oauth_timestamp"
val KEY_OAUTH_TOKEN = "oauth_token"
val KEY_OAUTH_VERSION = "oauth_version"
val OAUTH_VERSION_1_0 = "1.0"
val OAUTH_SIGNATURE_METHOD = "PLAINTEXT"
protected final val nonceBuffer: Array[Byte] = new Array[Byte](16)
override def calculateAndAddSignature(request: Request, requestBuilder: RequestBuilderBase[_]): Unit = {
val nonce: String = generateNonce
val timestamp: Long = System.currentTimeMillis() / 1000L
val signature = calculateSignature(request.getMethod, request.getUrl, timestamp, nonce, request.getFormParams, request.getQueryParams)
val headerValue = constructAuthHeader(signature, nonce, timestamp)
requestBuilder.setHeader(HEADER_AUTHORIZATION, headerValue);
}
/**
* from http://oauth.net/core/1.0/#signing_process
* oauth_signature is set to the concatenated encoded values of the
* Consumer Secret and Token Secret,
* separated by a ‘&’ character (ASCII code 38),
* even if either secret is empty.
* The result MUST be encoded again.
*/
def calculateSignature(method: String, baseURL: String, oauthTimestamp: Long, nonce: String, formParams: java.util.List[com.ning.http.client.Param], queryParams: java.util.List[com.ning.http.client.Param]) = {
val signedText = new StringBuilder(100)
signedText.append(consumerKey.secret)
signedText.append('&');
signedText.append(requestToken.secret)
UTF8UrlEncoder.encode(signedText.toString)
}
def constructAuthHeader(signature: String, nonce: String, oauthTimestamp: Long, sb: StringBuilder = new StringBuilder) = {
constructAuthHeader_sb(signature, nonce, oauthTimestamp).toString
}
def constructAuthHeader_sb(signature: String, nonce: String, oauthTimestamp: Long, sb: StringBuilder = new StringBuilder(250)) = {
sb.synchronized {
sb.append("OAuth ")
sb.append(KEY_OAUTH_CONSUMER_KEY)
sb.append("=\"")
sb.append(consumerKey.key)
sb.append("\", ")
sb.append(KEY_OAUTH_TOKEN)
sb.append("=\"")
sb.append(requestToken.token)
sb.append("\", ")
sb.append(KEY_OAUTH_SIGNATURE_METHOD)
sb.append("=\"")
sb.append(OAUTH_SIGNATURE_METHOD)
sb.append("\", ")
// careful: base64 has chars that need URL encoding:
sb.append(KEY_OAUTH_SIGNATURE)
sb.append("=\"");
sb.append(signature)
sb.append("\", ")
sb.append(KEY_OAUTH_TIMESTAMP)
sb.append("=\"")
sb.append(oauthTimestamp)
sb.append("\", ")
// also: nonce may contain things that need URL encoding (esp. when using base64):
sb.append(KEY_OAUTH_NONCE)
sb.append("=\"");
sb.append(UTF8UrlEncoder.encode(nonce))
sb.append("\", ")
sb.append(KEY_OAUTH_VERSION)
sb.append("=\"")
sb.append(OAUTH_VERSION_1_0)
sb.append("\"")
sb
}
}
def generateNonce = synchronized {
scala.util.Random.nextBytes(nonceBuffer)
// let's use base64 encoding over hex, slightly more compact than hex or decimals
Base64.encode(nonceBuffer)
}
}
object OAuthPlainTextCalculator {
def apply(consumerKey: ConsumerKey, token: RequestToken): WSSignatureCalculator = {
new OAuthPlainTextCalculator(consumerKey, token)
}
}
基于此处的文档:https://www.playframework.com/documentation/2.4.x/ScalaOAuth
我的 Twitter "reader" 运行没有问题。我可以授权 "my app" 并按预期通过 API 拨打电话。
我正在尝试对 FreshBooks 进行类似的处理,我似乎遇到了一个问题,即 oauth_signature_method
被预设为 HMAC-SHA1。
由于供应商对所有 API 调用强制执行 HTTPS,他们(仅)似乎支持 oauth_signature_method
正在与供应商合作确认这一点。
搜索文档和代码以查看签名方法是否可以某种方式选择,如果是这样 when/where 我会将该更改注入代码吗?
如果其他人被这个绊倒并找到了解决方案,我们将不胜感激!
看来2/3s的解决方案是破解代码如下:
val serviceInfo = ServiceInfo(
"https://sample.freshbooks.com/oauth/oauth_request.php",
"https://sample.freshbooks.com/oauth/oauth_access.php",
"https://sample.freshbooks.com/oauth/oauth_authorize.php", KEY)
val provider = new CommonsHttpOAuthProvider(serviceInfo.requestTokenURL, serviceInfo.accessTokenURL, serviceInfo.authorizationURL)
val consumer = new DefaultOAuthConsumer(serviceInfo.key.key, serviceInfo.key.secret)
consumer.setMessageSigner(new PlainTextMessageSigner())
def retrieveAccessToken(token: RequestToken, verifier: String): Either[OAuthException, RequestToken] = {
consumer.setTokenWithSecret(token.token, token.secret)
try {
provider.retrieveAccessToken(consumer, verifier);
Right(new RequestToken(consumer.getToken(), consumer.getTokenSecret()))
} catch {
case ex: OAuthException => Left(ex)
}
}
def retrieveRequestToken(callbackURL: String): Either[OAuthException, RequestToken] = {
try {
provider.retrieveRequestToken(consumer, callbackURL)
Right(new RequestToken(consumer.getToken(), consumer.getTokenSecret()))
} catch {
case ex: OAuthException => Left(ex)
}
}
我说 2/3s 因为拼图的最后一块使用 OAuthCalculator
WS.url("https://sample.freshbooks.com/api/2.1/xml-in")
.sign(OAuthCalculator(KEY, credentials))
.post(data)
.map(result => Ok(result.body))
}
而且我找不到改变它的方法——我可能不得不重写整个 class 或创建一个新的。奇怪的是,我们有两种方法以一种方式做基本相同的事情,而另一种方法以完全不同的方式做这件事。
这完全是黑客攻击,因为我们应该能够
consumer.setMessageSigner(new PlainTextMessageSigner())
直接进入初始代码
我在 github 中发布了一个可行的解决方案:https://github.com/techmag/OAuthPlainTextCalculator
为方便起见,我将在此处重复代码:
class OAuthPlainTextCalculator(consumerKey: ConsumerKey, requestToken: RequestToken) extends WSSignatureCalculator with com.ning.http.client.SignatureCalculator {
import com.ning.http.client.{ Request, RequestBuilderBase }
import com.ning.http.util.UTF8UrlEncoder
import com.ning.http.util.Base64
val HEADER_AUTHORIZATION = "Authorization"
val KEY_OAUTH_CONSUMER_KEY = "oauth_consumer_key"
val KEY_OAUTH_NONCE = "oauth_nonce"
val KEY_OAUTH_SIGNATURE = "oauth_signature"
val KEY_OAUTH_SIGNATURE_METHOD = "oauth_signature_method"
val KEY_OAUTH_TIMESTAMP = "oauth_timestamp"
val KEY_OAUTH_TOKEN = "oauth_token"
val KEY_OAUTH_VERSION = "oauth_version"
val OAUTH_VERSION_1_0 = "1.0"
val OAUTH_SIGNATURE_METHOD = "PLAINTEXT"
protected final val nonceBuffer: Array[Byte] = new Array[Byte](16)
override def calculateAndAddSignature(request: Request, requestBuilder: RequestBuilderBase[_]): Unit = {
val nonce: String = generateNonce
val timestamp: Long = System.currentTimeMillis() / 1000L
val signature = calculateSignature(request.getMethod, request.getUrl, timestamp, nonce, request.getFormParams, request.getQueryParams)
val headerValue = constructAuthHeader(signature, nonce, timestamp)
requestBuilder.setHeader(HEADER_AUTHORIZATION, headerValue);
}
/**
* from http://oauth.net/core/1.0/#signing_process
* oauth_signature is set to the concatenated encoded values of the
* Consumer Secret and Token Secret,
* separated by a ‘&’ character (ASCII code 38),
* even if either secret is empty.
* The result MUST be encoded again.
*/
def calculateSignature(method: String, baseURL: String, oauthTimestamp: Long, nonce: String, formParams: java.util.List[com.ning.http.client.Param], queryParams: java.util.List[com.ning.http.client.Param]) = {
val signedText = new StringBuilder(100)
signedText.append(consumerKey.secret)
signedText.append('&');
signedText.append(requestToken.secret)
UTF8UrlEncoder.encode(signedText.toString)
}
def constructAuthHeader(signature: String, nonce: String, oauthTimestamp: Long, sb: StringBuilder = new StringBuilder) = {
constructAuthHeader_sb(signature, nonce, oauthTimestamp).toString
}
def constructAuthHeader_sb(signature: String, nonce: String, oauthTimestamp: Long, sb: StringBuilder = new StringBuilder(250)) = {
sb.synchronized {
sb.append("OAuth ")
sb.append(KEY_OAUTH_CONSUMER_KEY)
sb.append("=\"")
sb.append(consumerKey.key)
sb.append("\", ")
sb.append(KEY_OAUTH_TOKEN)
sb.append("=\"")
sb.append(requestToken.token)
sb.append("\", ")
sb.append(KEY_OAUTH_SIGNATURE_METHOD)
sb.append("=\"")
sb.append(OAUTH_SIGNATURE_METHOD)
sb.append("\", ")
// careful: base64 has chars that need URL encoding:
sb.append(KEY_OAUTH_SIGNATURE)
sb.append("=\"");
sb.append(signature)
sb.append("\", ")
sb.append(KEY_OAUTH_TIMESTAMP)
sb.append("=\"")
sb.append(oauthTimestamp)
sb.append("\", ")
// also: nonce may contain things that need URL encoding (esp. when using base64):
sb.append(KEY_OAUTH_NONCE)
sb.append("=\"");
sb.append(UTF8UrlEncoder.encode(nonce))
sb.append("\", ")
sb.append(KEY_OAUTH_VERSION)
sb.append("=\"")
sb.append(OAUTH_VERSION_1_0)
sb.append("\"")
sb
}
}
def generateNonce = synchronized {
scala.util.Random.nextBytes(nonceBuffer)
// let's use base64 encoding over hex, slightly more compact than hex or decimals
Base64.encode(nonceBuffer)
}
}
object OAuthPlainTextCalculator {
def apply(consumerKey: ConsumerKey, token: RequestToken): WSSignatureCalculator = {
new OAuthPlainTextCalculator(consumerKey, token)
}
}