はじめに
Node.jsでWebアプリを作っていると、必ずぶつかるのが「認証」の実装です。メールアドレスとパスワードでのログイン、Googleアカウントでのソーシャルログイン、APIトークンによる認証……。これらを毎回スクラッチで書いていては、時間がいくらあっても足りませんし、セキュリティ上の落とし穴にもはまりやすくなります。
そこで長年頼られてきたのがPassportです。Expressアプリに認証機能を組み込むためのミドルウェアで、認証方式ごとの実装を「Strategy(ストラテジー)」というプラグイン形式に切り出しているのが最大の特徴です。ユーザー名とパスワードによるローカル認証から、JWT、Google・GitHubなどのOAuthまで、500種類を超えるStrategyが公開されており、必要なものを組み合わせるだけで認証まわりを構築できます。
この記事では、Passportの基本的な仕組みから、実際のプロジェクトでよく使われる3つの認証パターン(ローカル認証・JWT認証・セッション管理)まで、コード例を交えて紹介します。
Passportとは
Passportはjaredhanson氏が開発したNode.js向けの認証ミドルウェアです。2011年から公開されており、Express・Koa・Fastifyなど各種フレームワークで利用できます。2024年にはOpenJS Foundationのプロジェクトへ移管され、現在も活発にメンテナンスが続いています。
Passport自体は認証の「枠組み」だけを提供し、実際の認証ロジックはpassport-localやpassport-jwt、passport-google-oauth20といったStrategyパッケージが担います。この設計により、認証方式を追加・変更してもアプリ本体のコードにはほとんど手を入れずに済みます。
主な特徴
- Strategyパターンによる拡張性 - ローカル認証・OAuth・JWT・SAMLなど、500種類以上のStrategyが公開されており、必要な認証方式だけを
use()で登録できる - フレームワーク非依存の軽量設計 - コア部分は認証の流れを制御するだけのシンプルなミドルウェアで、Expressのリクエストサイクルに自然に組み込める
- セッション管理との統合 -
serializeUser/deserializeUserによって、ログイン後のユーザー情報をセッションに永続化する仕組みが標準で用意されている
インストール
Passport本体に加えて、使用したいStrategyを個別にインストールします。
npm install passport
npm install passport-local # ユーザー名・パスワード認証
npm install passport-jwt # JWTトークン認証
npm install express-session # セッション管理を使う場合
基本的な使い方
Passportの基本パターンは「Strategyを定義する」「initialize()をミドルウェアとして組み込む」「ルートでauthenticate()を呼ぶ」の3ステップです。以下はpassport-localを使ったログイン処理の最小構成です。
const express = require('express')
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
// 認証ロジックをStrategyとして登録
passport.use(new LocalStrategy((username, password, done) => {
const user = findUserByUsername(username) // 任意のDB検索処理
if (!user) return done(null, false, { message: 'ユーザーが存在しません' })
if (!verifyPassword(password, user.hash)) {
return done(null, false, { message: 'パスワードが違います' })
}
return done(null, user)
}))
const app = express()
app.use(express.urlencoded({ extended: false }))
app.use(passport.initialize())
app.post('/login',
passport.authenticate('local', { session: false }),
(req, res) => res.json({ user: req.user })
)
passport.authenticate('local', ...)を通すと、LocalStrategyのコールバックが呼ばれ、認証に成功すればreq.userにユーザー情報がセットされます。認証方式を変えたいときは、authenticate()の第一引数の文字列(Strategy名)を差し替えるだけです。
実践的なユースケース
Passportの実装パターンは、大きく分けて「ローカル認証」「JWTによるAPI認証」「セッションを使ったログイン状態の維持」の3つに整理できます。それぞれ用途が異なるため、順に見ていきましょう。
パターン1: passport-localによるログインフォーム認証
会員制サイトの定番であるメールアドレス(またはユーザー名)とパスワードでのログインにはpassport-localを使います。パスワードは平文で保存せず、bcryptなどでハッシュ化した値と比較するのが基本です。
const LocalStrategy = require('passport-local').Strategy
const bcrypt = require('bcryptjs')
passport.use('local', new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
const user = await db.users.findOne({ email })
if (!user) return done(null, false, { message: 'no user' })
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) return done(null, false, { message: 'invalid password' })
return done(null, user)
}
))
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err || !user) return res.status(401).json({ error: info?.message })
req.login(user, (err) => {
if (err) return next(err)
return res.json({ message: 'ログイン成功', user: { id: user.id, email: user.email } })
})
})(req, res, next)
})
usernameFieldオプションでフォームのフィールド名をemailに変更できる点や、done()の第2引数にfalseを渡すことで認証失敗を明示的に表現できる点がポイントです。エラーメッセージをinfoオブジェクトに詰めておけば、フロントエンド側でそのままユーザーに表示できます。
パターン2: passport-jwtによるトークンベースのAPI認証
SPAやモバイルアプリ向けのAPIでは、セッションではなくJWT(JSON Web Token)でユーザーを識別するのが一般的です。passport-jwtはリクエストヘッダーからトークンを取り出し、検証する処理をStrategyとして提供します。
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt')
passport.use('jwt', new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (payload, done) => {
const user = await db.users.findById(payload.sub)
if (!user) return done(null, false)
return done(null, user)
}
))
// 保護したいAPIルートに適用
app.get('/api/me',
passport.authenticate('jwt', { session: false }),
(req, res) => res.json({ user: req.user })
)
ExtractJwt.fromAuthHeaderAsBearerToken()はAuthorization: Bearer <token>ヘッダーからトークンを自動抽出します。session: falseを指定することで、Passportにセッションを使わせず、リクエストごとにトークンだけで認証させることができます。REST APIやマイクロサービス間の認証で特に有効なパターンです。
パターン3: セッションによるログイン状態の維持
ブラウザ向けの一般的なWebアプリでは、ログイン後の状態をセッションに保存し続けたいケースが多くあります。PassportはserializeUser/deserializeUserでこの永続化処理を仲介します。
const session = require('express-session')
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
}))
app.use(passport.initialize())
app.use(passport.session())
// セッションにはユーザーIDだけを保存する
passport.serializeUser((user, done) => done(null, user.id))
// リクエストごとにIDからユーザー情報を復元する
passport.deserializeUser(async (id, done) => {
const user = await db.users.findById(id)
done(null, user)
})
// ログイン済みかどうかを判定するミドルウェア
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) return next()
res.status(401).json({ error: 'ログインが必要です' })
}
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.json({ message: `ようこそ、${req.user.name}さん` })
})
serializeUserではユーザーオブジェクト全体ではなくID程度の最小情報だけをセッションに書き込み、deserializeUserでリクエストのたびにDBから最新情報を引き直すのが定石です。こうすることでセッションストア(Redisなど)の負荷を抑えつつ、req.isAuthenticated()やreq.userをアプリ全体で使える状態にできます。
まとめ
Passportは、認証ロジックをStrategyという単位に分離することで、ローカル認証・JWT・OAuthといった異なる認証方式を同じ枠組みで扱えるようにしてくれるミドルウェアです。今回紹介したように、passport-localでのフォーム認証、passport-jwtでのAPI認証、セッションを使ったログイン状態の維持と、用途に応じてStrategyを組み替えるだけで実装できる手軽さが最大の魅力です。
自前で認証処理を書くとどうしても抜け漏れが出やすいので、まずは公式ドキュメントでサポートされているStrategyの一覧を眺めてみて、自分のプロジェクトに合うものを探してみてください。