はじめに
前回は HonoJS の基礎やミドルウェアを使った機能拡張について紹介しました。
今回は、アプリケーションのセキュリティを強化するために JWT (JSON Web Token) を使用した認証方式を導入してみましょう。
JWT を使った認証は、Web API では広く採用されており、マイクロサービスや SPA (Single Page Application) など様々なシナリオで使われています。
1. 追加パッケージのインストール
まずは、JSON Web Token (JWT) を扱うためのライブラリをインストールします。
npm install jsonwebtoken @types/jsonwebtoken
-
jsonwebtoken
: JWT の発行・検証に使用 -
@types/jsonwebtoken
: TypeScript 用の型定義
2. 環境変数 (.env) の導入
本番環境を見据える場合は、JWT のシークレットキーや他の機密情報を直接ソースコードに書くのは望ましくありません。
.env
ファイルを用意し、環境変数で管理するようにします。
npm install dotenv
次に .env
ファイルを用意し、例えば以下のように書きます。
JWT_SECRET=my-super-secret-key
PORT=3000
.gitignore
に .env
を追加し、公開リポジトリには含めないように注意してください。
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 図で示します。
- "クライアント" が "POST /login" にユーザー名やパスワードなどの資格情報を送信
- "Honoアプリ" が認証に成功すると、"JWTトークン発行"
- "クライアント" は受け取った "JWTトークン" を付与して API にアクセス
- "Honoアプリ (ミドルウェアで検証)" がトークンの妥当性をチェックし、OK なら処理を継続、NG ならエラー (401 など) を返却
5. コード例
5-1. src/routes/auth.ts (ログイン用)
ここではユーザーのログイン処理と JWT トークンの発行を行います。認証方法 (DB 連携など) は簡略化して、ユーザー名だけで通すようにしています。
実際の実装では適切なパスワード管理や認証基盤を用意してください。
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 を検証します。
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
を挟むことで、トークンがない/不正な場合にはアクセスを拒否するようにします。
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
) をまとめて起動します。
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)
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username":"admin"}' \
http://localhost:8787/login
レスポンス例 (正常時):
{
"message": "Login success",
"token": "eyJhbGciOiJI..."
}
6-2. ユーザー作成 (POST /users)
上で取得した "token"
を使い、Authorization ヘッダーに "Bearer <トークン>"
を設定します。
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 アプリのチュートリアルは完了です。ぜひ、セキュリティ周りを強化して、本番環境へステップアップしてみてください。