2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

アルサーガパートナーズAdvent Calendar 2021

Day 10

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

Posted at

#はじめに
普段は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

2
2
0

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
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?