Authentication Patterns and Standards
Security patterns and best practices for implementing authentication in the Posterns MVP using NextAuth.js v5 (Auth.js).
Security Requirements
Password Security
Hashing:
- •Algorithm: bcrypt
- •Salt rounds: 12 (minimum)
- •File:
lib/auth/password.ts
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(
password: string,
hashedPassword: string
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
Password Requirements:
- •Minimum 8 characters
- •At least one uppercase letter (A-Z)
- •At least one lowercase letter (a-z)
- •At least one number (0-9)
- •Validated with Zod schema
Validation Pattern:
import { z } from 'zod';
const password = z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number');
Token Security
Token Generation:
- •Algorithm:
crypto.randomBytes(32) - •Format: Hexadecimal string
- •File:
lib/auth/tokens.ts
import crypto from 'crypto';
export function generateVerificationToken(): string {
return crypto.randomBytes(32).toString('hex');
}
export function generatePasswordResetToken(): string {
return crypto.randomBytes(32).toString('hex');
}
Token Expiry:
- •Email verification: 24 hours
- •Password reset: 1 hour
export const TOKEN_EXPIRY = {
EMAIL_VERIFICATION: 24 * 60 * 60 * 1000, // 24 hours
PASSWORD_RESET: 60 * 60 * 1000, // 1 hour
} as const;
export function getTokenExpiry(
type: 'EMAIL_VERIFICATION' | 'PASSWORD_RESET'
): Date {
return new Date(Date.now() + TOKEN_EXPIRY[type]);
}
Token Usage:
- •One-time use: Delete token after successful use
- •Expiry check: Validate
expiresfield before accepting token - •Storage: Store in
VerificationTokentable with type field
Rate Limiting
Endpoints and Limits:
- •Login: 5 requests per minute
- •Register: 3 requests per minute
- •Forgot Password: 3 requests per minute
- •Verify Email: 5 requests per minute
- •Resend Verification: 2 requests per minute
Implementation (lib/rate-limit.ts):
export const authRateLimiter = {
login: (identifier: string) =>
rateLimit(`login:${identifier}`, 5, 60000),
register: (identifier: string) =>
rateLimit(`register:${identifier}`, 3, 60000),
forgotPassword: (identifier: string) =>
rateLimit(`forgot:${identifier}`, 3, 60000),
verifyEmail: (identifier: string) =>
rateLimit(`verify:${identifier}`, 5, 60000),
resendVerification: (identifier: string) =>
rateLimit(`resend:${identifier}`, 2, 60000),
};
Client Identification:
export function getClientIdentifier(request: Request): string {
const forwarded = request.headers.get('x-forwarded-for');
const ip = forwarded ? forwarded.split(',')[0].trim() : 'unknown';
return ip;
}
Rate Limit Response:
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{ status: 429 }
);
}
Authentication Flows
Registration Flow
- •User submits: name, email, password, confirmPassword
- •Validate input: Zod schema validation
- •Normalize email: Convert to lowercase
- •Check existing user: Query database for email
- •Hash password: bcrypt with 12 salt rounds
- •Create user: Insert into database
- •Generate token: 32-byte random hex string
- •Create verification token: Store in VerificationToken table
- •Send email: Email verification link via Resend
- •Return success: Prompt user to check email
API Route Pattern (app/api/auth/register/route.ts):
export async function POST(request: Request) {
try {
// Rate limiting
const clientId = getClientIdentifier(request);
const rateLimitResult = authRateLimiter.register(clientId);
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{ status: 429 }
);
}
const body = await request.json();
// Validate input
const validated = registerSchema.safeParse(body);
if (!validated.success) {
return NextResponse.json(
{ error: 'Invalid input', details: validated.error.flatten() },
{ status: 400 }
);
}
const { name, email, password } = validated.data;
const normalizedEmail = email.toLowerCase();
// Check existing user
const existingUser = await prisma.user.findUnique({
where: { email: normalizedEmail },
});
if (existingUser) {
return NextResponse.json(
{ error: 'An account with this email already exists' },
{ status: 400 }
);
}
// Hash password
const hashedPassword = await hashPassword(password);
// Create user
const user = await prisma.user.create({
data: {
name,
email: normalizedEmail,
password: hashedPassword,
},
});
// Create verification token
const token = generateVerificationToken();
await prisma.verificationToken.create({
data: {
identifier: normalizedEmail,
token,
expires: getTokenExpiry('EMAIL_VERIFICATION'),
type: 'EMAIL_VERIFICATION',
},
});
// Send verification email
const emailResult = await sendVerificationEmail(normalizedEmail, token);
if (!emailResult.success) {
console.error('Failed to send verification email:', emailResult.error);
return NextResponse.json(
{
message: 'Account created but verification email could not be sent.',
userId: user.id,
emailFailed: true,
},
{ status: 201 }
);
}
return NextResponse.json(
{
message: 'Account created successfully. Please check your email.',
userId: user.id,
},
{ status: 201 }
);
} catch (error) {
console.error('Registration error:', error);
return NextResponse.json(
{ error: 'An error occurred during registration' },
{ status: 500 }
);
}
}
Login Flow
- •User submits: email, password, rememberMe (optional)
- •Rate limit check: 5 requests per minute
- •Validate input: Zod schema validation
- •Normalize email: Convert to lowercase
- •Find user: Query database for email
- •Verify password: bcrypt.compare with stored hash
- •Check email verified: Ensure user verified email
- •Create session: NextAuth session with user data
- •Return success: Redirect to dashboard
Credentials Provider Pattern (lib/auth.config.ts):
import Credentials from 'next-auth/providers/credentials';
import { loginSchema } from '@/lib/validations/auth';
import { verifyPassword } from '@/lib/auth/password';
export default {
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// Validate input
const validated = loginSchema.safeParse(credentials);
if (!validated.success) {
throw new Error('Invalid credentials');
}
const { email, password } = validated.data;
const normalizedEmail = email.toLowerCase();
// Find user
const user = await prisma.user.findUnique({
where: { email: normalizedEmail },
});
if (!user || !user.password) {
throw new Error('Invalid credentials');
}
// Verify password
const isValid = await verifyPassword(password, user.password);
if (!isValid) {
throw new Error('Invalid credentials');
}
// Check email verified
if (!user.emailVerified) {
throw new Error('Please verify your email before logging in');
}
return {
id: user.id,
name: user.name,
email: user.email,
emailVerified: user.emailVerified,
};
},
}),
],
};
Email Verification Flow
- •User clicks link: From verification email with token parameter
- •Rate limit check: 5 requests per minute
- •Validate token: Check token exists and not expired
- •Update user: Set
emailVerifiedto current timestamp - •Delete token: Remove used token from database
- •Return success: Show success message, allow login
Password Reset Flow
- •User requests reset: Submits email address
- •Rate limit check: 3 requests per minute
- •Find user: Query database for email
- •Generate token: 32-byte random hex string
- •Create reset token: Store in VerificationToken table with 1-hour expiry
- •Send email: Password reset link via Resend
- •User clicks link: Token in URL parameter
- •User submits new password: With token from URL
- •Validate token: Check exists and not expired
- •Hash new password: bcrypt with 12 salt rounds
- •Update user: Replace old password hash
- •Delete token: Remove used token
- •Invalidate sessions: Force re-login
- •Return success: Redirect to login
Email Service Integration
Resend API Configuration
Environment Variables:
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxx EMAIL_FROM=noreply@posterns.lv
Email Sending Pattern (lib/email/index.ts):
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendVerificationEmail(
email: string,
token: string
): Promise<{ success: boolean; error?: string }> {
try {
const verificationUrl = `${process.env.NEXTAUTH_URL}/verify-email?token=${token}`;
await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: email,
subject: 'Verify your email address',
html: `
<p>Please verify your email address by clicking the link below:</p>
<a href="${verificationUrl}">Verify Email</a>
<p>This link will expire in 24 hours.</p>
`,
});
return { success: true };
} catch (error) {
console.error('Email send error:', error);
return { success: false, error: String(error) };
}
}
Session Management
NextAuth.js v5 Session Configuration
Auth Configuration (lib/auth.ts):
import NextAuth from 'next-auth';
import authConfig from './auth.config';
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.emailVerified = user.emailVerified;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.emailVerified = token.emailVerified as Date | null;
}
return session;
},
},
});
Session Access Patterns:
Server Components:
import { auth } from '@/lib/auth';
export default async function Page() {
const session = await auth();
if (!session?.user) {
redirect('/login');
}
return <div>Welcome {session.user.name}</div>;
}
API Routes:
import { auth } from '@/lib/auth';
export async function GET(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return new Response('Unauthorized', { status: 401 });
}
// User is authenticated
const userId = session.user.id;
// ... handle request
}
Client Components:
'use client';
import { useSession } from 'next-auth/react';
export function UserProfile() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <div>Loading...</div>;
}
if (!session?.user) {
return <div>Not logged in</div>;
}
return <div>Hello {session.user.name}</div>;
}
Security Best Practices
Input Validation
Always use Zod schemas:
import { z } from 'zod';
// Validate ALL user inputs
const validated = schema.safeParse(userInput);
if (!validated.success) {
return error response;
}
// Use validated.data (never use userInput directly)
SQL Injection Prevention
Use Prisma parameterization (never raw SQL unless absolutely necessary):
// Good - Prisma handles parameterization
const user = await prisma.user.findUnique({
where: { email: userEmail },
});
// Bad - Vulnerable to SQL injection
const user = await prisma.$queryRaw`
SELECT * FROM User WHERE email = ${userEmail}
`;
XSS Prevention
React auto-escapes by default, but:
- •Never use
dangerouslySetInnerHTMLwith user input - •Validate all inputs with Zod
- •Sanitize before storing in database
CSRF Protection
NextAuth.js provides built-in CSRF protection:
- •CSRF tokens automatically included in forms
- •State-changing operations use POST/PUT/DELETE (never GET)
- •Verify origin header in API routes
Timing Attacks
Use constant-time comparisons for sensitive operations:
// Good - bcrypt.compare is constant-time
const isValid = await bcrypt.compare(password, hashedPassword);
// Good - crypto.timingSafeEqual for token comparison
import crypto from 'crypto';
function compareTokens(a: string, b: string): boolean {
const bufferA = Buffer.from(a, 'hex');
const bufferB = Buffer.from(b, 'hex');
if (bufferA.length !== bufferB.length) {
return false;
}
return crypto.timingSafeEqual(bufferA, bufferB);
}
Error Messages
Don't reveal if email exists:
// Good - Generic message
if (!user || !isPasswordValid) {
throw new Error('Invalid credentials');
}
// Bad - Reveals email existence
if (!user) {
throw new Error('Email not found');
}
if (!isPasswordValid) {
throw new Error('Wrong password');
}
Testing Authentication
Manual Testing Checklist
- • Register new user with valid data
- • Try registering with existing email (should fail)
- • Try registering with weak password (should fail)
- • Verify email verification link works
- • Try logging in before email verification (should fail)
- • Login with correct credentials
- • Login with incorrect password (should fail)
- • Test rate limiting (make 10 rapid requests)
- • Request password reset
- • Reset password with valid token
- • Try reusing password reset token (should fail)
- • Try using expired token (should fail after 1 hour)
- • Test "remember me" functionality
- • Test session expiry
- • Test logout functionality
Security Audit Checklist
- • Passwords are hashed with bcrypt (12 rounds minimum)
- • Tokens are generated with crypto.randomBytes (32 bytes minimum)
- • Rate limiting is applied to all auth endpoints
- • Email addresses are normalized (lowercase)
- • Email verification is required
- • Password requirements are enforced
- • Error messages don't reveal user existence
- • No secrets in client-side code
- • Environment variables used for sensitive config
- • HTTPS enforced in production
- • CSRF protection enabled
- • Session timeout configured
- • No SQL injection vulnerabilities
- • XSS prevention implemented
- • Proper input validation with Zod
Common Vulnerabilities to Avoid
1. Weak Password Storage
❌ Don't:
- •Store plaintext passwords
- •Use weak hashing (MD5, SHA1)
- •Use insufficient salt rounds
✅ Do:
- •Use bcrypt with 12+ salt rounds
- •Never store plaintext passwords
2. Insufficient Rate Limiting
❌ Don't:
- •Allow unlimited login attempts
- •Skip rate limiting on sensitive endpoints
✅ Do:
- •Implement rate limiting on all auth endpoints
- •Use IP-based identification
- •Return 429 status for rate limit exceeded
3. Predictable Tokens
❌ Don't:
- •Use sequential IDs for tokens
- •Use timestamps or guessable patterns
✅ Do:
- •Use crypto.randomBytes (32+ bytes)
- •Use sufficient entropy
4. Missing Email Verification
❌ Don't:
- •Allow unverified users full access
- •Skip email verification
✅ Do:
- •Require email verification before login
- •Set reasonable token expiry (24 hours)
5. Exposing User Existence
❌ Don't:
- •"Email not found"
- •"Password incorrect"
- •Different error messages for different failures
✅ Do:
- •"Invalid credentials" for all login failures
- •Same response time regardless of failure reason