[筆記] Twitter API v1.1 踩坑記

最近在重構遠古時期寫的社群整合登入功能,主要是想把各平台引用的庫都拿掉,

畢竟只是要做簡單的 Token 授權和存取用戶個人資料,用到整個庫還是太肥了,

其中 Facebook、Google、Discord 甚至是 QQ、微博都已經採用 OAuth 2.0 標準,

唯獨 Twitter 主要還是採用相當古老的 OAuth 1.0a,真的是想要搞死人阿。

目前 Twitter 已經有部分 API 支援 OAuth 2.0 驗證,可以 [在此查看] 比較表格

驗證方式差異

OAuth 1.0a 最麻煩之處就是每次請求都要使用 Consumer Key / Secret 加上 Token / Token Secret 和請求參數來產生一個雜湊簽名,再將所有驗證參數帶入請求的 Headers 中。

POST /oauth/access_token?oauth_token=AAAAA&oauth_verifier=BBBBB HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_callback="https%3A%2F%2Fexample.com%2F", oauth_consumer_key="CCCCC", oauth_nonce="DDDDD", oauth_signature="EEEEE", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1600111186", oauth_version="1.0"

相比之下,OAuth 2.0 只需要在每次請求中帶上 Bearer Token 驗證就能打天下,實在是方便了許多。

GET /v1/people/me?personFields=emailAddresses%2Cgenders%2Cnames%2Cphotos HTTP/1.1
Host: people.googleapis.com
Authorization: Bearer XXXXXXXXXX

 

深坑一:麻煩的編碼過程

先來說一下雜湊簽名的產生方法,這裡以第一步要取得 Request Token 為例,

首先要將包含 oauth_* 開頭的必要參數以 Key / Value 配對的方式收集好,

將 Key、Value 分別以 RFC 3986 標準編碼(Percent encoding)後,以 key 的字母順序來排序,

再將整個列表以 key=value 格式,並使用 & 符號來拼接各項參數,成為 Param String:

oauth_callback=https%3A%2F%2Fexample.com%2F&oauth_consumer_key=TWITTER_CONSUMER_KEY&oauth_nonce=NONCE&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1600111186&oauth_version=1.0

這種格式其實就是 XHR 中常會用到的 Query 參數格式,上面的步驟在 PHP 中等同:

$params = array(
  'oauth_consumer_key' => TWITTER_CONSUMER_KEY,
  'oauth_signature_method' => 'HMAC-SHA1',
  'oauth_timestamp' => time(),
  'oauth_version' => '1.0',
  'oauth_callback' => 'https://example.com/',
  'oauth_nonce' => NONCE,
);
ksort($params);
$paramString = http_build_query($params);

接著將 HTTP 請求方法(大寫)、請求 API 路徑以及上面獲得的 Param String 都以 Percent encode 編碼,並使用 & 相連結合成 Base String。

POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&oauth_callback%3Dhttps%253A%252F%252Fexample.com%252F%26oauth_consumer_key%3DTWITTER_CONSUMER_KEY%26oauth_nonce%3DNONCE%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1600111186%26oauth_version%3D1.0

接著準備簽名要用的 Key,是將 Consumer secret 以及 Token Secret(若無,則用空字串)以 Percent encode 編碼,並使用 & 相連成 Signing key。

TWITTER_CONSUMER_SECRET&

最後則是利用 HMAC-SHA1 算法,以 Signing key 為金鑰來計算 Base String 的雜湊值,以二進制格式輸出並用 base64 格式編碼。

以上的步驟在 PHP 中的實作方法為(以下函式使用 Class 中的寫法):

function getSignature() {
  $paramString = ...;

  $signature = self::calculateSignature(
    'POST',
    'https://api.twitter.com/oauth/request_token',
    $paramString
  );
}
private function calculateSignature($method, $endpoint, $paramString, $tokenSecret = '') {
  $baseStrings = self::urlencodeRfc3986(array(
    strtoupper($method),
    $endpoint,
    $paramString
  ));
  $baseString = implode('&', $baseStrings);

  $keys = self::urlencodeRfc3986(array(
    TWITTER_CONSUMER_SECRET,
    $tokenSecret
  ));
  $key = implode('&', $keys);

  return base64_encode(hash_hmac('sha1', $baseString, $key, true));
}
private function urlencodeRfc3986($input) {
  $output = '';
  if (is_array($input)) {
    $output = array_map([$this, 'urlencodeRfc3986'], $input);
  }
  else if (is_scalar($input)) {
    $output = rawurlencode($input);
  }
  return $output;
}

終於,走了這麼長的一段路才成功拿到了 oauth_signature 參數所需要的雜湊簽名。

 

深坑二:Header 帶的驗證字串格式不同

在上一個步驟拿到 oauth_signature 之後,要再把這個參數塞回去原本的參數列表 $params 中,

並和上面的步驟一樣先將 Key、Value 以 Percent encoding 編碼,但不需要再另外排序,

接著再依序拼接組合成 Header String,但問題就來了,這裡的拼接格式和上面的格式並不相同!

於是我就在這邊鬼打牆好一段時間,不斷得到 215: Bad Authentication data. 的回應。

