passport入門

## やったこと

今まで一からアプリを作ったことがなかったのですが、ついに一から作ってみましょうって言われました。
ログイン周りはpassportを使うのがいいとのことだったので使おうとしたけどどこで何してるかわかんないし
もうとにかくパッパラパーだったので大まかなに必要な処理と何をしているかをまとめてみました。
あまり自信がないので情報に誤りがありましたら是非教えてください。

(今回、ユーザー情報はAWSのdynamoDBに保存しています。)

## 認証機能の実装に必要なモジュール

* passport
* passport-local

## 認証機能の実装
今回は基本的なログインとログアウトの処理を書いています
### ログイン

`app.js`に passport の処理を記載する必要があります。

ただ`app.js`内に全ての処理を書くと可読性が大変悪いのでいくつかのファイルに分けて処理を実装させましょう。

処理の内容については随所にコメントがあるので参考にしてください

```app.js
const Authenticator = require("./services/auth/authenticator");
const connectFlash = require("connect-flash");

// アプリ自体のsessionの設定
app.use(
  session({
    cookie: { maxAge: 1000 * 60 * 60 * 24 },
    secret: "woot",
    resave: false,
    saveUninitialized: false
  })
);

// req.flashを使うために必要
app.use(connectFlash());

// passportの初期化
Authenticator.initialize(app);
// 認証情報を持たせる
Authenticator.setStrategy();
```

```services/auth/authenticator.js
const passport = require("passport");
const LocalStrategy = require("passport-local");
const authSetting = require("../config/auth.json");
const Users = require("./userDataGetter");

class Authenticator {
  static initialize(app) {
    // passportの初期化
    app.use(passport.initialize());

    // セッション管理をするための設定
    app.use(passport.session());

    // ログイン成功後指定されたオブジェクトをシリアライズして保存する際の
    // シリアライズ処理をフックするもの
    passport.serializeUser((user, done) => {
      return done(null, user);
    });

    passport.deserializeUser((serializedUser, done) => {
      // dynamoDBから指定したユーザIDの情報を取得する
      Users.get(serializedUser.user_id)
        .then(user => {
          // 認証に成功したら以下のものを返す(今回はユーザIDとユーザ名、権限)
          return done(null, {
            user_id: user.user_id,
            user_type: user.user_type,
            user_name: user.user_name
          });
        })
        .catch(() => {
          return done(null, false);
        });
    });
  }

  static setStrategy() {
    // passport.use：ストラテジーの設定
    // ストラテジー：ユーザIDとパスワードを用いた懸賞やOAuthを用いた権限付与、OpenIDを用いた分散認証を実装する
    // localStrategy：ユーザIDとパスワードを用いた認証の実装部分
    passport.use(
      new LocalStrategy(
        {
          usernameField: authSetting.usernameField,
          passwordField: authSetting.passwordField,
          passReqToCallback: true
        },
        (req, userID, password, done) => {
          Users.authorize(userID, password)
            .then(userIdInformation => {
              // 認証に成功したらユーザ情報を返す
              return done(null, userIdInformation);
            })
            .catch(err => {
              // 認証に失敗したらfalseを返し、req.flashを使ってエラーメッセージを表示させる
              req.flash("login_error", err);
              return done(null, false);
            });
        }
      )
    );
  }

  static authenticate(req, res, next) {
    // 認証情報を参照
    passport.authenticate(authSetting.strategyName, {
      // ログインに成功した時のリダイレクト先
      successRedirect: Authenticator.redirect.success,
      // ログインに失敗した時のリダイレクト先と表示メッセージ
      failureRedirect: Authenticator.redirect.failure,
      failureFlash: "メールアドレスまたはパスワードに誤りがあります"
    })(req, res, next);
  }

  // 認証が完了しているか確認(routes内で使う)
  static isAuthenticated(req, res, next) {
    // 認証が完了している時は次の処理に進む
    if(req.isAuthentocated()) {
      return next();
    } else {
      // 認証が終わっていなかったらログイン画面にリダイレクトする
      return res.redirect(Authenticator.redirect.failure);
    }
  }
}

module.exports = Authenticator;

Authenticator.redirect = {
  success: "/",
  failure: "/login",
  permission: "/"
};
```

```auth.json
{
  "usernameField": "userID",
  "passwordField": "password",
  "strategyName": "local"
}
```

