mcpField
O
oauth

oauth

github.com/mcollinaセキュリティ2.0k1.4k 回インストール2 週間前に更新
リポジトリ

oauth について

FastifyでOAuth 2.0/2.1を実装する開発者向けガイドです。実装手順に加え、RFCに基づく検証項目とセキュリティ上の注意点を扱います。

主な機能

  1. 01PKCE付き認可コードフロー
  2. 02JWTクレームの検証
  3. 03ルートアクセス制御ミドルウェア
  4. 04リフレッシュトークンのローテーション
  5. 05RFCセキュリティチェックリスト

活用シーン

  1. 01FastifyアプリでのOAuth実装
  2. 02トークン検証のデバッグ
  3. 03Fastifyルートの保護
  4. 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=Strict Cookie を使用してください。
  • オーディエンスの検証を省略する — サービス間でトークンが再利用される可能性があります。
  • インプリシットフローを使用する — 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 を参照してください。

コメント