# Auth Security Patterns

> Auto-enforce authentication security best practices. Activates when implementing password policies, rate limiting, session security, CSRF protection, or security headers in auth flows.

- Skill: `smicolon/auth-security-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add smicolon/auth-security-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/smicolon/auth-security-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: smicolon (https://skillmd.com/u/smicolon)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/smicolon/auth-security-patterns

---


# Auth Security Patterns

This skill enforces authentication security best practices for Better Auth implementations.

## Password Security

### Strong Password Policy
```typescript
import { betterAuth } from 'better-auth'

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    password: {
      minLength: 12,
      maxLength: 128,
      requireLowercase: true,
      requireUppercase: true,
      requireNumber: true,
      requireSpecialChar: true,
      // Custom validation
      validate: (password) => {
        // Check against common passwords
        if (commonPasswords.includes(password.toLowerCase())) {
          return 'Password is too common'
        }
        // Check for repeated characters
        if (/(.)\1{2,}/.test(password)) {
          return 'Password cannot have 3+ repeated characters'
        }
        return true
      },
    },
  },
})
```

### Password Hashing
```typescript
// Better Auth uses bcrypt by default with cost factor 10
// For higher security requirements:
export const auth = betterAuth({
  advanced: {
    password: {
      hash: async (password) => {
        const salt = await bcrypt.genSalt(12) // Higher cost
        return bcrypt.hash(password, salt)
      },
      verify: async (password, hash) => {
        return bcrypt.compare(password, hash)
      },
    },
  },
})
```

## Rate Limiting

### Global Rate Limiting
```typescript
import { rateLimit } from 'better-auth/plugins/rate-limit'

export const auth = betterAuth({
  plugins: [
    rateLimit({
      window: 60,  // 1 minute window
      max: 100,    // 100 requests per window
      keyGenerator: (req) => {
        // Rate limit by IP
        return req.headers.get('x-forwarded-for') || req.ip
      },
    }),
  ],
})
```

### Endpoint-Specific Limits
```typescript
rateLimit({
  endpoints: {
    // Strict limits for auth endpoints
    'sign-in': {
      window: 300,  // 5 minutes
      max: 5,       // 5 attempts
    },
    'sign-up': {
      window: 3600, // 1 hour
      max: 3,       // 3 registrations
    },
    'reset-password': {
      window: 3600, // 1 hour
      max: 3,       // 3 reset requests
    },
    'verify-email': {
      window: 60,   // 1 minute
      max: 5,       // 5 verification attempts
    },
  },
})
```

### Progressive Delays
```typescript
rateLimit({
  endpoints: {
    'sign-in': {
      window: 300,
      max: 5,
      // Add delay after failed attempts
      onRateLimitExceeded: async (req, res) => {
        const attempts = await getFailedAttempts(req.ip)
        const delay = Math.min(attempts * 1000, 30000) // Max 30s
        await new Promise(resolve => setTimeout(resolve, delay))
      },
    },
  },
})
```

## Session Security

### Secure Session Configuration
```typescript
export const auth = betterAuth({
  session: {
    expiresIn: 60 * 60 * 24 * 7,  // 7 days max
    updateAge: 60 * 60 * 24,      // Extend daily on activity

    // Cookie settings
    cookie: {
      name: '__session',
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      path: '/',
      domain: process.env.COOKIE_DOMAIN,
    },

    // Session cache (reduce DB lookups)
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5, // 5 minute cache
    },

    // Require fresh session for sensitive ops
    freshAge: 60 * 10, // 10 minutes
  },
})
```

### Session Invalidation
```typescript
import { signOut, useSession } from '@/auth/client'

// Sign out from current device
await signOut()

// Sign out from all devices
await signOut({ revokeAllSessions: true })

// Server-side: Invalidate specific session
await auth.api.invalidateSession({ sessionId })

// Server-side: Invalidate all user sessions
await auth.api.invalidateUserSessions({ userId })
```

### Session Binding
```typescript
// Bind session to device fingerprint
export const auth = betterAuth({
  session: {
    // Store device info
    onSessionCreated: async (session, user, request) => {
      await prisma.session.update({
        where: { id: session.id },
        data: {
          userAgent: request.headers.get('user-agent'),
          ipAddress: request.ip,
        },
      })
    },
    // Validate on each request
    onSessionValidate: async (session, request) => {
      const storedIp = session.ipAddress
      const currentIp = request.ip

      // Warn on IP change (but don't block for mobile users)
      if (storedIp !== currentIp) {
        await logSecurityEvent('session_ip_change', {
          sessionId: session.id,
          oldIp: storedIp,
          newIp: currentIp,
        })
      }

      return true
    },
  },
})
```

## CSRF Protection

### Token-Based CSRF
```typescript
export const auth = betterAuth({
  csrf: {
    enabled: true,
    // Double submit cookie pattern
    cookieName: '__csrf',
    headerName: 'x-csrf-token',
    // Token rotation
    rotateOnAuthentication: true,
  },
})

