Skills oauth
Implements OAuth 2.0/2.1 authorization flows in Fastify applications — configures authorization code with PKCE, client credentials, device flow, refresh token rotation, JWT validation, and token introspection/revocation endpoints. Use when setting up authentication, authorization, login flows, access tokens, API security, or securing Fastify routes with OAuth; also applies when troubleshooting token validation errors, mismatched redirect URIs, CSRF issues, scope problems, or RFC 6749/6750/7636/8252/8628 compliance questions.
install
source · Clone the upstream repo
git clone https://github.com/mcollina/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/mcollina/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/oauth" ~/.claude/skills/mcollina-skills-oauth && rm -rf "$T"
manifest:
skills/oauth/SKILL.mdsource content
When to use
Use this skill when you need to:
- Implement or debug an OAuth 2.0/2.1 flow in a Fastify application
- Validate tokens, configure PKCE, or set up refresh token rotation
- Secure Fastify routes and plugins with access-control middleware
- Resolve RFC compliance questions or identify security anti-patterns
Step-by-step: Authorization Code + PKCE in Fastify
1. Install dependencies
npm install @fastify/oauth2 @fastify/cookie @fastify/session fastify-plugin
2. Register the OAuth 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).
3. Handle the callback and exchange the code
// 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 validation middleware (token introspection hook)
// 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:
- Verify
,exp
,iss
, andaud
on every request — never skip (RFC 7519 §4)sub - Use
(asymmetric RS256/ES256) rather than HS256 for tokens issued by a third-party serverfastify.jwt.verify
5. Protecting routes
// 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. Refresh token rotation
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, } }
Security checklist
| Requirement | RFC reference |
|---|---|
| Validate redirect URI against allowlist | RFC 6749 §3.1.2 |
| PKCE (S256) for all public clients | RFC 7636 §4.2 |
Validate to prevent CSRF | RFC 6749 §10.12 |
Validate , , 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 |
Common anti-patterns
- Storing tokens in localStorage — use
,HttpOnly
,Secure
cookies insteadSameSite=Strict - Skipping audience validation — allows token reuse across services
- Using implicit flow — deprecated in OAuth 2.1; use authorization code + PKCE
- Accepting
in browser apps — tokens in URL fragments leak in logs/referrersresponse_type=token - Symmetric signing (HS256) for third-party tokens — use RS256/ES256 with JWKS endpoint
Further implementation references
- See
for device authorization flow (RFC 8628) implementationDEVICE_FLOW.md - See
for JWKS rotation, caching strategies, and opaque token introspectionTOKEN_VALIDATION.md - See
for machine-to-machine service authentication patternsCLIENT_CREDENTIALS.md - See
for native/mobile app flows (RFC 8252) and custom URI schemesMOBILE_OAUTH.md