AgentSkillsCN

auth-security-skills

Next.js 16 的安全身份验证与授权模式。适用于实现登录系统、会话管理、访问控制、多因素认证、密码哈希以及防范身份验证绕过攻击等场景。对于保护用户账户至关重要。

SKILL.md
--- frontmatter
name: auth-security-skills
description: Secure authentication and authorization patterns for Next.js 16. Use when implementing login systems, session management, access control, MFA, password hashing, and preventing authentication bypass attacks. Essential for protecting user accounts.
license: MIT

Auth Security Skills

Implement secure authentication and authorization for cyber-hardened Next.js 16 applications with Auth.js v5.

Table of Contents

  1. Secure Auth.js Setup
  2. Password Security
  3. Session Management
  4. Access Control
  5. Multi-Factor Authentication
  6. Brute Force Protection
  7. OAuth Security
  8. JWT Security
  9. Best Practices

Secure Auth.js Setup

Basic Configuration

typescript
// auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Google from 'next-auth/providers/google';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
import { verifyPassword } from '@/lib/security/password';
import { z } from 'zod';

const loginSchema = z.object({
  email: z.string().email().max(255),
  password: z.string().min(1).max(128),
});

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  
  providers: [
    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) {
        // Validate input
        const result = loginSchema.safeParse(credentials);
        if (!result.success) {
          return null;
        }
        
        const { email, password } = result.data;
        
        // Find user
        const user = await prisma.user.findUnique({
          where: { email: email.toLowerCase() },
          select: {
            id: true,
            email: true,
            name: true,
            password: true,
            role: true,
            lockedUntil: true,
            failedAttempts: true,
          },
        });
        
        if (!user || !user.password) {
          // Prevent timing attacks - always verify even with no user
          await verifyPassword('dummy', '$2b$12$dummy.hash.for.timing.attack.prevention');
          return null;
        }
        
        // Check if account is locked
        if (user.lockedUntil && user.lockedUntil > new Date()) {
          throw new Error('Account temporarily locked. Try again later.');
        }
        
        // Verify password
        const isValid = await verifyPassword(password, user.password);
        
        if (!isValid) {
          // Increment failed attempts
          await prisma.user.update({
            where: { id: user.id },
            data: {
              failedAttempts: { increment: 1 },
              lockedUntil: user.failedAttempts >= 4 
                ? new Date(Date.now() + 15 * 60 * 1000) // Lock for 15 mins after 5 attempts
                : null,
            },
          });
          return null;
        }
        
        // Reset failed attempts on successful login
        await prisma.user.update({
          where: { id: user.id },
          data: {
            failedAttempts: 0,
            lockedUntil: null,
            lastLogin: new Date(),
          },
        });
        
        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
        };
      },
    }),
  ],
  
  session: {
    strategy: 'jwt',
    maxAge: 24 * 60 * 60, // 24 hours
  },
  
  pages: {
    signIn: '/login',
    error: '/auth/error',
  },
  
  callbacks: {
    async jwt({ token, user, trigger, session }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }
      
      // Handle session update
      if (trigger === 'update' && session) {
        token.name = session.name;
      }
      
      return token;
    },
    
    async session({ session, token }) {
      session.user.id = token.id as string;
      session.user.role = token.role as string;
      return session;
    },
    
    async signIn({ user, account }) {
      // Log sign-in attempts
      console.info('[AUTH] Sign-in attempt', {
        userId: user.id,
        provider: account?.provider,
        timestamp: new Date().toISOString(),
      });
      
      return true;
    },
  },
  
  events: {
    async signIn({ user, isNewUser }) {
      console.info('[AUTH] Successful sign-in', {
        userId: user.id,
        isNewUser,
        timestamp: new Date().toISOString(),
      });
    },
    
    async signOut({ token }) {
      console.info('[AUTH] Sign-out', {
        userId: token?.id,
        timestamp: new Date().toISOString(),
      });
    },
  },
});

Type Extensions

typescript
// types/next-auth.d.ts
import 'next-auth';

declare module 'next-auth' {
  interface User {
    role?: string;
  }
  
  interface Session {
    user: {
      id: string;
      role: string;
      email: string;
      name?: string | null;
    };
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    id: string;
    role: string;
  }
}

Password Security

Argon2 Password Hashing

typescript
// lib/security/password.ts
import argon2 from 'argon2';

