JWT認証とは?仕組み・保存方法・安全な検証を図解

公開 更新 約6分で読む著者:admin

JWTは、ユーザーIDや有効期限などの情報を受け渡すためのトークン形式です。ログイン機能を丸ごと提供する仕組みではなく、署名の検証だけで、あらゆる操作を許可してよいわけでもありません。この記事では、よく使われる署名付きJWTを中心に、構造・保存・検証を順番に整理します。

更新:2026年10月24日。旧版の固定秘密鍵、トークン保存方式の不整合、認証と認可の説明を改めました。

JWTの3つの部分:中身を隠すものではない

ヘッダー
方式・トークンの種類
ペイロード
ユーザーID・期限・宛先
署名/MAC
改ざんを検出する値
図1:一般的なJWS Compact形式は、3つの部分をドットでつなぐ。暗号化するJWEは別の構造を持ちます。

ヘッダーとペイロードはBase64urlで表現されます。これは暗号化ではないため、トークンを受け取った人は内容を読めます。パスワード、秘密鍵、不要な個人情報は入れません。署名は内容を書き換えられたか確認する仕組みであり、内容を読めなくする仕組みではありません。JWTの仕様はRFC 7519に定められています。

ログインからAPI利用までの流れ

  1. 本人確認:認証サーバーがログイン情報を確認する。
  2. 発行:用途・利用先・期限を限定したトークンを発行する。
  3. 提示:クライアントがAPIへのリクエストに添える。
  4. 検証:APIが署名、発行者、宛先、期限、用途を確認する。
  5. 権限確認:その利用者が対象データを操作できるか判定する。
図2:「有効なトークン」と「操作する権限がある」は別の確認です。

例えば、ユーザーAの正しいトークンでも、ユーザーBの非公開データを読めるわけではありません。検証済みのユーザーIDとデータの所有者を照合するなど、API側の認可処理が必要です。発行者や利用先の確認、種類の異なるトークンを混用しない設計はRFC 8725でも重視されています。

CookieとlocalStorageは何が違う?

ブラウザでの保存方式と注意点
方式特徴必要な対策
HttpOnly CookieJavaScriptから値を読み取れない。対象の通信にブラウザが自動添付する。Secure、適切なSameSite、CSRF対策、XSS対策。
localStorageJavaScriptから読め、ブラウザを閉じても残る。XSSでトークンが流出し得る。長期トークンの安易な保存を避ける。
メモリ内ページを再読み込みすると通常は消える。更新方法が別途必要。実行中の悪意あるスクリプトへの万能な防御ではない。

単一のWebサービスなら、ブラウザにはHttpOnly CookieのセッションIDだけを渡し、API用トークンはサーバー側で扱う構成も検討できます。JWTを使うこと自体を目的にせず、必要な機能から選びます。HttpOnlyでもXSSによる不正な操作まで防げるわけではありません。保存先と攻撃対策の詳細はOWASPのHTML5 SecurityとSession Managementを参照してください。

Node.jsで発行と検証を学ぶ最小例

次は署名と検証の学習用です。ユーザー登録、パスワード確認、Cookie、更新トークンを備えた認証サーバーではありません。Node.js 22系以降とjsonwebtoken 9系を想定します。実サービスの鍵やトークンを、この例や外部のデコードサイトへ貼り付けないでください。

BASH
mkdir jwt-basics
cd jwt-basics
npm init -y
npm install jsonwebtoken@9
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"

最後のコマンドが出力する64桁の16進文字列を、ローカルの.envにJWT_SECRET_HEX=生成した文字列として保存します。.gitignoreには.envを追加します。32バイトの乱数は256ビットです。鍵を短い単語や公開済みの例文で代用しません。

demo.cjsを作成します。

JAVASCRIPT
const jwt = require('jsonwebtoken');
const assert = require('node:assert/strict');

const hex = process.env.JWT_SECRET_HEX;
if (typeof hex !== 'string' || !/^[0-9a-f]{64}$/i.test(hex)) {
  throw new Error('JWT_SECRET_HEXに32バイトのランダムな鍵を設定してください');
}
const key = Buffer.from(hex, 'hex');
const options = {
  algorithms: ['HS256'],
  issuer: 'demo-auth',
  audience: 'demo-api'
};

// 実サービスでは、本人確認が成功した後だけ発行する。
const token = jwt.sign({ purpose: 'access' }, key, {
  algorithm: 'HS256',
  subject: 'demo-user',
  issuer: 'demo-auth',
  audience: 'demo-api',
  expiresIn: '5m'
});

function verifyAccessToken(value) {
  const claims = jwt.verify(value, key, options);
  if (typeof claims !== 'object' || claims === null ||
      typeof claims.sub !== 'string' || !claims.sub ||
      typeof claims.exp !== 'number' || claims.purpose !== 'access') {
    throw new Error('必要なクレームがありません');
  }
  return claims;
}

assert.equal(verifyAccessToken(token).sub, 'demo-user');
assert.throws(() => jwt.verify(token, key, {
  ...options, audience: 'another-api'
}));
console.log('本人の識別情報を検証でき、異なる宛先は拒否できました');
BASH
node --env-file=.env demo.cjs

実装の要点は、鍵が未設定なら止めること、検証側でアルゴリズムを固定すること、発行者・宛先を照合することです。jwt.decode()で中身を読むだけでは検証になりません。APIの使い方はjsonwebtokenの開発元ドキュメントに対応させています。実行環境や依存関係による差は、導入時に確認してください。

JWTとセッションは、どちらがよい?

判断軸サーバー側セッション署名付きJWT
ログアウト・即時失効保存しているセッションを無効にできる。期限切れを待つだけでは即時失効しない。失効管理などを別途設計する。
複数サーバー共通の保存先などを用意する。信頼する鍵・発行者・宛先・検証規則を共有する。
ブラウザ中心の小規模サービス必要な機能を満たせることが多い。運用の複雑さに見合う必要性があるか検討する。

JWTでも、更新トークンの管理、鍵の更新、ユーザーの停止状態の確認などでサーバー側の状態を持ちます。「JWTなら保存領域が不要」「ユーザーが増えても無制限」とは言えません。

本番導入前に確認すること

HTTPS、秘密情報の管理、適切な期限、失効・鍵更新、ログイン試行の制限、パスワードの安全なハッシュ化、データ単位の認可、ログの秘匿を確認します。Cookieを使う場合はCSRF対策も必要です。リフレッシュトークンはアクセストークンと区別して検証し、漏えい時の失効と再利用検知を設計します。短い有効期限だけで安全が保証されるわけではありません。

まず「本人確認」「トークン検証」「権限確認」を分けて考えること。この区別ができると、JWTを採用すべき場面と、既存の認証基盤に任せた方がよい場面を判断しやすくなります。

\ 最新情報をチェック /

記事一覧に戻る

コメントを書く

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です