关于 oauth
面向 Fastify 开发者的 OAuth 2.0/2.1 实现指南,涵盖认证流程的搭建与排错,并结合 RFC 校验要点和安全误区说明。
主要功能
- 01授权码流程与 PKCE
- 02JWT 声明校验
- 03路由访问控制中间件
- 04刷新令牌轮换
- 05RFC 安全检查清单
适用场景
- 01在 Fastify 应用中实现 OAuth
- 02排查令牌校验问题
- 03保护 Fastify 路由
- 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 参考 |
|---|---|
| 根据允许列表验证重定向 URI | RFC 6749 §3.1.2 |
| 所有公共客户端均使用 PKCE(S256) | RFC 7636 §4.2 |
验证 state 以防止 CSRF | 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 中弃用;请使用授权码 + 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