34
以 Authlib 實現 OAuth 1 的 Twitter 登入
雖然 Twitter 在台灣一直不是主流,不過在國外 Twitter 是可以與 FB 相抗衡的平台之一,而目前主流的第三方認證機制 OAuth 最初也是由 Twitter 設計的,不過相較於 LINE、Google、FB 都已經採用 OAuth 2 標準做為第三方登入的機制,反而 Twitter 是少數還在用 OAuth 1 的業者,這裡我們會用 Authlib 套件來實做 Twitter 登入,並且簡單講講 OAuth 1 的認證流程(複雜的我也講不了)。
這裡的 OAuth 1 主要是參考 Twitter 的文件而來,畢竟現在似乎只剩下 Twitter 還在用 OAuth 1 了⋯⋯吧?!
我們從大流程講起,從用戶按下「Sign in with Twitter」開始,會進行下面的程序:
- 用戶在我們的 app 按下「Sign in with Twitter」
- 我們的 app 對 Twitter 發送一個 callback 網址及請求一組 request token
- Twitter 回覆 request token 以及認可 callback 網址
- 將用戶重導到 Twitter 的認證頁面
- 用戶授權我們的 app 取得他的 access token
- Twitte 回覆同樣的 request token 以及一個 request token verifier 給我們的 app
- 用戶從 Twitter 的認證頁被重導到 callback 網址
- 我們的 app 發送 request token 和 request token verifier 給 Twitter 要求用他們取得 access token
- Twitter 回覆 access token 與 access token secret
- 我們的 app 把用戶導向會員頁面
也可以配圖看,順便伸展一下脖子:
走這一大段路,最終要拿到的就是 access token,但得先拿到 request token 和 request token secret,而 request token secret 又得透過用戶認證取得,取得後再拿 request token & request token secret 向 Twitter 取得 access token。
雖然寫起來彷彿經歷千辛萬苦,但實際上整個過程不超過十秒,如同滄海一粟,稍縱即逝。
上面的流程實際上是簡化過的版本,真正 post 來 post 的去的資料要複雜的多,並且還要用 HMAC-SHA1 編碼的簽名做防偽,幸好各大語言早就都有 OAuth 套件可以直接使用,省去自己造輪子的功夫,所以我們不求甚解的直接跳到使用套件的部份。(技術債 + 1、time to market - 3,划算)
對於 OAuth 1 細節有興趣的朋友,推薦這篇〈OAuth 1.0 簡介〉。
第一步是文書作業,到 Twitter Developer Portal 開立專案,啟用 3-legged OAuth,取得 Twitter API Key 以及 Twitter API Secret Key,這組 API key 在前面的 OAuth 1 流程稱為 consumer key,這種名稱不一致的情況在下文會持續發生,一個物件在 OAuth、Twitter、Authlib 內各自表述,就像台灣中國九二會談一樣毫無共識。
下面的表格整理了三者命名的對應,希望不要被這些混亂的命名搞亂。
OAuth 命名 |
Twitter 命名 |
Authlib 命名 |
用途 |
---|---|---|---|
Service Provider | OAuth Server | 提供 OAuth 認證的平台 | |
Consumer | App | OAuth Client | 向 OAuth server 要求認證的 app |
Consumer Key | API Key | Client ID | 存取 Twitter API |
Consumer Secret | API Secret Key | Client Secret | 產生 HMAC-SHA1 防偽簽名 |
Callback | Callback URL | Redirct URI | 用戶認證後 Twitter 把用戶導向 app 的網址 |
下文開始 Authlib 實現 Twitter 登入,以下的程式碼都執行在 Python shell 內,並且程式碼經過排版便於閱讀。
引入 authlib
元件及定義一些基礎參數:
from authlib.integrations.requests_client import OAuth1Session
client_id: str = 'my_client_id' # TWITTER_API_KEY
client_secret: str = 'my_client_secret' # TWITTER_API_SECRET_KEY
client: OAuthSession = OAuth1Session(
client_id = client_id,
client_secret = client_secret
)
我們會用這個 client
與 Twitter API 做交流。上面的 client_id
與 client_secret
的值比較正規的做法應該用 python-dotenv 隱藏起來,但這裡就不講究那麼多了。
用 client
去向 Twitter API 取得 request_token
:
request_token_url: str = 'https://api.twitter.com/oauth/request_token'
request_token:dict = client.fetch_request_token(url=request_token_url)
print(request_token)
# request_token
{
'oauth_token' : 'my_oauth_token',
'oauth_token_secret' : 'my_auth_token_secret',
'oauth_callback_confirmed' : 'true'
}
注意到在流程圖有提到要求 request token 時要傳送一個 callback URL,但是在此並沒有指定,Twitter 會用 Twitter Developer Portal 內的 Callback URLs 的第一個值,所以不用在程式內指定,想在程式內指定也是可以的,以 staging 環境為例:
from authlib.integrations.requests_client import OAuth1Session
client_id: str = 'my_client_id' # TWITTER_API_KEY
client_secret: str = 'my_client_secret' # TWITTER_API_SECRET_KEY
redirect_uri = 'https://staging.example.com/auth/twitter'
client: OAuthSession = OAuth1Session(
client_id = client_id,
client_secret = client_secret,
redirect_uri = redirect_uri
)
不過這裡的 redirect_uri
的網址也還是得要事先登錄到 Twitter Developer Portal 的 Callback URLs,否則是會被 Twitter API 阻擋的。
和 Twitter API 的交互之所以看起來這麼簡單是 Authlib 的功勞,它幫我們做掉了 OAuth 1 要求的其他欄位的值以及生成防偽簽章的苦工,只留下真正必要的參數給我們填入。
用剛拿到的 request_token
去組出一個 Twitter 用戶認證網址:
oauth_token: str = request_token['oauth_token']
oauth_token_secret: str = request_token['oauth_token_secret']
authenticate_url: str = 'https://api.twitter.com/oauth/authenticate'
client.create_authorization_url(
url = authenticate_url,
request_token = oauth_token
)
# Authorization URL
'https://api.twitter.com/oauth/authenticate?oauth_token=my_oauth_token'
得到 authorization URL 後,把用戶導過去,用戶會看到 Twitter 的認證畫面:
用戶按下 Sing In 後,Twitter 會把用戶重導到我們指定的 callback URL,並附上我們的 oauth_token
與 oauth_verifier
。
假設我們的 app 在 Twitter Developer Portal 登錄的 callback URL 是 https://example.com/auth/twitter
,Twitter 導引用戶回來時會在 callback URL 後附加 oauth_token
與 oauth_verifier
兩個參數,因此收到的請求是這樣的:
resp_url: str = 'https://example.com/auth/twitter?oauth_token=my_oauth_token&oauth_verifier=my_oauth_verifier'
再拿 oauth_token
與 oauth_verifier
去向 Twitter API 要 access token:
client.parse_authorization_response(resp_url)
access_token_url: str = 'https://api.twitter.com/oauth/access_token'
token: dict = client.fetch_access_token(access_token_url)
print(token)
# token
{
'oauth_token' : 'user_oauth_token',
'oauth_token_secret': 'user_oauth_token_secret',
'user_id' : 'user_user_id',
'screen_name' : 'user_screen_name'
}
注意,最後拿到的 oauth_token
和 oauth_token_secret
是代表那位用戶的,而在此之前和 Twitter API 交互的 oauth_token
是屬於我們的 app 的,名字一樣但意義不同。
最後拿到的代表用戶的 oauth_token
、oauth_token_secret
、user_id
、screen_name
應該存入我們自己的會員資料庫內,做為後續認證之用,但要注意的是不要拿 screen_name
辨識用戶,因為 Twitter 的用戶名是可以改的。
有了用戶授權給我們的 access token,除了讓我們做第三方登入外,也可以拿 access token 去做其他的應用,例如獲得用戶帳號的其它資料,用的是 verify_credentials
這支 API:
twitter_account_verify_credentials_uri = 'https://api.twitter.com/1.1/account/verify_credentials.json'
resp = client.get(
url = twitter_account_verify_credentials_uri,
params = {'skip_status': True}
)
print(resp.json())
# resp.json()
{
...
'protected': False,
'friends_count': 5,
'favourites_count': 9,
'utc_offset': None,
'time_zone': None,
'geo_enabled': False,
'verified': False,
'statuses_count': 7,
'lang': None,
'contributors_enabled': False,
'is_translator': False,
'is_translation_enabled': False,
'profile_background_tile': False,
'profile_use_background_image': True,
'has_extended_profile': True,
'default_profile': True,
'default_profile_image': False,
'following': False,
'follow_request_sent': False,
'notifications': False,
'translator_type': 'none',
'suspended': False,
'needs_phone_verification': False,
...
}
當然能拿到的資料僅限於用戶在 Twitter 認證頁同意授權給我們的部份,另外在 Twitter Developer Portal 也要對額外要求的用戶資料目的提交給 Twitter 審查,以最基本的用戶 email 為例,就要提供 terms of service 和 privacy policy 給 Twitter 才能取得。
前面的範例是以 Authlib 做為 OAuth client 實做 OAuth 1 流程的程式碼,目的是親自實證 OAuth 1 的交互流程,而在真實的 web 後端應用場景,Authlib 有更好的與 web 框架整合好的 API 可以使用,目前 Authlib 已有與 Starlette、FastAPI、Flask、Django 的整合模組。
以 Starlette 為例,一個最陽春的 Starlette web app 大概長這樣:
from starlette.applications import Starlette
from starlette.requests import Request
app = Starlette()
先幫它加上 authlib
的元件及初始化:
from starlette.applications import Starlette
from starlette.requests import Request
from authlib.integrations.starlette_client import OAuth
app = Starlette()
oauth = OAuth()
再加上 OAuth server 的各個基本 API 網址:
from starlette.applications import Starlette
from starlette.requests import Request
from authlib.integrations.starlette_client import OAuth
app = Starlette()
oauth = OAuth()
oauth.register(
name = 'twitter',
api_base_url = 'https://api.twitter.com/1.1/',
request_token_url = 'https://api.twitter.com/oauth/request_token',
request_token_params = None,
access_token_url = 'https://api.twitter.com/oauth/access_token',
access_token_params = None,
authorize_url = 'https://api.twitter.com/oauth/authenticate',
aurhorize_params = None,
client_id = 'my_client_id',
client_secret = 'my_client_secret',
client_kwargs = None,
)
因為 OAuth 1 是標準化的協議,所以必然會有 request_token_url
、access_token_url
、authorize_url
,也必然會有 client_id
、client_secret
兩個值,儘管命名可能不一樣。
再次提醒,client_id
、client_secret
的值應該要放在 .env 內,這裡只是示範才省略。
加上兩個 route,一個是讓用戶按下 Sign in with Twitter 的網址,一個是讓 Twitter callback 回來的網址:
from starlette.applications import Starlette
from starlette.requests import Request
from authlib.integrations.starlette_client import OAuth
app = Starlette()
oauth = OAuth()
oauth.register(
name = 'twitter',
api_base_url = 'https://api.twitter.com/1.1/',
request_token_url = 'https://api.twitter.com/oauth/request_token',
request_token_params = None,
access_token_url = 'https://api.twitter.com/oauth/access_token',
access_token_params = None,
authorize_url = 'https://api.twitter.com/oauth/authenticate',
aurhorize_params = None,
client_id = 'my_client_id',
client_secret = 'my_client_secret',
client_kwargs = None,
)
@app.route('/login/twitter')
async def login_via_twitter(request: Request):
twitter = oauth.create_client('twitter')
redirect_uri = request.url_for('authorize_twitter')
return await twitter.authorize_redirect(request, redirect_uri)
@app.route('/auth/twitter')
async def authorize_twitter(request: Request):
twitter = oauth.create_client('twitter')
token = await twitter.authorize_access_token(request)
user = await twitter.parse_id_token(request, token)
return user
Authlib 對 web 框架的整合封裝度更高,前面我們還要手動照 OAuth 1 流程跑,取得 request token 什麼的,這裡完全不用,一個 authorize_redirect()
就把拿 request token 和導引用戶到 Twitter 認證頁的工作全做掉了,後面的 callback 網址收到 Twitter 導回來的用戶也是一個函式 authorize_access_token()
搞定前面手動拿 token 交換來交換去的工作,真心感謝 Authlib 大大,祝 Authlib 大大好人一生平安。
最後,Authlib 有用到 Starlette 的 session middleware 管理用戶從我們的 app 跳轉到 Twitter 又跳回來的 session,所以再把 session middleware 加進來:
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.middleware.sessions import SessionMiddleware
from authlib.integrations.starlette_client import OAuth
app = Starlette()
app.add_middleware(SessionMiddleware, secret_key="㊙️")
oauth = OAuth()
oauth.register(
name = 'twitter',
api_base_url = 'https://api.twitter.com/1.1/',
request_token_url = 'https://api.twitter.com/oauth/request_token',
request_token_params = None,
access_token_url = 'https://api.twitter.com/oauth/access_token',
access_token_params = None,
authorize_url = 'https://api.twitter.com/oauth/authenticate',
aurhorize_params = None,
client_id = 'my_client_id',
client_secret = 'my_client_secret',
client_kwargs = None,
)
@app.route('/login/twitter')
async def login_via_twitter(request: Request):
twitter = oauth.create_client('twitter')
redirect_uri = request.url_for('authorize_twitter')
return await twitter.authorize_redirect(request, redirect_uri)
@app.route('/auth/twitter')
async def authorize_twitter(request: Request):
twitter = oauth.create_client('twitter')
token = await twitter.authorize_access_token(request)
user = await twitter.parse_id_token(request, token)
return user
這裡是以 Starlette 做示範,在其他的 web 框架上也差不多,三招搞定 OAuth client:
register()
authorize_redirect()
authorize_access_token()
如果是以 Starlette 為基礎的 FastAPI,那上面那塊程式碼更是可以幾乎完全套用。
上面我們談到 web 後端的 OAuth 1 流程,如果把前後端分離的狀況也考慮進來,那流程會更複雜:
流程如下:
- 用戶在前端按下「Sign in with Twitter」後,後端向 Twitter 索取 request token 並得到 Twitter 認證頁的網址,把 Twitter 認證頁網址丟給前端,前端把用戶導到 Twitter 認證頁。
- 用戶在 Twitter 認證頁按下 Sign In,Twitter 把用戶導到我們指定的 callback URL,並得到 request token verifier,前端把 request token verifier 傳給後端,後端拿 request token 和 request token verifier 向 Twitter 索取用戶的 access token。
- 得到用戶的 access token 後,後端產生自己的 bearer token 交給前端,至此用戶登入完畢,爾後前端發送到後端的請求都附上 bearer token 識別身份,這裡的 bearer token 是 OAuth 2 的 bearer token,也就是說在我們的前端和後端之間,又走了最陽春的 OAuth 2 的認證機制,這部分並非本文的重點,在此點到為止,後續有機會再分享此處的細節。
這個流程的特點是 consumer key、consumer secret、request token secret、access token 都保留在後端,暴露到前端的則有 request token 和 request token verifier,因此:
- 整套 OAuth 1 流程中,用於簽名防偽的主角是 consumer secret,它是始終只在後端的,可以保障前端即使被植入惡意程式碼也無法自行對 Twitter 溝通。
- 用戶的 access token 也只在後端,同樣的保障了 access token 被盜的風險。
- 最後用於前後端認證的是我們自己發行的 bearer token(JWT),同樣的,它也有簽名防偽的機制,後端得以驗證前端的請求是否被竄改過,藉此保證了前端請求的真實性。
然而上述的流程並非唯一選項,有人也把和 Twitter OAuth 1 交互的部份都做在前端,這部份可以參閱〈Demystifying authentication with FastAPI and a frontend〉。
雖然 Authlib 把 OAuth 複雜的細節封裝成很簡單的三大函式,但對不了解 OAuth 1 流程的開發者來說,恐怕會陷入知其然卻不知期所以然的狀態,所以很不幸的,還是得回頭去認識 OAuth 1 的交互流程。
另外本篇的最開頭有提到,Twitter 是目前僅存仍在使用 OAuth 1 的大戶,而從 Twitter 開發人員的網站上也可以看到,Twitter 正在逐步向 OAuth 2 遷移,所以本文很有可能再過一季就會成為廢文,屆時再也沒有 OAuth 1 應用的場景,在花時間閱讀前請謹慎評估,雖然你已經看到最末了。
34