NodeJS 速習チュートリアル

Node.js API認証ガイド

1. API認証とは?

API認証とは、Node.js APIにアクセスするクライアントの身元を確認するプロセスです。
この包括的なガイドでは、さまざまな認証手法、セキュリティのベストプラクティス、およびNode.jsアプリケーションを効果的に保護するための実装パターンについて解説します。

1.1 なぜAPI認証が重要なのか

今日の相互接続された世界において、APIセキュリティはオプションではなく必須事項です。適切な認証を導入することで、以下のメリットが得られます。

セキュリティ上の利点:

  • アクセス制御: 許可されたユーザーのみにAPIアクセスを制限します。
  • データ保護: 機密情報を不正アクセスから守ります。
  • 本人確認: ユーザーが主張通りの人物であることを保証します。

ビジネス上の利点:

  • 利用状況分析: ユーザーやアプリケーションごとのAPI使用量を追跡します。
  • 収益化: 利用ベースの課金モデルを実装できます。
  • コンプライアンス: GDPRやHIPAAなどの規制要件を満たします。

1.2 認証手法の概要

用途に応じて、異なる認証手法を選択する必要があります。以下に比較表を示します。

手法最適なユースケース複雑さセキュリティレベル
セッションベース伝統的なWebアプリ
JWT (トークンベース)SPA、モバイルアプリ
APIキーサーバー間通信低 〜 中
OAuth 2.0サードパーティアクセス非常に高

2. 認証手法の詳細

Node.jsにおけるAPI認証にはいくつかの主要なアプローチがあります。

2.1 セッションベース認証

セッションベース認証は、クッキー(Cookie)を使用してユーザーの状態を維持します。

const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();

// リクエストボディのパース
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// セッションの設定
app.use(session({
  secret: 'あなたの秘密キー',
  resave: false,
  saveUninitialized: false,
  cookie: { 
    secure: process.env.NODE_ENV === 'production', 
    maxAge: 24 * 60 * 60 * 1000 // 24時間
  }
}));

// サンプルのユーザーデータベース
const users = [
  { id: 1, username: 'user1', password: 'password1' }
];

// ログインルート
app.post('/login', (req, res) => {
  const { username, password } = req.body;
 
  // ユーザーの検索
  const user = users.find(u => u.username === username && u.password === password);
 
  if (!user) {
    return res.status(401).json({ message: '認証情報が無効です' });
  }
 
  // セッションにユーザー情報を保存(パスワードは除外)
  req.session.user = {
    id: user.id,
    username: user.username
  };
 
  res.json({ message: 'ログイン成功', user: req.session.user });
});

// 保護されたルート
app.get('/profile', (req, res) => {
  // ユーザーがログインしているか確認
  if (!req.session.user) {
    return res.status(401).json({ message: '未認証です' });
  }
 
  res.json({ message: 'プロファイルにアクセスしました', user: req.session.user });
});

// ログアウトルート
app.post('/logout', (req, res) => {
  // セッションの破棄
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ message: 'ログアウトに失敗しました' });
    }
    res.json({ message: 'ログアウト成功' });
  });
});

app.listen(8080, () => {
  console.log('サーバーがポート8080で起動しました');
});

2.2 トークンベース認証 (JWT)

JSON Web Token (JWT) は、コンパクトで自己完結型のステートレス(Stateless)な認証メカニズムを提供します。セッションベースとは異なり、サーバー側にセッションデータを保存する必要がないため、マイクロサービスやスケーラブルなAPIアーキテクチャに最適です。

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json());

const JWT_SECRET = 'あなたのJWT秘密キー';

// サンプルデータベース
const users = [
  { id: 1, username: 'user1', password: 'password1', role: 'user' }
];

// ログインルート - トークンの生成
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);

  if (!user) {
    return res.status(401).json({ message: '認証情報が無効です' });
  }

  // JWTのペイロード作成
  const payload = {
    id: user.id,
    username: user.username,
    role: user.role
  };

  // トークンの署名
  const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });

  res.json({ message: 'ログイン成功', token });
});

