バックエンド開発

【Express + JWT】ExpressでJWTを使った認証機能を作る

【Express + JWT】ExpressでJWTを使った認証機能を作る

目次

  1. JWTとは?
  2. 認証機能の実装
    1. データベースを用意
    2. Express.jsでプロジェクトを作成
    3. ユーザー情報登録API
    4. ユーザー情報取得APIとユーザー情報削除API
    5. ログインAPI
    6. JWTのTokenを発行
    7. Tokenの確認
  3. まとめ

今回はExpressにて、JWT認証を実装してみようと思います。

認証機能は今ではさまざまなサービスで当たり前のように導入されているので、今回はその仕組みを勉強と実装方法について勉強していきたいと思います。

なおExpressはv4.16.1で実装していきたいと思います。

また、データベースはPostgreSQLを使っていきます。

それではいきましょう!

JWTとは?

JWT(JSON Web Token)とは

Json形式で表現された認証情報などをURL文字列などとして安全に送受信できるよう、符号化やデジタル署名の仕組みを規定した標準規格。

https://e-words.jp/w/JWT.html

今回実装するような認証機能において、著名、暗号化ができます。

主に認証用のトークン生成などで用いられます。

JWTの構造は下記のようになります。

  • ヘッダデータ
  • ペイロードデータ
  •  署名データ

上記は全てbase64形式で表され、ピリオドでつなぎます。

つまり下記のような感じになります。

[ヘッダ].[ペイロード].[署名]
例)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhyZnUzNTNtbzMiLCJuYW1lIjoi5bGx5Y-j5bm555-iIiwiZW1haWwiOiJtaWNreS1pYm9pYm8tMDUxN0BlendlYi5uZS5qcCIsImlhdCI6MTYzNzIzODgxNX0.mH_0ryemzAAzpYycqjvhkfL9gBnCVoJ-Vu_-LxrC6_w

※参考
https://qiita.com/Naoto9282/items/8427918564400968bd2b
https://techblog.yahoo.co.jp/advent-calendar-2017/jwt/

認証機能の実装

さて、JWTについて説明しましたが、文面だけではイメージが湧かないと思うので、実際にExpressを使って実装していきたいと思います。

今回、ユーザーデータを保持するために使うデータベースはPostgreSQLを使用します。

バージョンはv13.3です。

データベースを用意

まず、ユーザー情報データベースを用意しましょう。

PosgreSQLを使います。

※PostgreSQLでのデータベース構築手順は省きます。

下記のようなデータベースを用意しましょう。

データベース名は「expressdb」にして、その中に「users」テーブルを作ります。

「users」テーブルのカラムの内容は「id」、「name」、「email」、「password」です。

データ型は全て”text”で、「id」をprimary keyとします。

これでデータベースの準備は完了です。

Express.jsでプロジェクトを作成

まず、Express.jsでAPI開発用のプロジェクトを作成します。

今回は「exprees-generator」を使います。

任意のディレクトリで下記コマンドを実行して、「express-auth」という名前のプロジェクトディレクトリを作成します。

express express-auth

そしたら、「express-auth」というディレクトリが生成されます。

これでexpressの準備は完了です。

ユーザー情報登録API

まず、ユーザー情報をデータベースに登録するAPIを作りましょう。

今回、パスワードが簡単に盗まれないようにパスワードのハッシュ化という技術を使います。

ハッシュ化とは、

対象のデータに対してハッシュ関数と呼ばれる計算方法でハッシュ値と呼ばれる値を算出することをハッシュ化といいます。

https://recruit.cct-inc.co.jp/tecblog/security/hash-function/

ハッシュ化を行うことで、パスワードを別の値に置き換えます。

ですので、仮にパスワードを盗まれたとしても、ハッシュ化されたパスワードを下の形に復元することはかなり困難になります。

逆にハッシュ化していないと、パスワードが盗まれてしまったら、一目でパスワードの値がわかってしまうので、非常に危険です。

