[認証]SNSのテンプレートを作成してみた[解説編]

# はじめに
　SNSテンプレートの完成版はすでにGitHubのリポジトリに上げています。
　この記事ではそのリポジトリを元に、node.jsとexpressとpassportを使用した認証機能付きのwebサイトの作り方を紹介します。なお、環境作成としてリポジトリに上げているmongodbとnginxの解説はほとんど行いませんので悪しからず。

リポジトリのクローンなどを雑に解説したヤツ

https://qiita.com/king_dog_fun/items/fb833a71115199c02c39

# serverのディレクトリ構成

　express-generatorで生成される雛型を元に構成しています。大きな変更点と言えばconfigディレクトリの追加とpassport.jsの作成でしょうか。
　express-generatorのインストールと実行は次のコマンドで行うことが出来ます。試してみてね☆

```
npm install express-generator -g
express --view=pug directory_name
```

　ちーなーテンプレートエンジンはpugです。
　以下に書いてある以外にもファイルは存在しているのですが、認証という処理に特別大事そうなファイルだけをピックアップしています。

```
.
├── bin
├── config
│   └── passport.js
├── public
│   └── stylesheets
│       ├── signin.css
│       └── signup.css
├── routes
│   ├── auth.js
│   └── auth
│       ├── signin.js
│       ├── signup.js
│       ├── signout.js
├── views
│   ├── signin.pug
│   └── signup.pug
├── mongo.js
└── app.js
```

# モジュールのインストールについて

## エラー

```
npm install passport
```

　現バージョン(2022/08/11)のv4.5.1において、passportはどうやら一部のメソッドでエラーが発生するようです。エラーメッセージはこんな感じ。

```
server | TypeError: req.session.regenerate is not a function
server | at SessionManager.logIn (/usr/src/server/node_modules/passport/lib/sessionmanager.js:28:15)
server | at req.login.req.logIn (/usr/src/server/node_modules/passport/lib/http/request.js:39:26)
server | at strategy.success (/usr/src/server/node_modules/passport/lib/middleware/authenticate.js:256:13)
server | at verified (/usr/src/server/node_modules/passport-local/lib/strategy.js:83:10)
server | at Strategy._verify (/usr/src/server/config/passport.js:48:11)
```

