I try to create an App to send automagically chess tournament announcements to X. Everything works as expected except the OAuth-1.0 authorization. I followed Kevin Williams. HMAC-SHA1 by Aravind_G
Here is the code. I omitted the JSON part which works fine. Please believe me that i provide the raw credentials in a correct way :-)
package de.qno.tournamentadmin
import org.joda.time.DateTime
import sttp.client4.*
import sttp.model.*
import java.net.URLEncoder
import javax.crypto.spec.SecretKeySpec
import scala.util.Random
[…]
/**
* With thx to Kevin Williams
* https://medium.com/@kevinwilliams.dev/posting-to-x-twitter-with-oauth-1-0-8d4de172cfa6
*/
object Auth:
def urlEncode(text: String): String =
URLEncoder.encode(text, java.nio.charset.Charset.defaultCharset())
def generateNonce: String =
Random.alphanumeric.take(15).mkString
def sortData(data: Map[String, String]): Seq[(String, String)] =
data.toSeq.sortBy(_._1) // sort alphabetically
def reformatData(data: Seq[(String, String)]): Seq[String] =
data.map(x => s"${x._1}=${x._2}")
def reformatSignedData(data: Seq[(String, String)]): Seq[String] =
data.map(x => s"${x._1}=\"${urlEncode(x._2)}\"")
/**
* With thx to Aravind_G
* https://community.gatling.io/t/hmac-sha1-signature-generation-using-scala/5844
* @param text
* @return
*/
def sha1sign (text: String): String =
val shaHash = javax.crypto.Mac.getInstance("HmacSHA1")
val secretKey = s"${urlEncode(TournamentAdmin.xApiKeySecret)}&${urlEncode(TournamentAdmin.xAccessTokenSecret)}"
val signingKey = new SecretKeySpec(secretKey.getBytes, "HmacSHA1")
shaHash.init(signingKey)
val signatureHash = shaHash.doFinal(text.getBytes("UTF-8"))
val baseEncoder = java.util.Base64.getEncoder
val signatureString = (for byte <- signatureHash yield f"$byte%02X").mkString
baseEncoder.encodeToString(signatureString.getBytes)
def signedHeader: Header =
val collectAuthHeaderMap = Map(
"oauth_consumer_key" -> TournamentAdmin.xApiKey,
"oauth_signature_method" -> "HMAC-SHA1",
"oauth_timestamp" -> DateTime().getMillis.toString,
"oauth_nonce" -> generateNonce,
"oauth_token" -> TournamentAdmin.xAccesToken,
"oauth_version" -> "1.0"
)
val sortedData = sortData(collectAuthHeaderMap)
val reformattedData = reformatData(sortedData)
val oneStringData = "POST" + "&" + urlEncode(Twitter.createPostEndpoint) + "&" + urlEncode(reformattedData.mkString("&"))
val signature = sha1sign(oneStringData)
val signatureDataTupel: (String, String) = ("oauth_signature", signature) // add signature to data
val sortedSignedHeader = sortData(collectAuthHeaderMap + signatureDataTupel)
val reformattedSignedHeader = reformatSignedData(sortedSignedHeader)
val oneStringSignedHeader = "OAuth " + reformattedSignedHeader.mkString(", ")
Header("Authorization", oneStringSignedHeader)
object Twitter:
val createPostEndpoint = "https://api.x.com/2/tweets"
def createPost(text: String): XCreatePostResponse =
val textObj = XCreatePostBody(text)
val h = Auth.signedHeader
println(h)
basicRequest
.header(h)
.contentType("application/json")
.body(write(textObj))
.post(uri"$createPostEndpoint")
.response(asJson[XCreatePostResponse].getRight)
.send(DefaultSyncBackend())
.body
@main
def showHeader: Unit =
createPost("Test")
In response of the main method, i get a 401 with a JSON response:
{
"title": "Unauthorized",
"type": "about:blank",
"status": 401,
"detail": "Unauthorized"
}
The created header:
Authorization: OAuth oauth_consumer_key="xxx", oauth_nonce="nae0Pk46M7ox3yP", oauth_signature="NjM5NDZERkIxRUY1NEY3OUE4Qzc2MjY5ODI0NEYyM0NFNjE0RjZDMg%3D%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1726259880224", oauth_token="xxx", oauth_version="1.0"
I think i encoded sth that should not be encoded or otherwise. But i don't know where i went wrong. Who can show me the error of my ways? Perhaps someone knows a tutorial that shows the plain headers created?
TIA, QNo
Edit: In a first version, i omitted the essential steps. I hope i completed everything.
Edit: In a second edit, i fixed some self-detected bugs. Now i have an Authorization header in a correct format. So the problem has to be the sha1sign method(?)
Solved. I still don’t know how, but it works now after some refactoring.
package de.qno.tournamentadmin
import org.joda.time.DateTime
import sttp.client4.*
import sttp.model.*
import java.net.URLEncoder
import javax.crypto.spec.SecretKeySpec
import scala.util.Random
[…]
/**
* With thx to Kevin Williams
* https://medium.com/@kevinwilliams.dev/posting-to-x-twitter-with-oauth-1-0-8d4de172cfa6
*/
object Auth:
def urlEncode(text: String): String =
URLEncoder.encode(text, java.nio.charset.Charset.defaultCharset())
def generateNonce: String =
Random.alphanumeric.take(15).mkString
def canonicalize(data: Map[String, String]) : String =
data.map(x => urlEncode(x._1) -> urlEncode(x._2)) // percent encode keys and values
.toSeq.sortBy(_._1) // sort alphabetically
.map(x => s"${x._1}=${x._2}") // unite key and value with =
.mkString("&") // unite all with &
def canonicalizeWithQuotes(data: Map[String, String]) : String =
data.map(x => urlEncode(x._1) -> urlEncode(x._2)) // percent encode keys and values
.toSeq.sortBy(_._1) // sort alphabetically
.map(x => s"${x._1}=\"${x._2}\"") // unite key and value with =
.mkString("&") // unite all with &
/**
* With thx to Aravind_G
* https://community.gatling.io/t/hmac-sha1-signature-generation-using-scala/5844
* @param text
* @return
*/
def sha1sign (text: String, secretKey: String): String =
val shaHash = javax.crypto.Mac.getInstance("HmacSHA1")
shaHash.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1"))
val signatureHash = shaHash.doFinal(text.getBytes("UTF-8"))
java.util.Base64.getEncoder.encodeToString(signatureHash)
def signedHeader(key: String): Header =
val collectAuthHeaderMap = Map(
"oauth_consumer_key" -> TournamentAdmin.xApiKey,
"oauth_signature_method" -> "HMAC-SHA1",
"oauth_timestamp" -> (DateTime().getMillis / 1000).toString,
"oauth_nonce" -> generateNonce,
"oauth_token" -> TournamentAdmin.xAccesToken,
"oauth_version" -> "1.0"
)
val request = "POST" + "&" + urlEncode(Twitter.createPostEndpoint) + "&" + urlEncode(canonicalize(collectAuthHeaderMap))
val signature = sha1sign(request, key)
val signatureDataTupel: (String, String) = ("oauth_signature", signature) // add signature to data
val oneStringSignedHeader = "OAuth " + canonicalizeWithQuotes(collectAuthHeaderMap + signatureDataTupel).replace("&", ", ")
Header("Authorization", oneStringSignedHeader)
object Twitter:
val createPostEndpoint = "https://api.x.com/2/tweets"
def createPost(text: String, key: String): XCreatePostResponse =
val textObj = XCreatePostBody(text)
val h = Auth.signedHeader(key)
println(h)
basicRequest
.header(h)
.contentType("application/json")
.body(write(textObj))
.post(uri"$createPostEndpoint")
.response(asJson[XCreatePostResponse].getRight)
.send(DefaultSyncBackend())
.body
@main
def showHeader: Unit =
createPost("Test", TournamentAdmin.makeTwitterKey)