🐩

Passportを使ってExpressでパスワード認証機能を実装

2022/07/22に公開

Express × MongoDB で作られたWebアプリに認証機能を実装する際は Passport というミドルウェアが便利です。

今回はブラウザのCookieを利用した Username&Password 認証のやり方を解説します。

ユーザー情報は MongoDB に保存し登録時はアイコン画像をアップロードします。
DBへの保存処理や画像のアップロード処理で mongoose, express-fileupload というパッケージを利用しますが詳しくは触れません。

最終的なソースコードはこちら。
https://github.com/t-aono/express-mongodb-chat/tree/feature/authentication

パッケージのインストール

必要なパッケージを npm でインストールします。

npm i passport passport-local express-session

password-local : ローカル認証を行うために必要。
express-session : セッションの管理を行うために必要。

ユーザー登録画面の作成

まずはユーザー登録画面から作っていきます。
ユーザー名とパスワードに加えてアイコン画像も登録できるようにフォームを実装します。
views/signup.pug を作成。(html 側はテンプレートエンジンの pug を利用しています。)

doctype html 
html(lang="ja")
  head
    meta(charset="utf-8")
  body
    h1 新規登録
    form(action="/signup" method="POST" encType="multipart/form-data")
      input(type="text" name="username" placeholder="名前")
      input(name="password" placeholder="パスワード")
      input(type="file" name="avatar")
      button(type="submit") 送信

get, post でのルーティングを app.js に追加します。

var fileUpload = require("express-fileupload");

var User = require("./schema/User");

(略)

app.get("/signup", function (req, res, next) {
  return res.render("signup");
});

app.post("/signup", fileUpload(), function (req, res, next) {
  var avatar = req.files.avatar;
  avatar.mv("./avatar/" + avatar.name, function (err) {
    if (err) throw err;
    var newUser = new User({
      username: req.body.username,
      password: req.body.password,
      avatar_path: "/avatar/" + avatar.name,
    });
    newUser.save((err) => {
      if (err) throw err;
      return res.redirect("/");
    });
  });
});

avatar というフォルダを用意して mv メソッドを使ってアイコン画像を保存するようにしました。
User Schema は schma/User.js で定義してそれを利用します。

var mongoose = require("mongoose");

var User = new mongoose.Schema({
  username: String,
  password: String,
  date: { type: Date, default: new Date() },
  avatar_path: String,
});

module.exports = mongoose.model("User", User);

ログイン画面の作成

次にログイン画面です。
username, password をPOSTするためのフォームを作ります。
views/login.pugを作成します。

doctype html 
html(lang="ja")
  head
    meta(charset="utf-8")
  body
    h1 ログイン
    form(action="/login" method="POST")
      input(type="text" name="username" placeholder="名前")
      input(name="password" placeholder="パスワード")
      button(type="submit") 送信

get, post でのルーティングを app.js に追加します。

var passport = require("passport");
var LocalStrategy = require("passport-local").Strategy;
var session = require("express-session");

(略)

app.use(session({ secret: "HogeFuga" }));
app.use(passport.initialize());
app.use(passport.session());

(略)

app.get("/login", function (req, res, next) {
  return res.render("login");
});

app.post("/login", passport.authenticate("local"), function (req, res, next) {
  User.findOne({ _id: req.session.passport.user }, function (err, user) {
    if (err || !user || !req.session) {
      return res.redirect("/login");
    } else {
      req.session.user = {
        username: user.username,
        avatar_path: user.avatar_path,
      };
      return res.redirect("/");
    }
  });
});

passport.use(
  new LocalStrategy(function (username, password, done) {
    User.findOne({ username: username }, function (err, user) {
      if (err) return done(err);
      if (!user) {
        return done(null, false, { message: "Incorrect username." });
      }
      if (user.password !== password) {
        return done(null, false, { message: "Incorrect password." });
      }
      return done(null, user);
    });
  })
);

passport.serializeUser(function (user, done) {
  done(null, user._id);
});

passport.deserializeUser(function (id, done) {
  User.findOne({ _id: id }, function (err, user) {
    done(err, user);
  });
});

passport.authenticate("local") を指定することでセッション情報を検証できます。
値がない場合に new LocalStrategy() に処理が渡され username, password を DB の値と比較します。
認証が成功した場合は取得した user 情報をセッションに格納する流れです。
passport.serializeUser(), passport.deserializeUser() では user 情報のどの値をセッション管理するかを指定することで不都合が生じることを防止する働きがあります。

ログイン時はアイコンを表示

ログイン後のトップページでユーザーアイコンを表示するようにしたいので app.js を変更します。

app.use("/avatar", express.static(path.join(__dirname, "avatar")));

app.get("/", function (req, res) {
  Message.find({}, function (err, msgs) {
    if (err) throw err;
    return res.render("index", {
      messages: msgs,
      user: req.session && req.session.user ? req.session.user : null,
    });
  });
});

セッションに user が入っていればユーザー情報を渡します。
アイコンは画像ファイルを表示させたいので express.static()でフォルダを設定する必要があります。
表示側の views/index.pug も変更します。

body
  if user 
   nav 
    span #{user.username}
    img(src=user.avatar_path)
  else 
    nav
      a(href="/signup") ユーザー登録
      a(href="/login") ログイン

ログイン時は登録したユーザーアイコンが表示されるようになるはずです。

ログアウトの実装

/logout でリンクを押すとログアウトするようにします。

a(href="/logout" class="nav_item") ログアウト

app.js 側でルーティングを追加してセッションを消します。

app.get("/logout", function (req, res, next) {
  delete req.session.user;
  return res.redirect("/");
});

投稿時の認証チェック

非ログイン時に投稿できないようにしたい場合は認証状態のチェック関数を作って指定すればOKです。

app.get("/update", checkAuth, csrfProtection, function (req, res, next) {
	(略)
});

app.post(
  "/update",
  checkAuth,
  fileUpload(),
  csrfProtection,
  function (req, res, next) {
		(略)
  }
);

function checkAuth(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  } else {
    return res.redirect("/login");
  }
}

これでログインしないと投稿できないようになるはずです。

まとめ

Passport を使った Username&Password 認証のやり方について解説しました。
Webアプリの開発では認証機能を付ける場面が多いので何かの時に参考になれば幸いです。

参考

passport - npm

JavaScriptでのWeb開発 ~ Node.js + Express + MongoDB + ReactでWebアプリを開発しよう 〜 その1 〜(改訂版三版)

JavaScriptでのWeb開発 ~ Node.js + Express + MongoDB + ReactでWebアプリを開発しよう ~ その2(iOS対応版)

Discussion