🤩

【Python】OAuth2.0認証を利用してTwitter APIと連携し、認証されたTwitter IDを得る方法

2023/01/28に公開

はじめてZennを利用します。どんな感じなのかなと思い、試しに書いている感じですので、何卒宜しくお願い致します。皆さんもそうだと思いますが、学びならやっておりますので、間違った情報もあるかと思います。その際は、遠慮なくご指摘していただけますと幸いです。

はじめに

弊社でWeb3関連のプロダクトを開発するにあたり、ウォレットアドレス(ユーザログインで利用)とTwitterのアカウントを連携させる必要性がでてきました。海外のWeb3プロダクトを見ていても、OAuth2.0が主流のようです。

Twitterは昔Pythonで遊んでた頃にAPI使っていたものの、今回OAuth2.0は初めてです。実際にどうやって連携させているのかわかりませんでしたが、散らばっている知識を組み合わせながら試してみました。意外と詳しい記事がなくて困りました!

今回はAccess Tokenを取得して、アクセス許可したTwitterのIDを得るところまで実装していきたいと思います。

前提知識

  • Twitter API
  • PKCE(Proof Key Code Exchange)
  • Python
  • OAuth2.0の仕組み
  • (Postman)

実装

全体の流れ

  1. Twitter APIを利用するためにDeveloper Portalで諸設定を行う
  2. Authorization Requestを行う
  3. Access Tokenを取得する
  4. Access Tokenを利用してTwitter IDを得る

Client IDとClient Secretを公式から取得する

Twitter API周りの設定方法など詳細は公式ドキュメントや他の記事を参照してください。

https://developer.twitter.com/en

ここは重要なところだけ詳しく説明していきます。まずはDeveloper Portalのダッシュボードに行きましょう。

https://developer.twitter.com/en/portal/dashboard

Redirect URIはOAuth2.0認証後にリダイレクトするURLを入れてください。このRedirect URIは後ほどAuthorization Requestを行う際に利用します。

次にClient IDとClient Secretを取得しましょう。

sample.py
TWITTER_CLIENT_ID = os.getenv("TWITTER_CLIENT_ID")
TWITTER_CLIENT_SECRET = os.getenv("TWITTER_CLIENT_SECRET")

ハードコーディングでも良いですが、念のためこのような形で環境変数から読みとることにします。

Authorization RequestのためのURLを生成する

下準備は整いましたので、早速コーディングしていきます。下記はAuthorization RequestのためのURLを生成する完成コードと出力結果です。

get_authorization_request.py
import base64
import dotenv
import hashlib
import json
import os
import urllib.parse as parse

dotenv.load_dotenv()
TWITTER_CLIENT_ID = os.getenv("TWITTER_CLIENT_ID")

TOKEN_URL = "https://api.twitter.com/2/oauth2/token"
AUTH_URL = "https://twitter.com/i/oauth2/authorize"
REDIRECT_URI = "http://localhost:3000/twitter/redirect"
SCOPE = ["users.read", "tweet.read"]

state = hashlib.sha256(os.urandom(32)).hexdigest()

code_verifier = hashlib.sha256(os.urandom(128)).hexdigest()
code_challenge_sha256 = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge_sha256).decode().rstrip("=")

query_parameter = parse.urlencode({
    'client_id': TWITTER_CLIENT_ID,
    'redirect_uri': REDIRECT_URI,
    'state': state,
    'scope': ' '.join(SCOPE),
    'response_type': 'code',
    'code_challenge': code_challenge,
    'code_challenge_method': 'S256',
})

request_url = f'{AUTH_URL}?{query_parameter}'
output
{
'request_url': 'https://twitter.com/i/oauth2/authorize?client_id=N04tOTZUbjIwb0VIMDBMVlcxN1I6MTpjaQ&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fredirect&state=b2262218cd73792b71b862397afa0a0c1c3077f17e9947890701a7b1d9fe32d6&scope=users.read+tweet.read+follows.read&response_type=code&code_challenge=JOgyZWsvZLPy_u18dr4d4nBYH1pKzPJFtac_afNSrHY&code_challenge_method=S256',
'state': 'b2262218cd73792b71b862397afa0a0c1c3077f17e9947890701a7b1d9fe32d6', 
'code_verifier': '15d1dc906b3739ecaca6e721cdf0cf72845d1714a173eafd678812d97f295f15'
}