// JWT検証用ミドルウェア
const authenticateJWT = (req, res, next) => {
  // Authorizationヘッダーの取得
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ message: 'Authorizationヘッダーがありません' });
  }

  // "Bearer <token>" からトークンを抽出
  const token = authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({ message: 'トークンがありません' });
  }

  try {
    // トークンの検証
    const decoded = jwt.verify(token, JWT_SECRET);
    // リクエストにユーザー情報を添付
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(403).json({ message: 'トークンが無効または期限切れです' });
  }
};

// 保護されたルート
app.get('/profile', authenticateJWT, (req, res) => {
  res.json({ message: 'プロファイルにアクセスしました', user: req.user });
});

// ロールベースのルート
app.get('/admin', authenticateJWT, (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ message: 'アクセス拒否: 管理者権限が必要です' });
  }
  res.json({ message: '管理者パネルにアクセスしました' });
});

app.listen(8080, () => {
  console.log('サーバーがポート8080で起動しました');
});

2.3 OAuth 2.0 認証

OAuth 2.0 は認可のための標準プロトコルであり、アプリケーションがHTTPサービス上のユーザーアカウントに対して制限付きのアクセスを取得できるようにします。

OAuth 2.0 のフロー概要:

  1. ユーザーがアプリ内の「[プロバイダー]でログイン」をクリック。
  2. ユーザーはプロバイダーのログインページにリダイレクトされる。
  3. ユーザーが認証し、アプリへのアクセスを許可する。
  4. プロバイダーは認可コード(Authorization Code)を持ってアプリにリダイレクト。
  5. アプリはコードをアクセストークン(Access Token)と交換。
  6. アプリは許可された範囲内でユーザーデータにアクセス可能になる。

2.3.1 Passport.js による実装

passportpassport-google-oauth20 を使用したGoogleログインの例:

// 戦略の構成
passport.use(new GoogleStrategy({
    clientID: 'あなたのGOOGLE_CLIENT_ID',
    clientSecret: 'あなたのGOOGLE_CLIENT_SECRET',
    callbackURL: 'http://localhost:8080/auth/google/callback'
  },
  (accessToken, refreshToken, profile, done) => {
    // データベースでユーザーを検索または作成
    const user = {
      id: profile.id,
      displayName: profile.displayName,
      email: profile.emails[0].value,
      provider: 'google'
    };
    return done(null, user);
  }
));

// 認証チェック用ミドルウェア
const isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
};

2.4 APIキー認証

APIキーは、クライアントを認証するためのシンプルな方法です。主にサーバー間通信(Server-to-Server)や、ユーザーコンテキストなしでプロジェクトを識別する場合に適しています。

APIキーのベストプラクティス:

  • キーを安全に保存する(環境変数、シークレット管理サービス)。
  • 定期的にキーをローテーション(更新)する。
  • キーの露出を防ぐため常に HTTPS を使用する。
  • キーごとのレート制限(Rate Limiting)を実装する。
const authenticateApiKey = (req, res, next) => {
  const apiKey = req.headers['x-api-key'] || req.query.apiKey;

  if (!apiKey) {
    return res.status(401).json({ error: 'APIキーが必要です' });
  }

  const keyData = apiKeys.get(apiKey);
  if (!keyData) {
    return res.status(403).json({ error: '無効なAPIキーです' });
  }

  req.apiKey = keyData;
  next();
};

2.5 ベーシック認証 (Basic Authentication)

HTTPベーシック認証は、Authorization ヘッダーにエンコードされた認証情報を使用します。

const basicAuth = (req, res, next) => {
  const authHeader = req.headers.authorization;
 
  if (!authHeader || !authHeader.startsWith('Basic ')) {
    res.setHeader('WWW-Authenticate', 'Basic realm="API Authentication"');
    return res.status(401).json({ message: '認証が必要です' });
  }
 
  // 認証情報の抽出とデコード
  const encodedCredentials = authHeader.split(' ')[1];
  const decodedCredentials = Buffer.from(encodedCredentials, 'base64').toString('utf-8');
  const [username, password] = decodedCredentials.split(':');
 
  // ...バリデーションロジック...
  next();
};

2.6 多要素認証 (MFA)

TOTP(Time-based One-Time Password) を使用してセキュリティレイヤーを追加します。

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// MFAのセットアップ:シークレットの生成
app.post('/register', (req, res) => {
  const secret = speakeasy.generateSecret({ name: `MyApp:${username}` });
  
  // QRコードの生成
  QRCode.toDataURL(secret.otpauth_url, (err, dataUrl) => {
    res.json({ mfaSecret: secret.base32, qrCode: dataUrl });
  });
});

