passport localでmongodbのユーザーコレクションを認証

#はじめに
<http://www.passportjs.org/>でのpassport-localの解説が個人的にわかりにくいように感じたのでできる限りシンプルなログインおよびログイン状態の維持、未ログイン時に任意のルートへのアクセスをリダイレクトする方法を載せようと思いました。

#前提知識
passport.jsを使おうと思う人がこれらを全く知らないことはないと思いますが、以下の知識は最低限あると仮定して書きます。

* node
* express
* mongoose

#npmパッケージ

今回のサンプルに使用するnpmパッケージは以下の通りです。

```package.json
"dependencies": {
    "body-parser": "^1.18.3",
    "cookie-parser": "^1.4.4",
    "express": "^4.16.4",
    "express-session": "^1.15.6",
    "mongoose": "^5.4.15",
    "passport": "^0.4.0",
    "passport-local": "^1.0.0"
  }
```

#基礎的なルーティング

とりあえずルートを設定します。
index.htmlには/adminへのパスがありますが、後でログイン状態でなければルートへ戻されるようにします。

```server.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const passport = require('passport');
const { Strategy } = require('passport-local'); 
const mongoose = require('mongoose');
const session = require('express-session');
const cookieParser = require('cookie-parser');

const server = express();
const port = process.env.PORT || 8080;

server.get('/', (req, res) => {
    res.sendFile(path.join(__dirname + '/index.html/');
});

server.listen(port);
```

```index.html
<!DOCTYPE html>
<html>
    <body>
        <form action="/admin" method="POST">
            <input type="text" name="username" placeholder="user name">
            <input type="password" name="password" placeholder="password">
            <button>login</button>
        </form>
    </body>
</html>
```

#ユーザーデータベースの作成
```user.json
{
  "username": "test",
  "password": "test"
}

```
シンプルに済ませたいので上記のドキュメントを"database"というデータベース、"users"コレクションに作成します。
今回はテストですので、リスクも気にせずログインのためのURIはハードコードすることにします。
データベースURIの例

```uri.txt
mongodb://<dbuser>:<dbpassword>@ds00000.mlab.com:61335/sample
```

#パスポートとセッションの設定
ログインのための認証ストラテジーと、ログイン状態維持のためのセッションの設定をしていきます。
以下をserver.jsに追加します。

```server.js

mongoose.connect(YOUR_DATABASE_URI,{useNewUrlParser: true});
const db =  mongoose.connection;
const Schema = mongoose.Schema;

const userSchema = new Schema({
    username: String,
    password: String
});

db.on('error', console.error.bind(console, 'connection error:'));

server.use(bodyParser.urlencoded({extended: true}));
//body-parserはrequest streamをまるまる抽出してreq.bodyとして扱えるようにするミドルウェア。
//後でindex.htmlのformから受け取った値を認証するための関数に渡すために必要。

server.use(cookieParser());//cookieをparse
server.use(session({ resave: false, saveUninitialized:false, secret: 'something quite long and nonsense',
    cookie: {
        secure: false,
        maxAge: 3600000
    }
}));//expressアプリケーションにおいてセッションを用いることの宣言。
//これは下記のpassport.sessionより上位になければ正常に動作しない。
//secretは長くてあなたのオリジナルであればなんでもいい。

server.use(passport.initialize());//passport初期化のためのミドルウェア
server.use(passport.session());//cookieによるセッション維持を可能にするためのミドルウェア

passport.use(new Strategy //認証のための関数の宣言
    (async (username, password, done) => {
        try{
            await db.model('database', userSchema, 'users')
            .findOne({username: username}, (err, user) => {
                if(err){
                    return done(err);
                }
                if(!user){
                    return done(null, false);
                }
                if(user.toObject().password != password){
                    return done(null, false);
                }
                return done(null, user);
            })
        } catch(err) {
            console.log(err);
        }
    }
));
```
done()とは認証の終了後に呼び出されるコールバックです。
認証が成功した際にはドキュメントのオブジェクトを返し、失敗した際にはfalseを返すのがいいでしょう。
またそもそも認証以前にエラーがあった際の分岐も入れておくのが推奨されます。

これで認証するためのひな型ができました。
それでは実際に認証を通して/adminへ行くこと、認証に失敗すればルートへリダイレクトされること、および認証されていない状態ではadminからはリダイレクトされる処理へ進みましょう。

#認証
```server.js
server.post('/admin', passport.authenticate('local', {failureRedirect:'/' }),
    (req, res) => {
        res.send('Success!');
    }
);

server.get('/admin',(req, res) => {
    if(!req.user){//下記のデシリアライズ処理でユーザー情報がreq streamにあるか判断する
        res.redirect('/');
    }
    else{
        res.send('still logged in!')
    }
});

passport.serializeUser( (user, cb) => {
    cb(null, user);
});//認証に成功しセッションが確立されたら
//ブラウザのcookieに保存させるため情報をバイトコードに変換する処理

passport.deserializeUser( async (id, cb) => {
    try{
        await db.model('database', userSchema, 'users').findById(id, (err, user) => {
            cb(err, user);
        })
    } catch(err) {
        console.log(err);
    }
});
//passportミドルウェアより必要に応じてユーザー情報を最並列化して返す処理
//req.userでリクエストから情報を受け渡せるようになる

```

#server.jsの最終的な全体

```server.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const passport = require('passport');
const { Strategy } = require('passport-local'); 
const mongoose = require('mongoose');
const session = require('express-session');
const cookieParser = require('cookie-parser');

const server = express();
const port = process.env.PORT || 8080;

mongoose.connect(YOUR_DATABASE_URI,{useNewUrlParser: true});
const db =  mongoose.connection;
const Schema = mongoose.Schema;

const userSchema = new Schema({
    username: String,
    password: String
});

db.on('error', console.error.bind(console, 'connection error:'));

server.use(bodyParser.urlencoded({extended: true}));
server.use(cookieParser());
server.use(session({ resave: false, saveUninitialized:false, secret: 'something quite long and nonsense',
    cookie: {
        secure: false,
        maxAge: 3600000
    }
}));

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

passport.use(new Strategy 
    (async (username, password, done) => {
        try{
            await db.model('database', userSchema, 'users')
            .findOne({username: username}, (err, user) => {
                if(err){
                    return done(err);
                }
                if(!user){
                    return done(null, false);
                }
                if(user.toObject().password != password){
                    return done(null, false);
                }
                return done(null, user);
            })
        } catch(err) {
            console.log(err);
        }
    }
));

server.get('/', (req, res) => {
    res.sendFile(path.join(__dirname + '/index.html/');
});

server.post('/admin', passport.authenticate('local', {failureRedirect:'/' }),
    (req, res) => {
        res.send('Success!');
    }
);

server.get('/admin',(req, res) => {
    if(!req.user){
        res.redirect('/');
    }
    else{
        res.send('still logged in!')
    }
});

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

passport.deserializeUser( async (id, cb) => {
    try{
        await db.model('database', userSchema, 'users').findById(id, (err, user) => {
            cb(err, user);
        })
    } catch(err) {
        console.log(err);
    }
});

server.listen(port);

```

#最後に
passport-localによるログインは成功してみればとても簡単なのですが、どの言語やどのフレームワークでもそうかもしれませんが、慣れるまではずっと仕様書をたらいまわしにされている気分で目が回りますし理解できない自分がみじめになるものです。
一度でも自分の手で動作させることに成功してから読むほうがずっとわかりやすくなってより深い興味と理解につながると思います。
これからpassport-localを触ってみる人の助けに少しでもなればうれしいです。