blog.dopana

Back

JSON Web Token(JWT) は、現代のWeb開発においてデファクトスタンダードとなっている認証方式の1つです。Single Page Application(React, Vue, Svelte)からマイクロサービス、モバイルアプリのAPI認証に至るまで、幅広く活用されています。

しかし、JWTを正しく安全に実装することは多くのエンジニアがつまずくポイントでもあります。「トークンはどこに保存すべきか?」「セッションの有効期限切れはどう更新するか?」「XSSとCSRFの両方をどう防ぐか?」

本記事では、JWTを用いたWeb認証の基本概念から実践的なアーキテクチャまでをわかりやすく解説します。

1. 10歳でもわかる解説(ELI5):ロッカーの鍵 vs. 電子リストバンド#

JWTが誕生した理由を理解するために、遊園地の例えを使って従来のセッション認証と比較してみましょう。

従来の方式(セッション認証):ロッカーの番号札#

  • 入園時に受付で番号札(Session ID)を渡されます。
  • あなたの情報(名前、VIPランク、入園時刻)はすべて受付にある台帳(データベースやRedis)に記録されます。
  • ジェットコースターに乗るたび、スタッフはインカムで受付に確認します。「102番の札を持っている人はVIPですか?」。
  • 問題点: 10万人の客が一斉に50個のアトラクションに乗ると、受付への問い合わせがパンクしてしまいます。

現代の方式(JWT認証):電子署名入りリストバンド#

  • 入園時に、情報があらかじめ印字されたスマートリストバンド(JWT)を渡されます。
  • リストバンドには 氏名: Alice, ランク: VIP, 有効期限: 18:00 と書かれています。
  • さらに、運営本部の偽造不可能な電子署名(Digital Signature)が刻印されています。
  • アトラクションに乗る際、スタッフはその場で電子署名の真正性を確認するだけでOKです。本部に問い合わせる必要はありません。
flowchart TD
    subgraph Traditional["1. 従来のセッション認証 - Stateful"]
        Client1["Client"] -->|Session IDを送信| Server1["App Server"]
        Server1 -->|リクエスト毎にDB照会| DB["Database / Redis<br/>Session Store"]
    end

    subgraph JWTWay["2. JWT認証 - Stateless"]
        Client2["Client"] -->|JWTトークンを送信| Server2["App Server"]
        Server2 -->|その場で署名を自己検証| Server2
    end

2. JWTを構成する3つの要素#

JWTはBase64URLでエンコードされた3つのパーツがドット(.)で連結された文字列です:

Header.Payload.Signature\text{Header} . \text{Payload} . \text{Signature}

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQWxpY2UiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MDAwMDAwMDB9.4a5b6c...
text
  1. Header(ヘッダー): トークンのタイプ(JWT)と暗号化アルゴリズム(HS256RS256)を定義。
  2. Payload(ペイロード): 伝達するデータ(Claims)。ユーザーID(sub)、権限(role)、有効期限(exp)など。
  3. Signature(署名): HeaderとPayloadをサーバーの秘密鍵で暗号署名したもの。

[!WARNING] Payloadは暗号化されていません! 単にBase64URLでエンコードされているだけなので、誰でも中身をデコードして読むことができます。パスワードやクレジットカード番号などの機密情報は絶対にPayloadに含めないでください。

3. 実務標準:デュアルトークン構成(Access & Refresh Token)#

単一のJWTだけを使用すると、有効期限を長くすれば漏洩リスクが高まり、短くすればユーザーが頻繁にログアウトされてしまいます。

Webアプリケーションにおけるベストプラクティスは2種類のトークンを併用する設計(Dual-Token Pattern)です:

  • Access Token: 有効期限が極めて短い(10〜15分)。APIリクエストの認証に使用。
  • Refresh Token: 有効期限が長い(7〜30日)。Access Tokenが切れた際に、再ログインなしで新しいAccess Tokenを取得するために使用。
flowchart TD
    User["Client Browser"]
    AuthServer["Auth API Server"]
    ResourceServer["Protected Resource Server"]

    User -->|1. POST /login - ログイン情報| AuthServer
    AuthServer -->|2. Access TokenとRefresh Cookieを返却| User
    
    User -->|3. Bearer Token付きでAPIリクエスト| ResourceServer
    ResourceServer -->|4. データ返却 200 OK| User

    User -->|5. 有効期限切れ 401 Unauthorized| ResourceServer
    User -->|6. POST /refresh - HttpOnly Cookie| AuthServer
    AuthServer -->|7. 新しいAccess Tokenを発行 - Token Rotation| User

4. トークンはどこに保存すべきか?(XSS vs. CSRF)#

フロントエンドにおける保存先の選定はセキュリティの最重要課題です:

保存場所XSSのリスクCSRFのリスク総合評価
localStorage / sessionStorage❌ 危険(悪意あるJSから直接読み取られる)✅ 安全(ブラウザが自動送信しない)長期トークンの保存には非推奨
通常のCookie(HttpOnly なし)❌ 危険(document.cookie で窃取可能)❌ 危険(クロスサイト送信される)非常に危険
HttpOnly + Secure + SameSite Cookie✅ 安全(JavaScriptからアクセス不可)✅ 安全(SameSite がクロスサイト攻撃を遮断)Refresh Tokenの推奨保存先
メモリ(In-Memory JS変数 / State)✅ 安全(タブを閉じると消滅、永続化しない)✅ 安全(自動送信されない)Access Tokenの推奨保存先

推奨される保存戦略:#

  1. Access Token: React / Vue 等のメモリ内ステートに保持する。
  2. Refresh Token: HttpOnly + Secure + SameSite=Lax(または Strict)Cookie に格納する。
  3. ユーザーがリロードした際は、バックグラウンドで /auth/refresh を1回呼び出してメモリ上にAccess Tokenを復元する(Silent Refresh)。

5. Node.js / Express & TypeScript による実装例#

5.1. ログインとトークン発行#

5.2. 認証ミドルウェア#

5.3. トークン更新エンドポイント(Token Rotation)#

6. やってはいけないセキュリティの落とし穴#

[!TIP] JWTセキュリティ・チェックリスト:

  1. 強固な秘密鍵: 最低でも256ビットのランダム文字列またはRSA/ECDSA公開鍵暗号を使用する。
  2. "alg": "none" の拒否: トークン検証時に許可するアルゴリズムを明示的にホワイトリスト化する。
  3. Audience (aud) と Issuer (iss) の検証: 自社の認証サーバーが発行した正当なクライアント向けトークンであるかを確認する。
  4. Token Rotation(トークンローテーション): Refresh Token使用時に古いトークンを無効化し、新しいRefresh Tokenを再発行する。

7. まとめ#

JWTを用いたWeb認証は、適切に設計すればステートレスで高いスケーラビリティを発揮します。「デュアルトークン構成」を採用し、Access Tokenはメモリ上、Refresh TokenはHttpOnly Cookieで管理することで、UXを損なわずに最高レベルのセキュリティを実現できます。

参考文献#