// MFAの検証
app.post('/verify-mfa', (req, res) => {
  const verified = speakeasy.totp.verify({
    secret: user.mfaSecret,
    encoding: 'base32',
    token: req.body.token
  });
  // ...
});

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

認証を実装する際、セキュリティは不可欠な要素です。以下のガイドラインに従ってください。

3.1 パスワードのセキュリティ

  • プレーンテキストで保存しない: 必ず bcryptArgon2 などの強力なハッシュアルゴリズムを使用してください。
  • 強力なパスワードを強制する: 最小長、特殊文字、数字の使用を必須にします。
  • パスワードの定期的変更: ユーザーに定期的な更新を促します。
const bcrypt = require('bcrypt');
const saltRounds = 10;

// パスワードのハッシュ化
async function hashPassword(plainPassword) {
  return await bcrypt.hash(plainPassword, saltRounds);
}

// パスワードの検証
async function verifyPassword(plainPassword, hashedPassword) {
  return await bcrypt.compare(plainPassword, hashedPassword);
}

3.2 トークンのセキュリティ

  • 短命なアクセストークンを使用する: 通常は 15 〜 60 分が一般的です。
  • リフレッシュトークン(Refresh Token)の実装: 再ログインなしで新しいアクセストークンを取得できるようにします。
  • セキュアな保存: Webアプリでは HttpOnly, Secure, SameSite クッキーを使用してください。

3.3 一般的なセキュリティ対策

  • 常にHTTPSを使用する: すべてのトラフィックを暗号化します。
  • レート制限の実装: ブルートフォース攻撃を防止します。
  • セキュリティヘッダーの使用: CSP、X-Content-Type-Options、X-Frame-Options などを設定します。
  • 監視とログ: 認証試行の監査ログを保持します。

4. 認証手法の組み合わせと応用

実世界のアプリケーションでは、複数の手法を組み合わせることがよくあります。

4.1 リフレッシュトークンとブラックリストの実装

ログアウト時にトークンを無効化するために、トークンのブラックリスト(Blacklist)管理が必要になる場合があります。

app.post('/refresh-token', (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshTokens.has(refreshToken)) {
    return res.status(403).json({ message: '無効なリフレッシュトークンです' });
  }

  try {
    const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
    const accessToken = jwt.sign({ id: decoded.id }, JWT_SECRET, { expiresIn: '15m' });
    res.json({ accessToken });
  } catch (error) {
    refreshTokens.delete(refreshToken);
    return res.status(403).json({ message: '期限切れのリフレッシュトークンです' });
  }
});

5. 認証におけるHTTPヘッダーと戦略

5.1 主要なHTTPヘッダー

  • Authorization: 認証トークンを送信するための標準ヘッダー。
  • フォーマット: JWTやOAuth 2.0では Authorization: Bearer <token>、ベーシック認証では Authorization: Basic <base64> を使用します。

5.2 APIタイプ別の推奨戦略

APIタイプ推奨される認証手法備考
パブリックAPIAPIキー実装が容易、利用状況の追跡に最適
サーバー間APIJWT または Mutual TLS低オーバーヘッド、高セキュリティ
モバイル/WebアプリOAuth 2.0 + JWT優れたUX、サードパーティ連携が容易
SPA (単一ページアプリ)JWT + リフレッシュトークンフロントエンドフレームワークと相性が良い
IoTデバイスAPIクライアント証明書 or APIキーデバイスの計算能力の制限を考慮

6. まとめ

Node.js API認証の主要な概念を振り返りましょう:

  1. 認証手法の選択: セッション、JWT、OAuth 2.0、APIキーなど、プロジェクトの要件に合わせて選択します。
  2. セキュリティ第一: パスワードは必ずハッシュ化し、HTTPSを強制し、短命なトークンを使用してください。
  3. 拡張性: MFAの導入や、リフレッシュトークンによるセッション管理の高度化を検討してください。

適切な認証戦略を構築することは、ユーザーの信頼を獲得し、アプリケーションの価値を長期的に守ることにつながります。