// Argon2id configuration (OWASP recommended)
const ARGON2_OPTIONS: argon2.Options = {
  type: argon2.argon2id,
  memoryCost: 65536, // 64 MB
  timeCost: 3,       // 3 iterations
  parallelism: 4,    // 4 parallel threads
  hashLength: 32,    // 32 bytes output
};

export async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, ARGON2_OPTIONS);
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  try {
    return await argon2.verify(hash, password);
  } catch {
    return false;
  }
}

// Check if password needs rehash (after configuration change)
export async function needsRehash(hash: string): Promise<boolean> {
  return argon2.needsRehash(hash, ARGON2_OPTIONS);
}

Password Strength Validation

typescript
// lib/validations/password.ts
import { z } from 'zod';

// Common passwords list (abbreviated - use full list in production)
const COMMON_PASSWORDS = new Set([
  'password', '123456', 'qwerty', 'admin', 'letmein',
  'welcome', 'monkey', 'dragon', 'master', 'password1',
]);

export const passwordSchema = z
  .string()
  .min(12, 'Password must be at least 12 characters')
  .max(128, 'Password is too long')
  .refine(
    (password) => /[A-Z]/.test(password),
    'Password must contain at least one uppercase letter'
  )
  .refine(
    (password) => /[a-z]/.test(password),
    'Password must contain at least one lowercase letter'
  )
  .refine(
    (password) => /[0-9]/.test(password),
    'Password must contain at least one number'
  )
  .refine(
    (password) => /[^A-Za-z0-9]/.test(password),
    'Password must contain at least one special character'
  )
  .refine(
    (password) => !COMMON_PASSWORDS.has(password.toLowerCase()),
    'This password is too common'
  )
  .refine(
    (password) => !/(.)\1{2,}/.test(password),
    'Password cannot contain 3+ repeated characters'
  );

export function calculatePasswordStrength(password: string): {
  score: number;
  feedback: string[];
} {
  let score = 0;
  const feedback: string[] = [];
  
  // Length bonus
  if (password.length >= 12) score += 1;
  if (password.length >= 16) score += 1;
  if (password.length >= 20) score += 1;
  
  // Character variety
  if (/[a-z]/.test(password)) score += 1;
  if (/[A-Z]/.test(password)) score += 1;
  if (/[0-9]/.test(password)) score += 1;
  if (/[^A-Za-z0-9]/.test(password)) score += 2;
  
  // Deductions
  if (/(.)\1{2,}/.test(password)) {
    score -= 1;
    feedback.push('Avoid repeated characters');
  }
  
  if (/^[A-Za-z]+$/.test(password)) {
    feedback.push('Add numbers or symbols');
  }
  
  return {
    score: Math.max(0, Math.min(10, score)),
    feedback,
  };
}

Secure Password Reset

typescript
// lib/security/password-reset.ts
import { randomBytes } from 'crypto';
import { prisma } from '@/lib/prisma';

export async function createPasswordResetToken(
  email: string
): Promise<string | null> {
  const user = await prisma.user.findUnique({
    where: { email: email.toLowerCase() },
  });
  
  if (!user) {
    // Return null but don't reveal user existence
    return null;
  }
  
  // Generate secure token
  const token = randomBytes(32).toString('hex');
  const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
  
  // Store hashed token
  const hashedToken = await hashToken(token);
  
  await prisma.passwordResetToken.create({
    data: {
      userId: user.id,
      token: hashedToken,
      expires,
    },
  });
  
  // Delete any old tokens for this user
  await prisma.passwordResetToken.deleteMany({
    where: {
      userId: user.id,
      expires: { lt: new Date() },
    },
  });
  
  return token;
}

export async function validatePasswordResetToken(
  token: string
): Promise<string | null> {
  const hashedToken = await hashToken(token);
  
  const resetToken = await prisma.passwordResetToken.findFirst({
    where: {
      token: hashedToken,
      expires: { gt: new Date() },
    },
    include: { user: true },
  });
  
  return resetToken?.userId || null;
}

async function hashToken(token: string): Promise<string> {
  const { createHash } = await import('crypto');
  return createHash('sha256').update(token).digest('hex');
}

Session Management

Secure Session Configuration

typescript
// lib/session/config.ts

export const SESSION_CONFIG = {
  // Maximum session age
  maxAge: 24 * 60 * 60, // 24 hours
  
  // Idle timeout (extend on activity)
  idleTimeout: 30 * 60, // 30 minutes
  
  // Secure cookie settings
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax' as const,
    path: '/',
  },
  
  // Session regeneration on privilege change
  regenerateOnRoleChange: true,
};

Session Activity Tracking

typescript
// lib/session/activity.ts
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

