その他

【Express】register/login/verifyTokenのAPI実装と解説

その他
この記事は約12分で読めます。

概要

  • express – 4.18.2
  • crypto.js – 3.0.0
  • jsonwebtoken – 9.0.0
  • mongoose – 6.9.0
  • dotenv – 16.0.3
  • express-validator – 6.14.3

インプルの藤谷です。
expressを利用してユーザー新規登録、ログインとJWTの検証機能までを解説します。
JWTのリフレッシュや、passport-jwtでのtoken検証、TypeScriptの利用はせずに、簡易なものに留めます。
実務レベルではなく、流れを掴む程度のイメージで見て頂けると幸いです。

解説

server起動

expressインスタンスを作成し、3000番portをnpm startで起動します。
mongoDBを利用する為、ORMにはmongooseを使います。
pathは/api/v1をbaseURLとし、require内のディレクトリ配下に格納されたauth.jsをrouteとして見に行きます。

// app.ts

const express = require("express");
const mongoose = require("mongoose");
const app = express()
const port = 3000;
require("dotenv").config();

app.use(express.json());

app.use("/api/v1", require("./src/v1/routes/auth"));

mongoose.set("strictQuery", true);

try {
  mongoose.connect(process.env.MONGODB_URL);
  console.log("db接続正常");
} catch (error) {
  console.log("error");
}

app.listen(port, () => {
  console.log("local server started");
});

mongoDBのschemaは以下の定義です。

// schema.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    require: true,
    unique: true,
  },
  password: {
    type: String,
    require: true,
  },
});

module.exports = mongoose.model("User", userSchema);

Route

routerオブジェクトでエンドポイントを指定し、バリデーション検証、バリデーション失敗時にエラーハンドリング、成功時のメソッド実行を呼び出しています。
verifyTokenメソッドに関しては、middlewareでtoken検証用メソッドを実行し、通ればresponseとしてstatus code 200と、json形式のuserを返却させています。

// routers/auth.js

const router = require("express").Router();
const validation = require("../validate/validateError");
const { registerValidator, loginValidator } = require("../validate/auth");
const userController = require("../controllers/auth");
const tokenHandler = require("../middleware/jwtVerify");

router.post(
  "/register",
  verifyToken
  registerValidator,
  validation.validateError,
  userController.register
);

router.post(
  "/login",
  verifyToken
  loginValidator,
  validation.validateError,
  userController.login
);

module.exports = router;

express-validator

expressのバリデーションライブラリであるexpress-validatorを利用してカラムの検証条件を定義します。
usernameに関しては「既に同じユーザー名が存在していれば」の、カスタムバリデーションを定義します。
エンドポイント毎に定義を分けてexportし、routeから呼び出しています。

// validate/auth.js

const { body } = require("express-validator");
const User = require("../models/user");

exports.registerValidator = [
  body("username")
    .isLength({ min: 6 })
    .withMessage("user name must be at least 6 characters long")
    .not(),
  body("username").custom((value) => {
    return User.findOne({ username: value }).then((user) => {
      if (user) {
        return Promise.reject("this username is already registered.");
      }
    });
  }),
  body("password")
    .isLength({ min: 6 })
    .withMessage("password must be at least 6 characters")
    .not(),
  body("confirmPassword")
    .isLength({ min: 6 })
    .withMessage("confirmation password must be at least 6 characters")
    .not(),
];

exports.loginValidator = [
  body("username")
    .isLength({ min: 6 })
    .withMessage("user name must be at least 6 characters long")
    .not(),
  body("password")
    .isLength({ min: 6 })
    .withMessage("password must be at least 6 characters")
    .not(),
];

エラーハンドリングは共通処理とし、errors.isEmpty()が論理否定で記述され、errorsオブジェクトが空でなければ400の処理とerrorsのjsonが返却されます。

// validate/validateError.js

const { validationResult } = require("express-validator");

exports.validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  next();
};

メソッド定義 register / login

各エンドポイントのバリデーション成功時に実行されるメソッドがまとまっていますので、個別に解説していきます。

// controller/auth.js

const CryptoJS = require("crypto-js");
const JWT = require("jsonwebtoken");
const User = require("../models/user");

