npx skills add https://github.com/mcollina/skills --skill oauth在以下场景中使用此技能:
npm install @fastify/oauth2 @fastify/cookie @fastify/session fastify-plugin
// 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 — 公共客户端始终使用
generateStateFunction: (req) => req.session.state = crypto.randomUUID(),
checkStateFunction: (req, callback) =>
req.query.state === req.session.state ? callback() : callback(new Error('State mismatch')),
})
})
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
验证检查点: 在继续之前,请确认 callbackUri 与授权服务器上注册的重定向 URI 完全匹配(RFC 6749 §3.1.2)。
// routes/auth.ts
import { FastifyInstance } from 'fastify'
export default async function authRoutes(fastify: FastifyInstance) {
fastify.get('/login/callback', async (request, reply) => {
// @fastify/oauth2 会自动验证 state 并交换授权码
const tokenResponse = await fastify.oauth2.getAccessTokenFromAuthorizationCodeFlow(request)
// 仅存储所需内容;切勿记录原始令牌
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('/')
})
}
// 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()
// 验证必需的声明(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// routes/api.ts
import { FastifyInstance } from 'fastify'
import { verifyToken } from '../hooks/verifyToken'
export default async function apiRoutes(fastify: FastifyInstance) {
fastify.addHook('onRequest', verifyToken) // 应用于此作用域内的所有路由
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 }
})
}
async function refreshAccessToken(fastify: FastifyInstance, refreshToken: string) {
const newToken = await fastify.oauth2.getNewAccessTokenUsingRefreshTokenFlow({ refresh_token: refreshToken })
// 如果使用轮换机制,请始终替换存储的刷新令牌(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 |
HttpOnly、Secure、SameSite=Strict 的 Cookieresponse_type=token — URL 片段中的令牌会在日志/引用来源中泄露DEVICE_FLOW.md 了解设备授权流程(RFC 8628)的实现TOKEN_VALIDATION.md 了解 JWKS 轮换、缓存策略和不透明令牌内省CLIENT_CREDENTIALS.md 了解机器对机器服务认证模式MOBILE_OAUTH.md 了解原生/移动应用流程(RFC 8252)和自定义 URI 方案每周安装量
212
代码仓库
GitHub 星标数
1.5K
首次出现
2026年1月31日
安全审计
已安装于
codex204
gemini-cli203
opencode203
github-copilot203
kimi-cli202
cursor202
Use this skill when you need to:
npm install @fastify/oauth2 @fastify/cookie @fastify/session fastify-plugin
// 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')),
})
})
Validation checkpoint: Confirm callbackUri exactly matches a registered redirect URI at the authorization server before proceeding (RFC 6749 §3.1.2).
// 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('/')
})
}
// 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 })
}
}
Validation checkpoints:
exp, iss, aud, and sub on every request — never skip (RFC 7519 §4)fastify.jwt.verify (asymmetric RS256/ES256) rather than HS256 for tokens issued by a third-party server// 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 }
})
}
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,
}
}
| Requirement | RFC reference |
|---|---|
| Validate redirect URI against allowlist | RFC 6749 §3.1.2 |
| PKCE (S256) for all public clients | RFC 7636 §4.2 |
Validate state to prevent CSRF | RFC 6749 §10.12 |
Validate iss, aud, exp on every JWT | RFC 7519 §4 |
| Rotate refresh tokens on every use | RFC 6749 §10.4 |
| Use HTTPS everywhere; reject HTTP redirect URIs | RFC 6749 §3.1.2.1 |
| Rate-limit token endpoints | OAuth 2.1 §7 |
HttpOnly, Secure, SameSite=Strict cookies insteadresponse_type=token in browser apps — tokens in URL fragments leak in logs/referrersDEVICE_FLOW.md for device authorization flow (RFC 8628) implementationTOKEN_VALIDATION.md for JWKS rotation, caching strategies, and opaque token introspectionCLIENT_CREDENTIALS.md for machine-to-machine service authentication patternsMOBILE_OAUTH.md for native/mobile app flows (RFC 8252) and custom URI schemesWeekly Installs
212
Repository
GitHub Stars
1.5K
First Seen
Jan 31, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex204
gemini-cli203
opencode203
github-copilot203
kimi-cli202
cursor202
Better Auth 最佳实践指南:集成、配置与安全设置完整教程
30,700 周安装