Passportを使って認証機能を実装する

## 概要

**Passport**、**passport-local**を使ってごく簡単な認証フローを確認してみます。

### Passportの公式サイト

http://www.passportjs.org/

### ソースコード

今回の実装内容のソースコードは以下にあります。

https://github.com/qianer-fengtian/express-passport

### 環境

|ソフトウェア|バージョン|
|:-|:--|
|Node.js|14.15.0|
|npx|6.14.8|
|Express|4.16.1|
|Passport|0.4.1|
|passport-local|1.0.0|

## Expressアプリを作成する

express-generatorを使って今回認証機能を実装するためのExpressアプリを作ります。

```bash
npx express-generator --view=pug express-passport
cd express-passport
yarn install
yarn start
```

## ログインページを作成する

`/login`に対してPOSTリクエストできるページを作成します。

```pug:views/login.pug
extends layout

block content
  h1 ログイン

  form(action="/login" method="POST")
    div
      label(for="username") ユーザ名
      input(type="text" name="username")
    div
      label(for="password") パスワード
      input(type="password" name="password")
    div
      button(type="submit") ログイン
```

## Passportを使って認証APIを作成する

最初に必要なライブラリをインストールします。

```bash
yarn add passport passport-local express-session
```

### Passportの基本

Passportでは、認証を行うのに以下の3つを設定する必要があります。
（セッションは任意ですが、リクエストの度に認証するAPIでもなければ基本的に必要となります）

- ストラテジー（Authentication strategies）
- ミドルウェア（Application Middleware）
- セッション（Sessions）

### ストラテジーを設定する

Passportでは、**ストラテジー**という方法でリクエストを認証します。

ストラテジーは今回実装するユーザ名・パスワードの組み合わせの検証（passport-local）のほか、**OAuth**を使った委任認証、**OpenID**を使った連携認証などいろいろあります。

また、ストラテジーはリクエストの認証に使われるよりも前に設定しておく必要があります。
今回は`app.js`に以下のような感じで実装しました。

```javascript:app.js
const passport = require('passport');
const LocalStorategy = require('passport-local').Strategy;

const users = [
  { username: 'alice', password: 'alice', age: 22 },
  { username: 'bob', password: 'bob', age: 21 },
  { username: 'carol', password: 'carol', age: 30 }
]

const User = {
  findOne({ username }) {
    return users.find(user => user.username === username) || null
  }
}

passport.use(new LocalStorategy(function(username, password, done) {
  try {
    const user = User.findOne({ username })
    if (user == null) {
      return done(null, false)
    }
    if (user.password !== password) {
      return done(null, false)
    }
    delete user.password
    return done(null, user)
  } catch (err) {
    done(err)
  }
}))
```

`LocalStorategy`の引数として渡された関数は**verify callback**と呼ばれ、ここでは与えられた認証情報（credentials）を有するユーザを検索して認証情報が妥当かどうか検証しています。
認証の結果に応じて`done()`関数に対して以下のように引数を渡しています。

|認証結果|第1引数|第2引数|
|:--:|:--|:--|
|成功|null|認証されたユーザオブジェクト|
|失敗|null|false|
|サーバエラー|Errorオブジェクト|-|

サーバ側でエラーが発生した（例えばDBに接続できないなど）場合、上記のように第1引数にエラーを入れておきます。クライアント側のエラーであれば基本的にエラーは入れず、認証失敗として`done(null, false)`とします。

#### `passport.initialize()`ミドルウェアで初期化する

永続的なセッションが必要なら**express-session**ミドルウェア及び`passport.session()`を使う必要があります。

```javascript:app.js
app.use(session({
  secret: "cats",
  resave: false,
  saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
```

ちなみに公式サイトでは、以下のように**body-parser**をインストールしていますが、

```javascript
app.use(bodyParser.urlencoded({ extended: false }));
```

v4.16.0以降のExpress.jsでは標準搭載されているそうです。

参考: [Body-ParserがExpressにexpress.json()として標準搭載されている話 - Qiita](https://qiita.com/atlansien/items/c587a0bf2f7f9022107c)

今回はexpress-generatorで作成しているので既に必要な記述はされています。

```javascript
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
```

#### ユーザのシリアライズ・デシリアライズ

永続的なセッションが必要な場合、セッションに対してユーザインスタンスをシリアライズして保存し、リクエストが来たときにセッションからユーザインスタンスをデシリアライズして保存できるようにします。

```javascript:app.js
passport.serializeUser(function(user, done) {
  done(null, user.username);
});

passport.deserializeUser(function(username, done) {
  const user = User.findOne({ username })
  done(null, user);
});

```

なるべく保存されるセッション内のデータ量を小さくする為にここでは`user.username`だけ入れています。

### 認証APIを作成する

```javascript:routes/index.js
router.post('/login', passport.authenticate('local', {
  successRedirect: '/',
  failureRedirect: '/login',
}))

router.post('/logout', function(req, res, next) {
  req.logOut();
  res.redirect('/login');
})
```

### 認証チェックミドルウェアを作成する

認証チェックミドルウェアを作って、アクセスを制限したいパスに対して付与します。

```javascript:middleware/authenticated.js
module.exports = function (req, res, next) {
  if (!req.isAuthenticated()) {
    res.redirect('/login')
    return
  }
  next()
}
```

こんな感じのミドルウェアを作って、例えば`/`に付与すると、認証されていなければアクセスすることができないようになります。

```javascript:routes/index.js
const authenticated = require('../middleware/authenticated');

router.get('/', authenticated, function(req, res, next) {
  res.render('index', { title: 'Express', username: req.user.username });
});
```

## 動作確認

```bash
yarn start
```

http://localhost:3000 にアクセスし、認証されていないので`/login`に遷移することを確認。

適当なユーザでログインしてクッキーがセットされることを確認します。

```http
HTTP/1.1 302 Found
Set-Cookie: connect.sid=*****; Path=/; HttpOnly
```

## 補足

password-localではユーザ名とパスワードのプロパティ名がデフォルトで`username`、`password`ですが、変更する場合はストラテジーを設定する際に以下のように設定を追加します。

```javascript
passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'passwd'
  },
  function(username, password, done) {
    // ...
  }
));
```

## 参考

- [Passport.js](http://www.passportjs.org/)
- [passport-local](http://www.passportjs.org/packages/passport-local/)
-  [Body-ParserがExpressにexpress.json()として標準搭載されている話 - Qiita](https://qiita.com/atlansien/items/c587a0bf2f7f9022107c)