Passport.js

認証Node.jsExpress認証戦略OAuthローカル認証ソーシャルログインセキュリティ

認証ライブラリ

Passport.js

概要

Passport.jsは、Node.jsアプリケーション向けの非侵入的でモジュラーな認証ミドルウェアです。2025年現在、500を超える認証戦略(strategies)をサポートし、ローカル認証、OAuth、OpenID、SAMLなど幅広い認証方式に対応しています。Expressベースのwebアプリケーションに簡単に組み込むことができ、データベースやルーティングに関する仮定を一切しないため、開発者が最大限の柔軟性を持ってアプリケーションレベルの決定を行えます。戦略パターンを採用したプラガブルアーキテクチャにより、Facebook、Google、TwitterなどのソーシャルログインからエンタープライズSAML統合まで対応可能です。

詳細

Passport.jsは、Jared Hansonによって開発され、Node.jsエコシステムにおける認証の標準ライブラリとしての地位を確立しています。2025年現在も活発に開発が続けられており、パスワードレス認証、新しいOAuth 2.1標準への対応、セキュリティの強化が図られています。

アーキテクチャの特徴

Passport.jsの核となるのは「戦略(Strategy)」パターンです。各認証方式は独立した戦略として実装され、アプリケーションは必要な戦略のみを選択して使用できます。これにより、シンプルなローカル認証から複雑な連携認証まで、統一されたAPIで扱うことができます。

主要な機能

  • 認証のみに特化: セッション管理やルーティングは含まず、純粋に認証のみを責務とする
  • 戦略ベースアーキテクチャ: プラガブルな設計により500+の認証戦略から選択可能
  • フレームワーク非依存: Express以外のNode.jsフレームワークでも使用可能
  • シリアライゼーション制御: ユーザーオブジェクトのセッション保存方法を完全制御

2025年の新機能・トレンド

  • Descope戦略: パスワードレス認証を簡素化する新戦略のリリース
  • OAuth 2.1対応: 最新のOAuth仕様への準拠強化
  • セキュリティ向上: OWASP準拠のセキュリティ機能の強化
  • パスワードレス認証: 生体認証やメールリンク認証の標準化
  • ソーシャルログイン最適化: 2025年研究によると登録率30%向上を実現

メリット・デメリット

メリット

  • 圧倒的な柔軟性: 500+の戦略から選択可能、カスタム戦略も簡単に作成
  • 軽量で非侵入的: 既存アプリケーションへの組み込みが容易
  • 成熟したエコシステム: 長期間の運用実績と豊富なコミュニティリソース
  • Express統合: Express.jsとの親和性が高く、設定が簡単
  • セキュリティベストプラクティス: 一般的なセキュリティ脆弱性への対策を内蔵
  • プラガブル設計: 必要な機能のみを選択的に導入可能
  • 豊富なドキュメント: 包括的な公式ドキュメントとチュートリアル

デメリット

  • Node.js限定: 他の言語・プラットフォームでは使用不可
  • 設定の複雑さ: 多数の戦略から適切な組み合わせを選択する学習コスト
  • 戦略品質の差: サードパーティ戦略の品質やメンテナンス状況にばらつき
  • Express依存度: Express以外のフレームワークでは追加設定が必要
  • TypeScript対応: 一部の戦略でTypeScript型定義が不完全
  • 非同期処理: Promise/async-awaitよりもコールバックベースの設計

参考ページ

書き方の例

インストールと基本設定

# 基本パッケージのインストール
npm install passport express express-session
npm install passport-local passport-google-oauth20 passport-facebook

# 型定義(TypeScript使用時)
npm install --save-dev @types/passport @types/passport-local
npm install --save-dev @types/passport-google-oauth20 @types/passport-facebook
// app.js - Express アプリケーションの基本設定
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const GoogleStrategy = require('passport-google-oauth20').Strategy;

const app = express();

// ミドルウェア設定
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// セッション設定(本番環境では適切なSessionStoreを使用)
app.use(session({
  secret: process.env.SESSION_SECRET || 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS必須
    maxAge: 24 * 60 * 60 * 1000 // 24時間
  }
}));

// Passport初期化
app.use(passport.initialize());
app.use(passport.session());

// ユーザーのシリアライゼーション(セッションに保存)
passport.serializeUser((user, done) => {
  // 実際の実装では、user.idのみをセッションに保存
  done(null, user.id);
});

// ユーザーのデシリアライゼーション(セッションから復元)
passport.deserializeUser(async (id, done) => {
  try {
    // データベースからユーザー情報を取得
    const user = await getUserById(id);
    done(null, user);
  } catch (error) {
    done(error, null);
  }
});

ローカル認証戦略の実装

// strategies/local.js - ローカル認証戦略
const bcrypt = require('bcrypt');