後來熊熊一看才發現,原來 Header 驗證的格式是以 key="value" 整理,再以  連接的方式拼接而成,而不是單純的做成 Query String 的格式就行,

所以只好將字串拼接分成兩種情況,給 Header 用的跟不是給 Header 用的(簽名用的):

private function makePayload($params, $forHeader = false) {
  if ($forHeader) {
    $payload = [];
    foreach ($params as $key => $value) {
      $payload[] = self::urlencodeRfc3986($key).'="'.self::urlencodeRfc3986($value).'"';
    }
    return implode(', ', $payload);
  }
  else {
    ksort($params);
    return http_build_query($params);
  }
}

產完 Header String 之後,在字串前加上 OAuth  前綴,放到 Header 裡的 Authorization 欄位就能完成請求驗證啦!

順帶一提,此 API 回傳的內容使用 PHP 中的 parse_str 就能將結果解析成 Key / Value 配對的陣列。

 

深坑三:一個參數,各自表述

在前面所有驗證步驟都完事之後,接著就要進入最終正式環節,就是竊取存取使用者已授權的資料。

因為只記錄踩坑過程,這裡就不贅述將使用者導向授權頁面和用 Request Token 交換 Access Token 的細節,

由於我的需求是獲取使用者 ID、名稱、性別(Twitter 沒開放)、大頭貼和信箱,所以選擇 account/verify_credentials 這支 API,

這支 API 的驗證的方法和上面的步驟都一樣,整理參數列表、計算雜湊簽名後再把參數放到 Header 中驗證,

要注意的是,這支 API 是用 GET 請求,有使用到文件中 Parameters 裡的欄位都要一並收集到參數列表中。

GET /1.1/account/verify_credentials.json?skip_status=true&include_email=true HTTP/1.1
Host: api.twitter.com
Authorization: OAuth include_email="true", oauth_consumer_key="AAAAAAAAAAAAAAAAAAAAAAAAA", oauth_nonce="XXXXXXXXXX", oauth_signature="bBHPX5CFknxirhTLpTJU4gy0mkk%3D", oauth_signature_method="HMAC-SHA1", skip_status="true", oauth_timestamp="1600372433", oauth_token="000000000-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", oauth_version="1.0"

但問題來了,不管我怎麼驗證,永遠都會收到 32: Could not authenticate you. 的錯誤:

這問題真的卡了我滿長一段時間,本來一直朝著參數帶錯或是少帶的方向去找原因,但掃完文檔還是毫無頭緒,

索性就先把額外加的 GET 參數拿掉嘗試,沒想到還真的請求成功了,所以問題點一定就是在簽名或 Header 驗證上,

後來又仔細重看了 Authorizing a request 裡的每一字每一句後才驚覺完全漏掉了這段:

You should be able to see that the header contains 7 key/value pairs, where the keys all begin with the string “oauth_”.

原來是 GET 參數只有要在計算雜湊簽名的時候才需要帶入參數列表中,Header 驗證的欄位只會有不多不少剛剛好 7 個 oauth_ 開頭的參數(為了方便辨識有修改格式):

Authorization: OAuth
  oauth_consumer_key="AAAAAAAAAAAAAAAAAAAAAAAAA",
  oauth_nonce="XXXXXXXXXX",
  oauth_signature_method="HMAC-SHA1",
  oauth_signature="bBHPX5CFknxirhTLpTJU4gy0mkk%3D",
  oauth_timestamp="1600358031",
  oauth_token="000000000-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  oauth_version="1.0"

Header 驗證的問題算是水落石出了,但到目前為止,Code 32 的錯誤還是存在,所以接下來就要改朝簽名問題的方向走...

好的,一個下午過去了,結果其實在 Creating a signature 的文檔中就有提到:

The base URL is the URL to which the request is directed, minus any query string or hash parameters.

也就是說,計算雜湊簽名的時候帶入的 $endpoint 參數必須是最基本的、不帶任何參數或後綴的 Base URL,

例如這支 API 的 Base URL 就是 https://api.twitter.com/1.1/account/verify_credentials.json

但我當初在 StackOverflow 上找到的 ✨最佳解答✨ 卻是這樣說:

If you want to get email from Twitter OAuth api, the url in signature should have ?include_email=true.

總之就是遇到問題除了 Google 之外,官方文檔還是要重讀一遍,才不會被來路不明的解答誤導了,

終於,在掉入一連串的深坑之後,成功從坑裡面爬出來了,這次的踩坑記也在這裡畫下完美(?)的句點。

 

最後附上官方文檔網址供參考。

https://developer.twitter.com/en/docs

想隨時追蹤最新資訊?歡迎使用 RSS 訂閱最新文章 »

您或許會感興趣的文章

隨機推薦

共有 6 則迴響

  1. serena

    #1 @

    這個我做了好久 ==

    • Lay

      #1 @

      只能怪 Twitter 沒跟上時代

  2. serena

    #2 @

    上星期終於做出來了

  3. feifan

    #3 @

    请问,您所使用的测试post工具是什么? :thinking:

發表迴響

*