X(旧Twitter)をつかった認証機能

# X(旧Twitter)をつかった認証機能
最近Webサービスを作ってみたいなと思って，とりあえずTwitterでのユーザ認証機能は必須だよなと思ったのでその備忘録です．

## 実行環境
* python = "^3.10"
* fastapi = "^0.115.12"
* requests-oauthlib = "^2.0.0"
## シーケンス図

```mermaid
sequenceDiagram
    participant U as User Client
    participant B as Browser
    participant S as FastAPI Server (auth_router)
    participant T as Twitter

    %% Login フロー
    U->>B: ログインボタン押下
    B->>S: GET /login
    S->>T: OAuth1リクエストトークン取得リクエスト
    T-->>S: (oauth_token, oauth_token_secret)
    S->>S: セッションにトークンを保存
    S-->>B: Twitter認証ページへリダイレクト
    B->>T: ユーザ認証 & 許可
    T-->>B: Callback URLにoauth_verifier付与

    %% Callback フロー
    B->>S: GET /callback?oauth_verifier=...&oauth_token=...
    S->>S: セッションからトークンを取得・検証
    S->>T: アクセストークン交換リクエスト
    T-->>S: アクセストークン & ユーザ情報取得
    S->>S: セッションにユーザ情報を保存
    S-->>B: 取得したuser_infoをJSON形式で返す
    B->>U: user_infoを表示
```
## 準備
まずは[X開発者プラットフォーム](https://developer.x.com/ja)にログインします．  

Project > User authentication settingsにアクセス
* App infoにCallback URIをいれる．
  * ※ とりあえず `http://localhost:8000/callback` とか入れといてＯＫ．
* Website URL には自分のtwitterアカウントのページでも入れといてください．

Project > Keys and tokensにアクセス
* Consumer Keysを生成して`API_KEY`と`API_SECRET`をメモってください．

## ソースコード
main.py部分は省略します．router部分以降だけ書きます．


```py3:python3
from ast import parse
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, RedirectResponse
from requests_oauthlib import OAuth1Session

# 準備でメモしたやつを入れてね
X_API_KEY = "your_api_key"
X_API_SECRET = "your_api_secret"
REDIRECT_URL = "http://localhost:8000/callback"

router = APIRouter()
@router.get("/login", response_class=RedirectResponse)
async def login(request: Request):
    x_session = OAuth1Session(X_API_KEY, X_API_SECRET, callback_uri=REDIRECT_URL)
    res = x_session.post('https://api.x.com/oauth/request_token')
    request_token = dict(parse.parse_qsl(res.content.decode("utf-8")))
    session = request.session
    session['oauth_token'] = request_token['oauth_token']
    session['oauth_token_secret'] =request_token['oauth_token_secret']
    return RedirectResponse(url=f"https://api.x.com/oauth/authenticate'?{parse.urlencode({'oauth_token': session['oauth_token']})}")

@router.get("/callback", response_class=JSONResponse)
async def callback(request: Request, oatuh_verifier: str, oauth_token: str):
    session = request.session
    x_session = OAuth1Session(X_API_KEY, X_API_SECRET, session['oauth_token'], session['oauth_token_secret'])
    res = x_session.post('https://api.x.com/oauth/access_token', data={'oauth_verifier': oatuh_verifier})
    access_token = dict(parse.parse_qsl(res.content.decode("utf-8")))
    session['access_token'] = access_token['oauth_token']
    session['access_token_secret'] = access_token['oauth_token_secret']
    x_access_session = OAuth1Session(X_API_KEY, X_API_SECRET, session['access_token'], session['access_token_secret'])
    user_info = x_access_session.get('https://api.x.com/2/users/me').json().get('data', {})
    
    return JSONResponse(content=user_info)
```
login APIを実行すると，以下のようなJsonResponseが返ってくるはずです．
```
 {
    'name': 'Hoge', 
    'id': '11111111111111111', 
    'profile_image_url': 'https://pbs.twimg.com/profile_images/nya-nya.jpg', 
    'username': 'my_twitter_account'
}
```