// ローカル戦略設定
passport.use(new LocalStrategy({
    usernameField: 'email', // デフォルトは'username'
    passwordField: 'password'
  },
  async (email, password, done) => {
    try {
      // ユーザー検索
      const user = await getUserByEmail(email);
      if (!user) {
        return done(null, false, { message: 'メールアドレスが見つかりません' });
      }

      // パスワード検証
      const isValidPassword = await bcrypt.compare(password, user.password);
      if (!isValidPassword) {
        return done(null, false, { message: 'パスワードが正しくありません' });
      }

      // 認証成功
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// ログインルート
app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      return res.status(500).json({ error: 'サーバーエラー' });
    }
    
    if (!user) {
      return res.status(401).json({ error: info.message });
    }

    // ログイン実行
    req.logIn(user, (err) => {
      if (err) {
        return res.status(500).json({ error: 'ログインエラー' });
      }
      
      return res.json({
        message: 'ログイン成功',
        user: {
          id: user.id,
          username: user.username,
          email: user.email
        }
      });
    });
  })(req, res, next);
});

OAuth2(Google)認証戦略

// strategies/google.js - Google OAuth戦略
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "/auth/google/callback"
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // 既存ユーザーを検索
      let user = await getUserByGoogleId(profile.id);
      
      if (user) {
        // 既存ユーザー
        return done(null, user);
      }

      // 新規ユーザー作成
      user = await createUserFromGoogle(profile);
      return done(null, user);
    } catch (error) {
      return done(error, null);
    }
  }
));

// Google認証開始
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

// Google認証コールバック
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    // 認証成功時のリダイレクト
    res.redirect('/dashboard');
  }
);

ミドルウェアと認証保護

// middleware/auth.js - 認証ミドルウェア
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  
  // JSON APIの場合
  if (req.xhr || req.headers.accept.indexOf('json') > -1) {
    return res.status(401).json({ error: '認証が必要です' });
  }
  
  // Webページの場合
  res.redirect('/login');
}

function ensureRole(role) {
  return (req, res, next) => {
    if (!req.isAuthenticated()) {
      return res.status(401).json({ error: '認証が必要です' });
    }
    
    if (!req.user.roles || !req.user.roles.includes(role)) {
      return res.status(403).json({ error: '権限が不足しています' });
    }
    
    next();
  };
}

// 認証が必要なルート
app.get('/profile', ensureAuthenticated, (req, res) => {
  res.json({
    user: {
      id: req.user.id,
      username: req.user.username,
      email: req.user.email
    }
  });
});

// 管理者権限が必要なルート
app.get('/admin', ensureRole('admin'), (req, res) => {
  res.json({ message: '管理者ページ' });
});

カスタム戦略の実装

// strategies/custom.js - カスタム認証戦略
const passport = require('passport-strategy');
const util = require('util');

// カスタム戦略クラス
function ApiKeyStrategy(options, verify) {
  if (typeof options === 'function') {
    verify = options;
    options = {};
  }
  
  if (!verify) {
    throw new TypeError('ApiKeyStrategy requires a verify callback');
  }
  
  this.name = 'apikey';
  this._verify = verify;
  this._apiKeyField = options.apiKeyField || 'apikey';
  this._apiKeyHeader = options.apiKeyHeader || 'x-api-key';
  
  passport.Strategy.call(this);
}

// Strategy を継承
util.inherits(ApiKeyStrategy, passport.Strategy);

// 認証実装
ApiKeyStrategy.prototype.authenticate = function(req, options) {
  options = options || {};
  
  // APIキーを取得(ヘッダーまたはクエリパラメータから)
  const apiKey = req.headers[this._apiKeyHeader] || 
                 req.query[this._apiKeyField] || 
                 req.body[this._apiKeyField];
  
  if (!apiKey) {
    return this.fail({ message: 'API key is required' }, 401);
  }
  
  const self = this;
  
  function verified(err, user, info) {
    if (err) {
      return self.error(err);
    }
    if (!user) {
      return self.fail(info);
    }
    self.success(user, info);
  }
  
  try {
    this._verify(apiKey, verified);
  } catch (ex) {
    return self.error(ex);
  }
};

// カスタム戦略の登録
passport.use(new ApiKeyStrategy(
  async (apiKey, done) => {
    try {
      // APIキーでユーザーを検索
      const user = await getUserByApiKey(apiKey);
      if (!user) {
        return done(null, false, { message: 'Invalid API key' });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// API認証が必要なルート
app.get('/api/data', 
  passport.authenticate('apikey', { session: false }),
  (req, res) => {
    res.json({
      message: 'API認証成功',
      data: 'セキュアなデータ',
      user: req.user.username
    });
  }
);

テストとセキュリティ設定

// test/auth.test.js - 認証テスト
const request = require('supertest');
const app = require('../app');
const chai = require('chai');
const expect = chai.expect;

describe('Authentication', () => {
  describe('POST /login', () => {
    it('should authenticate valid user', async () => {
      const res = await request(app)
        .post('/login')
        .send({
          email: '[email protected]',
          password: 'correct_password'
        });
      
      expect(res.status).to.equal(200);
      expect(res.body.message).to.equal('ログイン成功');
      expect(res.body.user).to.have.property('id');
    });
    
    it('should reject invalid credentials', async () => {
      const res = await request(app)
        .post('/login')
        .send({
          email: '[email protected]',
          password: 'wrong_password'
        });
      
      expect(res.status).to.equal(401);
      expect(res.body.error).to.be.a('string');
    });
  });
});

// security/config.js - セキュリティ設定
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

// セキュリティヘッダー
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
}));

// レート制限
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 5, // 最大5回の試行
  message: 'ログイン試行回数が上限に達しました。15分後に再試行してください。',
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/login', loginLimiter);