このハッシュ化の技術はパスワード管理においても非常に重要なものなのです。

さて、まずはパスワードをハッシュ化させるにあたって、「bcrypt」というパッケージをインストールしましょう。

プロジェクトのディレクトリ内にcdで移動し、下記コマンドでインストールしましょう。

//npmの場合
npm install bcrypt

//yarnの場合
yarn add bcrypt

インストールが完了したら、ユーザー情報登録APIを作っていきます。

まず、今回使うデータベース情報を記述しておきたいので、「express-auth」ディレクトリ直下に「db」というフォルダを作成しましょう。

そして、その中にpool.jsというファイルを作成してください。

pool.jsには下記のように記述しましょう。

const { Pool } = require("pg");

const pool = new Pool({
  user: "ユーザー名",
  host: "127.0.0.1",
  database: "expressdb",
  password: "パスワード",
  port: 5432,
});

module.exports = pool;

さて続いて、「routes」ファイル内のindex.jsファイルを開きましょう。

そして、その中に下記のように記述してください。

const express = require("express");
const router = express.Router();

const pool = require("../db/pool");

const bcrypt = require("bcrypt");
const saltRounds = 10;

//ユーザー情報登録API
router.post("/auth/register", function (req, res, next) {
  const idLength = 10;
  const idSource = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < idLength; i++)
    id += idSource[Math.floor(Math.random() * idSource.length)];
  const { name, email, password } = req.body;
  const hassedPassword = bcrypt.hashSync(password, saltRounds);
  pool.query(
    "INSERT INTO users VALUES ($1, $2, $3, $4)",
    [id, name, email, hassedPassword],
    function (error, results) {
      if (error) {
        res.status(500).json({
          status: "500 Internal Server Error",
          error: error,
        });
      }
      res.status(201).json({
        status: "success",
      });
    }
  );
});

module.exports = router;

今回、ハッシュ化のためのパッケージである「bcrypt」を利用するので、インポートしましょう。

そして、「saltRounds」を設定します。

「saltRounds」は、ハッシュ化を何回行うかの設定です。

10を設定しましたが、10とはnの10乗を意味し、1024回ハッシュ化を行います。

const bcrypt = require("bcrypt");
const saltRounds = 10;

続いて、ユーザー情報登録APIの記述ですが、アクセスURIを「/auth/register」としました。

「id」ですが、今回は10桁のランダムな英数字にしたいので、下記のようなプログラムでランダム文字列を生成します。

  const idLength = 10;
  const idSource = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < idLength; i++)
    id += idSource[Math.floor(Math.random() * idSource.length)];

その後、リクエストボディから「name」、「email」、「password」の三つのデータを取ってきます。

そして、パスワードはハッシュ化を行うので、ハッシュ化の関数を使いましょう。

const hassedPassword = bcrypt.hashSync(password, saltRounds);

そこまで完了したら、「id」「name」「email」「password」の4つのデータをselectコマンドでデータベースに登録してあげましょう。

ここまでできたら、試しに実行してみます。

「Postman」などのWeb APIのテストクライアントサービスを使ってアクセスしてみましょう。(VSCodeのRest Clientを使うのもありです)

下記のようにPostmanで「http:localhost/auth/register」にアクセスしましょう。

その際にbodyに「スティーブ」の情報をセットしてみましょう。

これで、「Send」してみてください。

「”status”: “success”」と出ました。

成功しているようです。

「pgAdmin」でテーブルを確認してみましょう。

ちゃんとデータが登録されているようです。

「password」も問題なくハッシュ化されています。

これでユーザー情報登録APIは完成です。

ユーザー情報取得APIとユーザー情報削除API

ユーザー情報を全件取得するAPIと特定のユーザー情報を削除するAPIも作っておきましょう。

まず、取得するAPIです。

「routes」ディレクトリ内のindex.jsファイルに下記のようにユーザー情報取得APIの記述を追記しましょう。

const express = require("express");
const router = express.Router();

const pool = require("../db/pool");

