目次

Introduction

先日、会社の勉強会で同僚がjwtについて話していて、とてもわかりやすいな自分も基礎からしっかり復習しないとなと思いました。
ということで今回はjwtの基礎からjwtを使用してどうやってアプリケーションで認証するか説明していきたいと思います。
今回、npmのパッケージであるjsonwebtokenを使用しますが、jsonwebtokenの詳しい説明はしません。

JWTについて

JWTの概要

公式ではjwtについてこのように記載があります

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

要は、認証情報をjson形式で表せるものです。
APIなどのリクエスト時にheaderやbodyにつけることでリクエストしてきたリクエスト元が正しいかリクエストを処理していいか判断することが可能になります
まー詳しくは自分で調べてみてください

JWTの構成

jwtの中身について軽く解説していきます。これも公式に記載がありますが、噛み砕いて説明します。
jwtは3つの構成で HEADER,PAYLOAD,VERIFY SIGNATUREに分かれています。

構成説明
HEADERJWTの構成の説明。 algは使用しているアルゴリズム。typはJWTで固定
PAYLOADjwtの検証するためのデータを格納している。説明は下記で記載
VERIFY SIGNATUREHEADER・PAYLOAD・シークレットをHEADERのアルゴリズムでエンコードされた値

上記をbase64でエンコードした値をつなげた物をjwtとして扱います
実際に確かめてみましょう。画像のHEADERとなる部分をbase64でデコードしてみます

$ echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
{"alg":"HS256","typ":"JWT"}

本当にbase64されていましたね。確かめるまでもないですが..

次にPAYLOADでよく使用される値の説明をします
詳しくはこちら→RFC JWT

claim nameexplanation
iss(Issuer)JWTの発行者。アプリケーション名やドメイン名になることが多いです
exp(Expiration Time)JWTの使用期限。期限を過ぎると検証で失敗になる
sub(Subject)発行する対象を一意に特定する値。アプリケーション固有の値を使用するため、アカウントIDやユーザーIDなどを指定するのが一般的
aud(Audience)JWTの受信者
nbf(Not Before)not-beforeの日付/時間より後か等しくなければならない。nbfより未来の時刻でないと検証を成功にしてはならない
iat(Issued At)JWTを発行した時間
jti(JWT ID)JWTごとにユニークなID。subが同じJWTでも発行が異なるJWTならjtiは別でなければいけない

検証

JWTの説明はある程度終わったのでJWTを使用した検証をしてみましょう
今回はtypescriptを使用して環境を作成します また、jsonwebtokenで検証できるClaimの項目は以下のリンクから把握します → https://jwt.io/libraries?language=Node.js

root
 ┗ package.json
 ┗ index.ts
// package.json
{
  "name": "jwt-verify",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "dev": "ts-node-dev index.ts"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.14",
    "@types/jsonwebtoken": "^8.5.9",
    "ts-node-dev": "^2.0.0",
    "typescript": "^4.8.3"
  }
}

typescriptの設定ファイルを作成しましょう

$ npx tsc --init
# tsconfig.jsonが作成される

以下index.tsのコードです

import express from 'express';
import {sign, verify, decode, JwtPayload, VerifyErrors, Jwt} from 'jsonwebtoken';

const app = express();
app.use(express.json());

app.post('/sign', (_req: express.Request, res: express.Response) => {
  const jwt = sign({}, 'privateKey');

  res.json({token: jwt});
});

app.post('/decode', (req: express.Request, res: express.Response) => {
  const decoded = decode(req.body.token);

  res.json({decoded});
});

app.post('/verify', (req: express.Request, res: express.Response) => {
  verify(
    req.body.token,
    'privateKey',
    (error: VerifyErrors | null, decoded: Jwt | JwtPayload | string | undefined) => {
      if (error) {
        res.json({error});
      }

      res.json({decoded});
  }
  );
});

app.listen(3000, () => console.log('listening on port 3000'));

iatのみの検証

まずは、起動します

$ npm run dev

では、一番簡素なjwtを作成してみましょう

$ curl -X POST localhost:3000/sign
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NjM1Njg2NDV9.qVg43Mz1jN7Cv1AIwFvLgRnueVRL62RCE_YIrEO8b1g"}

JWTができました。検証してみましょう

$ curl -X POST -H "Content-Type: application/json" -d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NjM1Njg2NDV9.qVg43Mz1jN7Cv1AIwFvLgRnueVRL62RCE_YIrEO8b1g"}' localhost:3000/verify
> {"decoded":{"iat":1663568645}}

iatだけのPAYLOADができていました。jsonwebtokenは何も指定しないと現在時刻のunixtimeをiatとします。iatの場合はprivateKeyさえあっていれば検証は成功します。試しにverifyのprivateKeypprivateKeyにしてもう一度トークンを検証してみましょう

$ curl -X POST -H "Content-Type: application/json" -d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NjM1Njg2NDV9.qVg43Mz1jN7Cv1AIwFvLgRnueVRL62RCE_YIrEO8b1g"}' localhost:3000/verify
> {"error":{"name":"JsonWebTokenError","message":"invalid signature"}}

エラーになりましたね。これはprivateKeyが変わったことによりsignatureによる検証がうまく実行できなくエラーになったことを意味しています。

expを追加した検証

次に有効期限のついた場合のjwt検証を行っていきます。
index.tsのsign関数を以下に変更します

app.post('/sign', (_req: express.Request, res: express.Response) => {
  const exp = Math.floor(new Date().getTime() / 1000) + 60;
  const jwt = sign({exp}, 'privateKey');

  res.json({token: jwt});
});

JWT作成から1分間有効なJWTになります。
作成してすぐは検証に成功しますが、1分経つ(expより未来になる)と検証は失敗します

$ curl -X POST -H "Content-Type: application/json" -d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjM1ODkzNTYsImlhdCI6MTY2MzU4OTI5Nn0.QwZSIkHVuU78EZXezw56BeXnHe63NExjQZyR2CQjKLY"}' localhost:3000/verify
> {"error":{"name":"TokenExpiredError","message":"jwt expired","expiredAt":"2022-09-19T12:09:16.000Z"}}

このようにしてJWTはPAYLOADの様々なClaimによって二者間のリクエスト時の認証を可能にします。 jsonwebtokenのverify関数はClaimによって認証の可否を検証します。decode関数はJWTを検証せず、decodeするだけなので注意が必要です。試しにexpが切れたJWTをverifyではなく、decodeAPIで指定するとエラーなく処理できることが可能なことがわかると思います。

終わりに

jwtの説明をコードも交えながら説明しました。触れてみてわかると思いますがjwt自体はそんなに難しいものではありません。簡単な認証処理なら誰でも実装できると思います。しかしjwtを利用したシステムの脆弱性は検索すると多く指摘されており使用するには注意が必要です。技術を知るだけでなくその技術の使用方法までしっかり熟知した上で使用していきたいですね。
熟知するまで勉強していたら何も作れませんが…

参考