Authentication and Security Best Practices

September 20, 2024 (1y ago)

Authentication and Security Best Practices

Security is crucial for every application. Here's how to implement authentication properly and protect your users.

Password Hashing

Never Store Plain Passwords

import bcrypt from 'bcrypt';
 
const SALT_ROUNDS = 12;
 
// Hash password before storing
async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}
 
// Verify password
async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}
 
// Usage
const hash = await hashPassword('userPassword123');
const isValid = await verifyPassword('userPassword123', hash);

JWT Authentication

Generate Tokens

import jwt from 'jsonwebtoken';
 
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
 
interface TokenPayload {
  userId: string;
  email: string;
  role: string;
}
 
function generateAccessToken(payload: TokenPayload): string {
  return jwt.sign(payload, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
}
 
function generateRefreshToken(payload: TokenPayload): string {
  return jwt.sign(payload, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
}
 
function verifyAccessToken(token: string): TokenPayload {
  return jwt.verify(token, ACCESS_TOKEN_SECRET) as TokenPayload;
}

Auth Middleware

import { NextRequest, NextResponse } from 'next/server';
 
export async function authMiddleware(request: NextRequest) {
  const authHeader = request.headers.get('authorization');
 
  if (!authHeader?.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'Missing authorization header' },
      { status: 401 }
    );
  }
 
  const token = authHeader.split(' ')[1];
 
  try {
    const payload = verifyAccessToken(token);
    // Add user to request
    request.headers.set('x-user-id', payload.userId);
    return NextResponse.next();
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid or expired token' },
      { status: 401 }
    );
  }
}

Refresh Token Flow

async function refreshTokens(refreshToken: string) {
  try {
    const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET) as TokenPayload;
    
    // Check if refresh token is in whitelist (database)
    const storedToken = await db.refreshToken.findUnique({
      where: { token: refreshToken }
    });
    
    if (!storedToken) {
      throw new Error('Invalid refresh token');
    }
 
    // Generate new tokens
    const newAccessToken = generateAccessToken({
      userId: payload.userId,
      email: payload.email,
      role: payload.role,
    });
 
    return { accessToken: newAccessToken };
  } catch (error) {
    throw new Error('Invalid refresh token');
  }
}

OAuth 2.0 with NextAuth.js

Installation

npm install next-auth

Configuration

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import GitHubProvider from 'next-auth/providers/github';
 
const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.role = token.role;
      return session;
    },
  },
});
 
export { handler as GET, handler as POST };

Input Validation

Zod Schema Validation

import { z } from 'zod';
 
const userSchema = z.object({
  email: z.string().email('Invalid email format'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain uppercase letter')
    .regex(/[0-9]/, 'Password must contain number'),
  name: z.string().min(2).max(100),
});
 
// Validate input
function validateUserInput(data: unknown) {
  const result = userSchema.safeParse(data);
  
  if (!result.success) {
    throw new ValidationError(result.error.issues);
  }
  
  return result.data;
}

SQL Injection Prevention

// Bad - SQL Injection vulnerable
const query = `SELECT * FROM users WHERE email = '${email}'`;
 
// Good - Parameterized query
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [email]);
 
// Good - ORM (Prisma)
const user = await prisma.user.findUnique({
  where: { email },
});

XSS Prevention

// Sanitize HTML output
import DOMPurify from 'dompurify';
 
const cleanHTML = DOMPurify.sanitize(userInput);
 
// React automatically escapes by default
// But be careful with dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
 
// Set Content Security Policy headers
// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-eval'",
  },
];

CSRF Protection

// Use SameSite cookies
const cookieOptions = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict' as const,
  maxAge: 60 * 60 * 24 * 7, // 7 days
};
 
// Implement CSRF token
import { randomBytes } from 'crypto';
 
function generateCSRFToken(): string {
  return randomBytes(32).toString('hex');
}

Rate Limiting

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
 
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
});
 
async function rateLimitMiddleware(request: Request) {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);
 
  if (!success) {
    return new Response('Too Many Requests', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'X-RateLimit-Reset': reset.toString(),
      },
    });
  }
}

Environment Variables

# .env.local (never commit!)
DATABASE_URL=postgresql://...
ACCESS_TOKEN_SECRET=your-256-bit-secret
REFRESH_TOKEN_SECRET=another-256-bit-secret
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...

Security Checklist

Security is not optional—it's a requirement. Protect your users and your application!