const bcrypt = require("bcrypt");
const saltRounds = 10;

//ユーザー情報取得API 追記
router.get("/users", function (req, res, next) {
  pool.query("SELECT * FROM users", function (error, results) {
    if (error) {
      throw error;
    }
    res.status(200).json({
      data: results.rows,
    });
  });
});

//ユーザー情報登録API
router.post("/auth/register", function (req, res, next) {
  const idLength = 10;
  const idSource = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < idLength; i++)
    id += idSource[Math.floor(Math.random() * idSource.length)];
  const { name, email, password } = req.body;
  const hassedPassword = bcrypt.hashSync(password, saltRounds);
  pool.query(
    "INSERT INTO users VALUES ($1, $2, $3, $4)",
    [id, name, email, hassedPassword],
    function (error, results) {
      if (error) {
        res.status(500).json({
          status: "500 Internal Server Error",
          error: error,
        });
      }
      res.status(201).json({
        status: "success",
      });
    }
  );
});

module.exports = router;

シンプルにselectコマンドでデータベースからデータを取得しているだけです。

「http://localhost:5000/users」にGETで実行して下記のように、ユーザー情報が取得できていればOKです。

続いて、特定のユーザー情報を削除するAPIを作ります。

同じように「routes」ディレクトリ内のindex.jsを開いて、下記のように削除APIを追記してください。

const express = require("express");
const router = express.Router();

const pool = require("../db/pool");

const bcrypt = require("bcrypt");
const saltRounds = 10;

//ユーザー情報取得API
router.get("/users", function (req, res, next) {
  pool.query("SELECT * FROM users", function (error, results) {
    if (error) {
      throw error;
    }
    res.status(200).json({
      data: results.rows[0],
    });
  });
});

//ユーザー情報登録API
router.post("/auth/register", function (req, res, next) {
  const idLength = 10;
  const idSource = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < idLength; i++)
    id += idSource[Math.floor(Math.random() * idSource.length)];
  const { name, email, password } = req.body;
  const hassedPassword = bcrypt.hashSync(password, saltRounds);
  pool.query(
    "INSERT INTO users VALUES ($1, $2, $3, $4)",
    [id, name, email, hassedPassword],
    function (error, results) {
      if (error) {
        res.status(500).json({
          status: "500 Internal Server Error",
          error: error,
        });
      }
      res.status(201).json({
        status: "success",
      });
    }
  );
});

//ユーザー情報削除API 追記
router.delete("/delete/:id", function (req, res, next) {
  const id = req.params.id;
  pool.query("DELETE FROM users WHERE id = $1", [id], function (
    error,
    results
  ) {
    if (error) {
      res.status(500),
        json({
          status: "500 Internal Server Error",
          error: error,
        });
    }
    if (results.rowCount === 0) {
      res.status(400).json({
        status: "400 Bad Request",
        message: "データが存在しません。",
      });
    } else {
      res.status(200).json({
        status: "success",
        message: "データを削除しました。",
      });
    }
  });
});

module.exports = router;

ユーザー情報登録APIの下に追記しました。

シンプルにdeleteコマンドで削除します。

その際にパスパラメーターから取得したidをwhereで指定して、そのidに合致したユーザー情報を削除します。

「http://localhost:5000/delete/opfig83hx8」をDELETEで実行してみてください。

※パスパラメーターのidの部分は上記とは異なると思うので、適切なidを入れてください。

上記のように「success」が返って来れば成功です。

「pgadmin」でテーブルを見てみて、下記のように「スティーブ」のデータが消えていれば、成功です。

ログインAPI

続いて、ログインAPIを作ります。

動きとしては、ユーザーがログイン画面で、「email」と「password」を入力したと想定して、その送られてきた二つの情報が既存のユーザー情報のどれかと合致しているかどうかを判定して、その結果を返すといったものです。

パスワードのチェックには「bcrypt.compare」を利用します。

リクエストされてきたパスワードと、データベースのパスワードを比較するのですが、ハッシュ化したパスワードを比較するので、「bcrypt.compare」が必要です。

