【Node.js Express】Passport.jsを用いて認証機能を実装する。(passportLocalMongooseをプラグイン)

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

###要件
- pasport.jsを用いたパスワード認証を作成する。
- 認証ストラテジにはpassportLocalMongooseをプラグインで使用。
- 入力する値はname email password
- 認証（ログイン、サインアップ）以外のページはexpressデフォルトのホームページ("/")のみ。（そのページに認証をかける。)
 
###アプリケーションに必要なパッケージをインストールする。

前提

・　express-generatorで雛形を作成済。
・　mongooseを用いてmongodbと接続完了。
・　package.json↓ 

```package.json
{
  "name": "passport_app",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "nodemon ./bin/www"
  },
  "dependencies": {
    "body-parser": "^1.19.0", 
    "cookie-parser": "~1.4.4",  //依存
    "express-session": "^1.17.1",  //依存
    "mongoose": "^5.11.9", //依存
    "passport": "^0.4.1", //本体
    "passport-local-mongoose": "^6.0.1",　//本体
    "ejs": "^3.1.5",
    "express": "~4.16.1",
    "express-ejs-layouts": "^2.5.0",
  }
}
```
前提を構築するまでの手順は、下記の記事を参考にしてください。
[【アプリ開発 １】【Node.js express Docker】 Dockerを用いてNode.js Express MongoDB(mongoose)の環境を構築する【2020年12月】]
(https://qiita.com/sho_U/items/43f6483aac8ca45a12f6)

#一旦サインアップからユーザーを登録できるまで作成する。

##とりあえずのビューを（共通、ログイン、サインアップ）作成する。
EJSレイアウトレンダリングをロードし、共通の土台レイアウトを作成する。

```app.js
const layouts = require("express-ejs-layouts");//モジュールロード

app.use(layouts);//ミドルウェアとして組み込む。
```

views/layout.ejs を作成する。
ヘッダーとメインコンテンツだけシンプルな構成

```html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="/style.css">
</head>

<body>
  <header>
    <p>名前：</p>
  </header>

  <div class="main">
    <%- body %>
  </div>

</body>

</html>

```

##ログイン画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/9efaa9fc-b356-0eb4-9537-3f101d4a77ad.png)

views/users/login.ejs

```html
<form action="/users/login" method="POST">
  <label>Email</label>
  <input type="text" name="email" autofocus required>
  <label>Password</label>
  <input type="password" name="password" required>
  <button type="submit">Login</button>
</form>
```

##サインアップ画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/d637e03a-6fb7-6b02-cb0c-5bf54c7bd4d5.png)


view/users/signup.ejs

```html
<form action="/users/create" method="POST">
  <label>name</label>
  <input type="text" name="name" autofocus required>
  <label>Email</label>
  <input type="text" name="email" autofocus required>
  <label>Password</label>
  <input type="password" name="password" required>
  <button type="submit">Singup</button>
</form>
```

express-ejs-layoutsにより、ヘッダーを設定。後ほど名前には、nameを表示させる。

##とりあえずのルーティング（ログイン、サインアップ）を作成する。

```app.js
const usersRouter = require('./routes/users');
//分離ファイルの読み込み

app.use('/users', usersRouter);
// '/users'タッチ時のミドルウェアに分離ファイルを指定
```

```routes/users.js
const express = require('express');
const router = express.Router();
const usersController = require('../controllers/usersController');

router.get('/login', usersController.login);
router.get('/new', usersController.new);
router.post('/create', usersController.create);
//routerに各メソッドと、遷移先を組み込み。

module.exports = router; //routerオブジェクトをエクスポート
```

##とりあえずのコントーラー（ログイン、サインアップ）を作成する。

```controllers/usersController.js

const usersController = {
  login: (req, res) => {
    res.render("../views/users/login", { layout: false });
   //ログイン画面にはヘッダーは不要なので、layoutを表示させない様にしている。
  },
  new: (req, res) => {
    res.render("../views/users/signup", { layout: false });
   //サインアップ画面にはヘッダーは不要なので、layoutを表示させない様にしている。
  },
  create: (req, res) => {
    res.render("/");
  }
}
module.exports = usersController;
```

##とりあえずのモデルを作成する。

```m_user.js
"use strict";

const mongoose = require("mongoose")

const userSchema = new mongoose.Schema({
  name: {  
    type: String,
    required: true,  //必須
    unique: true     //ユニーク
  },
  email: {  
    type: String,
    required: true,  //必須
    unique: true     //ユニーク
  },
  password: {  //nameのバリューにオブジェクトで渡す。
    type: String,
    min: [4, "Too short"]//最小値エラーメッセージ を設定
  },
});

module.exports = mongoose.model("User", userSchema)
```

##サインアップから保存処理。

```usersController.js
const User = require("../models/m_user");

const usersController = {

  //(略)

  new: (req, res) => {
    res.render("../views/users/signup", { layout: false });
  },
  create: (req, res) => {
   User.create({
      name: req.body.name,
      email: req.body.email,
      password: req.body.password
    },
      (error, result) => {
        if (error) { res.redirect("/users/new") }
      }
    )
  }
}
module.exports = usersController;
```

###ここまでの実装
サインアップ画面から、name、email、passwordをMongoDBに保存できる状態。

##Passportを導入する。

app.jsで必要なミドルウェア設定を行う。

```app.js
const passport = require("passport")
const cookieParser = require("cookie-parser");
const expressSession = require("express-session");
//必要なパッケージをロードする。
const User = require("./models/m_user");
//Userモデルをロード

app.use(cookieParser("secret"));
//クッキーの秘密鍵の設定
app.use(
  expressSession({
    secret: "secret",
    cookie: {
      maxAge: 4000000
    },
    resave: false,
    saveUninitialized: false
  })
);
//セッションの設定

app.use(passport.initialize());
//passportを初期化ミドルウェアを組み込む
app.use(passport.session());
//passportに対しセッションを使用するよう指定
app.use(testUser.createStrategy());
//デフォルトのログインストラテジーを設定する。
app.serializeUser(User.serializeUser());
app.deserializeUser(testUser.deserializeUser());
//passportを、ユーザーデータの圧縮・暗号化/復号を行うように設定
```
Userモデルにpassport-local-mongooseプラグインを設定する。

```m_user.js
"use strict";

const mongoose = require("mongoose");
const passportLocalMongoose = require("passport-local-mongoose");
//passport-local-mongooseをモジュールロード

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,  //必須
    unique: true     //ユニーク
  },
  email: {
    type: String,
    required: true,  //必須
    unique: true     //ユニーク
  },
   //passwordパラメータを削除(passportが自動的にハッシュとソルトのパラメーターを追加してくれる）
});

userSchema.plugin(passportLocalMongoose, {//userSchemaにpassportLocalMongooseをプラグイン
  usernameField: "email"
});//ログイン認証に使うパラメーターを指定(デフォルトのusernameから変更)

module.exports = mongoose.model("User", userSchema)
```

##コントローラーのcreateアクションにregisterメソッドを追記

```usersController.js
const User = require("../models/m_user");

const usersController = {
  login: (req, res) => {
    res.render("../views/users/login", { layout: false });
  },
  new: (req, res) => {
    res.render("../views/users/signup", { layout: false });
  },
  create: (req, res, next) => {
    if (req.skip) next();

    const newTestUser = {
      name: req.body.name,
      email: req.body.email,
    };

    User.register(newTestUser, req.body.password, (error, user) => {
    //registerメソッドはpassportLocalMongooseに組み込まれているメソッド。
    //第一引数に、認証以外に保存するパラメータ、第二引数に認証に用いるパラメータ(これがsaltとhash化される。）
      if (user) {
        console.log("success")
        next();
      } else {
        console.log(error.message);
        next();
      }
    })
  }
}
module.exports = usersController;
```

##サインアップから保存確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/901c6998-38d0-1834-c0a8-04000d75098f.png)
パスワードの代わりに、saltとhashが保存されている。

##ログイン時に認証を実装する。

コントローラーに、authenticateメソッドを追加する。

```js
const passport = require("passport");
//passportのロード

  authenticate: passport.authenticate("local", {
  //authenticateメソッドがstrategyから自動的に認証を確認。
    failureRedirect: "/users/login",  //成功時の遷移先
    successRedirect: "/",            //失敗時の遷移先
  }),
```

ルーティングのlogin入力ポスト時のルーティングを設定。

```routes/users.js
router.post("/users/login", usersController.authenticate);
```

ユーザーの状態を保存するミドルウェアを組み込み。

```app.js
app.use((req, res, next) => {
  res.locals.loggedIn = req.isAuthenticated();//ユーザーのログイン状態を req.localsに格納
  res.locals.currentUser = req.user;//ログインユーザー情報を格納
  next();
});
```

##ログイン状態で名前を表示させる。(併せてログアウト遷移ボタンを追加)

```views/layout.ejs
  <header>
    <% if (loggedIn) { %>
      <p>名前：<%= currentUser.name %></p>
      <a href="/users/logout">Log out</a>
      <% } %>
  </header>
```

##ルーティングにロウグアウトの遷移を追記

```routes/users.js
router.get('/logout', usersController.logout);
```

##ログアウト時の挙動をコントローラーに追記

```controllers/usersController.js
  logout: (req, res, next) => {
    req.logout();
    res.redirect("/users/login");
  },
```
##ログイン状態チェックのコントローラーを追記

```controllers/usersController.js
  loginCheck: (req, res, next) => {
    if (!req.isAuthenticated()) {
      res.redirect("/users/login");
    }
    next();
  }
```

## ホームページに認証チェックとしてミドルウェアを埋め込む

```routes/index.js
route.get('/', usersController.loginCheck, function (req, res, next) {
  res.render('index', { title: 'Express' });
});
```


参考：

[passport-local-mongoose - npm](https://www.npmjs.com/package/passport-local-mongoose)
[Passport | 認証](https://knimon-software.github.io/www.passportjs.org/guide/authenticate/)