LoginSignup
2
2

【Node.js / Express】JWTを使用した認証機能を実装

Last updated at Posted at 2023-07-19

ゴール

  • ログインAPI実行時にJWTを生成する。
  • ローカルストレージに生成したトークンを保存し、リクエスト時には保存したトークンをヘッダーAuthorizationに自動で付与されるようにする。
  • ヘッダーに付与されたトークンを元に認証チェックを行うミドルウェアを作成する。
  • Axiosのインターセプターで、401エラー発生時にログイン画面に遷移するようにする。

前提

CL(Vue)側実装

ログイン画面の実装

シンプルなログイン画面を実装しておきます。

src/views/LoginView.vue
<template>
  <div>
    <form>
      <h1 class="border-bottom my-4">Login</h1>
      <div class="mb-3">
        <label for="exampleInputEmail1" class="form-label">Email address</label>
        <input type="email" class="form-control" v-model="email" />
      </div>
      <div class="mb-3">
        <label for="exampleInputPassword1" class="form-label">Password</label>
        <input
          type="password"
          class="form-control"
          autocomplete
          v-model="password"
        />
      </div>
      <button type="button" class="btn btn-primary" @click="login()">
        Login
      </button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      email: "",
      password: "",
    };
  },
  methods: {
    async login() {
      try {
        const requestBody = {
          email: this.email,
          password: this.password,
        };
        const { data } = await this.axios.post("/login", requestBody);
        console.log("GENERATED TOKEN:", data);
        localStorage.setItem("accessToken", data);
        this.$router.push({
          name: "homeView",
        });
      } catch {
        alert("Authorization failed!");
      }
    },
  },
};
</script>

Screen Shot 2023-07-19 at 10.15.34.png

Axiosインターセプター設定

API実行時に401エラーが返却された場合は、作成したログイン画面に遷移するようにしようと思います。

API実行の度に上記の処理を書くのは面倒なので、エラーレスポンスを受けた際の共通処理として、インターセプターを定義します。

インターセプターとは、リクエスト送信時/レスポンス受信時に挟み込むことができる処理です。共通する処理はインターセプターとして定義することで共通化することができます。詳しくはこちらの記事で解説されています。

リクエスト

ローカルストレージに保存されたトークン(accessToken)を取得して、リクエストヘッダー「Authorization」を付与する処理を書きます。

myAxios.interceptors.request.use((config) => {
  const accessToken = localStorage.getItem("accessToken");
  config.headers = {
    ...config.headers,
    Authorization: `Bearer ${accessToken}`,
  };
  return config;
});

AuthorizationヘッダーにJWTを付与する際は、JWTの前に「Bearer」という文字列を付与することが一般的です。これはAuthorizationヘッダーに含めるトークンがJWTであることを明示的にすることにより、不正なトークン形式が入れられないようにするなどの効果があります。

レスポンス

エラーレスポンスを受けた際に、ステータスが401の場合はローカルストレージに保存されたトークン(accessToken)を削除して、ログイン画面(loginView)に遷移させる処理を書きます。

myAxios.interceptors.response.use((response) => {
  return response;
}, (error) => {
  if (error.response.status === 401) {
    alert("Your session has expired, please login again.");
    localStorage.removeItem("accessToken");
    router.push({
      name: "loginView"
    });
  }
  return Promise.reject(error);
});

API(Express)側実装

今回はJWTを使用した認証を実装します。

JWTとは、JSON Web Tokenの略で、何やら情報(ペイロード)を持たせることができる暗号のような文字列のことを指します。ログインしているユーザーをペイロードで指定したトークンの生成を行い、詳しい説明はこちらの記事を参照してください。

今回は、JWTの生成や検証をすることができるnpmパッケージ「jsonwebtoken」を使用します。以下コマンドでインストールをしてください。

npm install jsonwebtoken

signメソッドを使用してJWTを生成することができます。

const jwt = require("jsonwebtoken");

jwt.sign(
  { userId: 1, email: "whopper@example.com" },  // ペイロード(持たせる情報)
  "SECRET_KEY",  // 秘密鍵(.envファイル等で定義することを推奨します)
  {
    expiresIn: "100000" // オプション(主に有効期限/msを設定)
  }
);

