Passport.js の基本的な利用方法 (Node.js & Express)

# はじめに
## 本記事の目的
* Node.jsで一般的に利用される認証ミドルである Passport.js の動作を理解すること
* 公式サイトのドキュメントがわかりづらいので、Stepごとに理解できるように進めていく。

## Passportの動作の基本的な概念

* まずPassportをミドルウェアとして動作させる場合、HTTPリクエストの body にしかるべきラベルで IDとパスワードを入れてやる必要がある。デフォルトでは以下の通り。
 * `body: { username: 'andre', password: '12345' }`
 * これが仮に `{ name: 'andre', pass: '12345'}` のようになっていると、デフォルトでは動作しない。
* LocalStrategyのコールバック関数は、`done()`のコールバックを返すようにしていれば実装方法は任意でよく、とにかく与えられたIDとパスワードが正しいか（IDがある、パスワードが正しい、など）判断し、`done()`を返せばいい。
* 一般的に `app.post`でのミドルウェアの例が多いが、`router.post`でのミドルウェアとしても動作する。

## 前提環境
|ソフトウェア  |バージョン  |
|---|---|
|Node.js  |v10.15.3  |
|express  |4.16.4  |
|Passport|0.4.0|

# その1. 最小限の実装

* 極小の実装（セッション非利用、ユーザデータは埋め込み）で作成。
* `app.post`ではなく、`router.post`でやっている。
* Passportに渡すパラメータが正しいか確認するため、リクエストボディをコンソールに表示する。

## 挙動の解説

* express から、`http://localhost:3000/` へのリクエストを indexルータ で処理。
* passportにパラメータを渡すため、`app.use(express.urlencoded( { extended: false }))`が必要。
* 認証が成功した場合、passportミドルウェアはリクエストヘッダに`req.user`をセットし、次のコールバックに処理を渡す。
* 動作を確認するのが目的なので、認証失敗時のページは実装していない。

## 実装
```index.js
const express = require('express');
const router = express.Router();

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

const User1 = {
  name: "Taro",
  password: "Taro123"
};


passport.use(new LocalStrategy(
  (username, password, done) => {
    
    if(username !== User1.name){
      // Error
      return done(null, false);
    } else if(password !== User1.password) {
      // Error
      return done(null, false);
    } else {
      // Success and return user information.
      return done(null, { username: username, password: password});
    }
  }
));

router.use(passport.initialize());


router.get('/', (req, res) => res.render('index'));

router.post('/',
  (req, res, next) => {
    console.log(req);
    next();
  },
  passport.authenticate('local',
    {
      session: false,
      failureRedirect : '/failure'
    }
  ),
  (req, res) => {
    console.log(req.user);
    res.send('Success');
  }
);

module.exports = router;
```

* 参考に app.js と index.hbs も掲載する。

```app.js
const express = require('express');
const app = express();

app.set('view engine', 'hbs');

app.use(express.urlencoded( { extended: false }));
app.use('/', require('./routes/index'));


app.listen(3000, console.log('Server listening port 3000...'));
```

```index.hbs
<!DOCTYPE html>
<body>
  <form action="/" method="post">
    <input type="text" name="username" id="">
    <input type="password" name="password" id="">
    <input type="submit" value="Try Auth">
  </form>
</body>
</html>
```

以上となる。


# その2. セッションを利用する

* 次に、`express-session`を導入し一般的な利用方法に近づける。

## 動作概念

* Passport は認証が成功すると、セッション内にオブジェクト`passport: { user : obj }`を作成する。この動作をシリアライズという。
 * シリアライズ時のコールバックに渡される第1引数は、認証成功時に`done()`で返却したユーザ情報がそのまま渡る。
* httpリクエストがあった際に、セッション内のオブジェクトが`undefined`でなければ、req.userにオブジェクトを格納しルータに引き渡す。この動作をデシリアライズという。
* オブジェクトの形式は任意でよく、StringでもJSONでもいい。

## 実装

* シリアライズするオブジェクトは、`{ username: String, password: String }`としている。
* ログアウトの動作として、セッション内のユーザ情報を`undefined`にしている。

```app.js
const express = require('express');
const session = require('express-session');

const app = express();

app.set('view engine', 'hbs');

app.use(express.urlencoded( { extended: false }));
app.use(session({
  secret: 'keyboard cat',
  resave: true,
  saveUninitialized: false,
}));
app.use('/', require('./routes/index'));


app.listen(3000, console.log('Server listening port 3000...'));
```

