JWT認証とは?
JSON Web Token(JWT、「ジョット」と発音)は、デジタル署名されたトークンとして認証・認可情報を当事者間で送信するためのコンパクトで自己完結型の規格です。ユーザーがログインすると、サーバーはそのユーザーに関するクレームを含むJWTを発行します。クライアントはトークンを保存し、以降のすべてのリクエストで送信します。サーバーはトークンの署名を検証して真正性を確認し、データベースの参照は不要です。
JWTはRFC 7519で定義されており、REST API、シングルページアプリケーション、マイクロサービスアーキテクチャにおける主要な認証メカニズムです。設計上ステートレスであり、サーバーはセッションデータを保存する必要がないため、水平スケーリングが容易です。
JWTの仕組み
JWT認証の完全なフローは以下の通りです:
- ログイン:ユーザーがサーバーのログインエンドポイントにクレデンシャル(ユーザー名+パスワード)を送信します。
- トークン発行:サーバーがクレデンシャルをデータベースと照合して検証します。成功すると、ユーザーID、ロール、有効期限タイムスタンプを含むJWTに署名し、クライアントに返します。
- トークン保存:クライアントがトークンを保存します(メモリ、localStorage、またはhttpOnly Cookie — 詳細は後述)。
- 認証済みリクエスト:以降のすべてのAPIリクエストで、クライアントはJWTを
Authorizationヘッダーに含めます:Authorization: Bearer <token>。 - 検証:サーバーがヘッダーからトークンを抽出し、秘密鍵で署名を検証し、有効期限を確認し、クレームを読み取ります — すべてデータベースクエリなしで。
- レスポンス:トークンが有効であればリクエストを処理します。無効(期限切れ、改ざん、欠落)の場合は
401 Unauthorizedを返します。
JWTの構造
JWTはドットで区切られた3つのBase64URLエンコード部分で構成されます:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDkzMDAwMDAsImV4cCI6MTcwOTMwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
3つの部分は:
1. ヘッダー
{
"alg": "HS256", // Signing algorithm (HS256, RS256, ES256)
"typ": "JWT" // Token type
}
2. ペイロード(クレーム)
{
"userId": "123456",
"email": "user@example.com",
"role": "admin",
"iat": 1709300000, // Issued at (Unix timestamp)
"exp": 1709303600 // Expiry (1 hour from issuance)
}
標準クレーム名:iss(発行者)、sub(主体)、aud(対象者)、exp(有効期限)、iat(発行日時)、jti(JWT ID、失効用)。ペイロードはBase64URLエンコードされていますが、暗号化されていません — 誰でもデコードできます。パスワード、クレジットカード番号、その他の機密データをペイロードに保存しないでください。
3. 署名
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
署名はトークンがサーバーによって発行され、改ざんされていないことを証明します。ヘッダーまたはペイロードの文字が1つでも変わると、署名検証は失敗します。
Node.jsでのJWT実装
jsonwebtokenパッケージをインストール:
npm install jsonwebtoken bcryptjs express
ログインと保護されたルートの完全な例:
// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const express = require('express');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET; // Must be a long, random string
const JWT_EXPIRES_IN = '1h'; // Keep access tokens short-lived
// Login route — issue a JWT on successful authentication
router.post('/login', async (req, res) => {
const { email, password } = req.body;
// 1. Look up the user in your database
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 2. Compare password with stored hash
const validPassword = await bcrypt.compare(password, user.passwordHash);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 3. Sign the JWT — include only non-sensitive claims
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN, algorithm: 'HS256' }
);
res.json({ token, expiresIn: 3600 });
});
// Middleware to verify JWT on protected routes
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
// Explicitly specify the algorithm to prevent algorithm confusion attacks
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Protected route example
router.get('/profile', authenticateToken, (req, res) => {
res.json({ userId: req.user.userId, email: req.user.email });
});
module.exports = { router, authenticateToken };
ReactでのJWT保存
ブラウザでJWTを保存する場所は、アプリケーションのセキュリティに大きく影響します。主な選択肢はlocalStorageとhttpOnly Cookieです:
| プロパティ | localStorage | httpOnly Cookie |
|---|---|---|
| XSS脆弱性 | 高 — スクリプトから読み取り可能 | 低 — JavaScriptからアクセス不可 |
| CSRF脆弱性 | なし | あり — SameSite + CSRFトークンで軽減 |
| 自動送信 | いいえ — ヘッダーに手動追加が必要 | はい — ブラウザがすべてのリクエストで送信 |
| JSからアクセス可能 | はい | いいえ(httpOnlyフラグ) |
| 推奨用途 | 低リスクアプリ、公開データ | ユーザーデータを扱う本番アプリ |
本番アプリケーションではhttpOnly Cookieを推奨します。サーバーがログイン時にCookieを設定し、ブラウザが自動的に送信するため、JavaScriptがトークン値に触れることはありません。
localStorageを使用する場合(開発中の簡便性など)、可能な限りメモリ内にトークンを保存し、XSSリスクを軽減するための堅牢なContent Security Policyを実装してください:
// React — sending JWT from localStorage
async function fetchProfile() {
const token = localStorage.getItem('authToken');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
// Token expired — redirect to login or refresh
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return response.json();
}
リフレッシュトークン戦略
短寿命のアクセストークン(15分〜1時間)はトークン盗難のリスクを最小限にしますが、1時間ごとにログインを要求するのはUXとして最悪です。解決策はリフレッシュトークンペアです:
- アクセストークン:短寿命(15分〜1時間)、すべてのAPIリクエストで
Authorizationヘッダーに含めて送信。メモリまたはlocalStorageに保存。 - リフレッシュトークン:長寿命(7〜30日)、新しいアクセストークンの取得にのみ使用。常にhttpOnly、Secure、SameSite=StrictのCookieに保存。使用ごとにローテーション推奨。
// Server: issue both tokens on login
router.post('/login', async (req, res) => {
// ... validate credentials ...
const accessToken = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);
// Store refresh token hash in database for revocation support
await RefreshToken.create({ userId: user._id, token: hashToken(refreshToken) });
// Send refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days in ms
});
res.json({ accessToken });
});
// Server: refresh endpoint
router.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Optionally: check token hash exists in DB (enables revocation)
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch {
res.status(403).json({ error: 'Invalid refresh token' });
}
});
JWTセキュリティのベストプラクティス
- アクセストークンは短寿命に:15分〜1時間。盗まれたトークンは期限切れ後に無効になります。
- 強力なシークレットを使用:
JWT_SECRETは最低256ビットのランダムデータにすべきです。openssl rand -hex 32で生成してください。ハードコードは厳禁です。 - 常にアルゴリズムを指定:
jwt.verify()に{ algorithms: ['HS256'] }を渡してください。これにより、攻撃者がヘッダーに"alg": "none"を設定するアルゴリズム混乱攻撃を防止できます。 - 本番環境ではHTTPSのみ:通信中のJWTのセキュリティはトランスポート層に依存します。本番環境では常にTLSを使用してください。
- リフレッシュトークンはhttpOnly Cookieに保存:localStorageやsessionStorageには保存しないでください。
- トークン失効を実装:ログアウトやセキュリティイベント用に、短寿命のブロックリスト(Redisが最適)を管理するか、データベーストラッキング付きのローテーションリフレッシュトークンを使用してください。
- すべてのクレームを検証:すべてのリクエストで
exp(有効期限)、iss(発行者)、aud(対象者)を確認してください。jsonwebtokenライブラリは適切なオプションを渡せば自動的に検証します。
よくあるJWTの間違い
高リスクアプリでlocalStorageにJWTを保存
XSS脆弱性が1つでもあれば — サードパーティスクリプトも含めて — localStorageを読み取り全トークンを窃取できます。決済、個人データ、管理者操作を扱うアプリケーションでは、httpOnly Cookieのみを使用してください。
トークンに有効期限がない
expクレームのないトークンは永久に有効です。漏洩したトークンは永続的なバックドアとなります。署名時には常にexpiresInを設定してください。
algorithm none攻撃
初期のJWTライブラリはヘッダーの"alg": "none"を受け入れ、未署名のトークンを有効として扱っていました。jwt.verify()には常にalgorithmsオプションを渡して、許可するアルゴリズムを制限してください:
// Always do this — never allow the "none" algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });
ペイロードに機密データ
JWTペイロードはBase64URLエンコードされていますが、暗号化されていません。トークンを持つ誰でも、シークレットを知らなくてもペイロードをデコードして読むことができます。パスワード、SSN、クレジットカード番号、内部システム情報をJWTクレームに含めないでください。
アクセストークンとリフレッシュトークンに同じシークレットを使用
両方のトークンタイプに同じシークレットを使用すると、漏洩したアクセストークンがリフレッシュトークンとして使用される可能性があります。各トークンタイプに別々のシークレットを使用し、別々の環境変数に保存してください。
JWTトークンをデコードする
JWTのクレーム、有効期限、構造を確認する必要がありますか?devbit.devのJWTデコーダーにペーストすれば、ヘッダーとペイロードを即座にデコードし、有効期限を確認し、トークン形式を検証できます — すべてブラウザ内で処理され、データはサーバーに送信されません。
あらゆるJWTを瞬時にデコード & 検証
JWTトークンをペーストしてヘッダー、ペイロード、クレームをデコード。有効期限、アルゴリズム、構造を確認。100%クライアントサイド — トークンはブラウザから出ません。
JWTデコーダーを開く →関連開発者ツール
- JWTデコーダー & インスペクター — JSON Web Tokenを即座にデコードし、有効期限とクレームを確認
- CORSエラーデバッガー — Express、NginxでのAuthorizationヘッダーのCORSエラーを修正
- ハッシュジェネレーター — パスワードやトークン用のSHA-256、MD5、bcryptハッシュを生成
- HTTPステータスコード — 401 Unauthorizedと403 Forbiddenなど認証関連コードを理解
- ENV インスペクター — JWT_SECRETなどの環境変数を.envファイルで検証
- 全ての無料開発者ツールを見る