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-authConfiguration
// 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
- Hash passwords with bcrypt (cost factor 12+)
- Use HTTPS everywhere
- Implement proper JWT handling
- Validate and sanitize all inputs
- Use parameterized queries (prevent SQL injection)
- Set secure cookie flags
- Implement rate limiting
- Add CSRF protection
- Set security headers (CSP, HSTS, etc.)
- Keep dependencies updated
- Log security events
- Never expose sensitive data in errors
Security is not optional—it's a requirement. Protect your users and your application!