JWT入門 - JSON Web Tokenの構造・署名・使い方と注意点

JWT(JSON Web Token)は、JSON形式の情報をコンパクトに表現するためのトークン形式。主に認証・認可の場面でサーバーとクライアント間の情報伝達に使われる。

JWTの構造

JWTは . で区切られた3つのBase64URLエンコードされた文字列で構成される。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRhcm8gU2F0byIsImlhdCI6MTcxMTg4NjQwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

[ヘッダー].[ペイロード].[署名]

ヘッダー(Header)

署名アルゴリズムとトークンタイプを指定する。

{
  "alg": "HS256",
  "typ": "JWT"
}
フィールド説明
alg署名アルゴリズム(HS256, RS256など)
typトークンタイプ(通常 JWT

ペイロード(Payload)

実際のデータ(クレーム)を格納する。

{
  "sub": "1234567890",
  "name": "佐藤太郎",
  "email": "taro@example.com",
  "role": "admin",
  "iat": 1711886400,
  "exp": 1711890000
}

標準クレーム(Registered Claims)

クレーム名称説明
issIssuer発行者
subSubject主体(通常はユーザーID)
audAudience想定受信者
expExpiration Time有効期限(Unixタイムスタンプ)
nbfNot Beforeこの時刻より前は無効
iatIssued At発行時刻
jtiJWT IDJWT固有のID

ペイロードはBase64URLエンコードされているだけで暗号化されていない。誰でもデコードして内容を読める。機密情報は格納しない。

署名(Signature)

ヘッダーとペイロードの改ざんを検証するための署名。

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

秘密鍵(または公開鍵ペア)を使って生成するため、サーバーだけが有効な署名を作れる。サーバーは受け取ったJWTの署名を再計算して一致するか検証する。

署名アルゴリズム

対称アルゴリズム(HMAC)

同じ秘密鍵で署名と検証を行う。発行者と検証者が同じシステムの場合に適している。

アルゴリズム説明
HS256HMAC + SHA-256(最も一般的)
HS384HMAC + SHA-384
HS512HMAC + SHA-512

非対称アルゴリズム(RSA / ECDSA)

秘密鍵で署名し、公開鍵で検証する。認可サーバーとリソースサーバーが別の場合(OIDCなど)に適している。

アルゴリズム説明
RS256RSA + SHA-256
ES256ECDSA + SHA-256(RSAより鍵が短く高速)
PS256RSA-PSS + SHA-256

外部サービスのJWTを検証する場合(例: Google OIDCトークン)は、そのサービスが公開する公開鍵を使って検証する。

認証フロー

JWTを使った典型的な認証フローはBearer認証。

sequenceDiagram
    participant C as クライアント
    participant S as サーバー
    C->>S: POST /auth/login
    Note over C,S: { email, password }
    S-->>C: 200 OK
    Note over C,S: { token: "eyJ..." }
    C->>S: GET /api/profile
    Note over C,S: Authorization: Bearer eyJ...
    Note over S: 1. トークンを取り出す<br/>2. 署名を検証する<br/>3. 有効期限を確認する<br/>4. claimを使って処理
    S-->>C: 200 OK
    Note over C,S: { name: "佐藤太郎", ... }

実装例

Node.js(jsonwebtoken)

const jwt = require('jsonwebtoken')

const SECRET = process.env.JWT_SECRET

// トークン生成
const token = jwt.sign(
  { sub: user.id, email: user.email, role: user.role },
  SECRET,
  { expiresIn: '1h' }
)

// トークン検証
try {
  const payload = jwt.verify(token, SECRET)
  console.log(payload.sub)  // ユーザーID
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    // 有効期限切れ
  } else if (err instanceof jwt.JsonWebTokenError) {
    // 不正なトークン
  }
}

Python(PyJWT)

import jwt
from datetime import datetime, timedelta, timezone

SECRET = "your-secret-key"

# トークン生成
payload = {
    "sub": str(user.id),
    "email": user.email,
    "exp": datetime.now(timezone.utc) + timedelta(hours=1),
}
token = jwt.encode(payload, SECRET, algorithm="HS256")

# トークン検証
try:
    payload = jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
    pass  # 有効期限切れ
except jwt.InvalidTokenError:
    pass  # 不正なトークン

トークンの保存場所

保存場所メリットデメリット
localStorageシンプルで扱いやすいXSSで盗まれるリスク
SessionStorageタブを閉じると消えるXSSで盗まれるリスク
HttpOnly CookieJSからアクセス不可CSRF対策が必要

セキュリティを重視する場合は HttpOnly + Secure + SameSite=Strict の Cookie に保存するのが推奨される。

リフレッシュトークン

アクセストークンの有効期限を短く(15〜60分)設定し、長期間有効なリフレッシュトークンで再発行する構成が一般的。

1. ログイン → アクセストークン(15分)+ リフレッシュトークン(7日)を発行
2. APIアクセス時 → アクセストークンをAuthorizationヘッダーに付与
3. アクセストークン期限切れ → リフレッシュトークンで新しいアクセストークンを取得
4. リフレッシュトークン期限切れ → 再ログイン

セキュリティ上の注意点

alg: none 攻撃

alg フィールドを none にすることで署名検証をスキップさせる攻撃。ライブラリによっては脆弱性があった。

対策: 検証時に許可するアルゴリズムを明示的に指定する。

jwt.verify(token, SECRET, { algorithms: ['HS256'] })

機密情報の格納

ペイロードはBase64URLで可逆的にデコードできる。パスワード、クレジットカード番号などは格納しない。

有効期限の設定

無期限のJWTは一度漏洩すると永遠に悪用される。exp クレームは必ず設定する。

トークンの無効化

JWTはステートレスなため、サーバー側で発行済みトークンを一方的に無効化できない。ログアウト時の無効化には次のいずれかが必要。

  • ブラックリスト: 無効にしたい jti をDBに保存して検証時にチェック
  • 有効期限を短くする: 漏洩しても被害期間を最小化
  • リフレッシュトークンの破棄: リフレッシュトークンを無効化して更新不可にする

アルゴリズムの強度

HS256 の秘密鍵は十分な長さ(256bit以上)のランダム文字列を使用する。短い文字列や推測可能な文字列は総当たり攻撃に弱い。

JWTとセッションの比較

項目JWTセッション
状態管理ステートレス(サーバー不要)ステートフル(サーバーにセッション保存)
スケーラビリティ高い(DBアクセス不要)サーバー間でセッション共有が必要
無効化困難(有効期限まで有効)即時無効化が容易
サイズ大きい(数百バイト)小さい(セッションIDのみ)
向いている用途マイクロサービス、API間認証従来のWebアプリ

参考リンク