exports.register = async (req, res) => {
  const password = req.body.password;

  try {
    req.body.password = CryptoJS.AES.encrypt(password, process.env.SECRET_KEY);

    const user = await User.create(req.body);

    return res.status(200).json({ user });
  } catch (err) {
    return res.status(500).json(err);
  }
};

exports.login = async (req, res) => {
  const { username, password } = req.body;

 try {
  user = await User.findOne({ username });
  if (!user) {
    res.status(401).json({
      errors: {
        params: "username",
        message: "invalid user name.",
      },
    });
  }

  const decryptedPassword = CryptoJS.AES.decrypt(
    user.password,
    process.env.SECRET_KEY
  ).toString(CryptoJS.enc.Utf8);

  if (decryptedPassword !== password) {
    res.status(401).json({
      errors: {
        params: "password",
        message: "invalid password.",
      },
    });
  }

  const token = JWT.sign({ id: user._id }, process.env.TOKEN_SECRET_KEY, {
    expiresIn: "24h",
  });

  return res.status(201).json({ user, token });
 } catch (err) {
   return res.status(500).json(err);
 }
};

register

ユーザー新規登録APIです。
入力されたpasswordをCrypto.jsを用いてencode(暗号化)し、User.createでユーザーを作成します。
今回の暗号化方式は共通暗号鍵方式になるので、encodeとdecodeには同じenvの鍵を指定しています。
ここまで成功であれば、status 200と共に、userとtokenをjson形式で返却します。
try catchで記述しているので、処理が落ちればserver errorの500とerrorオブジェクトが返却されます。

login

req.bodyをusernameとpasswordに分割代入し、findOneでusernameを一つ検索します。存在しなければclient error(認証系)の401とerror messageを返却します。
usernameが存在すれば、passwordの検索に移ります。
Cryotp.jsで暗号化されたpasswordをdecodeし、toStringでキャストする事でpasswordを復号します。
復号されたpasswordと入力値のpasswordが一致すれば、jwt tokenの発行に移ります。エラー時はusernameと同様です。
最後に、JWT.signでjwt tokenを生成します。payloadに入るデータには暗号化されたidを利用します。tokenの有効期限は24時間です。
全て成功すれば、userとtokenをjson形式で201と共に返却します。
処理が落ちればcatchでエラーハンドリングが走ります。

メソッド定義 verifyToken

verifyToken

// middleware/jwtVerify.js

const JWT = require("jsonwebtoken");
const User = require("../models/user");

const tokenDecode = (req) => {
  const bearerHeader = req.headers.authorization;
  if (bearerHeader) {
    const bearer = bearerHeader.split(" ")[1];
    try {
      const decodedToken = JWT.verify(bearer, process.env.TOKEN_SECRET_KEY);
      return decodedToken;
    } catch {
      return false;
    }
  } else {
    return false;
  }
};

exports.verifyToken = async (req, res, next) => {
  const tokenDecoded = tokenDecode(req);
  if (tokenDecoded) {
    const user = await User.findById(tokenDecoded.id);
    if (!user) {
      return res.status(401).json("not authorized.");
    }
    req.user = user;
    next();
  } else {
    return res.status(401).json("not authorized.");
  }
};

jwt tokenの検証用メソッドを実装します。
verifyTokenの実行で、先ずはtokenのdecodeを行います。
tokenDecodeメソッドをreqオブジェクトを引数に実行します。
request headerのauthorizationにtokenが含まれていることを前提に、tokenを取り出してbearerHeaderに格納します。
格納できた場合、splitメソッドを用いてbearerとtoken部分を切り分けて一つの配列に格納し、index1の指定でtokenのみをbearerに格納します。
成功すれば、JWT.verifyでtokenの有効性を検証し、有効であればdecodeしてdecodedTokenに格納し、返却します。
verifyTokenに処理が戻り、tokenDecodedが存在すれば、findByIdでdecode済みのtokenから取得したidと、Userモデル内レコードのidを比較し、存在すればreq.userで返却。しなければ401と共に「権限がありません」を返却します。
以上で、loginとjwt検証が完了したはずです。

今回は以上となります。
ここまで読んでいただきありがとうございます。