verifyメソッドを使用して検証を行います。

const jwt = require("jsonwebtoken");

jwt.verify(
  token, // 検証対象のJWT
  "SECRET_KEY" // JWT生成時に使用する秘密鍵
);

ログインAPIを実装

実際にログインAPIを実装していきます。

まずはエンドポイントの追加です。ログインAPIなので/loginにしましょう。実行するコントローラーメソッドはAuthController.loginとします。

src/router.js
const AuthController = require("./controllers/AuthController.js");

router.post("/login", AuthController.login);

コントローラーを追加します。主にAuthServiceクラスのloginメソッドを実行する処理を書きます。メソッド実行時には、引数としてリクエストボディを渡します。

const AuthService = require("../services/AuthService");

module.exports = class AuthController {
  static async login(request, response, next) {
    try {
      const { body } = request;
      const authInfo = await new AuthService().login(body);
      response.json(authInfo);
    } catch (e) {
      next(e);
    }
  }
};

サービスを追加します。ここでは受け取ったリクエストボディemailpasswordを元に、実際にトークンを生成する処理を書きます。

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

module.exports = class AuthService {
  async login(body) {
    const { email, password } = body;
    const loggedInUser = await User.findOne({
      where: {
        email,
        password,
      },
    });

    if (!loggedInUser) throw new Error("Authorization failed!");

    try {
      const token = jwt.sign(
        { userId: loggedInUser.id, email: loggedInUser.email },
        "SECRET_KEY",
        { expiresIn: "600000" } // 10 minutes
      );
      return token;
    } catch {
      throw new Error("Token generation failed!");
    }
  }

  async signUp() {}
};

では、実際にログインAPIを実行してみます。usersテーブルに任意のレコードを追加しておいてください。
ログイン画面に遷移し、メールアドレスとパスワードを入力し、「Login」ボタンを押下してください。
Screen Shot 2023-07-19 at 10.17.42.png
認証情報が誤っていない場合は、コンソールに以下のようなログが出力されます。

GENERATED TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImVtYWlsIjoidGVzdEVtYWlsQGdtYWlsLmNvbSIsImlhdCI6MTY4OTcyOTYxMSwiZXhwIjoxNjg5NzI5NjEzfQ.vliqXAR7YRpGga2PXaH3nQyFhanaVsFxEcDBx-jGXc8

ローカルストレージを確認すると、生成されたJWTが保存されていることが確認できます。
Screen Shot 2023-07-20 at 9.18.30.png

ミドルウェアを実装

リクエストヘッダー「Authorization」で指定されたトークンが、有効な場合はAPIの実処理を実行し、無効な場合は401エラーを返すミドルウェアを作成します。
このミドルウェアを認証が必要なAPIに適用することで、API認証を適用します。
Screen Shot 2023-07-18 at 10.13.35.png

src/middlewares/verifyAccessToken.js
const jwt = require("jsonwebtoken");

module.exports = async function verifyAccessToken(request, response, next) {
  const token = request.headers.authorization.split(" ")[1];
  if (!token) response.status(401).send("Token must be provided!");

  try {
    const decodedToken = jwt.verify(token, "SECRET_KEY");
    console.log("=============AUTH TOKEN=============");
    console.debug(decodedToken);
    next();
  } catch {
    response.status(401).send("Authorization failed!");
  }
};

作成したミドルウェアを、APIに適用していきます。

src/router.js
router.get("/users", verifyAccessToken, UsersController.fetchAll);

該当のAPIを実行してみます。こちらのリポジトリを使用している場合は、URL/に遷移してください。「Get users」ボタンを押下すると、APIを実行することができます。
Screen Shot 2023-07-19 at 10.33.59.png

ミドルウェアが適用される為、未ログイン時またはトークンが無効(有効期限が切れているなど)の場合は、アラートが出力され、ログイン画面に遷移します。

試しにJWTの有効期限を短くしてみることで、有効期限が切れると401エラーが出力されることも確認できます。

jwt.sign(
  { userId: 1, email: "whopper@example.com" },
  "SECRET_KEY",
  {
    expiresIn: "2000"
  }
);

これで認証機能の実装が完了しました👏 最後まで読んでいただき、ありがとうございました。

giphy.gif

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2