「routes」ディレクトリ内のindex.jsにログインAPIを追記しましょう。

下記のように記述してください。

const express = require("express");
const router = express.Router();

const pool = require("../db/pool");

const bcrypt = require("bcrypt");
const saltRounds = 10;

//ユーザー情報取得API
router.get("/users", function (req, res, next) {
  pool.query("SELECT * FROM users", function (error, results) {
    if (error) {
      throw error;
    }
    res.status(200).json({
      data: results.rows[0],
    });
  });
});

//ユーザー情報登録API
router.post("/auth/register", function (req, res, next) {
  const idLength = 10;
  const idSource = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < idLength; i++)
    id += idSource[Math.floor(Math.random() * idSource.length)];
  const { name, email, password } = req.body;
  const hassedPassword = bcrypt.hashSync(password, saltRounds);
  pool.query(
    "INSERT INTO users VALUES ($1, $2, $3, $4)",
    [id, name, email, hassedPassword],
    function (error, results) {
      if (error) {
        res.status(500).json({
          status: "500 Internal Server Error",
          error: error,
        });
      }
      res.status(201).json({
        status: "success",
      });
    }
  );
});

//ユーザー情報削除API
router.delete("/delete/:id", function (req, res, next) {
  const id = req.params.id;
  pool.query("DELETE FROM users WHERE id = $1", [id], function (
    error,
    results
  ) {
    if (error) {
      res.status(500),
        json({
          status: "500 Internal Server Error",
          error: error,
        });
    }
    if (results.rowCount === 0) {
      res.status(400).json({
        status: "400 Bad Request",
        message: "データが存在しません。",
      });
    } else {
      res.status(200).json({
        status: "success",
        message: "データを削除しました。",
      });
    }
  });
});

//ログインAPI 追記
router.post("/auth/login", function (req, res, next) {
  const { email, password } = req.body;
  pool.query("SELECT * FROM users WHERE email = $1", [email], function (
    error,
    user
  ) {
    if (error) {
      res.status(400).json({
        status: "400 Bad Request",
        error: error,
      });
    }
    if (user.rowCount === 0) {
      return res.json({
        message: "email not found",
      });
    }
    bcrypt.compare(password, user.rows[0].password, function (error, results) {
      if (error) {
        return res.status(400).json({
          error: error.message,
        });
      }
      if (!results) {
        return res.json({
          message: "password is not correct",
        });
      }
      return res.json({
        message: "password is correct",
      });
    });
  });
});

module.exports = router;

リクエストボディにセットされた「email」と「password」を取ってきます。

そしてSELECTコマンドでボディから取ってきたemailと一致するデータを取得してきます。

その際にデータ取得したデータがなかった場合(Emailがデータベースのものと合致しなかった場合)、下記のように「email not found」のメッセージを返します。

    if (user.rowCount === 0) {
      return res.json({
        message: "email not found",
      });
    }

その後、「bcrypt.compare」を使います。

bcrypt.compare関数では、第一引数にリクエストボディから取ってきたパスワード、第二引数にデータベースから取ってきたパスワード、そして、第三引数には関数を入れます。

返ってきたデータが空だった場合、「password is not correct」というメッセージを返し、もしデータが正しく返ってきた場合は「password is correct」とメッセージが返ってきます。

    bcrypt.compare(password, user.rows[0].password, function (error, results) {
      if (error) {
        return res.status(400).json({
          error: error.message,
        });
      }
      if (!results) {
        return res.json({
          message: "password is not correct",
        });
      }
      return res.json({
        message: "password is correct",
      });
    });

さて、ここまでできたら、試してみましょう。

まず、現在、データベースがカラだと思うので、下記のようなデータを入れておきましょう。

{
	"name": "スティーブ",
	"email": "steve.mail@Email.com",
	"password": "fesda345321se"
}

ポストマンで、「http://localhost:5000/auth/login」にアクセスしましょう。