export async function recordSessionActivity(
  sessionId: string,
  userId: string
): Promise<void> {
  const key = `session:${sessionId}`;
  
  await redis.hmset(key, {
    lastActivity: Date.now(),
    userId,
  });
  
  await redis.expire(key, SESSION_CONFIG.maxAge);
}

export async function checkSessionValidity(
  sessionId: string
): Promise<boolean> {
  const key = `session:${sessionId}`;
  const session = await redis.hgetall(key);
  
  if (!session || !session.lastActivity) {
    return false;
  }
  
  const lastActivity = Number(session.lastActivity);
  const idleTime = Date.now() - lastActivity;
  
  // Check idle timeout
  if (idleTime > SESSION_CONFIG.idleTimeout * 1000) {
    await redis.del(key);
    return false;
  }
  
  return true;
}

export async function invalidateAllUserSessions(
  userId: string
): Promise<void> {
  // Scan for all sessions belonging to user
  const pattern = 'session:*';
  let cursor = 0;
  
  do {
    const [nextCursor, keys] = await redis.scan(cursor, {
      match: pattern,
      count: 100,
    });
    
    cursor = Number(nextCursor);
    
    for (const key of keys) {
      const session = await redis.hget(key, 'userId');
      if (session === userId) {
        await redis.del(key);
      }
    }
  } while (cursor !== 0);
}

Access Control

Role-Based Access Control (RBAC)

typescript
// lib/security/rbac.ts

export type Role = 'user' | 'moderator' | 'admin' | 'superadmin';
export type Permission = 
  | 'read:profile'
  | 'write:profile'
  | 'read:users'
  | 'write:users'
  | 'delete:users'
  | 'read:admin'
  | 'write:admin'
  | 'manage:system';

const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
  user: ['read:profile', 'write:profile'],
  moderator: ['read:profile', 'write:profile', 'read:users'],
  admin: [
    'read:profile', 'write:profile',
    'read:users', 'write:users',
    'read:admin', 'write:admin',
  ],
  superadmin: [
    'read:profile', 'write:profile',
    'read:users', 'write:users', 'delete:users',
    'read:admin', 'write:admin',
    'manage:system',
  ],
};

export function hasPermission(role: Role, permission: Permission): boolean {
  const permissions = ROLE_PERMISSIONS[role] || [];
  return permissions.includes(permission);
}

export function requirePermission(role: Role, permission: Permission): void {
  if (!hasPermission(role, permission)) {
    throw new Error(`Permission denied: ${permission}`);
  }
}

Protected Server Actions

typescript
// lib/security/auth-guard.ts
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { hasPermission, Permission, Role } from './rbac';

export async function requireAuth() {
  const session = await auth();
  
  if (!session?.user) {
    redirect('/login');
  }
  
  return session;
}

export async function requireRole(allowedRoles: Role[]) {
  const session = await requireAuth();
  
  if (!allowedRoles.includes(session.user.role as Role)) {
    throw new Error('Access denied');
  }
  
  return session;
}

export async function requirePermissionGuard(permission: Permission) {
  const session = await requireAuth();
  
  if (!hasPermission(session.user.role as Role, permission)) {
    throw new Error('Permission denied');
  }
  
  return session;
}

// Usage in Server Actions
// app/actions/admin.ts
'use server';

import { requirePermissionGuard } from '@/lib/security/auth-guard';

export async function deleteUser(userId: string) {
  const session = await requirePermissionGuard('delete:users');
  
  // Log the action
  console.info('[ADMIN_ACTION] Delete user', {
    adminId: session.user.id,
    targetUserId: userId,
    timestamp: new Date().toISOString(),
  });
  
  // Proceed with deletion
  await prisma.user.delete({ where: { id: userId } });
}

Resource-Level Access Control

typescript
// lib/security/resource-guard.ts
import { auth } from '@/auth';
import { prisma } from '@/lib/prisma';

