JWT認証:JSON Web Tokenの仕組み
· 12分で読む
目次
JSON Web Tokenとは?
JSON Web Token(JWT)は、2つの当事者間でクレームを表現するためのコンパクトでURLセーフな方法です。JWTは、現代のWebアプリケーション、API、マイクロサービスアーキテクチャにおける認証と認可の事実上の標準となっています。
ユーザーがログインすると、サーバーはユーザー情報を含むJWTを生成し、クライアントに送信します。その後、クライアントは後続のリクエストにこのトークンを含めて、自分の身元を証明します。これは、あなたの資格情報を含み、毎回発行機関に問い合わせることなく検証できるデジタルパスポートのようなものと考えてください。
サーバーがメモリやデータベースにセッション状態を保持する従来のセッションベース認証とは異なり、JWTはステートレスです。トークンを検証するために必要なすべての情報は、トークン自体に含まれています。このアーキテクチャ上の決定には、いくつかの利点があります:
- スケーラビリティ:ロードバランス環境でセッションストレージやスティッキーセッションが不要
- クロスドメイン認証:JWTは異なるドメインやサービス間でシームレスに機能
- モバイルフレンドリー:バックエンドAPIで認証する必要があるモバイルアプリに最適
- マイクロサービス:各サービスは中央セッションストアなしで独立してトークンを検証可能
- パフォーマンス:認証されたリクエストごとのデータベース検索を排除
コードを書かなくても、JWTデコーダーツールを使用して任意のJWTを検査およびデコードし、その内容を確認できます。これは、認証の問題をデバッグしたり、トークンに含まれるデータを理解したりする際に特に便利です。
プロのヒント:JWTはデフォルトで署名されていますが、暗号化されていません。誰でもJWTをデコードして内容を読むことができます。パスワード、クレジットカード番号、社会保障番号などの機密情報をJWTペイロードに保存しないでください。
JWT構造の解説
JWTは、ドット(.)で区切られた3つの異なる部分で構成されています:ヘッダー、ペイロード、署名です。実際のJWTは次のようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
各コンポーネントを分解して、安全で検証可能なトークンを作成するためにどのように連携するかを理解しましょう。
1. ヘッダー
ヘッダーには通常、トークンの処理方法を定義する2つの重要なフィールドが含まれています:
{
"alg": "HS256",
"typ": "JWT"
}
alg(アルゴリズム):トークンの署名に使用される暗号化アルゴリズムを指定typ(タイプ):これがJWTトークンであることを宣言
このJSONオブジェクトは、JWTの最初の部分を形成するためにBase64Urlエンコードされます。エンコーディングにより、HTTPヘッダーでの送信に適したURLセーフでコンパクトな形式になります。
2. ペイロード
ペイロードにはクレーム(ユーザーに関する記述と追加のメタデータ)が含まれます。これは、送信したい実際のデータを保存する場所です:
{
"sub": "1234567890",
"name": "John Doe",
"email": "[email protected]",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
クレームは3つのカテゴリに分類されます:
登録済みクレーム(標準化され推奨される):
sub(サブジェクト):ユーザーの一意の識別子iat(発行時刻):トークンが作成されたタイムスタンプexp(有効期限):トークンが期限切れになるタイムスタンプiss(発行者):トークンを発行した者を識別aud(オーディエンス):トークンの対象者を識別nbf(有効開始時刻):このタイムスタンプより前はトークンが無効jti(JWT ID):トークン自体の一意の識別子
パブリッククレームは、IANA JSON Web Tokenレジストリで定義されるか、衝突耐性のある名前(URLなど)を使用する必要があるカスタムクレームです。
プライベートクレームは、role、permissions、departmentなどの当事者間で合意されたカスタムクレームです。
3. 署名
署名は、JWTを安全で改ざん防止にするものです。エンコードされたヘッダー、エンコードされたペイロード、秘密鍵を取り、ヘッダーで指定されたアルゴリズムを適用することで作成されます:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
署名は2つの重要な目的を果たします:
- 整合性検証:トークンが発行されてから変更されていないことを保証
- 認証:トークンが秘密鍵にアクセスできる人によって作成されたことを証明
誰かがヘッダーやペイロードを変更しようとすると、署名検証が失敗し、トークンは拒否されます。
JWT認証の仕組み
JWTを正しく実装するには、完全な認証フローを理解することが不可欠です。ステップバイステップのプロセスは次のとおりです:
ステップ1:ユーザーログイン
ユーザーは認証エンドポイントに資格情報(ユーザー名とパスワード)を送信します。サーバーはこれらの資格情報をデータベースに対して検証します。
ステップ2:トークン生成
資格情報が有効な場合、サーバーはユーザー情報とクレームを含むJWTを生成します。サーバーは秘密鍵(対称アルゴリズムの場合)または秘密鍵(非対称アルゴリズムの場合)を使用してトークンに署名します。
ステップ3:トークン配信
サーバーはJWTをクライアントに返送します。通常、レスポンスボディに含まれます。クライアントは将来のリクエストのためにこのトークンを保存します。
ステップ4:認証されたリクエスト
後続のリクエストでは、クライアントはBearerスキーマを使用してAuthorizationヘッダーにJWTを含めます:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
ステップ5:トークン検証
サーバーはリクエストを受信し、ヘッダーからJWTを抽出し、秘密鍵または公開鍵を使用して署名を検証します。有効な場合、サーバーはペイロードからユーザー情報を抽出し、リクエストを処理します。
ステップ6:トークンの有効期限
トークンが期限切れになると、クライアントは再認証するか、リフレッシュトークンを使用して(次のセクションで説明)新しいトークンを取得する必要があります。
クイックヒント:クライアント側で有効期限をチェックしても、サーバー側で常にexpクレームを検証してください。クライアント側のチェックはバイパスできますが、サーバー側の検証は権威があります。
アクセストークンとリフレッシュトークン
堅牢なJWT認証システムは通常、連携する2種類のトークンを使用します:アクセストークンとリフレッシュトークンです。この違いを理解することは、安全なアプリケーションを構築するために重要です。
アクセストークン
アクセストークンは、保護されたリソースへのアクセスを許可する短命のJWT(通常15分から1時間)です。リクエストを認可するために必要なユーザーIDと権限が含まれています。
特徴:
- 短い有効期限(5〜60分)
- すべてのAPIリクエストに含まれる
- ユーザーの権限とロールを含む
- 有効期限前に取り消すことができない(ステートレス)
リフレッシュトークン
リフレッシュトークンは、新しいアクセストークンを取得するためだけに使用される長命のトークン(数日から数ヶ月)です。安全に保存され、トークンリフレッシュエンドポイントにのみ送信されます。
特徴:
- 長い有効期限(数日から数ヶ月)
- リフレッシュエンドポイントにのみ送信
- データベースで取り消すことができる
- 追加のメタデータ(デバイス、IP、ユーザーエージェント)と共に保存されることが多い
なぜ両方を使用するのか?
この二重トークンアプローチは、セキュリティとユーザーエクスペリエンスのバランスを取ります:
| 側面 | アクセストークンのみ | アクセス+リフレッシュトークン |
|---|---|---|
| セキュリティ | 低い(長命トークンがリスク) | 高い(短命アクセストークン) |
| ユーザーエクスペリエンス | 良い(再認証不要) | 良い(シームレスなトークンリフレッシュ) |
| 取り消し | 困難(ステートレス) | 可能(DBのリフレッシュトークン) |
| ネットワークオーバーヘッド | 低い | やや高い(リフレッシュリクエスト) |
| 実装の複雑さ | シンプル | 中程度 |
トークンリフレッシュフロー
- クライアントはアクセストークンが期限切れまたは期限切れ間近であることを検出
- クライアントは
/auth/refreshエンドポイントにリフレッシュトークンを送信 - サーバーはデータベースに対してリフレッシュトークンを検証
- サーバーは新しいアクセストークン(およびオプションで新しいリフレッシュトークン)を生成
- クライアントは新しいトークンを受信して保存
- クライアントは新しいアクセストークンで元のリクエストを再試行
プロのヒント:リフレッシュトークンのローテーションを実装してください — 使用されるたびに新しいリフレッシュトークンを発行し、古いものを無効化します。これにより、リフレッシュトークンが侵害された場合の被害を制限できます。
Node.js実装ガイド
Node.js、Express、jsonwebtokenライブラリを使用して、完全なJWT認証システムを構築しましょう。この実装には、アクセストークンとリフレッシュトークンの両方が含まれます。
インストール
npm install express jsonwebtoken bcrypt dotenv
環境設定
秘密鍵を含む.envファイルを作成します:
ACCESS_TOKEN_SECRET=your-super-secret-access-key-change-this
REFRESH_TOKEN_SECRET=your-super-secret-refresh-key-change-this
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d
トークン生成
const jwt = require('jsonwebtoken');
function generateAccessToken(user) {
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: process.env.ACCESS_TOKEN_EXPIRY }
);
}
function generateRefreshToken(user) {
return jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: process.env.REFRESH_TOKEN_EXPIRY }
);
}
ログインエンドポイント
const bcrypt = require('bcrypt');
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// データベースでユーザーを検索
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: '無効な資格情報' });
}
// パスワードを検証
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: '無効な資格情報' });
}
// トークンを生成
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// データベースにリフレッシュトークンを保存
await RefreshToken.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
name: user.name
}
});
} catch (error) {
res.status(500).json({ error: 'サーバーエラー' });
}
});
認証ミドルウェア
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'アクセストークンが必要です' });
}
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: '無効または期限切れのトークン' });
}
req.user = user;
next();
});
}
// 使用方法
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: '保護されたデータ',
user: req.user
});
});
トークンリフレッシュエンドポイント
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'リフレッシュトークンが必要です' });
}
try {
// データベースにリフレッシュトークンが存在することを確認
const storedToken = await RefreshToken.findOne({ token: refreshToken });
if (!storedToken) {
return res.status(403).json({ error: '無効なリフレッシュトークン' });
}
// トークン署名を検証
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, async (err, user) => {
if (err) {
return res.status(403).json({ error: '無効なリフレッシュトークン' });
}
// ユーザーデータを取得
const userData = await User.findById(user.userId);
// 新しいアクセストークンを生成
const accessToken = generateAccessToken(userData);
res.json({ accessToken });