```index.js
const express = require('express');
const router = express.Router();

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

const User1 = {
  name: "Taro",
  password: "Taro123"
};


passport.use(new LocalStrategy(
  (username, password, done) => {
    
    if(username !== User1.name){
      // Error
      return done(null, false);
    } else if(password !== User1.password) {
      // Error
      return done(null, false);
    } else {
      // Success and return user information.
      return done(null, { username: username, password: password});
    }
  }
));

passport.serializeUser((user, done) => {
  console.log('Serialize ...');
  done(null, user);
});

passport.deserializeUser((user, done) => {
  console.log('Deserialize ...');
  done(null, user);
});

router.use(passport.initialize());
router.use(passport.session());


router.get('/', (req, res) => {
  console.log(req.session);
  res.render('index', {user: req.user});
});

router.get('/failure', (req, res) => {
  console.log(req.session);
  res.send('Failure');
});

router.get('/success', (req, res) => {
  console.log(req.session);
  res.redirect('/');
});

router.post('/',
  passport.authenticate('local',
    {
      failureRedirect : '/failure',
      successRedirect : '/success'
    }
  )
);

router.post('/logout', (req, res) => {
  req.session.passport.user = undefined;
  res.redirect('/');
});

module.exports = router;
```

```index.hbs
<!DOCTYPE html>
<body>
  {{#if user}}
    <h2>Hi {{user.username}}!! You are now logged in.</h2>
    <form action="/logout" method="post">
      <input type="submit" value="LOGOUT">
    </form>
    <hr>
  {{/if}}

  <form action="/" method="post">
    <input type="text" name="username" id="">
    <input type="password" name="password" id="">
    <input type="submit" value="Try Auth">
  </form>
</body>
</html>
```

# その3. flashを利用する

## 動作概念

* flashは一度表示、厳密には一度`req.flash( '' )`が実行されるまで、セッション内にメッセージを蓄積できる機能。
* LocalStrategy内の認証失敗時の`done()`の第3引数に `{ message : String }` を渡すと、flashメッセージが蓄積され、`req.flash('error')`で呼び出せる。
    * メッセージのラベルは自動的に`error`になる点に注意。

## 実装

* 再掲部分が多いので、差分にコメントを入れる。app.js は変更なし。
* 認証失敗時のページでflashメッセージを表示するように変更した。

```index.js
const express = require('express');
const router = express.Router();

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const flash = require('connect-flash'); //変更

const User1 = {
  name: "Taro",
  password: "Taro123"
};


passport.use(new LocalStrategy(
  (username, password, done) => {
    
    if(username !== User1.name){
      // Error
      return done(null, false, { message : 'User does not exist' }); //変更
    } else if(password !== User1.password) {
      // Error
      return done(null, false, { message : 'Password incorrect' }); //変更
    } else {
      // Success and return user information.
      return done(null, { username: username, password: password});
    }
  }
));

passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((user, done) => {
  done(null, user);
});

router.use(passport.initialize());
router.use(passport.session());
router.use(flash()); //変更


router.get('/', (req, res) => {
  console.log(req.session);
  res.render('index', {user: req.user});
});

router.get('/failure', (req, res) => {
  console.log(req.session);
  res.render('index', { message: req.flash( 'error' )}); //変更
});

router.get('/success', (req, res) => {
  console.log(req.session);
  res.redirect('/');
});

router.post('/',
  passport.authenticate('local',
    {
      failureRedirect : '/failure',
      successRedirect : '/success',
      failureFlash: true   //変更
    }
  )
);

router.post('/logout', (req, res) => {
  req.session.passport.user = undefined;
  res.redirect('/');
});

module.exports = router;
```

```index.hbs
<!DOCTYPE html>
<body>
  {{#if user}}
    <h2>Hi {{user.username}}!! You are now logged in.</h2>
    <form action="/logout" method="post">
      <input type="submit" value="LOGOUT">
    </form>
    <hr>
  {{/if}}
  {{#if message}}
    <p>{{message}}</p>
    <hr>
  {{/if}}

  <form action="/" method="post">
    <input type="text" name="username" id="">
    <input type="password" name="password" id="">
    <input type="submit" value="Try Auth">
  </form>
</body>
</html>
```

以上！

# 更新
* ログアウト処理で、`req.session.passport.user = undefined;` としたが、`req.logout();`が標準で提供される