export async function canAccessResource(
  resourceType: 'post' | 'project' | 'comment',
  resourceId: string,
  action: 'read' | 'write' | 'delete'
): Promise<boolean> {
  const session = await auth();
  
  if (!session?.user) return false;
  
  const userId = session.user.id;
  const role = session.user.role;
  
  // Admins can access everything
  if (role === 'admin' || role === 'superadmin') {
    return true;
  }
  
  // Check ownership
  switch (resourceType) {
    case 'post': {
      const post = await prisma.post.findUnique({
        where: { id: resourceId },
        select: { authorId: true, published: true },
      });
      
      if (!post) return false;
      
      // Read access for published posts
      if (action === 'read' && post.published) return true;
      
      // Write/delete requires ownership
      return post.authorId === userId;
    }
    
    case 'project': {
      const project = await prisma.project.findUnique({
        where: { id: resourceId },
        select: { ownerId: true, members: { select: { userId: true } } },
      });
      
      if (!project) return false;
      
      const isMember = project.members.some(m => m.userId === userId);
      const isOwner = project.ownerId === userId;
      
      if (action === 'read') return isMember || isOwner;
      if (action === 'write') return isMember || isOwner;
      if (action === 'delete') return isOwner;
      
      return false;
    }
    
    default:
      return false;
  }
}

Multi-Factor Authentication

TOTP Implementation

typescript
// lib/security/mfa.ts
import { authenticator } from 'otplib';
import QRCode from 'qrcode';
import { prisma } from '@/lib/prisma';
import { encrypt, decrypt } from '@/lib/security/encryption';

// Configure TOTP
authenticator.options = {
  step: 30,
  window: 1,
};

export async function setupMFA(userId: string): Promise<{
  secret: string;
  qrCodeUrl: string;
}> {
  const user = await prisma.user.findUnique({ where: { id: userId } });
  if (!user) throw new Error('User not found');
  
  // Generate secret
  const secret = authenticator.generateSecret();
  
  // Create TOTP URL
  const otpauth = authenticator.keyuri(
    user.email,
    'YourApp',
    secret
  );
  
  // Generate QR code
  const qrCodeUrl = await QRCode.toDataURL(otpauth);
  
  // Store encrypted secret (pending verification)
  const encryptedSecret = await encrypt(secret);
  await prisma.user.update({
    where: { id: userId },
    data: { pendingMfaSecret: encryptedSecret },
  });
  
  return { secret, qrCodeUrl };
}

export async function verifyAndEnableMFA(
  userId: string,
  token: string
): Promise<boolean> {
  const user = await prisma.user.findUnique({ where: { id: userId } });
  if (!user?.pendingMfaSecret) return false;
  
  const secret = await decrypt(user.pendingMfaSecret);
  const isValid = authenticator.verify({ token, secret });
  
  if (isValid) {
    // Move to active MFA
    await prisma.user.update({
      where: { id: userId },
      data: {
        mfaSecret: user.pendingMfaSecret,
        mfaEnabled: true,
        pendingMfaSecret: null,
      },
    });
  }
  
  return isValid;
}

export async function verifyMFAToken(
  userId: string,
  token: string
): Promise<boolean> {
  const user = await prisma.user.findUnique({ where: { id: userId } });
  if (!user?.mfaSecret || !user.mfaEnabled) return false;
  
  const secret = await decrypt(user.mfaSecret);
  return authenticator.verify({ token, secret });
}

export async function disableMFA(userId: string): Promise<void> {
  await prisma.user.update({
    where: { id: userId },
    data: {
      mfaSecret: null,
      mfaEnabled: false,
    },
  });
}

MFA Login Flow

typescript
// app/actions/mfa-login.ts
'use server';

import { signIn } from '@/auth';
import { verifyMFAToken } from '@/lib/security/mfa';
import { cookies } from 'next/headers';

export async function loginWithMFA(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
  const mfaToken = formData.get('mfaToken') as string | null;
  
  // First, verify credentials
  const user = await verifyCredentials(email, password);
  
  if (!user) {
    return { error: 'Invalid credentials' };
  }
  
  // Check if MFA is required
  if (user.mfaEnabled) {
    if (!mfaToken) {
      // Store temporary session for MFA step
      const cookieStore = await cookies();
      cookieStore.set('mfa_pending', user.id, {
        httpOnly: true,
        secure: true,
        maxAge: 300, // 5 minutes
      });
      
      return { requiresMFA: true };
    }
    
    // Verify MFA token
    const isValidMFA = await verifyMFAToken(user.id, mfaToken);
    if (!isValidMFA) {
      return { error: 'Invalid MFA code' };
    }
  }
  
  // Complete sign-in
  await signIn('credentials', {
    email,
    password,
    redirect: false,
  });
  
  return { success: true };
}

Brute Force Protection

Login Attempt Tracking

typescript
// lib/security/brute-force.ts
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

const MAX_ATTEMPTS = 5;
const LOCKOUT_DURATION = 15 * 60; // 15 minutes
const ATTEMPT_WINDOW = 60 * 60; // 1 hour

