mcpField
O
oauth

oauth

github.com/mcollina安全2.0k1.4k 次安装2 周前更新
代码仓库

关于 oauth

面向 Fastify 开发者的 OAuth 2.0/2.1 实现指南,涵盖认证流程的搭建与排错,并结合 RFC 校验要点和安全误区说明。

主要功能

  1. 01授权码流程与 PKCE
  2. 02JWT 声明校验
  3. 03路由访问控制中间件
  4. 04刷新令牌轮换
  5. 05RFC 安全检查清单

适用场景

  1. 01在 Fastify 应用中实现 OAuth
  2. 02排查令牌校验问题
  3. 03保护 Fastify 路由
  4. 04检查 OAuth 安全误区

安装

复制下面的命令,在终端中运行。

$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 中使用授权码 + 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)
  • 对第三方服务签发的令牌,使用 fastify.jwt.verify(非对称 RS256/ES256),而非 HS256
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 参考
根据允许列表验证重定向 URIRFC 6749 §3.1.2
所有公共客户端均使用 PKCE(S256)RFC 7636 §4.2
验证 state 以防止 CSRFRFC 6749 §10.12
每次验证 JWT 时都验证 iss、aud、expRFC 7519 §4
每次使用时轮换刷新令牌RFC 6749 §10.4
全程使用 HTTPS;拒绝 HTTP 重定向 URIRFC 6749 §3.1.2.1
对令牌端点实施速率限制OAuth 2.1 §7

常见反模式

  • 将令牌存储在 localStorage 中——改用 HttpOnly、Secure、SameSite=Strict Cookie
  • 跳过受众验证——会导致令牌可在不同服务间重复使用
  • 使用隐式授权流程——已在 OAuth 2.1 中弃用;请使用授权码 + PKCE
  • 在浏览器应用中接受 response_type=token——URL 片段中的令牌会泄露到日志和 referrer 中
  • 对第三方令牌使用对称签名(HS256)——请通过 JWKS 端点使用 RS256/ES256

更多实现参考

  • 设备授权流程(RFC 8628)的实现请参阅 DEVICE_FLOW.md
  • JWKS 轮换、缓存策略和不透明令牌内省请参阅 TOKEN_VALIDATION.md
  • 机器到机器服务身份验证模式请参阅 CLIENT_CREDENTIALS.md
  • 原生/移动应用流程(RFC 8252)和自定义 URI 方案请参阅 MOBILE_OAUTH.md

评论

支持 Markdown