#はじめに

「APIの認証にJWTを使っています」——よく聞くフレーズですが、JWTとは何で、なぜ使われるのでしょうか?

前回学んだサーバーサイドセッションは、セッションデータをサーバーに保存していました。JWTは異なるアプローチで、トークン自体に情報を含めます。これにより、サーバー側でセッションストレージを持たなくても認証が可能になります。

この記事を読むと、以下のことができるようになります:

  • JWTの構造と仕組みを理解できる
  • 署名検証の役割がわかる
  • JWTの保存場所の議論を理解し、適切な選択ができる
  • リフレッシュトークンの役割がわかる

#JWTとは

JWT(JSON Web Token、「ジョット」と読む)とは、JSON形式の情報を安全にやり取りするためのトークン形式です。

見た目は長い文字列です:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuWxseeUsOWkqumDjiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

ピリオド.で区切られた3つのパートで構成されています。

#JWTの構造

ヘッダー.ペイロード.署名
(Header).(Payload).(Signature)

#1. ヘッダー(Header)

トークンのメタ情報(アルゴリズム、タイプ)をBase64URLエンコードしたものです。

{
  "alg": "HS256",
  "typ": "JWT"
}

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

#2. ペイロード(Payload)

実際に伝えたい情報(クレーム)をBase64URLエンコードしたものです。

{
  "sub": "1234567890",
  "name": "山田太郎",
  "iat": 1516239022,
  "exp": 1516242622
}

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuWxseeUsOWkqumDjiIsImlhdCI6MTUxNjIzOTAyMn0

#標準クレーム

クレーム説明
sub (Subject)トークンの主体(ユーザーID等)
iat (Issued At)発行日時(UNIXタイムスタンプ)
exp (Expiration)有効期限
iss (Issuer)発行者
aud (Audience)対象者

#3. 署名(Signature)

ヘッダーとペイロードを結合し、秘密鍵で署名したものです。

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

#なぜ署名が必要なのか

ペイロードはBase64URLエンコードされているだけで、暗号化されていません。誰でもデコードして内容を見ることができます。

// ペイロードのデコード(誰でもできる)
atob('eyJzdWIiOiIxMjM0NTY3ODkwIn0');
// → {"sub":"1234567890"}

では、なぜ安全なのか? それは署名があるからです。

#署名の役割

  1. 改ざん検知: ペイロードを書き換えると、署名が一致しなくなる
  2. 発行者の証明: 秘密鍵を知っている人しか有効な署名を作れない
攻撃者が role: "admin" に書き換えても...
→ 署名を再計算できない(秘密鍵を知らない)
→ サーバーが署名検証で拒否

#署名アルゴリズム

アルゴリズム種類説明
HS256対称鍵共有秘密鍵で署名・検証
RS256非対称鍵秘密鍵で署名、公開鍵で検証
ES256非対称鍵楕円曲線暗号、RS256より短い鍵

マイクロサービス間や外部サービスとの連携では、非対称鍵(RS256、ES256)が推奨されます。

#JWTの認証フロー

[クライアント]                           [サーバー]
    |                                        |
    |------ ログイン情報 ------------------->|
    |                                        |
    |                              認証成功 → JWT生成
    |                                        |
    |<----- JWT (アクセストークン) ----------|
    |                                        |
    |------ Authorization: Bearer {JWT} ---->|
    |                                        |
    |                              署名検証 → ペイロード取得
    |                                        |
    |<----- レスポンス ----------------------|

サーバーはJWTの署名を検証し、ペイロードからユーザー情報を取得します。セッションストレージへのアクセスは不要です。

#サーバーサイドセッションとの比較

観点サーバーサイドセッションJWT
データ保存場所サーバートークン自体
スケーラビリティセッション共有が必要ステートレス、共有不要
即時無効化可能困難
トークンサイズ小さい(IDのみ)大きい(情報を含む)
セキュリティデータはサーバーで保護ペイロードは見られる

#JWTの保存場所

JWTをクライアントのどこに保存するかは、議論の多いトピックです。

#選択肢1: localStorage

// 保存
localStorage.setItem('token', jwt);

// 取得
const token = localStorage.getItem('token');

// リクエストに付与
fetch('/api/data', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});
メリットデメリット
実装が簡単XSSで盗まれる
CSRFの影響を受けないJavaScriptからアクセス可能
Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict
メリットデメリット
XSSで盗まれないCSRFの考慮が必要
JavaScriptからアクセス不可Cookie送信の制約

セキュリティを重視する場合、HttpOnly Cookieが推奨されます。XSS脆弱性があっても、トークンを盗まれるリスクを軽減できます。

Set-Cookie: access_token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Path=/

#リフレッシュトークン

JWTは一度発行されると、有効期限まで無効化できません。そのため、有効期限を短く設定し、リフレッシュトークンで更新する方式が一般的です。

#2種類のトークン

トークン有効期限用途
アクセストークン短い(5分〜1時間)APIアクセス
リフレッシュトークン長い(数日〜数週間)アクセストークンの更新

#更新フロー

[クライアント]                           [サーバー]
    |                                        |
    |------ API呼び出し (アクセストークン) --->|
    |                                        |
    |<----- 401 Unauthorized (期限切れ) ------|
    |                                        |
    |------ リフレッシュトークン送信 ---------->|
    |                                        |
    |<----- 新しいアクセストークン -----------|
    |                                        |
    |------ API再呼び出し (新トークン) ------->|
    |<----- 成功 -----------------------------|

#リフレッシュトークンの保存

リフレッシュトークンは長期間有効なため、より安全に保管する必要があります。

# アクセストークン: メモリまたは短命Cookie
# リフレッシュトークン: HttpOnly Cookie(別パス)

Set-Cookie: refresh_token=xyz...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh

#JWTの注意点

#1. 機密情報を入れない

ペイロードは誰でも読めます。パスワードや個人情報を含めないでください。

// ❌ 危険
{
  "email": "user@example.com",
  "password": "secret123"
}

// ✅ OK
{
  "sub": "user_123",
  "role": "admin"
}

#2. 有効期限を適切に設定

長すぎる有効期限は、トークン漏洩時のリスクを高めます。

{
  "exp": 1700000000,  // 適切な有効期限を設定
  "iat": 1699999000
}

#3. 署名アルゴリズムの検証

サーバーは、受け取ったJWTのアルゴリズムを信用してはいけません。

// 攻撃: alg を none に変更
{
  "alg": "none",  // 署名なし!
  "typ": "JWT"
}

サーバー側で許可するアルゴリズムを明示的に指定してください。

#4. 即時無効化が必要な場合

JWTは即時無効化が困難です。以下の対策を検討してください:

  • 有効期限を短くする
  • ブラックリスト(無効化したトークンのリスト)を管理
  • 重要な操作では追加の認証を要求

#まとめ

  • JWTはヘッダー、ペイロード、署名の3部構成
  • ペイロードは暗号化されていない(誰でも読める)
  • 署名で改ざんを検知、秘密鍵なしに有効なJWTは作れない
  • 保存場所はHttpOnly Cookieが推奨
  • リフレッシュトークンで短い有効期限のアクセストークンを更新
  • 機密情報をペイロードに含めない

#次のステップ

JWTの基礎を理解したところで、次はOAuth 2.0とOIDCについて学びましょう。「Googleでログイン」「GitHubでログイン」——これらのソーシャルログインがどのように実現されているのかがわかります。

#参考リンク