0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Session】Token-Based Session Management(JWT)とは?

0
Posted at

はじめに

モバイル・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}

ポイント

  • typaccess/refreshを区別
  • jti失効リストで管理(rotationで古いRefreshを失効)
  • iss/aud検証を必ず(なりすまし向け防波堤)
  • 実運用は署名鍵のローテ、HS→RS/ES署名(公開鍵配布)を検討

セキュリティ設計の勘所

A. クライアントでの保管場所

  • 最優先はXSS対策
  • HttpOnly CookieでAccess/Refreshを保持するとXSS窃取に強い(CookieでもTokenベースは実現可)。
  • localStorageXSSに弱い。避けたい場合が多い。
  • CSRFはSameSite=Lax/StrictCSRFトークンで対処(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(攻撃・実装のベストプラクティス)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?