Twitter認証がよくわからん

PHP

OAuth1.0a認証

Twitterは外部アプリなどを認証する際にOAuth1.0aという仕組みを利用している。

具体的に言えばConsumer KeyとConsumer Secretを使ってoauth/request_tokenのAPIを叩くことによってoauth_token、oauth_token_secretなどの認証に必要なトークンを取得し、正しいURLからアクセスされたことを確認したのちにアクセストークンを取得するという仕組みである。

Request Token

以下の参考文献でPHPのコードを読んでそれをSwiftで書き直したものになります。

Twitter REST APIの使い方
Twitterが提供するREST APIの使い方をまとめています。

api_keyとapi_secretはそれぞれConsumer KeyとConsumer Secretを指します。

let api_key: String = ""
let api_secret: String = ""
let callback_url: String = ""
        
let request_url: String = "https://api.twitter.com/oauth/request_token"
let request_method: String = "POST"
let signature_key: String = api_secret.rawurlencode + "&" + access_token_secret.rawurlencode

        
var headers: [String: String] = [
    "oauth_callback" : callback_url,
    "oauth_consumer_key": api_key,
    "oauth_nonce": String(Int(Date().timeIntervalSince1970)),
    "oauth_signature_method": "HMAC-SHA1",
    "oauth_timestamp": String(Int(Date().timeIntervalSince1970)),
    "oauth_version": "1.0"
]
        
var request_params: String = (headers.sorted(by: {$0.0 < $1.0}).map({ "\($0.key)=\($0.value.rawurlencode)&amp;"}).reduce("", +))
request_params = String(request_params.prefix(request_params.count - 1))
request_params = request_params.urlencode

let encoded_request_method: String = request_method.rawurlencode
let encoded_request_url: String = request_url.rawurlencode
let signature_data: String = encoded_request_method + "&amp;" + encoded_request_url + "&amp;" + request_params

let signature: String = hmacsha1(message: Data(signature_data.utf8), key: Data(signature_key.utf8))
        
headers.updateValue(signature, forKey: "oauth_signature")
let headers_params: String = headers.sorted(by: {$0.0 < $1.0}).map({ "\($0.key)=\($0.value.urlencode),"}).reduce("", +)
        
let header: HTTPHeaders = [
    "Content-Type": "application/json",
    "Authorization": "OAuth \(String(headers_params.prefix(headers_params.count - 1)))"
]

HMAC-SHA1を計算するにはCommonCryptoが必要なので忘れずにインポートしておきましょう。

パーセントエンコーディングを毎回と長々と書くのはめんどくさいのでExtensionを使って簡単に計算できるようにします。

func hmacsha1(message:Data, key:Data) -> String {
    var macData = Data(count: Int(CC_SHA1_DIGEST_LENGTH))
    
    macData.withUnsafeMutableBytes { macBytes in
        message.withUnsafeBytes { messageBytes in
            key.withUnsafeBytes { keyBytes in
                CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1),
                       keyBytes,     key.count,
                       messageBytes, message.count,
                       macBytes)
            }
        }
    }
    return macData.base64EncodedString()
}

extension String {
    var rawurlencode: String {
        return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }
    
    var urlencode: String {
        let charset = NSCharacterSet(charactersIn:"&amp;:=\"#%/<>?@\\^`{|}").inverted
        return self.addingPercentEncoding(withAllowedCharacters: charset)!
    }
}

あとはAlamofireなりでPOSTリクエストを送ればText形式で以下のような形式の値が返ってきます。

oauth_token=5mb9VtYwa27HTVjK5OhoyyI503dWoPndDQ9G4V8yCI&oauth_token_secret=4dW4gGLic6oItvd0YySWRU5aLjBQsw1N9xDC3Wkqw&oauth_callback_confirmed=true

ここでoauth_tokenは認証用のURLを作成するのに必要で、oauth_token_secretは正しいURLからジャンプしたことをチェックするための仕組みです。

認証してみよう

https://api.twitter.com/oauth/authenticate?oauth_token={OAUTH_TOKEN}のURLにアクセスすればおなじみのあの画面が表示されます。

