はじめに
モバイル・SPA・マイクロサービス時代の定番がトークンベース のセッション管理。サーバーに状態(セッション)を持たず、署名付きトークン(JWTなど) をクライアントが保持して毎回送る方式です。
- APIスケールしやすい
- CORS/多クライアントに強い
- ただし失効・ローテーション設計が肝
Token-Basedの基本
-
Access Token:短寿命(例 5〜15分)。API呼び出し時に
Authorization: Bearer <token>で送る。 - Refresh Token:長寿命(例 数日〜数週間)。Access切れを安全に再発行するために使用。流出対策が超重要。
-
Claims(ペイロード):
-
iss(発行者),aud(受信者),sub(ユーザーID) -
exp/iat/nbf(期限/発行時刻/有効開始) -
jti(トークン識別子:失効リスト用) - 役割/権限などは最小限に
-
ざっくりフロー
HTTPやり取り視点(シーケンス)
実装最短レシピ(FastAPI + PyJWT)
目的:**最小構成で「発行・検証・更新」**を把握する
(実運用では鍵管理・失効リスト・ローテなどを強化)
# pip install fastapi uvicorn "pyjwt[crypto]"
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, Header
import jwt, uuid
SECRET = "change-me" # 実運用はKMS/環境変数で管理
ISSUER = "https://auth.example.com"
AUD = "example-api"
ACCESS_TTL = timedelta(minutes=10)
REFRESH_TTL = timedelta(days=7)
app = FastAPI()
revoked_jti = set() # デモ用の失効リスト(実運用はDB/Redis)
def make_token(sub: str, ttl: timedelta, token_type: str, extra: Optional[dict]=None):
now = datetime.now(timezone.utc)
payload = {
"iss": ISSUER, "aud": AUD, "sub": sub, "typ": token_type,
"iat": int(now.timestamp()), "nbf": int(now.timestamp()),
"exp": int((now+ttl).timestamp()), "jti": str(uuid.uuid4())
}
if extra: payload.update(extra)
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token: str, expected_type: str):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"], audience=AUD, issuer=ISSUER)
if payload.get("typ") != expected_type:
raise HTTPException(401, "invalid token type")
if payload.get("jti") in revoked_jti:
raise HTTPException(401, "revoked")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(401, "expired")
except jwt.PyJWTError as e:
raise HTTPException(401, f"invalid: {e}")
@app.post("/login")
def login(username: str, password: str):
if not (username == "admin" and password == "pass"): # ダミー認証
raise HTTPException(401, "bad credentials")
access = make_token(sub="user:1", ttl=ACCESS_TTL, token_type="access")
refresh = make_token(sub="user:1", ttl=REFRESH_TTL, token_type="refresh")
return {"access": access, "refresh": refresh}
@app.get("/me")
def me(authorization: Optional[str] = Header(None)):
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, "missing bearer")
token = authorization.split(" ",1)[1]
payload = verify_token(token, "access")
return {"sub": payload["sub"], "iat": payload["iat"], "exp": payload["exp"]}
@app.post("/refresh")
def refresh(refresh_token: str):
payload = verify_token(refresh_token, "refresh")
# Refresh Token Rotation(古いRefreshを失効)
revoked_jti.add(payload["jti"])
new_access = make_token(sub=payload["sub"], ttl=ACCESS_TTL, token_type="access")
new_refresh = make_token(sub=payload["sub"], ttl=REFRESH_TTL, token_type="refresh")
return {"access": new_access, "refresh": new_refresh}
ポイント
-
typでaccess/refreshを区別 -
jtiを失効リストで管理(rotationで古いRefreshを失効) -
iss/aud検証を必ず(なりすまし向け防波堤) - 実運用は署名鍵のローテ、HS→RS/ES署名(公開鍵配布)を検討
セキュリティ設計の勘所
A. クライアントでの保管場所
- 最優先はXSS対策。
- HttpOnly CookieでAccess/Refreshを保持するとXSS窃取に強い(CookieでもTokenベースは実現可)。
-
localStorageはXSSに弱い。避けたい場合が多い。 - CSRFは
SameSite=Lax/StrictやCSRFトークンで対処(Cookie運用時)。
B. 期限設計
- Accessは短寿命(5〜15分)
- Refreshは長寿命+Rotation(利用のたびに再発行し、旧トークンを失効)
- 連続不審更新を検知→Refreshチェーン全失効も検討
C. 失効とログアウト
- “完全スタットレス”だと強制ログアウトが難しい
- 対策:失効リスト(ブラックリスト) or 最終ログアウト時刻を参照
-
jti+失効ストア(Redis等)で即時無効化を実現
D. 署名鍵とアルゴリズム
- 署名アルゴリズムの明示(alg固定)
- 鍵はKMS等で管理、ローテーション(
kidヘッダでキー識別) - 受信側は公開鍵のキャッシュ更新戦略が必要(JWKS)
E. クレーム最小化
- 個人情報・権限を過剰に詰めない(露出・サイズ肥大)
- 権限はスコープ化し最小権限で
よくある落とし穴
-
alg=none/アルゴリズム混乱攻撃への脆弱な実装 -
aud/iss未検証(他サービス発行トークン混入) - 長寿命Access(失効困難&漏洩リスク増)
- Refreshを回収せずに再発行(Rotationしない)
- XSS対策不備の
localStorage保管 -
jti未採用で個別失効できない - 大きすぎるJWT(ヘッダ肥大・遅延)
Cookie-Based vs Token-Based(要点比較)
| 観点 | Cookie-Based(サーバー状態) | Token-Based(JWT等・基本無状態) |
|---|---|---|
| スケール | セッション共有が課題(Sticky/外部Store) | 無状態で水平スケール容易 |
| 失効/強制ログアウト | Store削除で即時 | 仕組みを作らないと難(失効表が必要) |
| クライアント | Web向けに自然 | Web/モバイル/API間連携に強い |
| CSRF | 対策必須(Cookie) | ヘッダ送信が基本で比較的楽(Cookie運用なら同様) |
| XSS | Cookie+HttpOnlyで強い | 保存先次第(Cookie推奨) |
| 実装のしやすさ | フレームワーク標準多い | Authサーバー/鍵運用/ローテ設計が要件 |
使い分けガイド
- 典型的Web+サーバーレンダリング中心:Cookie-Basedが自然で速い
- SPA+モバイル+外部API連携:Token-Based(Access短寿命+Refresh Rotation+失効表)
-
ゼロトラスト/マイクロサービス:Token-Based一択。公開鍵署名+
aud/iss厳格を基本に
まとめ
- トークン方式はスケールと多様なクライアントに強い
- 代わりに期限設計/失効/ローテ/鍵管理が設計の山
- 実装は最小例でも、Refresh Rotation + jti失効 + aud/iss検証は外さない
参考リンク
- RFC 7519: JSON Web Token (JWT)
- OWASP Cheat Sheet: JWT, Session Management
- OAuth 2.1 / OpenID Connect Core(実運用はIdP活用が現実的)
- IETF: JWT BCP(攻撃・実装のベストプラクティス)