Supabase JWT 驗證踩坑:從 Legacy JWT Secret 改用 JWT Signing Keys

記錄在 FastAPI 專案中整合 Supabase 認證時,從 Legacy JWT Secret (HS256) 遷移到 JWT Signing Keys (ES256/RS256) 的完整過程與原因。

問題背景

在開發 FastAPI + Supabase 專案時,需要在後端驗證前端傳來的 JWT token。最初AI,使用了 Legacy JWT Secret 搭配 HS256 演算法驗證:

# 原本的寫法(使用 Legacy JWT Secret)
import jwt

payload = jwt.decode(
    token,
    SUPABASE_JWT_SECRET,  # Legacy JWT Secret
    algorithms=["HS256"],
    audience="authenticated",
)
Code language: PHP (php)

問題原因:兩種 Key 的本質差異

Supabase 有兩套 JWT 簽署機制:

類型演算法驗證方式安全性
Legacy JWT SecretHS256(對稱加密)需要共享密鑰較低
JWT Signing KeysES256/RS256(非對稱加密)使用 JWKS 公鑰驗證較高

Legacy JWT Secret (HS256)

  • 對稱加密:簽署和驗證使用同一把密鑰
  • 需要把 Secret 存在後端環境變數
  • 如果 Secret 洩漏,攻擊者可以偽造 token

JWT Signing Keys (ES256/RS256)

  • 非對稱加密:私鑰簽署、公鑰驗證
  • 後端只需要公鑰(透過 JWKS endpoint 取得)
  • 即使公鑰曝露也無法偽造 token

解決方案:改用 JWKS 驗證

Supabase 提供 JWKS (JSON Web Key Set) endpoint,可以取得公鑰來驗證 token:

https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
Code language: HTML, XML (xml)

步驟 1:安裝 python-jose

# 使用 uv
uv add python-jose[cryptography]

# 或使用 pip
pip install python-jose[cryptography]
Code language: CSS (css)

步驟 2:修改環境變數

# .env(移除 JWT Secret,改用 Project Reference)
SUPABASE_PROJECT_REF=你的project-ref
Code language: PHP (php)

步驟 3:修改驗證程式碼

"""JWT 驗證(使用 JWKS 非對稱驗證)"""

import os
from functools import lru_cache

import requests
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt

SUPABASE_PROJECT_REF = os.environ.get("SUPABASE_PROJECT_REF", "")

security = HTTPBearer()

@lru_cache(maxsize=1)
def get_jwks() -> dict:
    """
    取得並快取 Supabase JWKS。
    使用 lru_cache 避免每次請求都重新取得。
    """
    if not SUPABASE_PROJECT_REF:
        raise ValueError("SUPABASE_PROJECT_REF 未設定")

    jwks_url = f"https://{SUPABASE_PROJECT_REF}.supabase.co/auth/v1/.well-known/jwks.json"
    response = requests.get(jwks_url, timeout=10)
    response.raise_for_status()
    return response.json()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
    """從 JWT token 解析並驗證用戶"""
    token = credentials.credentials

    try:
        jwks = get_jwks()

        # 注意:演算法要根據你的 JWKS 設定
        # 可以先 curl JWKS endpoint 確認是 ES256 還是 RS256
        payload = jwt.decode(
            token,
            jwks,
            algorithms=["ES256"],  # 或 RS256,視你的設定
            audience="authenticated",
            issuer=f"https://{SUPABASE_PROJECT_REF}.supabase.co/auth/v1",
        )
    except JWTError as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=f"無效的 Token: {str(e)}",
        )

    return {
        "email": payload.get("email"),
        "sub": payload.get("sub"),
    }
Code language: PHP (php)

步驟 4:確認演算法類型

先檢查你的 JWKS 使用什麼演算法:

curl https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
Code language: HTML, XML (xml)

回應範例:

{
  "keys": [{
    "alg": "ES256",  // <-- 看這裡!ES256 或 RS256
    "kty": "EC",
    "kid": "77327ed6-ecd7-4e2e-b5f8-edcc0320b6c2",
    ...
  }]
}
Code language: JavaScript (javascript)

根據 alg 欄位設定 algorithms 參數。

常見錯誤排除

錯誤 1:Signature verification failed

原因:演算法設定錯誤(例如 JWKS 是 ES256,但程式碼寫 RS256)

解決:確認 JWKS 的 alg 欄位,對應修改程式碼

錯誤 2:Token 缺少必要欄位

原因:token 不是 Supabase Auth 發出的,或是 anon key 產生的

解決:確保前端使用 supabase.auth.getSession() 取得的 access_token

錯誤 3:JWKS 請求失敗

原因:網路問題或 Project Reference 錯誤

解決

  1. 確認 SUPABASE_PROJECT_REF 正確
  2. 在容器環境中確認 DNS 解析正常

為什麼要改?

  1. 更安全:非對稱加密,公鑰曝露也無法偽造 token
  2. 官方推薦:Supabase 正在從 Legacy keys 過渡到新的 API keys 系統
  3. 自動輪換:JWKS 支援 key rotation,提升安全性
  4. 無需共享密鑰:後端只需要 Project Reference,不需要存放敏感的 Secret

結論

如果你的 Supabase 專案使用的是 JWT Signing Keys(可以從 Dashboard 的 API Keys 頁面確認),就必須使用 JWKS 方式驗證,而不是 Legacy JWT Secret。

這個改動雖然需要修改程式碼,但帶來更好的安全性和符合官方的發展方向。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *