【認証/Auth系】JWTをTypeScriptで実装する際のライブラリ選定📝

JWTをTypeScriptで実装する際のライブラリ選定📝
JWT(Json Web Token) については、こちらをご覧ください💁
まず結論をひと言でまとめると、これから TypeScript で JWT を扱うなら jose
を第一候補にしつつ、フレームワーク別の公式プラグイン(@nestjs/jwt
、@fastify/jwt
など)を併用するのが最も安全で保守しやすい選択肢です。
従来の定番である jsonwebtoken
もまだ広く使われていますが、ESM 対応や型定義、パフォーマンス/セキュリティ面で後発ライブラリが優位に立っています。
以下でライブラリごとの特徴、選択指針、実装例、そしてセキュリティ上の注意点を整理します。
1. コア JWT ライブラリ
ライブラリ | 特徴 | 公式 TypeScript 型 | ESM | パフォーマンス | 備考 |
---|---|---|---|---|---|
jose |
署名・暗号化(JWS/JWE)・JWKS をフルサポート。最新ドラフト仕様に追従し、Node だけでなく Edge/Cloudflare Workers でも動作 | ✔ | ✔ | ◎ | API が関数型で木構造が浅く、型安全に書ける (npm) |
jsonwebtoken |
最も歴史が長くスター数も多い。「とりあえず動く」標準 | △ (別途 @types) | CJS のみ | ○ | メンテは続いているが公開後 1 年半リリースなし (npm) |
fast-jwt |
jsonwebtoken と 1:1 API 互換を保ちつつ高速化。アルゴリズム混同脆弱性を修正済み |
✔ | ✔ | ◎ | Fastify 公式プラグインが内部で採用 (npm, GitLab Advisory Database) |
選択のヒント
-
新規開発 →
jose
もしくはfast-jwt
。ESM/Edge 対応・型安全性重視。 -
既存プロジェクトで
jsonwebtoken
を大量使用 → 互換 API のfast-jwt
へ段階的に移行しやすい。 -
マイクロサービス間で鍵公開を JWKS で共有 →
jose
+jwks-rsa
が鉄板 (npm)。
2. フレームワーク統合ミドルウェア
フレームワーク | 推奨プラグイン/戦略 | メモ |
---|---|---|
Express / Hono |
express-jwt (簡易)または express-oauth2-jwt-bearer (Auth0 製)を middleware に。どちらも内部で jose を利用する最新ブランチあり (curity.io) |
|
Passport.js |
passport-jwt ストラテジ。Strategy + Extractor で柔軟にヘッダや Cookie からトークン取得可 (GitHub) |
|
NestJS |
@nestjs/jwt + @nestjs/passport で Guards を構成。公式 Doc に RS256 サンプルあり (GitHub, NestJS ドキュメント) |
|
Fastify |
@fastify/jwt (内部で fast-jwt 利用)。v9 から自動的に ESM/TS 型付き (npm) |
|
Supabase Edge Functions | Supabase Auth Helper で JWT 検証。HS256 デフォルトを RS256 へ切り替える場合は Secret と JWKS を同期 (Supabase, GitHub) |
jose
+ RS256)
3. 実装スニペット(import { jwtVerify, createRemoteJWKSet } from 'jose';
import type { JWTVerifyOptions } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://example.com/.well-known/jwks.json')
);
export async function verifyAccessToken(token: string, opts?: JWTVerifyOptions) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://example.com/',
audience: 'my-api',
...opts,
});
return payload; // 型推論で iss/aud/exp などに補完が効く
}
createRemoteJWKSet
がキャッシュとリトライを内蔵し、フェッチで公開鍵を自動取得するのでマイクロサービス構成でも楽にスケールします (npm)。
4. セキュリティ & 運用ベストプラクティス
-
アルゴリズム混同対策
none
アルゴリズム・HS→RS 混同を防げるライブラリ (jose
,fast-jwt
) を使用し、alg
をホワイトリスト固定する (GitLab Advisory Database)。 -
キーのローテーション
JWKS + Key ID(kid
) を使うと、署名鍵の自動ローテートが可能。jwks-rsa
のキャッシュ+バックオフ戦略が便利 (npm)。 -
Node 22 以降の ES モジュール化
jose
やfast-jwt
は ESM ネイティブ。CommonJS しか対応しない旧ライブラリは将来互換性リスクがある (nodejs.org)。 -
検証時のクレームチェック忘れ
exp
,nbf
,iat
,aud
,iss
を必ず検証。jose
ならオプションで一括バリデーションできるため実装漏れを防げる (WorkOS — Your app, Enterprise Ready.)。 -
HTTP ヘッダから取り出すだけでなく CSRF 対策
SPA で Cookie に入れる場合はSameSite=Strict
とトークンベースの CSRF 対策を併用する。Express の公式セキュリティガイドも参照 (expressjs.com)。
5. ライブラリ選定フロー(簡易チャート)
-
利用フレームワークが専用プラグインを提供しているか?
→ Yes: まず公式プラグインを採用 (@nestjs/jwt
,@fastify/jwt
,passport-jwt
…)。 -
Edge Runtime / Bun / Cloudflare Workers 対応が必要か?
→ Yes:jose
一択。 -
既存コードが
jsonwebtoken
依存か?
→ Yes: 置き換えコストに応じて「そのまま続投」or 「fast-jwt
に drop-in 置換」。 -
暗号化 JWE が必要か?
→ Yes:jose
(暗号化 JWT をサポート)を採用。
まとめ
-
共通ロジックは
jose
で書き、 -
フレームワーク固有の認証フックは公式プラグインで巻き取る、
という 2 層構成が 2025 年時点での最適解です。これにより将来の Node バージョンアップや Web 標準化の流れにも乗り遅れにくくなります。

