AgentSkillsCN

auth-implementation-patterns

涵盖 JWT、OAuth2、会话管理与 RBAC 等认证与授权模式。适用于实施身份认证系统、保护 API 安全,或排查安全问题时使用。

SKILL.md
--- frontmatter
name: auth-implementation-patterns
description: Authentication and authorization patterns including JWT, OAuth2, session management, and RBAC. Use when implementing auth systems, securing APIs, or debugging security issues.

Authentication & Authorization Implementation Patterns

JWT Authentication

JWT Implementation

typescript
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

interface JWTPayload {
    userId: string;
    email: string;
    role: string;
    iat: number;
    exp: number;
}

function generateTokens(userId: string, email: string, role: string) {
    const accessToken = jwt.sign(
        { userId, email, role },
        process.env.JWT_SECRET!,
        { expiresIn: '15m' }
    );

    const refreshToken = jwt.sign(
        { userId },
        process.env.JWT_REFRESH_SECRET!,
        { expiresIn: '7d' }
    );

    return { accessToken, refreshToken };
}

function verifyToken(token: string): JWTPayload {
    try {
        return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
    } catch (error) {
        if (error instanceof jwt.TokenExpiredError) {
            throw new Error('Token expired');
        }
        if (error instanceof jwt.JsonWebTokenError) {
            throw new Error('Invalid token');
        }
        throw error;
    }
}

function authenticate(req: Request, res: Response, next: NextFunction) {
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'No token provided' });
    }

    const token = authHeader.substring(7);
    try {
        const payload = verifyToken(token);
        req.user = payload;
        next();
    } catch (error) {
        return res.status(401).json({ error: 'Invalid token' });
    }
}

Refresh Token Flow

typescript
class RefreshTokenService {
    async storeRefreshToken(userId: string, refreshToken: string) {
        const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
        await db.refreshTokens.create({
            token: await hash(refreshToken),  // Hash before storing
            userId,
            expiresAt,
        });
    }

    async refreshAccessToken(refreshToken: string) {
        let payload;
        try {
            payload = jwt.verify(
                refreshToken,
                process.env.JWT_REFRESH_SECRET!
            ) as { userId: string };
        } catch {
            throw new Error('Invalid refresh token');
        }

        const storedToken = await db.refreshTokens.findOne({
            where: {
                token: await hash(refreshToken),
                userId: payload.userId,
                expiresAt: { $gt: new Date() },
            },
        });

        if (!storedToken) throw new Error('Refresh token not found or expired');

        const user = await db.users.findById(payload.userId);
        if (!user) throw new Error('User not found');

        const accessToken = jwt.sign(
            { userId: user.id, email: user.email, role: user.role },
            process.env.JWT_SECRET!,
            { expiresIn: '15m' }
        );

        return { accessToken };
    }

    async revokeRefreshToken(refreshToken: string) {
        await db.refreshTokens.deleteOne({ token: await hash(refreshToken) });
    }

    async revokeAllUserTokens(userId: string) {
        await db.refreshTokens.deleteMany({ userId });
    }
}

Session-Based Authentication

typescript
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(
    session({
        store: new RedisStore({ client: redisClient }),
        secret: process.env.SESSION_SECRET!,
        resave: false,
        saveUninitialized: false,
        cookie: {
            secure: process.env.NODE_ENV === 'production',
            httpOnly: true,
            maxAge: 24 * 60 * 60 * 1000,
            sameSite: 'strict',
        },
    })
);

OAuth2 with Passport.js

typescript
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

passport.use(
    new GoogleStrategy(
        {
            clientID: process.env.GOOGLE_CLIENT_ID!,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
            callbackURL: '/api/auth/google/callback',
        },
        async (accessToken, refreshToken, profile, done) => {
            try {
                let user = await db.users.findOne({ googleId: profile.id });
                if (!user) {
                    user = await db.users.create({
                        googleId: profile.id,
                        email: profile.emails?.[0]?.value,
                        name: profile.displayName,
                        avatar: profile.photos?.[0]?.value,
                    });
                }
                return done(null, user);
            } catch (error) {
                return done(error, undefined);
            }
        }
    )
);