[海外のディスカッション](https://stackoverflow.com/questions/72375564/typeerror-req-session-regenerate-is-not-a-function-using-passport)を参考にpassportのバージョンを下げます。

## passportのインストール

```
npm install passport@0.5
```

　参考にしたまんま0.5まで下げていますが、どの程度下げれば問題がなくなるのかは調査しておりません。調査したいという人はやってみて、結果が出れば僕にも教えてくださいお願いします！

# app.js

```
.
└── app.js
```

　app.jsの変更点です。
　app.jsのファイルは見やすさのために極力変更するのを避けています。そのため次のような編集がされています。

```app.js
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// authorization
require("./config/passport")(app); //<--変更点

app.use('/', indexRouter);
app.use('/users', usersRouter);
```

　一行追加するだけです。
　他プログラムはすべてconfigディレクトリのpassport.jsにまとめてあります。綺麗なプログラムを書くための小技みたいな感じですね。

# passport.js

```
.
├── config
│   └── passport.js
```

```javascript:config/passport.js
const passport = require("passport");
const LocalStrategy = require("passport-local");
const bcrypt = require("bcrypt");
const cookieSession = require("cookie-session");
const mongo = require('../mongo.js');
const secret = "tekitounaSECRET";

const col = mongo.col('user');

module.exports = function (app) {
  passport.serializeUser(function (user, done) {
    done(null, user._id);
  });

  passport.deserializeUser(async function (id, done) {
    try {
      let query = { _id:id };
      let options = {
        projection:{ _id:1 ,}
      }
      const user = await col.findOne(query,options);
      done(null, user);
    } catch (error) {
      done(error, null);
    }
  });

  passport.use(new LocalStrategy({
      usernameField: "username",
      passwordField: "password",
    }, async function (username, password, done) {
      let query = { _id:username };
      let options = {
        projection:{ _id:1 , password:1 ,}
      }
      try{
        let user = await col.findOne(query,options);
        if(!user){
  	      return done(null,false);
        }else if(await bcrypt.compare(password,user.password)){
	      return done(null,user);
        }else{
 	      return done(null, false);
        }
      }catch(err){
        console.error(err);
        return done(err);
      }
    }
  ));

  app.use(
    cookieSession({
      name: "session",
      keys: [secret],

      // Cookie Options
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    })
  );

  app.use(passport.session());
};
```

　ここではpassportのミドルウェアとセッションを設定しています。
　全部解説なんてしてたらきりがないので、passportに関連するところだけ解説していきましょうか。

## serializeUser

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

　serializeUserはLocalStrategyのdoneによって起動し、**セッションに情報を格納すること**が出来ます。

## deserializeUser

```javascript
  passport.deserializeUser(async function (id, done) {
    try {
      let query = { _id:id };
      let options = {
        projection:{ _id:1 ,}
      }
      const user = await col.findOne(query,options);
      done(null, user);
    } catch (error) {
      done(error, null);
    }
  });
```

　deserializeUserでは**セッションからユーザー情報を取得すること**が出来ます。セッションについて多く変更をしていなければserializeUserで格納した情報を使用します。
　ただこのdoneのuserがどこでどのように使用されているのか僕はよく分かっていないのは秘密。

## LocalStrategy

```javascript
  passport.use(new LocalStrategy({
      usernameField: "username",
      passwordField: "password",
    }, async function (username, password, done) {
      let query = { _id:username };
      let options = {
        projection:{ _id:1 , password:1 ,}
      }
      try{
        let user = await col.findOne(query,options);
        if(!user){
  	      return done(null,false);
        }else if(await bcrypt.compare(password,user.password)){
	      return done(null,user);
        }else{
 	      return done(null, false);
        }
      }catch(err){
        console.error(err);
        return done(err);
      }
    }
  ));
```

　post通信で送られてきた情報を頼りにsignin出来るかどうかを判断します。
　mongodb(mongooseでもmysqlでも何でも)にあるユーザー情報は予め何かしらの方法で保存しておく必要があります。「モジュールだったらそこらへんの出来るようにしておいてくれないかな」と思った要素ですが、まぁ自由度を下げるのも問題なのでしょうね。
　今回はユーザー情報の保存にroutes/signup.jsで簡単なプログラムを仕込んでいます。

# routes

```
.
├── routes
│   ├── auth.js
│   └── auth
│       ├── signin.js
│       ├── signup.js
│       ├── signout.js
```

## auth.js

```javascript:auth.js
var express = require('express');
var router = express.Router();

var upRouter = require('./auth/signup');
var inRouter = require('./auth/signin');
var outRouter = require('./auth/signout');

router.use('/signup', upRouter);
router.use('/signin', inRouter);
router.use('/signout', outRouter);

module.exports = router;
```

　routesのルート。
　routesディレクトリ内のファイルを出来る限り増やし過ぎたくないということで用意したファイル。

server/auth/signin
server/auth/signup
server/auth/signout

　という感じのURLとなる。


## signup.js

```javascript:auth/signup.js
var express = require('express');
var router = express.Router();
var bcrypt = require('bcrypt');
const mongo = require('../../mongo.js');

const rounds = 8;

const col = mongo.col('user');

router.get('/', function(req, res){
  res.render("signup");
});

router.post('/', async function(req, res){
  let username = req.body.username;
  let password = req.body.password;
  if(!username && !password){
    res.send('input is empty');
    return;
  }
  let one = await col.findOne({_id:username});
  if(one){
    res.send('error : username is already in use');
    return;
  }
  let hash = await bcrypt.hash(password, rounds);
  let query = { _id:username, password:hash }
  col.insertOne(query);
  res.send('successfull');
  // user登録が完了
});


module.exports = router;
```

　ユーザー情報を登録するためのsignup関数。
　usernameという名のidとpassword以外は登録していませんが、SNSではもっと色々と登録するものだと思います。例えば誕生日とか？お好きなように改造してくださいな。
　そもそもメール認証などもしていないので、必要であれば改造してください。

## signin.js

```javascript:auth/signin.js
var express = require('express');
var router = express.Router();
const passport = require("passport");

router.get('/', function(req, res){
  res.render("signin");
});

router.post('/', function(req, res, next) {
  passport.authenticate('local', function(err, user, info) {
    if (err) { return next(err); }
    if (!user) { return res.redirect('/auth/signin'); }
    req.logIn(user, function(err) {
      if (err) { return next(err); }
      return res.send('signin complete');
    });
  })(req, res, next);
});

module.exports = router;
```

　passportの方で設定したLocalStrategyとserializeUserのミドルウェアによる認証です。
　方法としてはいくつもあるようですが、テンプレートということも考えて編集の幅がありそうな認証の方法を選びました。

## signout.js

```javascript:auth/signout.js
var express = require('express');
var router = express.Router();

router.get('/', function (req, res, next) {
  req.logout();
  res.redirect("/");
});

module.exports = router;
```

　他のメソッドと違ってかなり簡潔ですね。
　それもそのはずでただセッションをデストローイ！しているだけです。

# 終わりに
　passportのプログラムとして大事なところをざつ～に解説しました。
　自分にとって分からなかった部分だけを解説したので他の人にとっては分かり辛かったかもしれませんが、分からなかったら声をかけて頂ければ出来る限りはお教えします。
　まぁ、見てもらったなら分かるかもしれませんが、教えれられほどに知識が深くもないのですがｗ
　何かしら改良点などあればGitHubの方でもTwitterの方でも教えていただければ幸いです。

Twitter:

https://twitter.com/dog_king_fun

GitHub:

https://github.com/DogKingFun/sns_template