JWT Tokens Explained: Structure, Security & Decoding
Understand JSON Web Tokens from the inside out. Learn the header, payload, and signature structure, common claims, signing algorithms, and security best practices.
JWTとは何か
JSON Web Token(JWT、発音は "jot"〔ジョット〕)は、当事者間のクレームを表すコンパクトでURLセーフな文字列です。現代のWebアプリケーションにおける認証と認可を扱う際、最も一般的な方式のひとつです。
Webアプリにログインしたとき、サーバーがサーバーサイドのセッションを作らずにトークンを返す場合、そのトークンがJWTであることが多いです。ブラウザは以降のリクエストごとにそれを送り、サーバーはデータベースに問い合わせずに署名を検証できます。
JWTが使われる場所
- 認証。 ユーザーがログインすると、サーバーがJWTを発行します。クライアントはそれを保持し、リクエストごとに
Authorizationヘッダーで送ります。 - シングルサインオン(SSO)。 JWTにより、ユーザーは一度認証すれば複数サービスにアクセスできます。Auth0、Okta、Keycloakなどのアイデンティティプロバイダーが発行するJWTは、下流のサービスがそれぞれ独立して検証できます。
- APIの認可。 マイクロサービス同士がJWTを受け渡し、認証済みユーザーに代わって、特定の権限を伴うリクエストであったことを示します。
- 情報の受け渡し。 JWTは署名されているため、追加の検証手順なしに当事者間で信頼できるデータを運べます。
JWTの3つの構成要素
JWTは、ドットで区切られた3つのBase64URLエンコード部分からなります。
header.payload.signature
次の例は実際のJWTです(読みやすさのため省略しています)。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
各部分をデコードして確認します。
第1部: ヘッダー
ヘッダーはトークンの種類と署名アルゴリズムを表します。Base64URLをデコードすると次のとおりです。
{
"alg": "HS256",
"typ": "JWT"
}
alg は検証時に使うアルゴリズムを示し、typ はこれがJWTであること(JWEなど別種のトークンではないこと)を示します。
第2部: ペイロード(クレーム)
ペイロードにはクレーム、すなわちトークンが運ぶ実データが入ります。デコードすると次のとおりです。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
クレームはキーと値のペアです。標準化されたもの(登録クレーム)もあれば、必要に応じてカスタムクレームも追加できます。
ペイロードはBase64URLエンコードされているだけで、暗号化されていません。トークンを入手した者は誰でもデコードして内容を読めます。秘密情報、パスワード、機微な個人データをJWTのペイロードに入れないでください。
第3部: 署名
署名は、エンコード済みヘッダー、ドット、エンコード済みペイロードをつなげたものを秘密鍵で署名して計算します。
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
署名により、トークンが改ざんされていないことが保証されます。ヘッダーやペイロードの1文字でも変えると署名が一致せず、検証側はトークンを拒否します。
試してみる: 任意のJWTを JWT Decoder に貼り付けると、3つの部分をすぐに確認できます。
よく使われるJWTクレーム
JWTの仕様(RFC 7519)では、登録クレームの集合が定義されています。いずれも必須ではありませんが、広く使われています。
| クレーム | 名称 | 説明 |
|---|---|---|
sub | Subject | トークンの主体(多くはユーザーID) |
iss | Issuer | 発行者(例: 認証サーバーのURL) |
aud | Audience | 想定される受信者(例: APIのURL) |
exp | Expiration Time | このUnix時刻を過ぎるとトークンは無効 |
iat | Issued At | トークンが発行されたUnix時刻 |
nbf | Not Before | このUnix時刻より前はトークンは無効 |
jti | JWT ID | トークンの再利用を防ぐ一意の識別子 |
カスタムクレームは、これ以外に自分で追加する任意の項目です。role、permissions、email、org_id などがよく使われます。カスタムクレームは必要最小限にしてください。バイトが増えるほど、リクエストごとの転送量も増えます。
署名アルゴリズムの比較
アルゴリズムの選び方で、署名の生成と検証の方法が決まります。代表的な3つは次のとおりです。
| アルゴリズム | 種類 | 鍵 | 向いている用途 |
|---|---|---|---|
HS256 | 対称(HMAC) | 共有秘密鍵1つ | 発行者と検証者が同一サービスであるシンプルな構成 |
RS256 | 非対称(RSA) | 秘密鍵で署名、公開鍵で検証 | 分散システムやSSO。検証側は秘密鍵を不要 |
ES256 | 非対称(ECDSA) | 秘密鍵で署名、公開鍵で検証 | RS256と同様だが鍵が小さく、処理も速い傾向 |
対称方式(HS256) は設定が簡単で、発行者と検証者が同じ秘密を共有します。一方、検証に関わるすべてのサービスがその秘密にアクセスする必要があり、攻撃面が広がります。
非対称方式(RS256、ES256) は分散構成に適しています。認証サーバーが秘密鍵で署名し、各サービスは対応する公開鍵で検証できます。公開鍵は配布しても問題ありません。ES256は、RS256と同等の強度をより小さなトークンと高速な検証で実現できる、現代的な選択肢です。
セキュリティのベストプラクティス
JWTは強力ですが、誤用も起きやすい仕組みです。次の点を守ってください。
- ペイロードに秘密を入れない。 ペイロードはエンコードされているだけで暗号化されていないため、誰でもデコードできます。暗号化が必要なら、代わりにJWE(JSON Web Encryption)を検討してください。
- 有効期限を短くする。 アクセストークンは5〜15分程度で失効させ、長期セッションはリフレッシュトークンで支えます。短命にすれば、漏えい時の被害を抑えられます。
- 必ずHTTPSを使う。 HTTP上のJWTは転送路で盗聴されるおそれがあります。HTTPSは譲れない前提です。
- クレームをすべて検証する。
exp、iss、audは必ず確認してください。署名だけを検証して、期限切れや宛先違いのトークンを通さないでください。 alg: noneを受け入れない。 一部のライブラリは、ヘッダーが"alg": "none"のとき署名なしトークンを受け入れます。検証設定でこのアルゴリズムを明示的に拒否してください。- アルゴリズムを許可リストで制限する。 想定しているアルゴリズム(例: RS256のみ)だけを受け入れるよう検証器を設定し、アルゴリズム混同攻撃を防ぎます。
- トークンの保管を安全にする。
HttpOnlyクッキーはJavaScriptから読めないため、localStorageよりXSS対策の観点で有利です。localStorageを使う場合は、そのトレードオフを理解したうえで選んでください。
よくあるJWTの誤り
セキュリティ監査で繰り返し見られるパターンです。
- JWTをURLに載せる。 クエリ文字列はサーバーログ、ブラウザ履歴、Refererヘッダーなどに残ります。
Authorizationヘッダーやクッキーを使ってください。 - 署名検証を省略する。 Base64でデコードするだけで検証を飛ばすと、攻撃者は任意のトークンを偽造できます。
- HS256に弱い秘密を使う。
"secret"や"password123"のような共有秘密は、短時間で総当たりされる可能性があります。少なくとも256ビットの暗号論的乱数から作った文字列を使ってください。 - 失効しないトークン。
expがなければトークンは事実上ずっと有効です。漏えいしたあと、署名鍵を回転させない限り失効できません(鍵を変えると全トークンが無効になります)。 - データを詰め込みすぎる。 JWTはHTTPリクエストごとに送られます。4KBのトークンはAPI呼び出しのたびにオーバーヘッドになります。ペイロードは軽く保ってください。
トークンリフレッシュの流れ
短命のアクセストークンはすぐ期限切れになるため、ユーザーに再ログインさせずに新しいトークンを得る仕組みが必要です。それがリフレッシュトークンフローです。
- ユーザーがログインすると、サーバーはアクセストークン(短命、例: 15分)とリフレッシュトークン(長命、例: 7日間)を返します。
- クライアントはAPIリクエストごとに
Authorization: Bearerヘッダーでアクセストークンを送ります。 - アクセストークンが期限切れになると、APIは
401 Unauthorizedを返します。 - クライアントはリフレッシュトークンを専用の
/refreshエンドポイントに送ります。 - サーバーはリフレッシュトークンを検証し、新しいアクセストークン(必要なら新しいリフレッシュトークンも — リフレッシュトークンのローテーション)を発行して返します。
- クライアントは新しいアクセストークンで元のリクエストをやり直します。
リフレッシュトークンのローテーションは推奨される対策です。各リフレッシュトークンは一度しか使えません。攻撃者が盗んだトークンと、正当ユーザーの利用が競合すると、サーバーは再利用を検知してセッション全体を無効化できます。
// Simplified refresh flow (client-side)
const handleApiRequest = async (url, options) => {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${getAccessToken()}`,
},
});
if (response.status === 401) {
const refreshResponse = await fetch("/api/refresh", {
method: "POST",
body: JSON.stringify({ refresh_token: getRefreshToken() }),
});
if (refreshResponse.ok) {
const { access_token, refresh_token } = await refreshResponse.json();
saveTokens(access_token, refresh_token);
response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${access_token}`,
},
});
} else {
redirectToLogin();
}
}
return response;
};
JWTとセッション: どちらを使うか
Web開発でもっとも議論の多い論点のひとつです。どちらにも妥当な用途があります。
| 観点 | JWT | サーバーサイドセッション |
|---|---|---|
| 状態の置き場所 | クライアント(サーバーはステートレス) | サーバー(セッションストア/データベース) |
| スケーラビリティ | 容易 — サーバー間で共有状態が不要 | 共有セッションストア(Redis、DBなど)が必要 |
| 失効 | 難しい — ブロックリストか短い有効期限が必要 | 容易 — ストアからセッションを削除すればよい |
| ペイロードサイズ | 大きい(リクエストごとにクレームを運ぶ) | 小さい(セッションIDのクッキーのみ) |
| クロスドメイン | Bearerトークンとして送るため相性がよい | CORSとクッキー設定が必要 |
| マイクロサービス | 向いている — 各サービスが独立して検証できる | 各サービスがセッションストアを参照する必要がある |
| 実装の単純さ | 署名やリフレッシュなど要素が多い | 正しく実装するには比較的単純 |
JWT向きなのは、分散サービス、モバイルアプリ、サードパーティのAPI利用者など、サーバー側をステートレスに保ちたい認証の場面です。
セッション向きなのは、失効を簡単にしたい場合(例: 「すべての端末からログアウト」)、単一のモノリシックアプリである場合、従来型Webアプリで実装を単純に保ちたい場合です。
試してみる: JWT Decoder なら、任意のJWTをすぐにデコードして内容を確認できます。データはブラウザの外に送られません。