app.get('/api/auth/google', passport.authenticate('google', {
    scope: ['profile', 'email'],
}));

app.get(
    '/api/auth/google/callback',
    passport.authenticate('google', { session: false }),
    (req, res) => {
        const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
        res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`);
    }
);

Authorization Patterns

Role-Based Access Control (RBAC)

typescript
enum Role {
    USER = 'user',
    MODERATOR = 'moderator',
    ADMIN = 'admin',
}

const roleHierarchy: Record<Role, Role[]> = {
    [Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
    [Role.MODERATOR]: [Role.MODERATOR, Role.USER],
    [Role.USER]: [Role.USER],
};

function requireRole(...roles: Role[]) {
    return (req: Request, res: Response, next: NextFunction) => {
        if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
        if (!roles.some(role => roleHierarchy[req.user.role].includes(role))) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        next();
    };
}

Permission-Based Access Control

typescript
enum Permission {
    READ_USERS = 'read:users',
    WRITE_USERS = 'write:users',
    DELETE_USERS = 'delete:users',
    READ_POSTS = 'read:posts',
    WRITE_POSTS = 'write:posts',
}

const rolePermissions: Record<Role, Permission[]> = {
    [Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
    [Role.MODERATOR]: [Permission.READ_POSTS, Permission.WRITE_POSTS, Permission.READ_USERS],
    [Role.ADMIN]: Object.values(Permission),
};

function requirePermission(...permissions: Permission[]) {
    return (req: Request, res: Response, next: NextFunction) => {
        if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
        const hasAll = permissions.every(p =>
            rolePermissions[req.user.role]?.includes(p) ?? false
        );
        if (!hasAll) return res.status(403).json({ error: 'Insufficient permissions' });
        next();
    };
}

Resource Ownership

typescript
async function requireOwnership(resourceType: 'post' | 'comment', resourceIdParam: string = 'id') {
    return async (req: Request, res: Response, next: NextFunction) => {
        if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
        if (req.user.role === Role.ADMIN) return next();

        const resourceId = req.params[resourceIdParam];
        const resource = resourceType === 'post'
            ? await db.posts.findById(resourceId)
            : await db.comments.findById(resourceId);

        if (!resource) return res.status(404).json({ error: 'Resource not found' });
        if (resource.userId !== req.user.userId) return res.status(403).json({ error: 'Not authorized' });
        next();
    };
}

Password Security

typescript
import bcrypt from 'bcrypt';
import { z } from 'zod';

const passwordSchema = z.string()
    .min(12, 'Password must be at least 12 characters')
    .regex(/[A-Z]/, 'Must contain uppercase')
    .regex(/[a-z]/, 'Must contain lowercase')
    .regex(/[0-9]/, 'Must contain number')
    .regex(/[^A-Za-z0-9]/, 'Must contain special character');

async function hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, 12);
}

async function verifyPassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
}

Rate Limiting

typescript
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

const loginLimiter = rateLimit({
    store: new RedisStore({ client: redisClient }),
    windowMs: 15 * 60 * 1000,  // 15 minutes
    max: 5,
    message: 'Too many login attempts, please try again later',
    standardHeaders: true,
    legacyHeaders: false,
});

const apiLimiter = rateLimit({
    windowMs: 60 * 1000,
    max: 100,
    standardHeaders: true,
});

app.post('/api/auth/login', loginLimiter, async (req, res) => { /* ... */ });
app.use('/api/', apiLimiter);

Common Pitfalls

  • JWT in localStorage: Vulnerable to XSS, use httpOnly cookies
  • No Token Expiration: Tokens should expire (15-30min access, 7d refresh)
  • Client-Side Auth Only: Always validate server-side
  • Insecure Password Reset: Use secure tokens with expiration
  • No Rate Limiting: Vulnerable to brute force