🛂

TypeScriptでpassport-jwtを用いたapiサーバー作成

7 min read

概要

TypeScript と passport-jwt で認証が必要な webAPI を作っている記事が見当たらなかったので勉強も兼ねて作成しました。
github のソースコードは以下になります。

https://github.com/Msksgm/api-passport-jwt-typescript

また jwt は使い方によっては、脆弱性を生む原因となります。
関連として、OWASP TOP 10( https://owasp.org/Top10/ja/ ) に「アクセス制御不備」が選ばれています。
この記事はあくまで passport-jwt を使うときの参考にしていただき、jwt そのものは適切に使えるようにしてください。

環境構築

node のインストール方法は以下の記事のnode.js 14.17.0 をインストールまで参照してください。

https://zenn.dev/msksgm/articles/20211106-anyenv-nodenv

プロジェクトを作成

ディレクトリを作成

mkdir api-passport-jwt-typescript
cd $_
git init

package をインストールします。

yarn init -y
yarn add cookie-parser express nodemon passport passport-jwt passport-local
typesync
yarn

package.json を以下のように編集してください。

{
  "name": "api-passport-jwt-typescript",
  "version": "1.0.0",
  "main": "src/index.ts",
  "license": "MIT",
  "scripts": {
    "start": "nodemon ./src/index.ts"
  },
  .
  .
  .
}

環境変数

./.envを作成し、環境変数を記述します。

.envの内容です。
JWT の秘密鍵を記述していますが、今回は簡略化のため以下のようになっていて、本来は乱数を使うべきです。

JWT_SECRET="secret-jwt-cat"

その他

.gitignoreを以下のように作成します。
.env と/node_modules を.git の管理から外します。

/node_modules
.env

実装

最終的なディレクトリ構成は以下になります(node_modules と隠しファイルは除外して表示しています)。

.
├── package.json
├── src
│   ├── index.ts
│   ├── lib
│   │   └── security
│   │       └── index.ts
│   └── route
│       ├── cat.ts
│       ├── dog.ts
│       └── user.ts
└── yarn.lock

4 directories, 7 files

passport、 passport-local、 passport-jwt の設定

はじめに、./src/lib/security/index.tsを作成し、passport, passport-local, passport-jwtの設定を記述します。

それぞれのコメント文で内容を解説しています。
1 で passport-local の設定をします。username と password が一致するときにtrue、それ以外はfalseを返す処理です。
2 で passport-jwt の設定をします。jwt の検証をおこないます。
3 で最後の設定したpassportexportします。

import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import {
  Strategy as JWTStrategy,
  ExtractJwt,
  StrategyOptions,
} from "passport-jwt";

// 1 passport-localの設定
passport.use(
  new LocalStrategy(
    {
      usernameField: "username",
      passwordField: "password",
      session: false,
    },
    (username: string, password: string, done: any) => {
      if (username === "hoge" && password === "fuga") {
        return done(null, username);
      } else {
        return done(null, false, {
          message: "usernameまたはpasswordが違います",
        });
      }
    }
  )
);

// 2 passport-jwtの設定
const opts: StrategyOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET,
};

passport.use(
  new JWTStrategy(opts, (jwt_payload: any, done: any) => {
    done(null, jwt_payload);
  })
);

// 3 passportをexport
export default passport;

express の設定

先に express の説明をします。

1 は express の設定なので説明を省きます。
2 で、passport.initialize()によって、passportの初期化をおこなっています。
3 で router を追加しています。/user/catにはpassportの設定をせずに、/dogのみpassportの設定をしています。こうすることで、/dog以下の全ての path にpassportの認証を必須にすることができます。

import express from "express";
import cookieParser from "cookie-parser";

import passport from "./lib/security";
import catRouter from "./route/cat";
import userRouter from "./route/user";
import dogRouter from "./route/dog";

// 1 expressの設定
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

// 2 passportを初期化
app.use(passport.initialize());
// 3 routerを追加
app.use("/user", userRouter);
app.use("/cat", catRouter);
app.use("/dog", passport.authenticate("jwt", { session: false }), dogRouter);

app.listen(3000, () => {
  console.log("listen to " + 3000);
});

router の設定

./src/routeに routing の設定をおこないます。
作成したファイルは./src/route/cat.ts./src/route/dog.ts./src/route/user.tsです。

./src/route/user.ts

/loginに post 送ると、jwt が返ってくるスクリプトです。
1 の箇所で、jwt の token 作成と制限時間の設定をしています。
制限時間は 1m(60 秒)に設定しています。

import express from "express";
import jwt from "jsonwebtoken";
import passport from "../lib/security";

const router = express.Router();

router.post(
  "/login",
  passport.authenticate("local", { session: false }),
  (req, res, next) => {
    // 1 jwtのtokenを作成
    const user = req.user;
    const payload = { user: req.user };
    const token = jwt.sign(payload, process.env.JWT_SECRET as string, {
      expiresIn: "1m",
    });
    res.json({ user, token });
  }
);

export default router;

./src/route/cat.ts

1 では psssport を使用しないため、jwt の認証が不要です。
逆に、2 では passport を使用することで、jwt の認証がなければ結果が返ってきません。

import { Router } from "express";
import passport from "../lib/security";

const router = Router();

// 1 jwtが不要なapi
router.get("/public", (req, res, next) => {
  res.json("public cat");
});

// 2 jwtが必要なapi
router.get(
  "/private",
  passport.authenticate("jwt", { session: false }),
  (req, res, next) => {
    res.json("private cat");
  }
);

export default router;

./src/route/dog.ts

express の設定で、passportの認証必須にしたので、ここで記述しなくても認証が付与されています。

import { Router } from "express";

const router = Router();

router.get("/1", (req, res, next) => {
  res.json("number of dog is 1");
});

router.get("/2", (req, res, next) => {
  res.json("number of dog is 2");
});

export default router;

動作確認

実行

環境変数の読み込みを以下でおこないます。
dotenv というライブラリがありますが、本番環境に.env ファイルを置くことはしたくないので、今回はコマンドで読み込ませます。

export $(cat .env | grep -v ^# | xargs)

yarn startで起動します。

yarn start

api の確認

Postman で実行確認をします。

token を生成していない場合。

localhost:3000/cat/publicpublic catが返ってきます。

cat_public

それ以外のlocalhost:3000/cat/privatelocalhost:3000/dog/1localhost:3000/dog/2Unauthorizedが返ってきます。

cat_private
localhost:3000/cat/private に get した例

dog_private_1
localhost:3000/dog/1 に get した例

dog_private_2
localhost:3000/dog/2に get した例

token を生成する。

localhost:3000/user/loginに post することで、token が返ってきます。

login
localhost:3000/loginに post した例

Bearer Token の Token に上記で取得した token を指定することによって、認証が可能です。

cat_private
localhost:3000/cat/private に token つきで get した例

dog_private_1
localhost:3000/dog/1 に token つきで get した例

dog_private_2
localhost:3000/dog/2に token つきで get した例

また、1 分すぎるとUnauthorizedに戻ります。

unauthorized
1 分すぎた例

参考

参考にしたソースコード

https://github.com/khanh-website/api-passport-jwt/tree/65fe30cb5c35c1368166198fd4d1466f202a68b2/

参考にした記事

https://qiita.com/sa9ra4ma/items/67edf18067eb64a0bf40
https://qiita.com/Naoto9282/items/8427918564400968bd2b

Discussion

ログインするとコメントできます