ツイッター Ktorと共にOAuth 1.0
Posted on by Angus Cheng
新しいプロジェクトでプロバイダーに沢山のソーシャルメディアのサインを入れなければいけない。これを行うにはOAuthに関して学び、APIを通してFacebook、Google、Gitub、TwitterなどのOAuthプロバイダーとコミュニケーションを取る事が必要である。最初の3社は簡単に対応できるが、Twitterはそうではない。5時間かけてTwitterの書面を読み、ブログ投稿をし、ライブラリへ行き、コードを書いた。これをしてうまくいった。
ステップ1 - OAuth URLの設定とパラメーター
これ使い、 KtorのOAuthパッケージ, urlProvider
、consumerKey
と consumerSecret
を変更する必要がある。他のフィールドはそのままで良いです。
oauth("auth-oauth-twitter") {
urlProvider = { "${config.appConfig.backEndBaseUrl}/auth/callback/twitter" }
providerLookup = {
OAuthServerSettings.OAuth1aServerSettings(
name = "twitter",
requestTokenUrl = "https://api.twitter.com/oauth/request_token",
authorizeUrl = "https://api.twitter.com/oauth/authorize",
accessTokenUrl = "https://api.twitter.com/oauth/access_token",
consumerKey = "CONSUMER_KEY_FROM_TWITTER_DEV_PORTAL",
consumerSecret = "CONSUMER_SECRET_FROM_TWITTER_DEV_PORTAL",
)
}
client = httpClient
}
ステップ 2 – トリガー、コールバックの最終ポイントを作成する
ハンドラの設定を行い、OAuthフローのトリガーをさせる必要があり、これはKtorのパッケージで行ってくれます。もっと面白いのはコールバックURLです。TwitterコールはコールバックURLで トーケン
やトーケンシークレット
を使用します。APIへ依頼を出す際にはこのトーケンが必要となるのです。私の場合ユーザーのプロフィールでサインインする事が出来ますが、あなたの場合はユーザーの代わりにTweetする必要があるかもしれません。
authenticate("auth-oauth-twitter") {
get("/auth/login/twitter") {
// Redirects to 'authorizeUrl' automatically
}
get("/auth/callback/twitter") {
val response = twitterTransport.getUser(principal.tokenSecret, principal.token)
?: throw BadRequestException(ErrorType.FAILED_TO_FETCH_TWITTER_USER, "Failed to fetch twitter user")
val (email, name) = response
logger.info("{} logged in with Twitter.", response.email)
call.respondRedirect("/somewhere")
}
}
今の所、コードはシンプルで、Twitterが私のコールバック最終ポイントのコールを行い、私はユーザーのプロフィールを使って得たトーケンシークレットやトーケンを使います。TwitterTransport.getUser
の中では何が起きているでしょう?
ステップ 3 – ノンス作成
fun createNonce(): String {
val charPool : List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val randomString = ThreadLocalRandom.current()
.ints(32, 0, charPool.size)
.asSequence()
.map { charPool[it] }
.joinToString("")
return randomString
}
これでランダムな32のアルファベットストリングを作成します。
ステップ 4 – 数秒でタイムスタンプを獲得する
val timestamp = Instant.now().epochSecond
ステップ 5 - パラメーターの収集
include_email
の箇所でパラメーターを設定できます。下記のURLでパラメータの設定が可能です。 (https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true) リクエストしています。別のURLをクリックする場合、クエリパラメーターのURLも下記のパラメータへ含めてください。
val params = listOf(
"include_email" to "true",
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to oAuthToken,
"oauth_version" to "1.0"
)
ステップ 6 – パラメータストリングの作成
私のコードは com.google.common.net.PercentEscaper
を使用します。このパラメータのアルゴリズムの作成は下記から閲覧できます。 here パラメータヘッダー収集の下にあります。
private val escaper = PercentEscaper("_-.", false)
fun createParamString(params: List<Pair<String, String>>): String {
return params.map { (key, value) ->
escaper.escape(key) to escaper.escape(value)
}.sortedBy {
it.first
}.map { (key, value) ->
"$key=$value"
}.joinToString("&")
}
ステップ 7 – 署名ベースストリングの作成
fun createSignatureBaseString(method: String, url: String, parameterString: String): String {
` `val buffer = StringBuilder()
` `buffer.append(method.uppercase())
` `buffer.append("&")
` `buffer.append(escaper.escape(url))
` `buffer.append("&")
` `buffer.append(escaper.escape(parameterString))
` `return buffer.toString()
}
ステップ 8 – 署名キーの作成
fun createSigningKey(consumerSecret: String, oauthTokenSecret: String): String {
val buffer = StringBuilder()
buffer.append(escaper.escape(consumerSecret))
buffer.append("&")
buffer.append(escaper.escape(oauthTokenSecret))
return buffer.toString()
}
ステップ9 – 署名作成
org.apache.commons.codec.digest内のHmacUtilsを使用し、Base64edをし、バイトアレーを出します。
fun hmacSign(signingKey: String, signatureBaseString: String): String {
val hmacSha1 = HmacUtils.hmacSha1(signingKey, signatureBaseString)
return Base64.getEncoder().encodeToString(hmacSha1)
}
ステップ 10 – 承認ヘッダーの作成
ここで細かくストリングで分かれていたものを”,”でくっつけて全て収集します。
fun createAuthHeader(consumerKey: String, timestamp: Long, nonce: String, signature: String, token: String): String {
val pairs = listOf(
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature" to signature,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to token,
"oauth_version" to "1.0",
)
val collected = pairs.map { (key, value) ->
"${escaper.escape(key)}="${escaper.escape(value)}""
}.joinToString(", ")
return "OAuth $collected"
}
これで承認ヘッダーが完成しましたのでTwitterのAPIへHTTPの依頼が出来ます
OAuthEncoder.kt
class OAuthEncoder {
private val escaper = PercentEscaper("_-.", false)
fun createNonce(): String {
val charPool : List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val randomString = ThreadLocalRandom.current()
.ints(32, 0, charPool.size)
.asSequence()
.map { charPool[it] }
.joinToString("")
return randomString
}
fun createAuthHeader(consumerKey: String, timestamp: Long, nonce: String, signature: String, token: String): String {
val pairs = listOf(
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature" to signature,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to token,
"oauth_version" to "1.0",
)
val collected = pairs.map { (key, value) ->
"${escaper.escape(key)}="${escaper.escape(value)}""
}.joinToString(", ")
return "OAuth $collected"
}
fun hmacSign(signingKey: String, signatureBaseString: String): String {
val hmacSha1 = HmacUtils.hmacSha1(signingKey, signatureBaseString)
return Base64.getEncoder().encodeToString(hmacSha1)
}
fun createSigningKey(consumerSecret: String, oauthTokenSecret: String): String {
val buffer = StringBuilder()
buffer.append(escaper.escape(consumerSecret))
buffer.append("&")
buffer.append(escaper.escape(oauthTokenSecret))
return buffer.toString()
}
fun createSignatureBaseString(method: String, url: String, parameterString: String): String {
val buffer = StringBuilder()
buffer.append(method.uppercase())
buffer.append("&")
buffer.append(escaper.escape(url))
buffer.append("&")
buffer.append(escaper.escape(parameterString))
return buffer.toString()
}
fun createParamString(params: List<Pair<String, String>>): String {
return params.map { (key, value) ->
escaper.escape(key) to escaper.escape(value)
}.sortedBy {
it.first
}.map { (key, value) ->
"$key=$value"
}.joinToString("&")
}
fun createSignature(
consumerSecret: String,
oAuthTokenSecret: String,
method: String,
url: String,
params: List<Pair<String, String>>
): String {
val paramString = createParamString(params)
val signatureBaseString = createSignatureBaseString(method, url, paramString)
val signingKey = createSigningKey(consumerSecret, oAuthTokenSecret)
return hmacSign(signingKey, signatureBaseString)
}
}
TwitterTransport.kt
interface ITwitterTransport {
data class User(val email: String, val name: String)
fun getUser(oAuthTokenSecret: String, oAuthToken: String): User?
}
class TwitterTransport : ITwitterTransport {
private val logger = LoggerFactory.getLogger(this::class.java)
private val client = HttpClient.newBuilder().build()
private val consumerKey = "CONSUMER_KEY"
private val consumerSecret = "CONSUMER_SECRET"
private val encoder = OAuthEncoder()
override fun getUser(oAuthTokenSecret: String, oAuthToken: String): ITwitterTransport.User {
val url = "https://api.twitter.com/1.1/account/verify_credentials.json"
val nonce = encoder.createNonce()
val timestamp = Instant.now().epochSecond
val params = listOf(
"include_email" to "true",
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to oAuthToken,
"oauth_version" to "1.0"
)
logger.info("oAuthToken = {}", oAuthToken)
val signature = encoder.createSignature(
consumerSecret = consumerSecret,
oAuthTokenSecret = oAuthTokenSecret,
method = "get",
url = url,
params = params
)
val authHeader = encoder.createAuthHeader(
consumerKey = consumerKey,
timestamp = timestamp,
nonce = nonce,
signature = signature,
token = oAuthToken
)
val request = HttpRequest.newBuilder()
.uri(URI.create("$url?include_email=true"))
.header("Authorization", authHeader)
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
val body = parse<JsonNode>(response.body())
logger.info("${response.statusCode()} ${response.body()}")
val email = body["email"].asText()
val name = body["name"].asText()
return ITwitterTransport.User(email, name)
}
}
参考
[1] -依頼の承認, https://developer.twitter.com/en/docs/authentication/oauth-1-0a/authorizing-a-request
[2] -署名作成, https://developer.twitter.com/en/docs/authentication/oauth-1-0a/creating-a-signature