その際にリクエストボディに「steve.mail@Email.com」と「fesda345321se」を入れてアクセスしましょう。

上記のように返ってきたら成功です。

また、passwordを間違ったものにしてみたり、emailを間違ったものにしてみたりして正しい挙動になるかも確かめましょう。

email、passwordチェックはここまでです。

JWTのTokenを発行

さて、次はログインした際に、JWTのTokenを発行してみましょう。

そのためにまずnpmで「jsonwebtoken」をインストールしておきましょう。

下記コマンドを実行してください。

//npmの場合
npm install jsonwebtoken

//yarnの場合
yarn add jsonwebtoken

インストールが完了したら、前項のログインAPI内に追記していきます。

「routes」ディレクトリ内のindex.jsを開いて、まず「jsonwebtoken」をインポートしましょう。

const jwt = require("jsonwebtoken");

続いて、ログインAPI内を下記のように書き直しましょう。

//・・・省略

//ログインAPI
router.post("/auth/login", function (req, res, next) {
  const { email, password } = req.body;
  pool.query("SELECT * FROM users WHERE email = $1", [email], function (
    error,
    user
  ) {
    if (error) {
      res.status(400).json({
        status: "400 Bad Request",
        error: error,
      });
    }
    if (user.rowCount === 0) {
      return res.json({
        message: "email not found",
      });
    }
    bcrypt.compare(password, user.rows[0].password, function (error, results) {
      if (error) {
        return res.status(400).json({
          error: error.message,
        });
      }
      if (!results) {
        return res.json({
          message: "password is not correct",
        });
      }
      //Tokenの発行 書き換え
      const payload = {
        id: user.rows[0].id,
        name: user.rows[0].name,
        email: user.rows[0].email,
      };
      const token = jwt.sign(payload, "secret");
      return res.json({ token });
    });
  });
});

//・・・省略

JWTのTokenを発行するには最低でも「payload」と「secretキー(秘密鍵)」が必要になります。

「payload」には基本的にTokenに含めたい情報を設定します。

ですので、今回は「id」「name」「email」のユーザー情報を設定しました。

そして、ハッシュ化に利用する「secretキー(秘密鍵)」は任意です。

今回は、単純に「secret」としました。

      //Tokenの発行 書き換え
      const payload = {
        id: user.rows[0].id,
        name: user.rows[0].name,
        email: user.rows[0].email,
      };
      const token = jwt.sign(payload, "secret");
      return res.json({ token });

最終的にreturnで発行したTokenを返しています。

さて、実際に挙動の確認をしてみましょう。

ポストマンで「http://localhost:5000/auth/login」にアクセスします。

もちろんボディに値を設定してください。

上記のように発行されたTokenが返ってきたら、成功です。

Tokenの確認

最後にTokenの確認方法についてご紹介します。

Tokenをブラウザに渡したのちに、再度ブラウザからアクセスがあった場合、ブラウザから送られてきたTokenが正しいのかのチェックを行う必要があります。

このTokenのチェックには「jwt.verify」を使います。

GETリクエストのヘッダ内の「authorization」からTokenを取得し、Token作成時に利用した”secret”を使って確認を行います。

この際、Tokenは「authorization」の中で「Bearer トークン」の形式で入っています。

ヘッダから送られてきたTokenが正しかった場合は、payloadに入っている情報を返します。

「routes」フォルダ内のindex.jsに下記のように追記してください。

const express = require("express");
const router = express.Router();

const pool = require("../db/pool");

const bcrypt = require("bcrypt");
const saltRounds = 10;

const jwt = require("jsonwebtoken");

//ユーザー情報取得API
router.get("/users", function (req, res, next) {
  pool.query("SELECT * FROM users", function (error, results) {
    if (error) {
      throw error;
    }
    res.status(200).json({
      data: results.rows[0],
    });
  });
});