query_parameterを見ていきます。

  • client_id
    Twitter APIのDeveloper Portalで取得したClient ID
  • redirect_uri
    Developer Portalで設定したRedirect URI
  • state
    CSRF対策用の任意値
  • scope
    アクセス許可する権限範囲。下記のDocsにはtweet.writelike.writeなどについて詳細が書かれています。
    https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code
  • state
    CSRF対策用の任意値
  • response_type
    code固定
  • code_challenge
    code_challenge_methodで設定したアルゴリズムで算出した値。
  • code_challenge_method
    planeもしくはS256planeの場合は、code_challengecode_verifierは同じ値にする。S256の場合はcode_challengeの値をSHA256でハッシュ化してBASE64URL Encodingした値にする。今回はS256で試す。

こちらの記事を参照しました。

アクセス許可してCodeを得る

redirect_uri
https://twitter.com/i/oauth2/authorize?client_id=N04tOTZUbjIwb0VIMDBMVlcxN1I6MTpjaQ&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fredirect&state=b2262218cd73792b71b862397afa0a0c1c3077f17e9947890701a7b1d9fe32d6&scope=users.read+tweet.read+follows.read&response_type=code&code_challenge=JOgyZWsvZLPy_u18dr4d4nBYH1pKzPJFtac_afNSrHY&code_challenge_method=S256

先ほどのoutputrequest_urlのリンクで、自身が所有するTwitterのアカウントにアクセス許可をします。なお、アクセス許可してから30秒以内でAccess Tokenを取得する必要があります(詳しくは次)。 思ったよりも早いです!

アクセス許可をすると下記のresponseにリダイレクトされます。

response
http://localhost:3000/twitter/redirect?state=4b1ce2276ac4609a545401729073c007e2def72b0acba4ee22d02d62be7361bf&code=RlRRa0hHX3dNZzVYV1VFdkZjMU1lX2wxeVQzUDduVi15bE01NFdBVF95WVFVOjE2NzQ4MjgzOTU0MDk6MTowOmFjOjE
  • http://localhost:3000/twitter/redirect
    Developer Portalで設定したRedirect URIです。
  • state
    フロント側は完全無視してますのでこちらは利用しません。
    4b1ce2276ac4609a545401729073c007e2def72b0acba4ee22d02d62be7361bf
  • code
    Access Tokenを取得するために必要なCodeになります。RlRRa0hHX3dNZzVYV1VFdkZjMU1lX2wxeVQzUDduVi15bE01NFdBVF95WVFVOjE2NzQ4MjgzOTU0MDk6MTowOmFjOjE

エラー: Something went wrong

最初の関門かも知れません。request_urlのリンクを開くとこのようなエラーが表示されることもあります。

error
Something went wrong
You weren't able to give access to the App. Go back and
try logging in again.

たとえば、request_urlが設定したURIと違ったURIを入れてしまった場合、うまくAuthorization Requestを飛ばすことができません。http://localhost:3000/twitter/redirectと書いたつもりが、http://localhost:3000/redirectになっていたり。欠損している箇所を探してみましょう。

Access Tokenを取得する

下記はAccess Tokenを取得するための完成コードと出力結果です。

get_access_token.py
import hashlib
import os
import json
import urllib.parse as parse
import urllib.request as req
import base64
import dotenv

TWITTER_CLIENT_SECRET = os.getenv("TWITTER_CLIENT_SECRET")

# 生成されたCodeと生成したCode Verifierに置換する
code = "code"
code_verifier = "code_verifier"

data = parse.urlencode({
    'client_id': TWITTER_CLIENT_ID,
    'client_secret': TWITTER_CLIENT_SECRET,
    'redirect_uri': REDIRECT_URI,
    'grant_type': 'authorization_code',
    'code': code,
    'code_verifier': code_verifier
}).encode('utf-8')


# Basic認証でURL先からレスポンスを得る
authorization = base64.b64encode(
    f'{TWITTER_CLIENT_ID}:{TWITTER_CLIENT_SECRET}'.encode('utf-8')
).decode('utf-8')

request = req.Request(
    TOKEN_URL,
    headers={
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': f'Basic {authorization}'
    }
)

with req.urlopen(request, data=data) as token_res:
    res = token_res.read()

print(res)

output
{
  'token_type': 'bearer',
  'expires_in': 7200,
  'access_token':
'M0UxenAyN0hveVBpMVFlcTJ3SlVHLVdPSE5kYjdXWnp4X1pFa0ZZSi1ibDQzOjE2NzQ4MzUwNTIyMDM6MTowOmF0OjE',
  'scope': 'users.read tweet.read'
}

