文件站 OAuth Gate:機制與架構
Cloudflare Worker 如何用 Google 登入,把 nitra-fe-docs 鎖在 @nitra.com 之內
背景:為什麼需要這個 gate
文件站部署在 Cloudflare 的 *.workers.dev 上,預設是公開的。但這是內部團隊文件,只能讓 @nitra.com 的人看。worker/ 這個資料夾就是一道自架的 Google OAuth 登入閘門,擋在整個文件站前面。
兩個關鍵設定讓它「沒有破口」:wrangler.jsonc 的 run_worker_first: true 讓 Worker 在服務任何靜態檔之前先跑,preview_urls: false 則關掉會繞過認證的 preview 連結。結果是每一個請求都必須先通過這道閘門。
程式碼分成兩個檔:index.js 負責路由與 HTTP 層(cookie、轉址),auth.js 負責純密碼學原語(JWT、PKCE、與 Google 換 token、claims 驗證)。整套設計是無狀態(stateless)的 —— 因為 Worker 在請求之間不保證保留記憶體,所有「狀態」都簽進使用者的 cookie,而不是存在伺服器。
架構總覽:一個入口攔截所有請求
Worker 沒有像 Express 那樣的路由表。它只有一個入口 fetch(),所有請求都進這裡,再靠 if (url.pathname === …) 自己分流。/auth/* 是它親自處理的特例,其餘路徑一律「先驗 session、通過才放行靜態檔」。
兩個 Cookie:state 與 session
整套 gate 靠兩個簽名 cookie 記住狀態 —— 一個短期、給「登入過程中」用,一個長期、給「登入完成後」用。兩者都用同一把 SESSION_SECRET 以 HMAC-SHA256 簽名,且都帶 HttpOnly(JS 偷不到)。
| Cookie | oauth-state | session |
|---|---|---|
| 用途 | 登入「過程中」的暫存憑證 | 登入「完成後」的通行證 |
| 內容 | 簽名 JWT:state、verifier、returnTo | 簽名 JWT:email、exp |
| 壽命 | STATE_TTL = 10 分鐘 | SESSION_TTL = 24 小時 |
| 何時設 | handleLogin 導去 Google 時 | handleCallback 驗證通過時 |
| 何時清 | callback 成功後立刻清掉 | 24 小時後過期,需重新登入 |
| 屬性 | HttpOnly · SameSite=Lax · Path=/ · HTTPS 下加 Secure 與 __Host- 前綴 |
|
完整登入時序
從「使用者open受保護頁面」到「看到文件」,Browser、Worker、Google 之間依時間順序往返如下。注意 Cookie 的設定、PKCE 的 verifier、與 callback 換 token 都落在這條時間線上。
⑤⑥ 直接在 Browser↔Google 之間,跨過中間的 Worker lane(Worker 不參與)。
⑧⑨ 換 token 帶 code + verifier + client_secret;secret 只在 Worker、不進瀏覽器。
灰框 = 該 lane 的內部處理,不是訊息;head 顏色代表 role(見圖內 swatch)。
防護機制:PKCE、state 與 HMAC
登入流程裡有三個容易混淆但各司其職的安全機制:
PKCE(防 code 被攔截盜用)。登入前產生一個隨機 verifier(只留在自己的 cookie),把它的 SHA-256 challenge 送給 Google;換 token 時再出示原始 verifier,Google 自己 hash 比對。因為 hash 單向,攻擊者就算攔到 code 與 challenge 也反推不出 verifier,於是換不到 token。像是登入時交一個上鎖的盒子,換 token 時要出示鑰匙。
state(防 CSRF)。一個隨機值,簽進 oauth-state cookie,Google 原封帶回。callback 比對「回來的 state」是否等於「cookie 裡簽過的 state」,確認這個 callback 是我們自己發起的,不是攻擊者偽造的。
HMAC session(防偽造通行證)。session 是用 SESSION_SECRET 以 HMAC-SHA256 簽名的 JWT。要偽造別人的 session 就得算出正確簽章,而 HMAC 是單向 keyed hash,看再多 session 也反推不出 secret(只要 secret 夠長夠隨機)。安全性靠的是「secret 保密」,不是演算法保密。
安全設計總表
每一條威脅都有對應的防護與程式位置(S 編號對應 spec 的安全風險評估)。
| 威脅 | 防護 | 位置 |
|---|---|---|
| 非 @nitra.com 存取 | 五重 claims 檢查(aud / iss / email_verified / hd / email 結尾) | auth.js isAllowedClaims |
| CSRF | 簽名 state cookie + query 比對 | index.js handleCallback |
| Code 攔截盜用 | PKCE S256 | auth.js pkceChallenge |
| JWT alg-confusion | 忽略 token 的 alg,寫死 HS256 + timing-safe 比對 | auth.js verifyJWT |
| Cookie 竊取 | HttpOnly + Secure + __Host- 前綴 |
index.js cookieName / setCookie |
| Open-redirect | safeReturnTo 只收同源絕對路徑 | auth.js safeReturnTo |
| 快取洩漏 | 覆寫 Cache-Control: private, max-age=0 | index.js serveAsset |
| Secret 外洩 | callback try/catch 一律回通用訊息,不洩漏細節 | index.js handleCallback |