oauth について
FastifyでOAuth 2.0/2.1を実装する開発者向けガイドです。実装手順に加え、RFCに基づく検証項目とセキュリティ上の注意点を扱います。
主な機能
- 01PKCE付き認可コードフロー
- 02JWTクレームの検証
- 03ルートアクセス制御ミドルウェア
- 04リフレッシュトークンのローテーション
- 05RFCセキュリティチェックリスト
活用シーン
- 01FastifyアプリでのOAuth実装
- 02トークン検証のデバッグ
- 03Fastifyルートの保護
- 04OAuthのセキュリティ上の問題点の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mcollina/ skills --skill oauthインストール後、Claude Code で/oauthを入力して読み込まれたか確認します。
同梱ファイルtile.json
詳細
使用する場面
以下が必要な場合に、このスキルを使用してください。
- Fastify アプリケーションで OAuth 2.0/2.1 フローを実装またはデバッグする
- トークンを検証し、PKCE を設定する、またはリフレッシュトークンのローテーションを設定する
- アクセス制御ミドルウェアを使用して Fastify のルートとプラグインを保護する
- RFC への準拠に関する疑問を解決する、またはセキュリティ上のアンチパターンを特定する
手順:Fastify での Authorization Code + PKCE
1. 依存関係をインストールする
npm install @fastify/oauth2 @fastify/cookie @fastify/session fastify-plugin
2. OAuth プラグインを登録する
// plugins/oauth.ts
import fp from 'fastify-plugin'
import oauth2, { OAuth2Namespace } from '@fastify/oauth2'
import { FastifyInstance } from 'fastify'
export default fp(async function (fastify: FastifyInstance) {
fastify.register(oauth2, {
name: 'oauth2',
scope: ['openid', 'profile', 'email'],
credentials: {
client: {
id: process.env.CLIENT_ID!,
secret: process.env.CLIENT_SECRET!,
},
auth: {
authorizeHost: process.env.AUTH_SERVER!,
authorizePath: '/authorize',
tokenHost: process.env.AUTH_SERVER!,
tokenPath: '/token',
},
},
startRedirectPath: '/login',
callbackUri: process.env.CALLBACK_URI!,
pkce: 'S256', // RFC 7636 — always use for public clients
generateStateFunction: (req) => req.session.state = crypto.randomUUID(),
checkStateFunction: (req, callback) =>
req.query.state === req.session.state ? callback() : callback(new Error('State mismatch')),
})
})
検証チェックポイント: 続行する前に、callbackUri が認可サーバーに登録されたリダイレクト URI と完全に一致することを確認してください(RFC 6749 §3.1.2)。
3. コールバックを処理してコードを交換する
// routes/auth.ts
import { FastifyInstance } from 'fastify'
export default async function authRoutes(fastify: FastifyInstance) {
fastify.get('/login/callback', async (request, reply) => {
// @fastify/oauth2 verifies state and exchanges code automatically
const tokenResponse = await fastify.oauth2.getAccessTokenFromAuthorizationCodeFlow(request)
// Store only what you need; never log the raw token
request.session.set('accessToken', tokenResponse.token.access_token)
request.session.set('refreshToken', tokenResponse.token.refresh_token)
return reply.redirect('/')
})
fastify.get('/logout', async (request, reply) => {
await request.session.destroy()
return reply.redirect('/')
})
}
4. JWT 検証ミドルウェア(トークンイントロスペクションフック)
// hooks/verifyToken.ts
import { FastifyRequest, FastifyReply } from 'fastify'
import jwt from '@fastify/jwt'
export async function verifyToken(request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify()
// Validate required claims (RFC 7519)
const payload = request.user as Record<string, unknown>
const now = Math.floor(Date.now() / 1000)
if (typeof payload.exp === 'number' && payload.exp < now)
return reply.code(401).send({ error: 'token_expired' })
if (payload.iss !== process.env.EXPECTED_ISSUER)
return reply.code(401).send({ error: 'invalid_issuer' })
if (payload.aud !== process.env.EXPECTED_AUDIENCE)
return reply.code(401).send({ error: 'invalid_audience' })
} catch (err) {
return reply.code(401).send({ error: 'invalid_token', error_description: (err as Error).message })
}
}
検証チェックポイント:
- リクエストごとに
exp、iss、aud、subを検証してください。決して省略しないでください(RFC 7519 §4)。 - サードパーティサーバーが発行したトークンには、HS256 ではなく
fastify.jwt.verify(非対称鍵の RS256/ES256)を使用してください。
5. ルートを保護する
// routes/api.ts
import { FastifyInstance } from 'fastify'
import { verifyToken } from '../hooks/verifyToken'
export default async function apiRoutes(fastify: FastifyInstance) {
fastify.addHook('onRequest', verifyToken) // applies to all routes in this scope
fastify.get('/me', {
schema: {
response: { 200: { type: 'object', properties: { sub: { type: 'string' } } } },
},
}, async (request) => {
const user = request.user as { sub: string }
return { sub: user.sub }
})
}
6. リフレッシュトークンのローテーション
async function refreshAccessToken(fastify: FastifyInstance, refreshToken: string) {
const newToken = await fastify.oauth2.getNewAccessTokenUsingRefreshTokenFlow({ refresh_token: refreshToken })
// Always replace the stored refresh token if rotation is in use (RFC 6749 §10.4)
return {
accessToken: newToken.token.access_token,
refreshToken: newToken.token.refresh_token ?? refreshToken,
}
}
セキュリティチェックリスト
| 要件 | RFC 参照 |
|---|---|
| 許可リストに照らしてリダイレクト URI を検証する | RFC 6749 §3.1.2 |
| すべてのパブリッククライアントで PKCE(S256)を使用する | RFC 7636 §4.2 |
CSRF を防ぐため state を検証する | RFC 6749 §10.12 |
すべての JWT で iss、aud、exp を検証する | RFC 7519 §4 |
| 使用するたびにリフレッシュトークンをローテーションする | RFC 6749 §10.4 |
| あらゆる場所で HTTPS を使用し、HTTP のリダイレクト URI を拒否する | RFC 6749 §3.1.2.1 |
| トークンエンドポイントにレート制限を設ける | OAuth 2.1 §7 |
よくあるアンチパターン
- トークンを localStorage に保存する — 代わりに
HttpOnly、Secure、SameSite=StrictCookie を使用してください。 - オーディエンスの検証を省略する — サービス間でトークンが再利用される可能性があります。
- インプリシットフローを使用する — OAuth 2.1 では非推奨です。authorization code + PKCE を使用してください。
- ブラウザーアプリで
response_type=tokenを受け入れる — URL フラグメント内のトークンがログやリファラーから漏えいします。 - サードパーティトークンに対称署名(HS256)を使用する — JWKS エンドポイントを使用した RS256/ES256 を使用してください。
実装に関するその他の参考資料
- デバイス認可フロー(RFC 8628)の実装については、
DEVICE_FLOW.mdを参照してください。 - JWKS のローテーション、キャッシュ戦略、不透明トークンのイントロスペクションについては、
TOKEN_VALIDATION.mdを参照してください。 - マシン間のサービス認証パターンについては、
CLIENT_CREDENTIALS.mdを参照してください。 - ネイティブ/モバイルアプリのフロー(RFC 8252)とカスタム URI スキームについては、
MOBILE_OAUTH.mdを参照してください。
コメント
Markdown 対応