Cloudflare Workers + HonoJSチュートリアル②：JWT認証

## はじめに

前回は HonoJS の基礎やミドルウェアを使った機能拡張について紹介しました。

https://qiita.com/syukan3/items/3632f21915b7a78f1470

今回は、アプリケーションのセキュリティを強化するために **JWT (JSON Web Token)** を使用した認証方式を導入してみましょう。

JWT を使った認証は、Web API では広く採用されており、マイクロサービスや SPA (Single Page Application) など様々なシナリオで使われています。



## 1. 追加パッケージのインストール

まずは、JSON Web Token (JWT) を扱うためのライブラリをインストールします。

```bash
npm install jsonwebtoken @types/jsonwebtoken
```

- `jsonwebtoken` : JWT の発行・検証に使用
- `@types/jsonwebtoken` : TypeScript 用の型定義

---

## 2. 環境変数 (.env) の導入

本番環境を見据える場合は、JWT のシークレットキーや他の機密情報を直接ソースコードに書くのは望ましくありません。  
`.env` ファイルを用意し、環境変数で管理するようにします。

```bash
npm install dotenv
```

次に `.env` ファイルを用意し、例えば以下のように書きます。

```
JWT_SECRET=my-super-secret-key
PORT=3000
```

`.gitignore` に `.env` を追加し、公開リポジトリには含めないように注意してください。

```bash
echo '.env' >> .gitignore
```

---

## 3. ディレクトリ構成

前回は簡単な構成でしたが、認証や他の機能を追加していくとファイルが増えていきます。以下のようにディレクトリを整理してみましょう。

```
my-hono-crud/
├─ .env
├─ package.json
├─ tsconfig.json
├─ src/
│   ├─ index.ts        // エントリーポイント
│   ├─ routes/
│   │   └─ auth.ts     // 認証関連 (JWT 発行など)
│   ├─ middlewares/
│   │   └─ auth.ts     // JWT 検証用ミドルウェア
│   └─ users/
│       └─ index.ts
└─ ...
```

ポイントは、認証用コードとユーザー CRUD コードを分離し、さらに共通して使う JWT 検証ロジックなどは `middlewares` に置くことです。

---

## 4. JWT 認証の流れ

JWT 認証フローの概要を簡単な mermaid 図で示します。

```mermaid
flowchart LR
    Client["クライアント"]
    PostLogin["POST /login"]
    HonoApp["Honoアプリ"]
    JWT["JWTトークン発行"]
    JWTReq["JWT付きリクエスト"]
    HonoMiddleware["Honoアプリ (ミドルウェアで検証)"]
    Auth["認可 or 401エラー"]

    Client --> PostLogin --> HonoApp
    HonoApp --> JWT
    Client --> JWTReq --> HonoMiddleware
    HonoMiddleware --> Auth

```

- "クライアント" が "POST /login" にユーザー名やパスワードなどの資格情報を送信
- "Honoアプリ" が認証に成功すると、"JWTトークン発行"
- "クライアント" は受け取った "JWTトークン" を付与して API にアクセス
- "Honoアプリ (ミドルウェアで検証)" がトークンの妥当性をチェックし、OK なら処理を継続、NG ならエラー (401 など) を返却

---

## 5. コード例

### 5-1. src/routes/auth.ts (ログイン用)

ここではユーザーのログイン処理と JWT トークンの発行を行います。認証方法 (DB 連携など) は簡略化して、ユーザー名だけで通すようにしています。
実際の実装では適切なパスワード管理や認証基盤を用意してください。

```typescript
import { Context } from 'hono'
import jwt from 'jsonwebtoken'
import 'dotenv/config'

const JWT_SECRET = process.env.JWT_SECRET || 'default-secret'

/**
 * ユーザー認証 (仮) & JWT トークン発行
 */
export const loginHandler = async (c: Context) => {
  const { username } = await c.req.json()
  // ここでは単純に "admin" ユーザーだけを通す例
  if (username !== 'admin') {
    return c.json({ error: 'Invalid credentials' }, 401)
  }

  // JWT を発行
  const token = jwt.sign({ username }, JWT_SECRET, {
    expiresIn: '1h', // トークンの有効期限 (例: 1時間)
  })

  return c.json({ message: 'Login success', token })
}
```

### 5-2. src/middlewares/auth.ts (JWT 検証用)

すべての保護ルート (CRUD エンドポイントなど) にアクセスする前に、このミドルウェアで JWT を検証します。

```typescript
import { Context, Next } from 'hono'
import jwt from 'jsonwebtoken'
import 'dotenv/config'

const JWT_SECRET = process.env.JWT_SECRET || 'default-secret'

export const jwtAuthMiddleware = async (c: Context, next: Next) => {
  const authHeader = c.req.header('Authorization')
  if (!authHeader) {
    return c.json({ error: 'No token provided' }, 401)
  }

  const token = authHeader.replace('Bearer ', '')

  try {
    // トークンが正しいかを検証
    jwt.verify(token, JWT_SECRET)
    // 検証に成功したら次へ
    await next()
  } catch (err) {
    return c.json({ error: 'Invalid token' }, 401)
  }
}
```

