search
LoginSignup
1

More than 1 year has passed since last update.

【Node.js】ExpressとPassport.jsで認証機能を実装する

はじめに

普段はVue.js, Nuxt.jsに使っている者です。最近バックエンド側の知見も得ようということでExpressで個人アプリを作っているのですが、ログイン・ログアウトの実装において初めてPassport.jsというモジュールを使ったので忘れないうちにメモします。

実装の過程で躓いた箇所や不明で調べた箇所を実装コードとともに解説をしていきます。

server.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

middlware/authMiddlwere.js
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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1