#はじめに
普段はVue.js, Nuxt.jsに使っている者です。最近バックエンド側の知見も得ようということでExpressで個人アプリを作っているのですが、ログイン・ログアウトの実装において初めてPassport.jsというモジュールを使ったので忘れないうちにメモします。
実装の過程で躓いた箇所や不明で調べた箇所を実装コードとともに解説をしていきます。
if (process.env.NODE_ENV !== "production") {
require("dotenv").config();
}
const express = require("express");
const app = express();
const bcrypt = require("bcrypt");
const passport = require("passport");
const methodOverride = require("method-override");
const flash = require("express-flash");
const session = require("express-session");
const initializePassport = require("./middleware/authMiddleware.js");
const users = [];
initializePassport(
passport,
(email) => users.find((user) => user.email === email),
(id) => users.find((user) => user.id === id)
);
app.set("view-engine", "ejs");
app.use(express.urlencoded({ extended: false }));
app.use(express.static(__dirname + "/public"));
app.use(flash());
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
})
);
app.use(passport.initialize());
app.use(passport.session());
app.use(methodOverride("_method"));
app.get("/", checkAuthenticated, (req, res) => {
res.render("index.ejs", { name: req.user.name });
});
app.get("/login", checkNotAuthenticated, (req, res) => {
res.render("login.ejs");
});
app.post(
"/login",
checkNotAuthenticated,
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login",
failureFlash: true,
})
);
app.get("/register", checkNotAuthenticated, (req, res) => {
res.render("register.ejs");
});
app.post("/register", checkNotAuthenticated, async (req, res) => {
try {
const hashedPassword = await bcrypt.hash(req.body.password, 10);
users.push({
id: Date.now().toString(),
name: req.body.name,
email: req.body.email,
password: hashedPassword,
});
res.redirect("/login");
} catch {
res.redirect("/register");
}
});
app.delete("/logout", (req, res) => {
req.logOut();
res.redirect("/login");
});
function checkAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/login");
}
function checkNotAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return res.redirect("/");
}
next();
}
app.listen(3000);
#解説①
app.use(passport.session());
Q:express-sessionとpassport.session( )の違いは何か?
A:express-sessionはセッション変数を使うためのミドルウェア。passport.session( )とセッション管理を連携させるために呼んでいる。セッション管理をする上でexpress.sessionとpassport.session( )は必ず必要であり、express-sessionを先に呼ばなくてはならない。
req.isAuthenticated = function() { ~~~ }
Q:req.isAuthenticatedはどこから来ているのか
A:Passport側で元から作られている関数なので、宣言していなくても使用することができる
//passport側で既に作られている関数
req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance._userProperty) {
property = this._passport.instance._userProperty;
}
return (this[property]) ? true : false;
};
上記の関数がPassport側で記述されている( https://github.com/jaredhanson/passport/blob/a892b9dc54dce34b7170ad5d73d8ccfba87f4fcf/lib/passport/http/request.js#L74 )
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcrypt");
function initialize(passport, getUserByEmail, getUserById) {
const authenticateUser = async (email, password, done) => {
const user = getUserByEmail(email);
if (user == null) {
return done(null, false, { message: "No User With That Email" });
}
try {
if (await bcrypt.compare(password, user.password)) {
return done(null, user);
} else {
return done(null, false, { message: "Password Incorrect" });
}
} catch (e) {
return done(e);
}
};
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
return done(null, getUserById(id));
});
}
module.exports = initialize;
#解説②
const LocalStrategy = require("passport-local").Strategy;
Q:passport-localは何をしているのか?
A:Passportでは認証時の動作をストラテジーと呼んでいて、実装ではこのストラテジーの設定をする必要があるとのこと。 passport-localはユーザーIDとパスワード(emailなどに変更可)で認証するストラテジーの雛形を提供している。公式でlocal以外のストラテジーを確認することができる(http://www.passportjs.org/packages/ )
ストラテジー=認証方法で覚えても良さそうですね。
passport.use(new LocalStrategy( ~~~ );
Q:passport.use( new LocalStrategy( ) )は何をしているのか?
A:passportとストラテジーの連携(紐付け)を行なっている。セッション管理の紐付けはpassport.sessionでおこない、認証方法の紐付けはpassport.use( new LocalStrategy( ) )でおこなう必要がある。
passport.serializeUser((user, done) => { ~~~ }
Q:シリアライズとデシリアライズは何をしているのか?
A:詳細は添付記事に詳しく書かれていたので省略します。
データのやり取りではJSONオブジェクトは大きすぎるので一旦、直列化(シリアライズ)してデータ受信後に復元(デシリアライズ)をする。
ちなみにJSフレームワークではアプリケーションレベルでシリアライズ・デシリアライズを行っていてピュアなJSでシリアライズ・でシリアライズをする時はJSON.stringify()
とJSON.parse
を使う。
#さいごに
何気なくアプリ開発をしている中では認証機能をFirebase Authenticationなどに丸投げしていましたが、一から作るとなるとかなり労力がかかるなと思いました。しかしいつまでもブラックボックスにしているわけにもいかず、これを気にログイン・ログアウト機能に触れることができたのはよかったです。次はTokenでの実装もやってみたいです。
#参考URL