Open3

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

まさぴょん🐱まさぴょん🐱

JWTをTypeScriptで実装する際のライブラリ選定📝

JWT(Json Web Token) については、こちらをご覧ください💁
https://zenn.dev/manase/scraps/29281adc12c0dd

まず結論をひと言でまとめると、これから 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)

3. 実装スニペット(jose + RS256)

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. セキュリティ & 運用ベストプラクティス

  1. アルゴリズム混同対策
    none アルゴリズム・HS→RS 混同を防げるライブラリ (jose, fast-jwt) を使用し、alg をホワイトリスト固定する (GitLab Advisory Database)。
  2. キーのローテーション
    JWKS + Key ID(kid) を使うと、署名鍵の自動ローテートが可能。jwks-rsa のキャッシュ+バックオフ戦略が便利 (npm)。
  3. Node 22 以降の ES モジュール化
    josefast-jwt は ESM ネイティブ。CommonJS しか対応しない旧ライブラリは将来互換性リスクがある (nodejs.org)。
  4. 検証時のクレームチェック忘れ
    exp, nbf, iat, aud, iss を必ず検証。jose ならオプションで一括バリデーションできるため実装漏れを防げる (WorkOS — Your app, Enterprise Ready.)。
  5. HTTP ヘッダから取り出すだけでなく CSRF 対策
    SPA で Cookie に入れる場合は SameSite=Strict とトークンベースの CSRF 対策を併用する。Express の公式セキュリティガイドも参照 (expressjs.com)。

5. ライブラリ選定フロー(簡易チャート)

  1. 利用フレームワークが専用プラグインを提供しているか?
    → Yes: まず公式プラグインを採用 (@nestjs/jwt, @fastify/jwt, passport-jwt …)。
  2. Edge Runtime / Bun / Cloudflare Workers 対応が必要か?
    → Yes: jose 一択。
  3. 既存コードが jsonwebtoken 依存か?
    → Yes: 置き換えコストに応じて「そのまま続投」or 「fast-jwt に drop-in 置換」。
  4. 暗号化 JWE が必要か?
    → Yes: jose(暗号化 JWT をサポート)を採用。

まとめ

  • 共通ロジックjose で書き、
  • フレームワーク固有の認証フックは公式プラグインで巻き取る、
    という 2 層構成が 2025 年時点での最適解です。これにより将来の Node バージョンアップや Web 標準化の流れにも乗り遅れにくくなります。
まさぴょん🐱まさぴょん🐱

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 開発 / 本番運用時のセキュリティ注意

  1. HTTPS 必須。HTTP だとアクセストークンが盗聴・改竄されます。
  2. 長期保存先 — 例では localStorage を使いましたが、XSS 対策を徹底できない場合は HttpOnly; Secure; SameSite Cookie で送るか、Token を Memory に保持し Refresh-Token Flow を採用してください (YouTube)。
  3. アルゴリズム固定 — HMAC を使う場合は alg === HS256 をハードコードし、none アルゴリズムや混同攻撃を防ぎます (Stack Overflow, Auth0コミュニティ)。
  4. キーのローテーション — RS256+JWKS に切り替えれば公開鍵をフロントに配布するだけで済み、鍵更新も安全です(createRemoteJWKSet が便利) (Medium)。
  5. テスト & デバッグjwt.io などのオンラインデコーダに秘密鍵を貼らないこと。jose はブラウザ側でも jwtVerify が動くのでローカルで検証できます。
  6. Hono v4 以降jwt ミドルウェアは Cloudflare Workers / Bun でも動作するため、同一コードで Edge へ展開可能です (YouTube)。

まとめ

  • Honojwt ミドルウェアと jose の組み合わせで、わずか数十行で安全なサーバサイド JWT 署名/検証が実装可能。
  • React/Vite 側は標準 fetch をラップして Authorization ヘッダを自動付与すれば、API 呼び出しがシンプルに。
  • 本番では HTTPS・鍵管理・XSS/CSRF 対策 を必ず合わせて実施してください。

これで “パスワード送信 → JWT 交付 → 保護 API へアクセス” の最低限の流れを一通り体験できます。さらに RS256 + Refresh Token Flow へ拡張することで、より実戦的なスケーラブル構成へ発展させられます。