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

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

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

```javascript: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側で元から作られている関数なので、宣言していなくても使用することができる


```javascript
//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 ）


```javascript: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

https://github.com/jaredhanson/passport/blob/a892b9dc54dce34b7170ad5d73d8ccfba87f4fcf/lib/passport/http/request.js#L74

https://www.kwbtblog.com/entry/2019/05/04/094338

https://qiita.com/allein-s/items/c125af381600c5b7fa28