ただし、これは一回ポッキリのURLなので二回目以降にアクセスしても無効なURLと表示されて認証することができません。

任天堂のAPIは何度も同じURLを使えるのに、セコいですな。

よく考えたらこんな機能は要らなかった

で、苦労して(四時間くらいかけて)頑張ってSwiftで移植したのはいいのですが、これを利用しようとすると、

認証用のURLを作成→それを変数に代入→認証用URLを開く→CallbackURLで戻ってくる

という流れになり、コールバックした際にURLSchemeでHookできるのであれば「わざわざ認証用URLをSwiftでつくる意味があるのか」ということになるわけです。

これなら、最初からアクセスすると認証用のURLにジャンプしてくれるようなページまたはAPIをつくっておいて、そこにアクセスしたら認証画面が開き、認証後にコールバックするような仕組みでもいいわけです。

面倒になるのは認証用のURLを作成するために別途PHPなりでサーバを立てなければいけないことですが、できないことはないのでやってみました。

PHPで認証用のサイトをつくる

というわけで、結局PHPで認証用のデモサイトをつくることにしました。

<?php
require "vendor/autoload.php";
use Abraham\TwitterOAuth\TwitterOAuth;

session_start();
define("TWITTER_API_KEY", API_KEY);
define("TWITTER_API_SECRET", API_SECRET);
define("CALLBACK_URL", CALLBACK);

$connection = new TwitterOAuth(TWITTER_API_KEY, TWITTER_API_SECRET);
$request = $connection->oauth("oauth/request_token", array("oauth_callback" => CALLBACK_URL));

$_SESSION["oauth_token"] = $request["oauth_token"];
$_SESSION["oauth_token_secret"] = $request["oauth_token_secret"];

$url = $connection->url("oauth/authenticate", array("oauth_token" => $request["oauth_token"]));
header("Location: ".$url);

CALLBACKだけはダブルクオーテーションで挟んで文字列で書かないとダメですが、それ以外はベタコピーでオッケーです。

あとは適当にindex.phpとか名前をつけてアクセスするだけですね。

BetterSafariViewで開いてみる

stleamist/BetterSafariView
A better way to present a SFSafariViewController or start a ASWebAuthenticationSession in SwiftUI. - stleamist/BetterSafariView

Swiftのライブラリの一つであるBetterSafariViewにはWebAuthenticationSessionというSFSafariViewを開きつつ認証もしてくれるという便利な仕組みがあります。

デフォルトではGithubのOAuthのデモができるのですが、果たしてTwitterでもうまくいくのでしょうか?

すると、ちゃんとログインしてトークンをとってくることができました!!

ちなみに表示されてるoauth_tokenはinvalidなConsumer Keyから作成したのでもう使えないです。

ここから何ができるか

BetterSafariViewで認証ができたということは、SFSafariViewを使ってネイティブにSalmon Statsとの連携ができることを意味します。

今まではWebKitをつかってWebViewを作成し、そこでログインしたあとでWebViewのCookieを全部取得してその中からlaravel_sessionを探し、あればそのlaravel_sessionを使ってSalmon Statsからリザルトのアップロードに必要なapi-tokenを取得していました。

ただ、WebKitがSwiftUIからは標準サポートされておらずいろいろとバグが多いのでなんとかネイティブにできないかと考えていたわけです。

その点、SFSafariViewはSafariと同じように扱えるので便利なのですが「Cookieなどの情報がSafariと共有されないくせにSafariのインスタンスとして扱われる」のでアプリ側からCookieにアクセスできないという欠点がありました。

なのでこうしてネイティブにトークンを扱える仕組みが必要だったわけです。

[Python] OAuth認証でTwitter連携/ログインを実装する - Qiita
概要 PythonでTwitter連携を実装しようとしてみたところ、あんまり例がなかったのでつくってみました ドキュメントを読めば分かるという人はこちら → Implementing Sign in with Twitter...

さて、ここまででoauth_tokenとoauth_verifierが取得できたので次はここからアクセストークンを取得する必要があります。

これは割と簡単そうなので、ここまでできればもう一歩という感じはしますね。

コメント

タイトルとURLをコピーしました