export async function recordLoginAttempt(
  identifier: string,
  success: boolean
): Promise<{ locked: boolean; attemptsRemaining: number }> {
  const key = `login:attempts:${identifier}`;
  
  if (success) {
    // Clear attempts on successful login
    await redis.del(key);
    return { locked: false, attemptsRemaining: MAX_ATTEMPTS };
  }
  
  // Increment failed attempts
  const attempts = await redis.incr(key);
  await redis.expire(key, ATTEMPT_WINDOW);
  
  if (attempts >= MAX_ATTEMPTS) {
    // Lock the account
    const lockKey = `login:locked:${identifier}`;
    await redis.set(lockKey, '1', { ex: LOCKOUT_DURATION });
    
    return { locked: true, attemptsRemaining: 0 };
  }
  
  return {
    locked: false,
    attemptsRemaining: MAX_ATTEMPTS - attempts,
  };
}

export async function isAccountLocked(identifier: string): Promise<boolean> {
  const lockKey = `login:locked:${identifier}`;
  const locked = await redis.get(lockKey);
  return locked === '1';
}

export async function getLockoutRemaining(identifier: string): Promise<number> {
  const lockKey = `login:locked:${identifier}`;
  return await redis.ttl(lockKey);
}

OAuth Security

Secure OAuth Configuration

typescript
// OAuth provider security settings
// auth.ts

Google({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  
  // Request minimal scopes
  authorization: {
    params: {
      scope: 'openid email profile',
      prompt: 'select_account',
    },
  },
  
  // Verify email domain for organizational accounts
  profile(profile) {
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
      image: profile.picture,
    };
  },
}),

// Account linking protection in callbacks
callbacks: {
  async signIn({ user, account, profile }) {
    // Prevent account takeover via OAuth
    if (account?.provider !== 'credentials') {
      const existingUser = await prisma.user.findUnique({
        where: { email: user.email! },
        include: { accounts: true },
      });
      
      if (existingUser && existingUser.accounts.length === 0) {
        // Existing user without OAuth - require password verification
        return '/auth/link-account';
      }
    }
    
    return true;
  },
},

JWT Security

Secure JWT Configuration

typescript
// lib/security/jwt.ts
import { SignJWT, jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
const JWT_ISSUER = 'your-app';
const JWT_AUDIENCE = 'your-app-users';

export async function createSecureJWT(payload: {
  sub: string;
  role: string;
  [key: string]: unknown;
}): Promise<string> {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setIssuer(JWT_ISSUER)
    .setAudience(JWT_AUDIENCE)
    .setExpirationTime('1h')
    .sign(JWT_SECRET);
}

export async function verifySecureJWT(token: string): Promise<{
  valid: boolean;
  payload?: Record<string, unknown>;
  error?: string;
}> {
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET, {
      issuer: JWT_ISSUER,
      audience: JWT_AUDIENCE,
    });
    
    return { valid: true, payload: payload as Record<string, unknown> };
  } catch (error) {
    return {
      valid: false,
      error: error instanceof Error ? error.message : 'Invalid token',
    };
  }
}

Best Practices

Authentication Security Checklist

  1. Password Security

    • Use Argon2id for hashing
    • Enforce strong password policies
    • Implement secure password reset
  2. Session Management

    • Use secure cookie flags
    • Implement idle timeout
    • Regenerate session on privilege change
  3. Access Control

    • Check permissions on every request
    • Use principle of least privilege
    • Implement resource-level checks
  4. Multi-Factor Authentication

    • Offer TOTP as second factor
    • Provide backup codes
    • Allow MFA for sensitive operations
  5. Brute Force Protection

    • Rate limit login attempts
    • Implement progressive delays
    • Lock accounts after threshold

Dependencies to Install

bash
npm install argon2 otplib qrcode jose @upstash/redis
npm install -D @types/qrcode

Environment Variables

env
AUTH_SECRET=32-character-random-secret
JWT_SECRET=another-32-character-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

Prisma Schema Extensions

prisma
model User {
  id              String    @id @default(cuid())
  email           String    @unique
  password        String?
  role            String    @default("user")
  mfaEnabled      Boolean   @default(false)
  mfaSecret       String?
  pendingMfaSecret String?
  failedAttempts  Int       @default(0)
  lockedUntil     DateTime?
  lastLogin       DateTime?
  accounts        Account[]
}

model PasswordResetToken {
  id      String   @id @default(cuid())
  userId  String
  token   String   @unique
  expires DateTime
  user    User     @relation(fields: [userId], references: [id])
}