Security Best Practices

Production-ready security practices for authentication

Security Best Practices

Authentication security is critical for protecting user data and preventing unauthorized access. This guide covers essential security practices for production deployments.

Secret Management

Strong Secret Generation

The AUTH_SECRET is the foundation of your security. It must be:

  • At least 32 characters long
  • Cryptographically random
  • Unique per environment (dev, staging, prod)
  • Never committed to version control
# Generate a secure secret (32+ bytes)
openssl rand -base64 32

# Output example:
# h8vF2xK9mN5pQ7rT3wY6zA1bC4dE8fG2hJ5kL7mN9pQ=

# Store in .env.local (development)
AUTH_SECRET=h8vF2xK9mN5pQ7rT3wY6zA1bC4dE8fG2hJ5kL7mN9pQ=

# Use environment variables (production)
# Set in Vercel, AWS, etc. - never hardcode

Critical Security Warning

If your AUTH_SECRET is compromised, attackers can forge session tokens and impersonate any user. Rotate secrets immediately if exposure is suspected.

Secret Rotation

Implement secret rotation for added security:

Secret Rotation Strategy

Support old and new secrets during transition

// Support multiple secrets during rotation
const secrets = [
  process.env.AUTH_SECRET!,        // Current secret
  process.env.AUTH_SECRET_OLD,     // Previous secret (grace period)
].filter(Boolean);

// Try each secret when verifying tokens
async function verifyToken(token: string) {
  for (const secret of secrets) {
    try {
      const payload = await verify(token, secret);
      return payload;
    } catch (err) {
      continue; // Try next secret
    }
  }
  throw new Error("Invalid token");
}

// Always sign with current secret
const token = await sign(payload, process.env.AUTH_SECRET!);

HTTPS Everywhere

Enforce HTTPS in Production

Always use HTTPS in production to prevent man-in-the-middle attacks:

// Next.js middleware/proxy
export function proxy(request: NextRequest) {
  // Redirect HTTP to HTTPS in production
  if (
    process.env.NODE_ENV === "production" &&
    request.headers.get("x-forwarded-proto") !== "https"
  ) {
    return NextResponse.redirect(
      `https://${request.headers.get("host")}${request.nextUrl.pathname}`,
      301
    );
  }

  // Continue with auth handling
  return authHandler(request);
}

Secure Cookie Settings

The SDK uses secure cookie defaults, but verify in production:

{
  httpOnly: true,     // ✓ Prevents XSS attacks
  secure: true,       // ✓ HTTPS only
  sameSite: "lax",    // ✓ CSRF protection
  maxAge: 2592000,    // ✓ 30 days
  path: "/",          // ✓ Available on all routes
}

PKCE for OAuth

The SDK implements PKCE (Proof Key for Code Exchange) by default for all OAuth providers:

  • S256 Method: Uses SHA-256 challenge (most secure)
  • Automatic: No configuration needed
  • RFC 7636 Compliant: Follows OAuth 2.1 best practices
// PKCE is automatic - nothing to configure
const provider = google({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: process.env.GOOGLE_REDIRECT_URI!,
  // pkce: "S256" is the default
});

PKCE Benefits

PKCE prevents authorization code interception attacks, even if the code is stolen. It's especially important for public clients (mobile apps, SPAs).

CSRF Protection

OAuth State Parameter

The SDK automatically handles CSRF protection for OAuth flows:

  • Generates random state parameter
  • Stores in-memory with cookie fallback
  • Validates on callback
  • Prevents cross-site request forgery
// Automatic CSRF protection
// 1. User clicks "Sign in with Google"
const state = generateSecureToken(); // Random 32-byte token
csrfStore.set(state, true);          // Store server-side