```services/auth/userDataGetter.js
const aws = require("aws-sdk");
const bcrypt = require("bcryptjs");
const cryptConfig = require("../../config/crypto.json");
const salt = cryptConfig.salt;
const dynamodb = new aws.DynamoDB.DocumentClient({ region: "ap-northeast-1" });
const moment = require("moment");
const co = require("co");

class Users {
  constructor() {
    this.KeySchema = [];
    this.notFoundMessage = "レコードが見つかりませんでした";
  }

  getHashKey(keySchemaObject = this.KeySchema) {
    for (const keySchema of keySchemaObject) {
      if (keySchema.KeyType === Users.Keytypes.HASH) {
        return keySchema.AttributeName;
      }
    }
    return "";
  }

  getRangeKey(keySchemaObject = this.KeySchema) {
    for (const keySchema of keySchemaObject) {
      if (keySchema.KeyType === Users.Keytypes.RANGE) {
        return keySchema.AttributeName;
      }
    }
    return "";
  }
  // 最終ログイン時刻の更新
  updateLastLogin(userId) {
    const key = {
      user_id: userId
    };
    const lastLogin = String(moment().format("YYYY-MM-DDTHH:mm:ss.SSSZZ"));
    const item = {
      last_login: lastLogin
    };
    return this.update(key, item);
  }

  update(key, item) {
    const params = {
      TableName: "app-user",
      Key: key,
      ReturnValues: "UPDATED_NEW",
      UpdateExpression: "set #lastLogin = :lastLogin",
      ExpressionAttributeNames: {
        "#lastLogin": "last_login"
      },
      ExpressionAttributeValues: {
        ":lastLogin": item.last_login
      }
    };
    return new Promise((resolve, reject) => {
      dynamodb.update(params, (err, data) => {
        if (err) {
          return reject(err);
        } else {
          return resolve(data.Attributes);
        }
      });
    });
  }

  get(hashKey, rangeKey = null) {
    const Key = {};
    Key[this.getHashKey()] = hashKey;
    if (rangeKey !== null && this.getRangeKey()) {
      Key[this.getRangeKey()] = rangeKey;
    }
    const params = {
      TableName: "app-user",
      Key: {
        user_id: hashKey
      }
    };
    return new Promise((resolve, reject) => {
      dynamodb.get(params, (err, data) => {
        if (err) {
          return reject(err);
        } else if (!data.Item) {
          // getの処理はできたけどItemが存在しない = 合致するuser_idがなかったら
          // エラーメッセージを返す
          return reject(this.notFoundMessage);
        } else {
          // 合致するuser_idが存在したらユーザ情報を返す
          return resolve(data.Item);
        }
      });
    });
  }

  authorize(userId, password) {
    return new Promise((resolve, reject) => {
      // coの中でthisを使うと別の場所を見にいくため変数に置く必要がある
      const self = this;
      co(function*() {
        const user = yield self.get(userId);
        // 入力したパスワードをハッシュで暗号化したものとuserテーブルにある暗号化されたパスワードを比較
        // パスワードが一致しなかったら強制的にcatchに移動
        if (user.password !== bcrypt.hashSync(password, salt)) {
          return reject("パスワードが一致しません");
        }
        // パスワードが一致している場合は最終ログイン時刻の更新
        yield self.updateLastLogin(user.user_id);
        const userInfo = {
          user_id: user.user_id,
          user_type: user.user_type
        };
        return userInfo;
      })
        .then(userInfo => {
          return resolve(userInfo);
        })
        .catch(err => {
          return reject(err);
        });
    });
  }
}

Users.Keytypes = {
  HASH: "HASH",
  RANGE: "RANGE"
};

module.exports = new Users();
```

```routes/login.js
const express = require("express");
const router = express.Router();
const Authenticator = require("../services/auth/authenticator");

/* GET users listing. */
router.get("/", (req, res) => {
  // エラーメッセージがあるときは出力させる
  const message = req.flash();
  res.render("auth/login", {
    message: message.error,
    title: "ログイン"
  });
});

// ログイン情報がPOSTされたら認証処理に入る
router.post("/", (req, res, next) => {
  Authenticator.authenticate(req, res, next);
});

module.exports = router;
```

routes に入った時にセッションが切れていないか確認する処理を入れておくことをお勧めします

```ruotes/index.js
const express = require("express");
const router = express.Router();
const Authenticator = require("../services/auth/authenticator");
const logger = require("../services/lib/logger");

router.get("/", Authenticator.isAuthenticated, function(req, res) {
  const user = req.user;
  res.render("index", {
    user: user
  });
});
```

### ログアウト
``` routes/logout.js
const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
  req.logout();
  res.redirect("/");
});

module.exports = router;

```
ログアウトはこれだけでいけます。
ログインで苦しまされた後にこの簡素さにはびっくりしました。