//ユーザー情報登録API
router.post("/auth/register", function (req, res, next) {
  const idLength = 10;
  const idSource = "abcdefghijklmnopqrstuvwxyz0123456789";
  let id = "";
  for (let i = 0; i < idLength; i++)
    id += idSource[Math.floor(Math.random() * idSource.length)];
  const { name, email, password } = req.body;
  const hassedPassword = bcrypt.hashSync(password, saltRounds);
  pool.query(
    "INSERT INTO users VALUES ($1, $2, $3, $4)",
    [id, name, email, hassedPassword],
    function (error, results) {
      if (error) {
        res.status(500).json({
          status: "500 Internal Server Error",
          error: error,
        });
      }
      res.status(201).json({
        status: "success",
      });
    }
  );
});

//ユーザー情報削除API
router.delete("/delete/:id", function (req, res, next) {
  const id = req.params.id;
  pool.query("DELETE FROM users WHERE id = $1", [id], function (
    error,
    results
  ) {
    if (error) {
      res.status(500),
        json({
          status: "500 Internal Server Error",
          error: error,
        });
    }
    if (results.rowCount === 0) {
      res.status(400).json({
        status: "400 Bad Request",
        message: "データが存在しません。",
      });
    } else {
      res.status(200).json({
        status: "success",
        message: "データを削除しました。",
      });
    }
  });
});

//ログインAPI
router.post("/auth/login", function (req, res, next) {
  const { email, password } = req.body;
  pool.query("SELECT * FROM users WHERE email = $1", [email], function (
    error,
    user
  ) {
    if (error) {
      res.status(400).json({
        status: "400 Bad Request",
        error: error,
      });
    }
    if (user.rowCount === 0) {
      return res.json({
        message: "email not found",
      });
    }
    bcrypt.compare(password, user.rows[0].password, function (error, results) {
      if (error) {
        return res.status(400).json({
          error: error.message,
        });
      }
      if (!results) {
        return res.json({
          message: "password is not correct",
        });
      }
      //Tokenの発行
      const payload = {
        id: user.rows[0].id,
        name: user.rows[0].name,
        email: user.rows[0].email,
      };
      const token = jwt.sign(payload, "secret");
      return res.json({ token });
    });
  });
});

//Token確認API 追記
router.get("/auth/user", (req, res) => {
  const bearToken = req.headers["authorization"];
  const bearer = bearToken.split(" ");
  const token = bearer[1];

  jwt.verify(token, "secret", (error, user) => {
    if (error) {
      return res.sendStatus(403);
    } else {
      return res.json({
        user,
      });
    }
  });
});

module.exports = router;

ログインAPIの下にToken確認APIを追記しました。

下記のようにヘッダーのauthorizationから取ってきた情報をsplitで分割し、その中からTokenを持ってきてtoken変数に入れています。

  const bearToken = req.headers["authorization"];
  const bearer = bearToken.split(" ");
  const token = bearer[1];

下記ではjwt.verify関数を使って、Tokenのチェックを行なっています。

第一引数にauthorizationから取得したToken、第二引数にToken発行時に使用した”secret”、第三引数に関数を入れています。

この関数にはエラー時の処理と正しかった場合にpayloadの情報を返す処理が記載してあります。

  jwt.verify(token, "secret", (error, user) => {
    if (error) {
      return res.sendStatus(403);
    } else {
      return res.json({
        user,
      });
    }
  });

それでは、試してみましょう。

まず、「http://localhost:5000/auth/login」であらかじめログインしておいてください。

そこで発行された関数をヘッダーにkeyを「Authorization」、valueを「Bearer 発行されたToken」と設定し、Sendしてみてください。

下記のように、ユーザー情報が返ってきていれば、成功です。

まとめ

いかがだったでしょうか。

今回は、ExpressでJWT認証を実装してみるということをやってみました。

このようにJWTを使えば、Expressでも比較的簡単に認証機能を実装することができます。

次回は、Nuxt.jsでフロントエンド側を作り、今回作ったJWT認証APIを叩いて、簡単な認証アプリを作ってみたいと思います。

それでは今回はここまで!

お疲れ様でした!

関連記事

関連記事