Bearer認証(JWT)仕様まとめ

Table of Contents

概要

本APIは、Bearer認証(JWT)を採用しています。すべてのAPIリクエストには、有効なJWTトークンを含むAuthorizationヘッダーが必要です。

認証方式

Bearer認証(JWT)

ヘッダー形式:

Authorization: Bearer <JWT_TOKEN>

例:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTYxNjIzOTAyMiwiZXhwIjoxNjE2MjQyNjIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWTトークンの構造

JWTトークンは、以下の3つの部分で構成されています:

<Header>.<Payload>.<Signature>

1. Header(ヘッダー)

トークンのタイプと署名アルゴリズムを含みます。

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg:署名アルゴリズム(HS256、RS256等)
  • typ:トークンタイプ(常に”JWT”)

2. Payload(ペイロード)

ユーザー情報とクレームを含みます。

{
  "sub": "user_123",      // Subject: ユーザーID
  "role": "admin",        // Role: ユーザーロール(admin/user/device)
  "iat": 1616239022,      // Issued At: トークン発行時刻(UNIXタイムスタンプ)
  "exp": 1616242622,      // Expiration: トークン有効期限(UNIXタイムスタンプ)
  "device_id": "A1B2C3D4..." // デバイスID(デバイス認証の場合のみ)
}

必須クレーム:

  • sub(subject):ユーザーまたはデバイスの一意な識別子
  • role:ユーザーのロール
    • admin:管理者(すべてのAPI実行可能)
    • user:一般ユーザー(通知閲覧のみ)
    • device:デバイス(デバイス登録・更新のみ)
  • iat(issued at):トークン発行時刻
  • exp(expiration):トークン有効期限

オプションクレーム:

  • device_id:デバイスID(デバイス認証時)
  • session_f:セッションID(デバイス認証時)

3. Signature(署名)

改ざん防止のための署名です。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  SECRET_KEY
)

トークンの生成方法

環境変数の設定

JWTの署名に使用する秘密鍵を環境変数に設定します:

# .env ファイル
JWT_SECRET_KEY=your-256-bit-secret-key-here
JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=1

⚠️ 重要:

  • JWT_SECRET_KEYは最低256ビット(32文字以上)の強力なランダム文字列を使用してください
  • 本番環境では絶対にコードにハードコードせず、環境変数または秘密管理サービスを使用してください

PHP(Laravel)での生成例

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JWTService
{
    private string $secretKey;
    private string $algorithm;
    private int $expirationHours;

    public function __construct()
    {
        $this->secretKey = env('JWT_SECRET_KEY');
        $this->algorithm = env('JWT_ALGORITHM', 'HS256');
        $this->expirationHours = env('JWT_EXPIRATION_HOURS', 1);
    }

    public function generateAdminToken(string $userId, string $role = 'admin'): string
    {
        $issuedAt = time();
        $expiration = $issuedAt + ($this->expirationHours * 3600);
        $payload = [
            'sub' => $userId,
            'role' => $role,
            'iat' => $issuedAt,
            'exp' => $expiration,
        ];
        return JWT::encode($payload, $this->secretKey, $this->algorithm);
    }

    public function generateDeviceToken(string $sessionF, string $deviceId): string
    {
        $issuedAt = time();
        $expiration = $issuedAt + (24 * 3600);
        $payload = [
            'sub' => $sessionF,
            'role' => 'device',
            'device_id' => $deviceId,
            'session_f' => $sessionF,
            'iat' => $issuedAt,
            'exp' => $expiration,
        ];
        return JWT::encode($payload, $this->secretKey, $this->algorithm);
    }

    public function validateToken(string $token): object
    {
        try {
            return JWT::decode($token, new Key($this->secretKey, $this->algorithm));
        } catch (\Exception $e) {
            throw new \Exception('Invalid token: ' . $e->getMessage());
        }
    }
}

Node.js(Express)での生成例

const jwt = require("jsonwebtoken");

class JWTService {
  constructor() {
    this.secretKey = process.env.JWT_SECRET_KEY;
    this.algorithm = process.env.JWT_ALGORITHM || "HS256";
    this.expirationHours = parseInt(process.env.JWT_EXPIRATION_HOURS || "1");
  }

  generateAdminToken(userId, role = "admin") {
    const payload = { sub: userId, role: role };
    return jwt.sign(payload, this.secretKey, {
      algorithm: this.algorithm,
      expiresIn: `${this.expirationHours}h`,
    });
  }

  generateDeviceToken(sessionF, deviceId) {
    const payload = {
      sub: sessionF,
      role: "device",
      device_id: deviceId,
      session_f: sessionF,
    };
    return jwt.sign(payload, this.secretKey, {
      algorithm: this.algorithm,
      expiresIn: "24h",
    });
  }

  validateToken(token) {
    try {
      return jwt.verify(token, this.secretKey, { algorithms: [this.algorithm] });
    } catch (error) {
      throw new Error(`Invalid token: ${error.message}`);
    }
  }
}

module.exports = JWTService;

トークンの有効期限

トークンタイプ有効期限用途
管理者トークン1時間管理画面API操作
デバイストークン24時間デバイス登録・更新
リフレッシュトークン30日アクセストークンの再発行(オプション)

セキュリティベストプラクティス

1. 秘密鍵の管理

  • ✅ 環境変数または秘密管理サービス(AWS Secrets Manager、HashiCorp Vault等)で管理
  • ✅ 最低256ビット(32文字以上)の強力なランダム文字列を使用
  • ❌ コードにハードコードしない
  • ❌ GitHubなどのバージョン管理システムにコミットしない

2. トークンの保管(クライアント側)

モバイルアプリ(iOS/Android):

  • ✅ Keychain(iOS)またはKeyStore(Android)を使用
  • ❌ SharedPreferencesやUserDefaultsに平文保存しない

Webアプリ:

  • ✅ HttpOnly Cookie(XSS攻撃対策)
  • ⚠️ LocalStorage使用時はXSS対策を徹底

3. HTTPS必須

  • ✅ 本番環境では必ずHTTPSを使用
  • ✅ HSTSヘッダーを設定

4. トークンのローテーション

  • ✅ 定期的にトークンを再発行
  • ✅ パスワード変更時は既存トークンを無効化
  • ✅ 疑わしいアクティビティ検出時は即座に無効化

5. レート制限

  • ✅ 認証エンドポイントにレート制限を実装
  • ✅ 失敗回数に応じてアカウントを一時ロック

トラブルシューティング

エラー: “Invalid token”

原因:

  • トークンの有効期限切れ
  • トークンの改ざん
  • 秘密鍵の不一致

解決策:

  • トークンの有効期限を確認(expクレーム)
  • 秘密鍵が正しいか確認
  • トークンの形式が正しいか確認(ヘッダー・ペイロード・署名の3部構成)

エラー: “Missing or invalid authorization header”

原因:Authorizationヘッダーが含まれていない、またはヘッダーの形式が間違っている(Bearer プレフィックスが必要)。

正しい形式: Authorization: Bearer <token>
誤った形式: Authorization: <token>

エラー: “Insufficient permissions”

原因:要求されたロールと実際のロールが一致しない。

解決策:トークンのペイロードを確認し、正しいロール(adminuserdevice)が設定されているか確認してください。

参考リンク

コメント