Passportを使ってUser Login機能を追加する(エラーハンドリング編)
Login画面で、間違ったユーザー名やパスワードが入力されたときのエラーを表示したい。でもExpress.jsのエラーハンドリング、Passport.jsのエラーハンドリング、個別のuserLoginなどのfunction内でのエラーハンドリング、設定したいエラー文言をどこに書けば良いのか良く分からない💦
Unauthorizedって出て来たけど、なぜこのメッセージが出たのか分かっていない。

エラーハンドリング全体イメージ
- Passport.js → 認証処理の成否を判断。エラーがあればExpressに渡す。
- Express.js ルート関数内 → ロジックの途中で失敗すれば
next(error)
やres.status().json()
で返す。 - Express.js エラーハンドラ(middleware)→ 最後にすべてのエラーを受け取って、レスポンス(JSON形式のエラーメッセージなど)を返す。
- フロントエンドの関数(たとえば
userLogin
)→ fetchで受け取ったレスポンスをチェックし、UIに表示するために加工する。
Passport.jsでのエラーハンドリング
なんかlocalを以下のように書いていたけれども、これだと認証失敗時のレスポンス(メッセージ)をカスタマイズできないらしい。
export const local = passport.authenticate("local", {
session: true,
// Passport にログインの後処理まで任せる場合は以下のようなオプションを渡していく
// successRedirect: "/dashboard",
// failureRedirect: "/login",
});
app.post("/api/auth/login", local, handleUserLogin);
「ログイン成功ならリダイレクト、失敗ならエラー画面に移動」といった処理をPassport に自動でやってもらいたいときは、上記のようなObjectの形で設定したい項目を渡すだけだが、エラーメッセージを細かく設定したり、フロント側はReactで制御したいなら、以下のように関数内で passport.authenticate() を直接呼ぶスタイルにしてエラーメッセージはJSONを返すのが良いとのことなので、言われるがままに書き直す。
export const local = (req: Request, res: Response, next: NextFunction) => {
const middlewareFunction = passport.authenticate(
"local",
(err: Error | null, user: User | false, info: { message?: string }) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({
error: "AUTH_FAILED",
message: info?.message || "ログインに失敗しました",
});
}
req.login(user, (err: Error | null) => {
if (err) {
return next(err);
}
return next(); // 認証成功
});
}
);
middlewareFunction(req, res, next); // 重要!
};
このPassport の カスタムコールバック付き認証はとにかくごちゃごちゃと分かりにくいので分解して理解に繋げる。
まずは3行目。Passport の”local”ストラテジーを使って認証を行っているのは元のコードと同じなので問題ない。
4-20行目は目を凝らしてみるとfunctionになっている。どう言うことかというと、以下のように、passport.authenticate()の2つ目のパラメータにfunctionを渡すと、そのfunctionはLocalStrategy
による認証が終わったときに自動的に呼び出される。そのような仕組み付きのミドルウェアfunction(上記例ではmiddlewareFunction
)がreturnされるのだ。なのでこれをミドルウェアに組み込む。そう、Passport.jsはNode.js向けの認証ミドルウェアだって言うことを思い出して!
const middlewareFunction = passport.authenticate("local", callbackFunction);
作ったものを使いたいので、23行目のようにmiddlewareFunctionを呼ぶ!app.post("/api/auth/login", local, handleUserLogin);
のようにlocalを入れた時に、Expressでfunctionが受け取るのは(req, res, next)なので、それをmiddlewareFunction
を呼ぶ時にそのまま渡している。
// middlewareFunctionを作る
const middlewareFunction = passport.authenticate("local", callbackFunction);
// middlewareFunctionを呼ぶ
middlewareFunction(req, res, next);
(休憩☕️)
ここで一つ疑問。そもそも最初書いたコードではcallbackFunctionじゃなくて、objectの形だったけど問題なかったのか?
export const local = passport.authenticate("local", { session: true });
渡されたパラメータの型がcallbackFunctionか、 オプションオブジェクトかをPassport自体が見て処理を分ける仕組みになっているので大丈夫らしい。オプションオブジェクトが渡された場合はPassportが自動で認証結果に応じてリダイレクト処理をしてくれるため、コールバックを書く必要がないとのこと。これまで勉強していたReact、TypeScriptではパラメータの順番が大事だったりするから、型が変わるのは違和感あった。
さて、話を戻そう。
大枠が理解できたところでcallbackFunctionの中身に目を向ける。
(err: Error | null, user: User | false, info: { message?: string }) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({
error: "AUTH_FAILED",
message: info?.message || "ログインに失敗しました",
});
}
req.login(user, (err: Error | null) => {
if (err) {
return next(err);
}
return next(); // 認証成功
});
}
認証結果は(err, user, info)の形で返ってくる
まず、callbackFunctionの(err, user, info)のパラメータは、”local”ストラテジーを使って認証を行うとPassportが内部的に呼び出すcallback functionのパラメータのためExpressのmiddleware functionのパラメータとは別物であることを理解しておく。各パラメータの意味は以下の通りだが、ここで気を付けるポイントは認証に失敗した場合、userはnull
やundefined
ではなく、false
が返ってくる点。
(
err: Error | null, // 認証中に発生したシステムエラー(DB接続ミスなど)
user: User | false, // 認証に成功したユーザーオブジェクト。失敗時は false
info: { message?: string } // ストラテジーが提供するエラーメッセージ
) => { ... }
認証失敗してuserがfalseで返ってきたとき、JSONでreturn する。
その後の14行目のreq.login(user, callback)
はPassport.jsが提供するログイン処理の一部で、認証に成功するとPassport 内部で serializeUser(user, done)
を呼び出してユーザーをセッションに保存してくれる。
if (!user) {
return res.status(401).json({
error: "AUTH_FAILED",
message: info?.message || "ログインに失敗しました",
});
}
req.login(user, (err: Error | null) => {
if (err) {
return next(err);
}
return next(); // 認証成功
});
10行目の通り、ユーザー認証できなかった時に、”ログインに失敗しました”よりも先にinfo.messageが存在すればそちらが表示されるわけだが、これはどこからきているのか?
info.messageはどこから来るのか
遡っていくとPassportの設定をしているところで確かにエラーハンドリングしていた。なるほど、info.messageはここで設定した文言”ユーザー名またはパスワードが間違っています”がmessageとして降りてくるんだな。となると、ユーザー名やパスワード間違いといった基本的な認証失敗のケースの際はinfo.messageが存在するので”ログインに失敗しました”の文言を目にする機会もほぼなさそう。もうちょっと詳しく見ていく。
const strategy = new LocalStrategy(async function verify(
username,
password,
done
) {
try {
// データベースからユーザーを取得
const user = await db.user.findUnique({
where: { username },
select: {
id: true,
name: true,
username: true,
password: true,
},
});
// ユーザーが存在しない場合は、エラーを返す
if (!user) {
return done(null, false, {
message: "ユーザー名またはパスワードが間違っています",
});
}
const isValid = await verifyPassword(password, user.password);
// パスワードが間違っている場合は、エラーを返す
if (!isValid) {
return done(null, false, {
message: "ユーザー名またはパスワードが間違っています",
});
}
// パスワードが正しい場合は、ユーザーを返す
// この際に、パスワードを除外する
// _は特定のプロパティを除外する
const { password: _, ...safeUser } = user;
return done(null, safeUser);
} catch (error) {
return done(error);
}
});
passport.use(strategy);
done()について
Passport.jsのStrategy関数の中で使う done()
は、「Passportに認証の結果を伝えるためのコールバック関数」で、パラメータは以下の3つ。
done(error, user, info);
- error : エラーが発生したときに使う。先ほどの例では37行目の時にerrorを渡しているが、それ以外はnullを渡している。errorを渡すかどうかは、そのエラーがDB接続失敗などプログラム上のバグ・例外なのか、ユーザーの操作ミスなのかで判断する。パスワード間違いなどによる認証失敗時や、認証成功時はnullを渡す。
- user : 認証成功時に返すユーザーオブジェクト。
false
を返すと「認証失敗」扱いになる。 - info : 認証失敗時に追加情報を渡したいときに使うオプション(例: メッセージなど)。
最初に見ていた部分とようやく繋がってきた。
handleUserLogin
local
でPassportの認証が行われ、ログイン成功時にhandleUserLogin
の処理がされる。次はこのfunctionの中身を再確認する。
app.post(
"/api/auth/login",
local, // Passportの認証
handleUserLogin, // ログイン成功時の処理
);
すると、handleUserLogin
の中でもエラーハンドリングしていたけど、必要なのかしら??だってこのfunctionに辿り着いていると言うことはuserがfalseじゃなかったのでnext()が呼ばれた場合。ユーザー名やパスワードの入力間違いによるエラーは既にlocalの中でハンドルされて認証失敗の処理がされているからここには来ない。
認証は成功したけど、なぜかあったはずのreq.user
が 認証後に突然undefined
とかfalseになった場合(9行目〜)や、認証は成功してユーザー情報もあるけど、成功しなかった場合(22行目〜)と言うのはどういう状況かは分からないがとりあえず予期せぬエラーが発生した場合に備え念の為書いている感じ。
export const handleUserLogin: ChallengeAcceptedApiRequest<
never,
PostUserLoginApiResponse,
UserLoginApiRequest
> = async function (req, res) {
res.setHeader("Content-Type", "application/json");
try {
if (!req.user) {
return res.status(401).json({
error: "AUTH_ERROR",
message: "予期せぬエラーが発生しました",
});
}
return res.status(200).json({
user: {
id: req.user.id,
name: req.user.name,
username: req.user.username,
},
});
} catch (error) {
console.error("Login error:", error);
return res
.status(500)
.json({ error: "AUTH_ERROR", message: "予期せぬエラーが発生しました" });
}
};
試しに間違った認証情報で再度ログインを試す。

ちゃんと自分で設定したメッセージを表示できたので成功!

↑ディズニーランドでカモノハシ発見