fast-jwt vs jose vs jsonwebtoken📝

joseを使ったJWT認証のサンプル実装📝
概要 — jose
は Node・Edge・ブラウザすべてで動く型安全な JWT ライブラリで、Hono が公式に用意する jwt
ミドルウェアと非常に相性が良く、React/Vite からのフェッチも標準 fetch
API で完結します。
以下に「ログインでパスワードを送信 → サーバ側で検証して署名付きアクセストークンを返却 → 以降は Bearer <token>
ヘッダで保護 API にアクセス」という最小構成の TypeScript サンプルを示し、最後に本番環境向けのセキュリティ要点をまとめます。
1 セットアップと依存関係
環境 | ライブラリ | 備考 |
---|---|---|
FrontEnd (React + Vite) |
jose (v5+) / react-router-dom / 任意で @tanstack/react-query
|
jose はブラウザビルド同梱で Edge ランタイムでも動作 (npm) |
BackEnd (Hono @ Node ≥ 20) |
hono ^4 / hono/jwt / jose / bcryptjs
|
Hono 公式 JWT ミドルウェアが 2025-05 時点で最新版 v4 に同梱 (Hono, GitHub) |
# 片側ずつ
npm i jose
npm i hono bcryptjs # ← API
npm i react-router-dom # ← Front
2 バックエンド: Hono API (Node)
2-1 キー管理
開発用には HMAC-SHA256 で共有秘密鍵を .env
に置くだけでも動きますが、公開鍵配布が容易な RS256 (公開/秘密鍵ペア) を推奨します。jose
ならどちらも同じ API で扱えます (GitHub, Medium)。
// src/auth.ts
import { SignJWT, jwtVerify, type JWTPayload } from 'jose'
const encoder = new TextEncoder()
const secret = encoder.encode(process.env.JWT_SECRET!) // HS256 の例
export async function signAccessToken(payload: JWTPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret)
}
export async function verifyAccessToken(token: string) {
const { payload } = await jwtVerify(token, secret)
return payload
}
2-2 Hono ルーティング
// src/index.ts
import { Hono } from 'hono'
import { jwt } from 'hono/jwt' // 公式ミドルウェア
import { signAccessToken } from './auth'
import bcrypt from 'bcryptjs'
interface User { id: string; email: string; passwordHash: string }
const users: User[] = [
{ id: 'u1', email: 'alice@example.com', passwordHash: bcrypt.hashSync('Passw0rd!', 10) },
]
const app = new Hono()
app.post('/login', async c => {
const { email, password } = await c.req.json<{ email: string; password: string }>()
const user = users.find(u => u.email === email)
if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
return c.json({ message: 'Invalid credentials' }, 401)
}
const token = await signAccessToken({ sub: user.id, email: user.email })
return c.json({ token })
})
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET! })) // HS256 例
app.get('/api/me', c => {
// `jwtPayload` には verify 済みクレームが入る
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})
export default app
-
jwt
ミドルウェアはAuthorization: Bearer …
を自動検出し、失敗時は 401 を返します (Hono)。 -
bcryptjs
で平文パスワードは即ハッシュ比較し、決して保存しません。パスワード自体を JWT へ含めることは 厳禁 です。
3 フロントエンド: React / Vite
3-1 ログインフォーム
// src/Login.tsx
import { useState } from 'react'
export function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const res = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!res.ok) return alert('Login failed')
const { token } = await res.json()
localStorage.setItem('access_token', token) // 後述のセキュリティ注に注意
window.location.href = '/dashboard'
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="email" />
<input value={password} onChange={e => setPassword(e.target.value)} placeholder="password" type="password" />
<button>Sign in</button>
</form>
)
}
3-2 フェッチヘルパ
export async function apiFetch(input: RequestInfo, init: RequestInit = {}) {
const token = localStorage.getItem('access_token')
return fetch(input, {
...init,
headers: { ...(init.headers || {}), Authorization: `Bearer ${token}` },
})
}
3-3 保護ページ例
import { useEffect, useState } from 'react'
import { apiFetch } from './apiFetch'
export function Dashboard() {
const [user, setUser] = useState<any>(null)
useEffect(() => {
apiFetch('/api/me').then(async r => {
if (r.ok) setUser(await r.json())
else window.location.href = '/login'
})
}, [])
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
React 側はトークンの有無でルートガードするのが一般的です (DEV Community, Medium)。
4 開発 / 本番運用時のセキュリティ注意
- HTTPS 必須。HTTP だとアクセストークンが盗聴・改竄されます。
-
長期保存先 — 例では
localStorage
を使いましたが、XSS 対策を徹底できない場合はHttpOnly; Secure; SameSite
Cookie で送るか、Token を Memory に保持し Refresh-Token Flow を採用してください (YouTube)。 -
アルゴリズム固定 — HMAC を使う場合は
alg === HS256
をハードコードし、none
アルゴリズムや混同攻撃を防ぎます (Stack Overflow, Auth0コミュニティ)。 -
キーのローテーション — RS256+JWKS に切り替えれば公開鍵をフロントに配布するだけで済み、鍵更新も安全です(
createRemoteJWKSet
が便利) (Medium)。 -
テスト & デバッグ —
jwt.io
などのオンラインデコーダに秘密鍵を貼らないこと。jose
はブラウザ側でもjwtVerify
が動くのでローカルで検証できます。 -
Hono v4 以降 —
jwt
ミドルウェアは Cloudflare Workers / Bun でも動作するため、同一コードで Edge へ展開可能です (YouTube)。
まとめ
-
Hono の
jwt
ミドルウェアとjose
の組み合わせで、わずか数十行で安全なサーバサイド JWT 署名/検証が実装可能。 - React/Vite 側は標準
fetch
をラップしてAuthorization
ヘッダを自動付与すれば、API 呼び出しがシンプルに。 - 本番では HTTPS・鍵管理・XSS/CSRF 対策 を必ず合わせて実施してください。
これで “パスワード送信 → JWT 交付 → 保護 API へアクセス” の最低限の流れを一通り体験できます。さらに RS256 + Refresh Token Flow へ拡張することで、より実戦的なスケーラブル構成へ発展させられます。