### 5-3. src/users/index.ts (ユーザー CRUD)

認証が必要なエンドポイントとして、前回の CRUD 処理をまとめた例です。
共通で `jwtAuthMiddleware` を挟むことで、トークンがない/不正な場合にはアクセスを拒否するようにします。

```typescript
import { Hono } from 'hono'
import { jwtAuthMiddleware } from '../middlewares/auth'

// メモリ上に保持するユーザー一覧
type User = { id: number; name: string }
let users: User[] = []

const userApp = new Hono()

// ミドルウェアを適用
userApp.use('*', jwtAuthMiddleware)

// Create: ユーザー作成 (POST /users)
userApp.post('/', async (c) => {
  const { name } = await c.req.json()
  const newUser: User = {
    id: Date.now(),
    name
  }
  users.push(newUser)
  return c.json({ message: 'User created', user: newUser })
})

// Read: 全ユーザー取得 (GET /users)
userApp.get('/', (c) => {
  return c.json(users)
})

// Read: 特定ユーザー取得 (GET /users/:id)
userApp.get('/:id', (c) => {
  const userId = parseInt(c.req.param('id'))
  const user = users.find((u) => u.id === userId)
  if (!user) {
    return c.json({ error: 'User not found' }, 404)
  }
  return c.json(user)
})

// Update: ユーザー更新 (PATCH /users/:id)
userApp.patch('/:id', async (c) => {
  const userId = parseInt(c.req.param('id'))
  const { name } = await c.req.json()
  const userIndex = users.findIndex((u) => u.id === userId)

  if (userIndex === -1) {
    return c.json({ error: 'User not found' }, 404)
  }

  users[userIndex].name = name
  return c.json({ message: 'User updated', user: users[userIndex] })
})

// Delete: ユーザー削除 (DELETE /users/:id)
userApp.delete('/:id', (c) => {
  const userId = parseInt(c.req.param('id'))
  const userIndex = users.findIndex((u) => u.id === userId)

  if (userIndex === -1) {
    return c.json({ error: 'User not found' }, 404)
  }

  const deletedUser = users.splice(userIndex, 1)[0]
  return c.json({ message: 'User deleted', user: deletedUser })
})

export default userApp
```

### 5-4. src/index.ts (エントリーポイント)

`index.ts` からログイン用ルート (`auth.ts`) とユーザー CRUD ルート (`user.ts`) をまとめて起動します。

```typescript
import { Hono } from 'hono'
import { loginHandler } from './routes/auth'
import userApp from './users/index'
import 'dotenv/config'

const app = new Hono()

// ログイン用エンドポイント
app.post('/login', loginHandler)

// ユーザー CRUD エンドポイント
app.route('/', userApp)

export default app
```

---

## 6. 動作確認

### 6-1. ログイン (POST /login)

```bash
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"admin"}' \
  http://localhost:8787/login
```

レスポンス例 (正常時):
```json
{
  "message": "Login success",
  "token": "eyJhbGciOiJI..."
}
```

### 6-2. ユーザー作成 (POST /users)

上で取得した `"token"` を使い、Authorization ヘッダーに `"Bearer <トークン>"` を設定します。

```bash
curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJI..." \
  -d '{"name":"Alice"}' \
  http://localhost:8787/users
```

これ以降の GET /users, PATCH /users/:id, DELETE /users/:id も、同様に `"Authorization"` ヘッダーを付与してください。

---

## 7. まとめと今後のステップ

- **JWT 認証** を導入することで、不正アクセスを防ぎつつ API エンドポイントを保護できます。  
- **環境変数 (.env)** に機密情報 (JWT シークレットなど) をまとめ、本番環境と開発環境で設定を切り替えやすくします。  
- **フォルダ構成** を見直すことで、認証ロジックとビジネスロジックを分離し、可読性と保守性を高めます。

本番運用を視野に入れる場合は、さらに以下のような対策を検討するとよいでしょう。

- **HTTPS** の導入 (SSL/TLS 証明書設定)
- **CSRF 対策** (フォーム送信やブラウザ処理系の認証について)
- **Rate Limiting** (リクエスト数制限で DoS 対策)
- **ロギング・監視** (アクセスログやエラーログの集約)
- **DB 連携** (実際の RDB や NoSQL などへの移行)

これで、簡単な JWT 認証を組み込んだ Hono アプリのチュートリアルは完了です。ぜひ、セキュリティ周りを強化して、本番環境へステップアップしてみてください。

---
### 会社紹介

**株式会社 Mosaica**  
最先端テクノロジーで社会課題を解決し、持続可能な未来を創造する IT カンパニー。
AI ソリューション、クラウド統合、DX 推進、経営コンサルティングなど包括的なサービスでビジネス変革を支援しています。  

詳しくは [公式サイト](https://mosaica.co.jp/) までお気軽にご相談ください。
公式サイト： https://mosaica.co.jp/