// 2. Redirect to Google with state
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?state=${state}...`;

// 3. Google redirects back with state
const callbackState = request.url.searchParams.get("state");

// 4. Validate state matches
if (!csrfStore.has(callbackState)) {
  throw new Error("CSRF validation failed");
}

// 5. Clean up
csrfStore.delete(callbackState);

Custom Form CSRF

For custom forms (email sign-in, etc.), implement CSRF tokens:

CSRF Token for Forms

Protect custom authentication forms

// Generate CSRF token
import { generateCsrfToken, validateCsrfToken } from "@warpy-auth-sdk/core/utils/csrf";

// 1. Generate token when rendering form
const csrfToken = generateCsrfToken();

// 2. Include in form
<form method="POST" action="/api/auth/signin/email">
  <input type="hidden" name="csrfToken" value={csrfToken} />
  <input type="email" name="email" required />
  <button type="submit">Sign In</button>
</form>

// 3. Validate on submission
export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const csrfToken = formData.get("csrfToken") as string;

  if (!validateCsrfToken(csrfToken)) {
    return Response.json({ error: "Invalid CSRF token" }, { status: 403 });
  }

  // Proceed with authentication
}

Rate Limiting

Authentication Endpoints

Implement rate limiting to prevent brute force attacks:

Rate Limiting with Upstash

Limit authentication attempts

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

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

const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute
});

export async function POST(request: NextRequest) {
  // Get client IP
  const ip = request.headers.get("x-forwarded-for") || "unknown";

  // Check rate limit
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return Response.json(
      { error: "Too many requests. Please try again later." },
      { status: 429 }
    );
  }

  // Proceed with authentication
  const result = await authenticate(config, request);
  // ...
}

Per-User Rate Limiting

User-Specific Rate Limits

Limit failed login attempts per user

import { Redis } from "@upstash/redis";

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

async function checkFailedAttempts(email: string): Promise<boolean> {
  const key = `failed-login:${email}`;
  const attempts = await redis.get(key) || 0;

  if (attempts >= 5) {
    // Check if lockout period has passed
    const ttl = await redis.ttl(key);
    if (ttl > 0) {
      throw new Error(`Account locked. Try again in ${Math.ceil(ttl / 60)} minutes.`);
    }
  }

  return true;
}

async function recordFailedAttempt(email: string): Promise<void> {
  const key = `failed-login:${email}`;
  await redis.incr(key);
  await redis.expire(key, 900); // 15 minute lockout
}

async function clearFailedAttempts(email: string): Promise<void> {
  const key = `failed-login:${email}`;
  await redis.del(key);
}

// Usage
try {
  await checkFailedAttempts(email);
  const result = await authenticate(config, request);

  if (result.session) {
    await clearFailedAttempts(email);
  } else {
    await recordFailedAttempts(email);
  }
} catch (error) {
  await recordFailedAttempts(email);
  throw error;
}

Input Validation

Email Validation

Validate Email Input

Prevent injection attacks

import { z } from "zod";

const emailSchema = z.string().email().max(255);

export async function POST(request: NextRequest) {
  const { email } = await request.json();

  // Validate email format
  const result = emailSchema.safeParse(email);

  if (!result.success) {
    return Response.json(
      { error: "Invalid email address" },
      { status: 400 }
    );
  }

  // Sanitize email (lowercase, trim)
  const sanitizedEmail = result.data.toLowerCase().trim();

  // Proceed with authentication
  const authResult = await authenticate(config, request, {
    email: sanitizedEmail,
  });
}

Prevent SQL Injection

Always use parameterized queries or ORM:

// ✗ NEVER do this (vulnerable to SQL injection)
const user = await db.execute(
  `SELECT * FROM users WHERE email = '${email}'`
);

// ✓ Use parameterized queries
const user = await db.execute(
  "SELECT * FROM users WHERE email = ?",
  [email]
);

// ✓ Or use an ORM like Prisma
const user = await prisma.user.findUnique({
  where: { email },
});

XSS Prevention

Content Security Policy

CSP Headers

Prevent XSS attacks

// Next.js config
export const config = {
  headers: async () => [
    {
      source: "/:path*",
      headers: [
        {
          key: "Content-Security-Policy",
          value: [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self' data:",
            "connect-src 'self' https://accounts.google.com",
          ].join("; "),
        },
      ],
    },
  ],
};

Sanitize User Input

// ✗ Don't render raw HTML from user input
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// ✓ Escape user input automatically
<div>{userInput}</div>

// ✓ Or use a sanitization library
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

Session Security

Short-Lived Tokens

Use appropriate token expiration times:

// Standard user sessions: 30 days
maxAge: 60 * 60 * 24 * 30

// Admin/sensitive operations: 1 hour
maxAge: 60 * 60

// MCP agent tokens: 15 minutes
expiresIn: "15m"

// API tokens: 7 days
maxAge: 60 * 60 * 24 * 7

Session Revocation

Enable immediate session revocation with database adapter:

Revoke Sessions on Password Change

Sign out all devices when password changes

async function changePassword(userId: string, newPassword: string) {
  // Update password
  await db.user.update({
    where: { id: userId },
    data: { password: await hash(newPassword) },
  });

  // Revoke all existing sessions
  await adapter.deleteUserSessions(userId);

  // Force user to re-authenticate
}

Detect Session Hijacking

Track Session Metadata

Detect suspicious session activity

// Store session metadata
interface SessionMetadata {
  userAgent: string;
  ip: string;
  createdAt: Date;
  lastActivityAt: Date;
}

// Check for changes
async function validateSessionSecurity(
  session: Session,
  request: NextRequest
): Promise<boolean> {
  const currentIp = request.headers.get("x-forwarded-for");
  const currentUserAgent = request.headers.get("user-agent");

  const metadata = await getSessionMetadata(session.token);

  // Flag if IP or user agent changed
  if (
    metadata.ip !== currentIp ||
    metadata.userAgent !== currentUserAgent
  ) {
    // Log suspicious activity
    await logSecurityEvent({
      type: "session_hijack_attempt",
      sessionId: session.token,
      oldIp: metadata.ip,
      newIp: currentIp,
    });

    // Optionally revoke session
    await adapter.deleteSession(session.token);

    return false;
  }

  return true;
}

CAPTCHA Protection

Use CAPTCHA to prevent bot attacks:

CAPTCHA for Email Sign-in

Protect against automated attacks

import { createCaptchaProvider } from "@warpy-auth-sdk/core/captcha";

const config = {
  secret: process.env.AUTH_SECRET!,
  provider: email({ /* ... */ }),
  captcha: {
    provider: createCaptchaProvider({
      type: "recaptcha-v3",
      secretKey: process.env.RECAPTCHA_SECRET_KEY!,
    }),
    enabledMethods: ["email", "twofa"], // Only for email/2FA
    scoreThreshold: 0.5, // Minimum score for reCAPTCHA v3
  },
};

// Client-side
const captchaToken = await grecaptcha.execute(siteKey, { action: "login" });

await fetch("/api/auth/signin/email", {
  method: "POST",
  body: JSON.stringify({ email, captchaToken }),
});

Audit Logging

Log Security Events

Track authentication events

interface SecurityEvent {
  type: "login_success" | "login_failure" | "logout" | "session_revoked" | "suspicious_activity";
  userId?: string;
  email?: string;
  ip: string;
  userAgent: string;
  timestamp: Date;
  metadata?: Record<string, any>;
}

async function logSecurityEvent(event: SecurityEvent) {
  await db.securityLog.create({
    data: event,
  });

  // Alert on suspicious patterns
  if (event.type === "login_failure") {
    const recentFailures = await db.securityLog.count({
      where: {
        email: event.email,
        type: "login_failure",
        timestamp: { gt: new Date(Date.now() - 5 * 60 * 1000) },
      },
    });

    if (recentFailures >= 5) {
      await sendSecurityAlert({
        type: "brute_force_attempt",
        email: event.email,
        failureCount: recentFailures,
      });
    }
  }
}

// Usage
try {
  const result = await authenticate(config, request);
  await logSecurityEvent({
    type: "login_success",
    userId: result.session.user.id,
    email: result.session.user.email,
    ip: request.headers.get("x-forwarded-for") || "unknown",
    userAgent: request.headers.get("user-agent") || "unknown",
    timestamp: new Date(),
  });
} catch (error) {
  await logSecurityEvent({
    type: "login_failure",
    email: email,
    ip: request.headers.get("x-forwarded-for") || "unknown",
    userAgent: request.headers.get("user-agent") || "unknown",
    timestamp: new Date(),
    metadata: { error: error.message },
  });
}

OAuth Security

Redirect URI Validation

Always validate redirect URIs in OAuth providers:

# Must match EXACTLY in OAuth provider settings
GOOGLE_REDIRECT_URI=https://yourdomain.com/api/auth/callback/google

# NOT:
# http://yourdomain.com/... (wrong protocol)
# https://yourdomain.com/callback/google (wrong path)
# https://www.yourdomain.com/... (wrong subdomain)

State Parameter

The SDK automatically includes and validates the state parameter for CSRF protection.

Token Storage

Store OAuth tokens securely:

// ✓ Store access tokens in database (encrypted)
const encryptedToken = await encrypt(accessToken);
await db.account.update({
  where: { id: account.id },
  data: { accessToken: encryptedToken },
});

// ✗ Never expose access tokens to client
// ✗ Never log access tokens
// ✗ Never commit access tokens to version control

Production Checklist

Security Checklist

Before deploying to production, verify:
  • AUTH_SECRET is 32+ characters and cryptographically random
  • ✓ HTTPS is enforced on all pages
  • ✓ Secure cookies (httpOnly: true, secure: true, sameSite: "lax")
  • ✓ PKCE enabled for OAuth (automatic in SDK)
  • ✓ CSRF protection for all forms
  • ✓ Rate limiting on authentication endpoints
  • ✓ Input validation and sanitization
  • ✓ Content Security Policy headers
  • ✓ Session revocation mechanism (with database adapter)
  • ✓ Audit logging for security events
  • ✓ Regular security updates and dependency patches
  • ✓ OAuth redirect URIs whitelisted in provider settings
  • ✓ Environment variables (not hardcoded secrets)
  • ✓ CAPTCHA protection for public endpoints

Incident Response

If Secrets Are Compromised

  1. Immediately rotate AUTH_SECRET
  2. Revoke all active sessions
  3. Force all users to re-authenticate
  4. Review audit logs for suspicious activity
  5. Notify affected users if necessary

If User Account Is Compromised

  1. Revoke all sessions for the user
  2. Force password reset (if applicable)
  3. Review recent activity and API calls
  4. Notify the user
  5. Enable 2FA if not already enabled

Resources

Next Steps