昨天,我們終於把 Google Login 裡那些很容易混在一起的 Token 拆開了。
BuJo 第二版也從原本的 Access Token,改成用 ID Token 來確認登入者身分。
當時我真的有種:
「好!這次應該終於走對了吧?」
結果某次按下 Google 登入之後——
沒反應。
我打開主控台看,後端也沒有出現任何明顯的錯誤訊息。
前前後後查了好~久,最後才發現:
問題居然出在瀏覽器擴充功能!
停用之後,Google Login 就恢復正常了。
我當下真的只剩:
「蛤???這誰想得到?」
而這次真的碰到瀏覽器這一層的問題,也成了我們後來決定把 Google Login 從第二版改成第三版的原因之一。
只是第三版真正換掉的,遠不只是「登入畫面改成會跳頁」。
整條 OAuth Flow 裡,Frontend、Backend、瀏覽器和 Google 之間負責的事情,都跟著重新分配了。
第二版我們用的是 Google One Tap。
它就是我們平常逛網站時,會浮在目前頁面上的那種小型 Google 登入提示。
這種做法我自己其實滿喜歡的。
因為使用者不用整頁離開 BuJo,選完帳號就可以直接登入。
整個過程也不會打斷原本正在看的頁面。
使用者按下 BuJo 的 Google 登入按鈕後,會叫出 Google One Tap。
整條流程大概是:
BuJo Google 登入按鈕
↓
Google One Tap
↓
Frontend 拿到 Google ID Token
↓
Frontend 把 ID Token 傳給 BuJo Backend
↓
Backend 驗證 ID Token
但第三方登入跑在瀏覽器裡,也代表中間不是只有自己的 Frontend 和 Backend。
如果把一次登入可能經過的層次拉開來看:
UI
↓
Frontend
↓
第三方 SDK
↓
瀏覽器/執行環境(Browser / Environment) ← BuJo 這次卡在這裡
↓
Network
↓
Backend
瀏覽器這一層,本來就還可能受到很多東西影響:
瀏覽器/執行環境(Browser / Environment)
├─ 瀏覽器擴充功能(Browser Extension)
├─ 彈窗/登入提示行為(Popup / Prompt)
├─ Cookie/儲存政策(Cookie / Storage Policy)
├─ 隱私與追蹤保護(Privacy / Tracking Protection)
└─ 聯合登入機制(Federated Login)
而這次 BuJo 遇到的 Bug 真正定位到的,就是:
瀏覽器擴充功能(Browser Extension)。
這也不是我們唯一一次在瀏覽器這一層碰到狀況。
測試時,Safari 也曾經出現不能正常登入的情況,另外也遇過可能和不同來源或瀏覽器端政策有關的問題。
這些經驗讓我開始更在意:
第三方登入不是 Code 寫對、Backend 接得住就一定會正常,瀏覽器本身也會參與在這條流程裡。
找到瀏覽器擴充功能的問題之後,當下的登入確實恢復正常了。
但前面幾次測試累積下來,我開始有一個更實際的擔心:
如果之後是真實使用者遇到 Safari 不能登入,或其他瀏覽器端的狀況呢?
尤其這類問題不一定會直接跳出一個很明顯的錯誤訊息,使用者可能只會覺得:
「我按了,為什麼沒反應?」
也是因為這個考量,後來第三版,我們決定把 Google Login 改成由後端主導的 OAuth 重新導向流程。
也就是 OAuth 2.0 的:
Authorization Code Flow(授權碼流程)。
如果只看使用者看到的畫面,差異其實很好認。
One Tap 是在目前頁面上浮出 Google 登入提示;第三版則會整頁前往 Google,完成登入後再回到 BuJo。
但真正重要的差別,不是「有沒有跳頁」。
而是:
Google 的登入結果在哪一層被接住,以及後面的 OAuth Flow 由誰負責。
第二版:One Tap 第三版:Authorization Code Flow
BuJo Frontend BuJo Frontend
↓ ↓
googleOneTap() BuJo Backend
↓ ↓
Google One Tap 建立 Google Authorization URL
↓ ↓
Frontend 拿到 ID Token 瀏覽器 → Google
↓ ↓
Frontend 傳給 Backend Callback 回 Backend
↓ (code + state)
Backend 驗證 ID Token ↓
Backend 用 code 換 Token
↓
Backend 驗證 ID Token
所以第二版是:
Frontend 先從 Google 拿到 ID Token,再交給 Backend。
第三版則變成:
Frontend 先把登入交給 Backend,後面的 Authorization Code Exchange 也由 Backend 處理。
也就是說,第三版不是單純把 One Tap 換成另一種 Google Login UI。
真正重新分配的,是整段 OAuth 流程裡,各自負責的工作。
光看到:
Frontend
↓
Backend
↓
Google
↓
Backend
其實還是很難懂。
我真正開始看懂這條 Flow,是把每一條箭頭拆開問:
「現在是誰把什麼東西交給誰?」
先把完整路線攤開:
① Frontend
↓
BuJo Backend
(開始 Google Login)
② BuJo Backend
↓
建立 Google Authorization URL
(client_id、redirect_uri、response_type、scope、state)
↓
瀏覽器被重新導向到 Google
③ Google
↓
使用者完成登入
↓
瀏覽器被導回 BuJo Backend Callback
(code + state)
④ BuJo Backend
↓
驗證 state
↓
POST Google Token Endpoint
(Request Body 帶著 code、client_id、client_secret、
redirect_uri、grant_type)
⑤ Google
↓
回傳 Token Response
↓
BuJo Backend 驗證 ID Token
接下來就照這條路,一段一段打開。
Frontend 先把 Google Login 交給 Backend。
這時 Backend 會先組出一條:
等等要讓瀏覽器前往 Google 登入頁面的網址。
這條網址就是 Google 的授權端點(Authorization Endpoint)URL,後面再帶上這次登入需要的 Query Parameters。
概念上會像:
https://accounts.google.com/o/oauth2/v2/auth
?client_id=...
&redirect_uri=...
&response_type=code
&scope=openid%20email%20profile
&state=...
也就是:
「等等要讓瀏覽器前往 Google 登入頁面的網址,並且把這次登入需要的設定一起寫在網址後面。」
後面的每一個參數,都在告訴 Google 不同的事情:
client_id
→ 我是哪一個 OAuth Client?
redirect_uri
→ 登入完成後,要把瀏覽器送回哪裡?
response_type=code
→ 這次走 Authorization Code Flow,
登入完成後先回傳 Authorization Code
scope
→ 這次需要哪些身分/權限範圍?
state
→ 這次 OAuth Request 帶出去的比對值
所以 Backend 做的第一件事,可以先理解成:
BuJo Backend
↓
建立 Google Authorization URL
(帶著 client_id、redirect_uri、response_type、scope、state)
↓
回傳 Redirect Response
(告訴瀏覽器:請前往剛剛建立好的 Google Authorization URL)
↓
瀏覽器自動跳到 Google
↓
使用者看到 Google 的登入頁面
也就是說,Backend 不是自己跑去 Google 登入。
它做的是:
先把「要去哪裡+這次登入需要的設定」組成一條網址,再透過 Redirect 告訴瀏覽器前往那個網址。
真正被送去 Google 的,還是使用者現在正在操作的瀏覽器。
使用者在 Google 完成登入之後,瀏覽器會被導回 BuJo 預先設定好的回呼網址(Callback URL):
瀏覽器被導回 BuJo Backend Callback
(帶著 code + state)
Callback URL 可以先理解成:
我們事先告訴 Google:「登入完成之後,請把這趟流程送回這個 Backend Endpoint。」
所以這一步最重要的事情其實只有兩個:
Google 登入完成
↓
瀏覽器回到 BuJo Backend
(帶著 code + state)
其中的 code,就是昨天看過的 Authorization Code。
Backend 收到之後,下一步才會拿它去 Google Token Endpoint 換 Token。
這一段跟前面的 Redirect 不一樣。
前面是:
瀏覽器
↓
被重新導向到 Google
但現在是:
BuJo Backend
↓
主動送一個 HTTP POST Request
↓
Google Token Endpoint
目的很單純:
拿剛剛收到的 Authorization Code 去換 Token。
這次要交給 Google 的資料,不是放在跳轉網址上,而是放進 HTTP POST Request 的 Body。
BuJo 送出的內容可以先這樣看:
BuJo Backend
↓
POST Google Token Endpoint
Request Body
├─ grant_type=authorization_code
│ → 告訴 Google:這次要用 Authorization Code 換 Token
│
├─ code=剛剛收到的 Authorization Code
│ → 就是 Google 透過 Callback 帶回來的那張兌換碼
│
├─ client_id=BuJo 的 Client ID
│ → 告訴 Google:這個 Request 是哪一個 OAuth Client 發出的
│
├─ client_secret=BuJo Backend 保存的 Client Secret
│ → Backend 用來向 Google 識別/驗證這個 OAuth Client 的秘密值
│
└─ redirect_uri=BuJo 的 Callback URL
→ 告訴 Google:這次 Code 原本是搭配哪一個 Callback URL 使用
這時候整段 Code Exchange 就比較不像魔法了。
Backend 其實是在跟 Google 說:
「這是你剛剛發給我的 Code。我是 BuJo,這是這次登入使用的 Callback。請確認這一整組資料對得起來,再把 Token 給我。」
Google 確認這次交換沒問題之後,才會回傳 Token Response。
接著 BuJo Backend 再驗證裡面的 ID Token。
而 ID Token 到底要驗什麼,昨天已經拆過了,今天就不重講一次。
state 為什麼要一起出去,又一起回來?走到這裡,還剩下一個很重要的東西:
state。
前面的路線是:
BuJo
↓
Google
↓
BuJo
使用者的瀏覽器被送出去一趟,又從 Google 回來。
那 BuJo Backend 怎麼知道:
「現在回來的這個 Callback,真的對應我剛剛送出去的那一次 OAuth Request 嗎?」
這就是 state 的工作。
流程可以這樣看:
BuJo Backend 發起登入
(產生 state)
↓
Google Authorization Request
(帶著 state)
↓
使用者完成 Google Login
↓
瀏覽器被導回 BuJo Callback
(帶著 code + state)
↓
BuJo Backend 驗證 state
所以 state 不是拿來證明:
「這個使用者是誰?」
它在處理的是:
「現在回來的這一趟,是不是剛剛從我這裡出去的那一趟?」
除了把送出去的 OAuth Request 和回來的 Callback 對起來之外,state 也是 OAuth 裡防護跨站請求偽造(CSRF)的重要機制。
我覺得把它放進完整 Flow 之後,比單獨背一句「OAuth 要有 state」好懂很多。
因為這時候我真的看得到:
它為什麼要出去,又為什麼一定要跟著回來。
第三版的流程拆完之後,再回頭看一開始那個「按了沒反應」的 Bug,我覺得這次還留下另一個很好用的東西。
就是:
先確認問題最後成功走到哪一層。
可以先把路切成:
UI
↓
Frontend
↓
第三方 SDK
↓
瀏覽器/執行環境(Browser / Environment) ← BuJo 這次
↓
Network
↓
Backend
如果按鈕事件根本沒有觸發,就還不用急著查 Backend。
如果 Frontend 已經往下走,卻沒有真的送出 Request,就可以先把範圍留在 Frontend、第三方 SDK 或瀏覽器這幾層。
如果 Request 已經成功進 Backend,再往後查 Authentication、資料或其他後端問題。
我當時並不是拿著這張完整地圖照表 Debug。
是經過這次真的卡很久之後,我才慢慢把它整理成現在比較會用的方式:
先定位,再找原因。
這次真正值得留下來的,也不是:
「下次 Google Login 壞掉,記得先把 Extension 關掉。」
因為下一個 Bug 根本不一定長一樣。
比較有用的是:
就算一開始不知道答案,還是可以先讓問題範圍越來越小。
一開始看到 Google Login 前前後後換了三次,我真的很容易把它想成:
同一個功能,只是又換一種實作方式。
但把第二版和第三版完整拆開之後,我現在看到的已經不是:
One Tap → 整頁重新導向。
而是:
第二版
Frontend 直接接住 Google ID Token
↓
再交給 Backend 驗證
第三版
Backend 發起 Authorization Code Flow
↓
瀏覽器負責在 BuJo 和 Google 之間重新導向
↓
Callback 帶回 code + state
↓
Backend 再自己拿 Code 去 Google 換 Token
真正被重新分配的,是:
這整段 OAuth Flow 到底由誰負責。
也因為真的碰過瀏覽器擴充功能這次問題,我們才會重新思考:
是不是可以不要再讓登入入口這麼依賴 One Tap 的瀏覽器端行為?
最後才一路走到第三版。
Google 這邊終於把「你是誰」確認完了。
但故事還沒有真的結束。
因為 Google 認得你,不代表 BuJo 接下來的每一個 Request 都會自動記得你是誰。
下一篇,就來看另一把真正屬於 BuJo 自己的鑰匙:
Google 都已經驗完身分了,BuJo 為什麼還要再發自己的 JWT?