[jwt] jwtの基礎から使用方法まで
目次
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
に分かれています。

構成 | 説明 |
---|---|
HEADER | JWTの構成の説明。 algは使用しているアルゴリズム。typはJWTで固定 |
PAYLOAD | jwtの検証するためのデータを格納している。説明は下記で記載 |
VERIFY SIGNATURE | HEADER・PAYLOAD・シークレットをHEADERのアルゴリズムでエンコードされた値 |
上記をbase64でエンコードした値をつなげた物をjwtとして扱います
実際に確かめてみましょう。画像のHEADERとなる部分をbase64でデコードしてみます
$ echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
{"alg":"HS256","typ":"JWT"}
本当にbase64されていましたね。確かめるまでもないですが..
次にPAYLOAD
でよく使用される値の説明をします
詳しくはこちら→RFC JWT
claim name | explanation |
---|---|
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のprivateKey
をpprivateKey
にしてもう一度トークンを検証してみましょう
$ 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を利用したシステムの脆弱性は検索すると多く指摘されており使用するには注意が必要です。技術を知るだけでなくその技術の使用方法までしっかり熟知した上で使用していきたいですね。
熟知するまで勉強していたら何も作れませんが…