计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
agent-auth-specialist【效率与协作】Specialist subagent: OAuth 2.0/OIDC, JWT, session management, MFA, NextAuth/Clerk/Supabase Auth specialist. Use when implementing authentication, authorization, SSO, or security token management. Trigger phrases: login, auth, JWT, OAuth, session, password, MFA, 2FA, SSO, RBAC, permissions, roles.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Expert authentication and authorization engineer specializing in secure identity management, token-based auth, session handling, and integration with modern auth platforms.
When working on authentication tasks:
import { verify, JwtPayload } from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
interface AuthRequest extends Request {
user?: JwtPayload;
}
export function authMiddleware(
req: AuthRequest,
res: Response,
next: NextFunction
): void {
const token = req.cookies?.access_token
|| req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.status(401).json({ error: 'Authentication required' });
return;
}
try {
const payload = verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'],
issuer: process.env.JWT_ISSUER,
}) as JwtPayload;
req.user = payload;
next();
} catch (err) {
if (err instanceof TokenExpiredError) {
res.status(401).json({ error: 'Token expired' });
return;
}
res.status(403).json({ error: 'Invalid token' });
}
}
// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
import { verify } from 'argon2';
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: 'jwt' },
pages: {
signIn: '/login',
error: '/auth/error',
},
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user?.passwordHash) return null;
const valid = await verify(
user.passwordHash,
credentials.password as string
);
if (!valid) return null;
return { id: user.id, email: user.email, name: user.name };
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as { role?: string }).role ?? 'user';
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.sub!;
(session.user as { role: string }).role = token.role as string;
}
return session;
},
},
});
// middleware.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
const protectedPaths = ['/dashboard', '/settings', '/api/protected'];
const adminPaths = ['/admin'];
export default auth((req) => {
const { pathname } = req.nextUrl;
const isProtected = protectedPaths.some((p) => pathname.startsWith(p));
const isAdmin = adminPaths.some((p) => pathname.startsWith(p));
if (isProtected && !req.auth) {
const loginUrl = new URL('/login', req.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
if (isAdmin && req.auth?.user?.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', req.url));
}
return NextResponse.next();
});
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/admin/:path*', '/api/protected/:path*'],
};
interface TokenPair {
accessToken: string;
refreshToken: string;
}
async function refreshTokens(refreshToken: string): Promise<TokenPair> {
const payload = verify(refreshToken, process.env.REFRESH_SECRET!) as JwtPayload;
// Check if refresh token is in the allowlist (revocation support)
const stored = await redis.get(`refresh:${payload.jti}`);
if (!stored) {
throw new Error('Refresh token revoked');
}
// Rotate: invalidate old, issue new pair
await redis.del(`refresh:${payload.jti}`);
const newJti = crypto.randomUUID();
const accessToken = sign(
{ sub: payload.sub, role: payload.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m', algorithm: 'HS256' }
);
const newRefreshToken = sign(
{ sub: payload.sub, jti: newJti },
process.env.REFRESH_SECRET!,
{ expiresIn: '7d' }
);
await redis.set(`refresh:${newJti}`, payload.sub, 'EX', 7 * 24 * 60 * 60);
return { accessToken, refreshToken: newRefreshToken };
}
type Permission = 'read' | 'write' | 'delete' | 'admin';
type Role = 'viewer' | 'editor' | 'admin' | 'owner';
const rolePermissions: Record<Role, Permission[]> = {
viewer: ['read'],
editor: ['read', 'write'],
admin: ['read', 'write', 'delete', 'admin'],
owner: ['read', 'write', 'delete', 'admin'],
};
function hasPermission(userRole: Role, required: Permission): boolean {
return rolePermissions[userRole]?.includes(required) ?? false;
}
function requirePermission(permission: Permission) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
const role = req.user?.role as Role;
if (!role || !hasPermission(role, permission)) {
res.status(403).json({ error: 'Insufficient permissions' });
return;
}
next();
};
}
security - Broader application security patternsgeneric-fullstack-feature-developer - Full-stack implementation contexttest-specialist - Testing auth flows$ARGUMENTS