outputを見てください。access_tokenが発行されたアクセストークンです。expires_in`はアクセストークンの有効期限で、7200秒なので2時間有効となっています。これでTwitter APIを利用することが可能になります。

get_access_token.pyをみて見ましょう。ここで重要なのは、codecode_verifierです。「アクセス許可してCodeを得る」にてクエリパラメータで返ってきたcodeと「Authorization RequestのためのURLを生成する」で生成したcode_verifierを入力してください。30秒以内に行う必要があります。

また、フロントエンド側の処理を行っている人は、codecode_verifierしっかり紐づいているかに注意する必要があります。識別にstateが役立つかもしれません。よくcodeは最新だけれど、code_verifierは違う値になっていたということがあります。

さて、access_tokenを発行することができましたので今度は利用してみましょう。

Access Tokenを利用してTwitter IDを得る

ラストスパートですが、ここは比較的簡単です。サンプルさえあれば、他にも応用ができると思うので、色々と試してみてください。

下記が完成コードです。

get_twitter_id.py
def get_twitter_id(access_token):
    url = 'https://api.twitter.com/2/users/me'
    
    # Paramsの使い方も参考程度に
    params = {
        'user.fields': 'description'
    }

    # Access Tokenをセットする
    headers = {
        'Authorization': f'Bearer {access_token}'
    }

    res = requests.get(url, params=params, headers=headers)

    return eval(res.text)

get_twitter_id("Access Token")
output
{
  'data': {
	'description': 'description',
	'id': 'id',
	'name': 'name',
	'username': 'username'
  }
}

get_twitter_id.pyを見てください。get_twitter_id("Access Token")には当然先ほど発行したAccess Tokenが入ります。書かなくても分かると思いますが念のため書いておきます。

sample
get_twitter_id("M0UxenAyN0hveVBpMVFlcTJ3SlVHLVdPSE5kYjdXWnp4X1pFa0ZZSi1ibDQzOjE2NzQ4MzUwNTIyMDM6MTowOmF0OjE")

次にoutputを見てください。usernameがTwitter IDということになりますので、今回の目標は達成です。paramsの設定は、例えば、user.fieldsは上記では'description'しか利用していませんが、他にもcreated_atprofile_image_urlなどがあります。

https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me

まずはどのような挙動になるのかをPostmanで試してみることを推奨します。

https://developer.twitter.com/en/docs/tutorials/postman-getting-started

エラーが発生した場合の対処法

error
HTTPError: HTTP Error 400: Bad Request

アクセス許可してから30秒以内にAccess Tokenを得る

何度も書きましたが、30秒の制限があります。これを知らなかったときは本当にエラーの原因が何なのかわからなかったので、しっかり30秒以内にできているか確認すると良いでしょう。

Code Challenge MethodをPlaneにして試す

上記では、Code Challenge MethodをS256で実装しています。

もしかするとcode_challengecode_verifierが合っていない可能性がありますので、生成がうまくいかない場合は、code_challenge_methodplaneにして、code_challengecode_verifierの値を一致させてみてください。

Postmanで挙動を見直す

いきなりコーディングすると訳が分からないときがあります。どのパラメータがどのように機能するのか、Twitter側が用意してくれているPostmanを利用することによって、動きをシミュレーションすることができます。

もちろん、OAuth2.0のアクセストークンを取得するところから確認することができます。

その他

下記のようなエラーが出た場合は、こちらの記事が参考になります。

https://zenn.dev/kg0r0/articles/8b1cfe654a1cee

error
{
  "error": "unauthorized_client",
  "error_description": "Missing valid authorization header"
}
error
{
  "error": "invalid_request",
  "error_description": "Missing required parameter [client_id]."
}
error
{
  "error": "unauthorized_client",
  "error_description": "Missing valid authorization header"
}

まとめ

だいたい、調べているときって「まとめ」見ませんよね。私の感想を適当においておこうと思います。

開発しながらアウトプットを出しているエンジニアさんって本当にすごいなと思います。私はこれを作成するのに、3時間30分もかかりました…。

普段はNotionで開発ノートと呼んでいるメモを残しているのですが、見直すことはほとんどありません。見直すのは最初の環境構築ぐらいですかね。それでも、参考になった記事をメモしたり、気づきをメモしたりは重要だと思いますし、覚えるための一方にはなると思っています。

さすがに時間がかかりすぎなので、手の込んだものは書けないと思いますが、何かを一から復習したりするときにはZennを書きながら(アウトプットしながら)学習していくのも良さそうです。

本件とは関係のない書き手の拙い感想ですが、色々と発見ができて良かったと思います。間違っていることもございましょうがご参考になれば幸いです。

ChatGPTに負けないようにたくさん学ばねば!

参考文献

GitHubで編集を提案

Discussion