bpeldi2oerkd8の開発日誌

とあるひよっこエンジニアの成長の記録。

JWT認証を導入したAPI実装 - 自作サービスづくり10

今回もWeb開発に関する記事です。
前回はデザイン改善とSlackとの連携機能の実装を行いました。
developer-bpeldi2oerkd8.hatenablog.com

ここでは、ロゴの作成とSlack連携登録・編集機能を作ったため、
今回はいよいよJWT認証を導入したAPIの実装に移ります。

構成

構成は以下のようになっています。
f:id:bpeldi2oerkd8:20210710205316j:plain

手順としては、

  1. /api/v1/login にアクセスし、lattendanceにJWTをリクエス
  2. lattendance上でJWTを発行し、JSONでJWTを返す
  3. /api/v1/schedulesにアクセス時(以前実装したAPIの利用時)にヘッダーに発行したJWTを追加しリクエス
  4. 送られてきたJWTをもとに認証し、OKの場合のみ結果を返す(NGの時はエラー内容を返す)

順番に実装していきます。

JWTの発行

JWTの発行・認証時に jsonwebtoken を用います。
ドキュメントを見ると、JWTを発行するために jwt.sign() 、JWTを検証する ためにjwt.verify()を用いることが書いてあります。

これに従って、まずJWTの発行を実装します。
まず、JWTを使用できるようにするため、以下のようにapp.jsを変更します。

app.use(express.urlencoded({ extended: true }));

また、APIのログイン部分は以下の通りです。
事前に発行したチャンネルトークンとチャンネルIDをPOSTし、JWTを返します。

'use strict';
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const Room = require('../../models/room');
//APIのシークレットキー
const serverAPISecret = process.env.SERVER_API_SECRET || require('../../secret_info/server_api_info');

router.post('/', (req, res) => {
  //roomIDとroomTokenを取得
  const roomId = req.body.roomId;
  const roomToken = req.body.roomToken;

  Room.findByPk(roomId)
  .then((room) => {
    if(room && room.roomToken === roomToken) {
      //JWTを生成
      const token = jwt.sign({roomId: roomId}, serverAPISecret, {expiresIn: '1h'});
      res.json({
        status: 'OK',
        data: {
          token: token
        }
      });
    }
    else {
      res.json({
        status: 'NG',
        error: {
          messages: ['認証エラー']
        }
      });
    }
  });
});

module.exports = router;

JWTの認証

今度はJWTの認証を入れます。
認証を行う関数をverifyToken()とし、ミドルウェアとして引数に入れます。
verifyTokenの実装はこちらです。

'use strict';
const jwt = require('jsonwebtoken');

//APIのシークレットキー
const serverAPISecret = process.env.SERVER_API_SECRET || require('../../secret_info/server_api_info');

function verifyToken(req, res, next) {
  const authHeader = req.headers["authorization"];

  //HeaderにAuthorizationがあるかチェック
  if (authHeader) {
    //Bearerのチェック
    if (authHeader.split(" ")[0] === "Bearer") {
      try {
        const decoded = jwt.verify(authHeader.split(" ")[1], serverAPISecret);
        const roomId = req.params.roomId;

        //roomIdの検証
        if (decoded.roomId === roomId) {
          next();
        }
        else {
          res.json({
            status: 'NG',
            error: {
              messages: ['検証エラー']
            }
          });
        }
      } catch (e) {
        res.json({
          status: 'NG',
          error: {
            messages: ['Tokenエラー']
          }
        });
      }
    }
    else {
      res.json({
        status: 'NG',
        error: {
          messages: ['ヘッダー形式エラー']
        }
      });
    }
  }
  else {
    res.json({
      status: 'NG',
      error: {
        messages: ['ヘッダーエラー']
      }
    });
  }
}

module.exports = verifyToken;

これを以下のようにミドルウェアとして挟むことで認証を入れています。

router.post('/:roomId/users/:slackId/dates/:dateString',
  verifyToken,
  (req, res, next) => {

APIのテスト

APIが正しく実装されているかテストします。
今回はJestではなく、VS Code上で手軽にREST APIのテストができるREST Clientを使います。
使い方はこちらのページが参考になります。
qiita.com

では、実際にリクエストを書いていきます。

# JWT発行テスト
POST http://localhost:8000/api/v1/login
Content-Type: application/json

{
  "roomId": "TEST0001",
  "roomToken": "事前に発行したチャンネルトークン"
}

###
# 認証がない場合の出欠確認テスト(認証エラー)
GET http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-02

###
# 認証がある場合の出欠確認テスト
GET http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-02
Authorization: Bearer {先ほど返ってきたJWT}

###
# 認証がある場合の出欠確認テスト
GET http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-03
Authorization: Bearer {先ほど返ってきたJWT}

###
# 認証がある場合の出欠更新テスト
POST http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-02
Authorization: Bearer {先ほど返ってきたJWT}
Content-Type: application/json

{
  "availability": 2
}

正しいJSONが返ってきました。

botの実装の変更と動作確認

APIにJWT認証が追加されたため、botの実装を変更しました。
まず、/api/v1/loginにアクセスし、JWTが返ってきてからヘッダーに追加しリクエストしています。
github.com

動作確認をします。
正しく結果が返ってくることが確認できました。
f:id:bpeldi2oerkd8:20210712234141j:plain


以上でJWT認証付きのAPIの実装が完了しました。
次回は、Herokuへのデプロイをしたいと思います。