// Client: Include CSRF token
const csrfToken = getCookie('__csrf')
await fetch('/api/auth/sign-out', {
  method: 'POST',
  headers: {
    'x-csrf-token': csrfToken,
  },
})
```

### SameSite Cookie Protection
```typescript
session: {
  cookie: {
    sameSite: 'strict', // Strictest CSRF protection
    // Or 'lax' for balance between security and usability
  },
}
```

## Security Headers

### Recommended Headers
```typescript
// Middleware or server config
const securityHeaders = {
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  'Content-Security-Policy': [
    "default-src 'self'",
    "script-src 'self'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self'",
    "frame-ancestors 'none'",
  ].join('; '),
}
```

## Account Security

### Account Lockout
```typescript
export const auth = betterAuth({
  emailAndPassword: {
    lockout: {
      enabled: true,
      maxAttempts: 5,
      lockoutDuration: 15 * 60, // 15 minutes
      // Notify user
      onLockout: async (user) => {
        await sendEmail({
          to: user.email,
          subject: 'Account Locked',
          html: 'Your account has been locked due to multiple failed login attempts.',
        })
      },
    },
  },
})
```

### Email Verification
```typescript
export const auth = betterAuth({
  emailAndPassword: {
    requireEmailVerification: true,
    verificationTokenExpiry: 60 * 60 * 24, // 24 hours
    sendVerificationEmail: async (user, token, url) => {
      await sendEmail({
        to: user.email,
        subject: 'Verify your email',
        html: `<a href="${url}">Verify email</a>`,
      })
    },
  },
})
```

### Password Reset Security
```typescript
export const auth = betterAuth({
  emailAndPassword: {
    resetPasswordTokenExpiry: 60 * 60, // 1 hour
    sendResetPasswordToken: async (user, token, url) => {
      await sendEmail({
        to: user.email,
        subject: 'Reset your password',
        html: `<a href="${url}">Reset password</a>`,
      })

      // Log for security audit
      await logSecurityEvent('password_reset_requested', {
        userId: user.id,
        email: user.email,
      })
    },
    onPasswordReset: async (user) => {
      // Invalidate all existing sessions
      await auth.api.invalidateUserSessions({ userId: user.id })

      // Notify user
      await sendEmail({
        to: user.email,
        subject: 'Password changed',
        html: 'Your password was recently changed.',
      })
    },
  },
})
```

## Security Logging

### Audit Trail
```typescript
export const auth = betterAuth({
  advanced: {
    hooks: {
      onSignIn: async (user, session) => {
        await logSecurityEvent('sign_in', {
          userId: user.id,
          sessionId: session.id,
          method: session.method, // 'email', 'google', etc.
        })
      },
      onSignOut: async (user, session) => {
        await logSecurityEvent('sign_out', {
          userId: user.id,
          sessionId: session.id,
        })
      },
      onSignUp: async (user) => {
        await logSecurityEvent('sign_up', {
          userId: user.id,
          email: user.email,
        })
      },
    },
  },
})

async function logSecurityEvent(event: string, data: Record<string, any>) {
  await prisma.securityLog.create({
    data: {
      event,
      data,
      timestamp: new Date(),
      ip: data.ip,
      userAgent: data.userAgent,
    },
  })
}
```

## Security Checklist

- [ ] Password minimum 12 characters with complexity
- [ ] Rate limiting on all auth endpoints
- [ ] Session cookies are httpOnly and secure
- [ ] CSRF protection enabled
- [ ] Email verification required
- [ ] Account lockout after failed attempts
- [ ] Security headers configured
- [ ] Password changes invalidate sessions
- [ ] Security events logged
- [ ] 2FA available for users

## Anti-Patterns

```typescript
// ❌ WRONG: Weak password policy
password: { minLength: 6 }

// ✅ CORRECT: Strong policy
password: { minLength: 12, requireUppercase: true, ... }

// ❌ WRONG: No rate limiting
emailAndPassword: { enabled: true }

// ✅ CORRECT: With rate limiting
plugins: [rateLimit({ ... })]

// ❌ WRONG: Long-lived sessions
session: { expiresIn: 60 * 60 * 24 * 365 } // 1 year

// ✅ CORRECT: Reasonable expiry
session: { expiresIn: 60 * 60 * 24 * 7 } // 7 days

// ❌ WRONG: Cookies without flags
cookie: { name: 'session' }

// ✅ CORRECT: Secure cookie flags
cookie: { httpOnly: true, secure: true, sameSite: 'lax' }
```

