Authentication Security Best Practices for Modern Web Apps
January 20, 2024

Authentication Security Best Practices for Modern Web Apps
Authentication is the foundation of application security, yet it's often implemented with insufficient attention to security details. In this guide, we'll explore essential security practices for implementing robust authentication in your Next.js and Firebase applications.
The Security Landscape
Modern web applications face numerous authentication-related threats:
- Credential stuffing attacks using leaked passwords
- Session hijacking through XSS or network interception
- Brute force attacks on login endpoints
- Social engineering targeting password reset flows
- Account enumeration through timing attacks
Our starter kit addresses these concerns with production-ready security measures.
Core Security Principles
1. Never Trust Client-Side Data
// ❌ Bad: Trusting client claims
function AdminPage() {
const { user } = useAuth();
if (user?.claims?.admin) {
return <AdminDashboard />;
}
return <AccessDenied />;
}
// ✅ Good: Server-side verification
// middleware.ts
export async function middleware(request: NextRequest) {
const token = await getAuthToken(request);
if (!token) return redirectToLogin();
const decodedToken = await admin.auth().verifyIdToken(token);
if (!decodedToken.admin) {
return new Response('Forbidden', { status: 403 });
}
}
2. Implement Proper Session Management
// Session configuration in our auth provider
const authConfig = {
// Session timeout
sessionTimeout: 24 * 60 * 60 * 1000, // 24 hours
// Refresh token rotation
refreshTokenRotation: true,
// Secure cookie settings
cookies: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict'
}
};
Firebase Auth Security Features
Email Verification
Our starter kit enforces email verification:
// components/auth/email-verification.tsx
export function EmailVerificationGuard({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
if (user && !user.emailVerified) {
return <EmailVerificationPrompt />;
}
return <>{children}</>;
}
Why email verification matters:
- Prevents account takeover through typos
- Ensures communication channel validity
- Reduces fake account creation
- Enables secure password reset
Multi-Factor Authentication (MFA)
While not included in the basic starter, Firebase Auth supports MFA:
// Example MFA implementation
import {
multiFactor,
PhoneAuthProvider,
RecaptchaVerifier
} from 'firebase/auth';
async function enableMFA(user: User) {
const multiFactorSession = await multiFactor(user).getSession();
const phoneAuthCredential = PhoneAuthProvider.credential(
verificationId,
verificationCode
);
const multiFactorAssertion = PhoneAuthProvider.assertion(
phoneAuthCredential
);
await multiFactor(user).enroll(multiFactorAssertion, multiFactorSession);
}
Password Security
Strong Password Requirements
// lib/password-validation.ts
export const passwordRequirements = {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true,
prohibitCommonPasswords: true
};
export function validatePassword(password: string): ValidationResult {
const errors: string[] = [];
if (password.length < passwordRequirements.minLength) {
errors.push(`Password must be at least ${passwordRequirements.minLength} characters`);
}
if (passwordRequirements.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('Password must contain an uppercase letter');
}
// Additional validations...
return {
isValid: errors.length === 0,
errors
};
}
Secure Password Reset
// lib/auth/password-reset.ts
export async function initiatePasswordReset(email: string) {
try {
await sendPasswordResetEmail(auth, email, {
// Custom action code settings
url: `${process.env.NEXT_PUBLIC_APP_URL}/login`,
handleCodeInApp: false,
});
// Log security event (without sensitive data)
await logSecurityEvent({
type: 'password_reset_requested',
email: hashEmail(email), // Hash for privacy
timestamp: new Date(),
ipAddress: getClientIP(),
});
} catch (error) {
// Don't reveal whether email exists
console.error('Password reset error:', error);
}
}
Session Management Best Practices
Automatic Session Refresh
// providers/auth-provider.tsx
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
if (firebaseUser) {
// Force token refresh for long-running sessions
const token = await firebaseUser.getIdToken(true);
setUser(firebaseUser);
} else {
setUser(null);
}
});
// Set up periodic token refresh
const refreshInterval = setInterval(async () => {
if (auth.currentUser) {
await auth.currentUser.getIdToken(true);
}
}, 45 * 60 * 1000); // Refresh every 45 minutes
return () => {
unsubscribe();
clearInterval(refreshInterval);
};
}, []);
}
Secure Logout
// lib/auth/logout.ts
export async function secureLogout() {
try {
// Clear all local storage
localStorage.clear();
sessionStorage.clear();
// Clear any client-side caches
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
}
// Sign out from Firebase
await signOut(auth);
// Redirect to login page
window.location.href = '/login';
} catch (error) {
console.error('Logout error:', error);
// Force reload as fallback
window.location.reload();
}
}
Protection Against Common Attacks
Rate Limiting
// middleware.ts - Simple rate limiting
const rateLimitMap = new Map<string, { count: number; lastReset: number }>();
export async function rateLimit(ip: string, limit: number = 5, windowMs: number = 15 * 60 * 1000) {
const now = Date.now();
const windowStart = now - windowMs;
const record = rateLimitMap.get(ip) || { count: 0, lastReset: now };
if (record.lastReset < windowStart) {
record.count = 1;
record.lastReset = now;
} else {
record.count++;
}
rateLimitMap.set(ip, record);
return record.count <= limit;
}
CSRF Protection
// Next.js API route with CSRF protection
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
// Verify origin header
const origin = request.headers.get('origin');
const allowedOrigins = [process.env.NEXT_PUBLIC_APP_URL];
if (!origin || !allowedOrigins.includes(origin)) {
return new Response('Forbidden', { status: 403 });
}
// Verify authentication token
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const token = authHeader.substring(7);
try {
const decodedToken = await admin.auth().verifyIdToken(token);
// Process authenticated request
} catch (error) {
return new Response('Invalid token', { status: 401 });
}
}
Monitoring and Alerting
Security Event Logging
// lib/security/logging.ts
interface SecurityEvent {
type: 'login' | 'logout' | 'failed_login' | 'password_reset' | 'suspicious_activity';
userId?: string;
email?: string;
ipAddress: string;
userAgent: string;
timestamp: Date;
metadata?: Record<string, any>;
}
export async function logSecurityEvent(event: SecurityEvent) {
try {
await addDoc(collection(db, 'security_events'), {
...event,
// Add additional context
sessionId: generateSessionId(),
fingerprint: await generateDeviceFingerprint(),
});
// Alert on suspicious patterns
if (await detectSuspiciousActivity(event)) {
await sendSecurityAlert(event);
}
} catch (error) {
console.error('Failed to log security event:', error);
}
}
Anomaly Detection
// Simple anomaly detection
async function detectSuspiciousActivity(event: SecurityEvent): Promise<boolean> {
const recentEvents = await getRecentSecurityEvents(event.ipAddress, '1h');
// Multiple failed logins
const failedLogins = recentEvents.filter(e => e.type === 'failed_login').length;
if (failedLogins >= 5) return true;
// Login from new location
if (event.type === 'login' && await isNewLocation(event.ipAddress, event.userId)) {
return true;
}
// Rapid requests from same IP
if (recentEvents.length > 100) return true;
return false;
}
User Account Security
Account Lockout
// lib/auth/account-lockout.ts
export async function checkAccountLockout(email: string): Promise<boolean> {
const userDoc = await getDoc(doc(db, 'users', hashEmail(email)));
if (userDoc.exists()) {
const userData = userDoc.data();
const { failedAttempts = 0, lockedUntil } = userData;
// Check if account is currently locked
if (lockedUntil && lockedUntil.toDate() > new Date()) {
return true;
}
// Reset failed attempts if lock period expired
if (lockedUntil && lockedUntil.toDate() <= new Date()) {
await updateDoc(userDoc.ref, {
failedAttempts: 0,
lockedUntil: null
});
}
}
return false;
}
Privacy Controls
// User privacy settings
interface PrivacySettings {
dataRetention: '1year' | '2years' | '5years';
analyticsOptOut: boolean;
marketingOptOut: boolean;
sessionTimeout: '15min' | '1hour' | '24hours';
deleteAccountRequest?: Date;
}
export async function updatePrivacySettings(
userId: string,
settings: Partial<PrivacySettings>
) {
await updateDoc(doc(db, 'users', userId), {
privacySettings: settings,
updatedAt: new Date()
});
// Apply settings immediately
if (settings.sessionTimeout) {
await updateSessionTimeout(userId, settings.sessionTimeout);
}
}
Compliance and Data Protection
GDPR Compliance
// Data export for GDPR requests
export async function exportUserData(userId: string) {
const userData = await getDoc(doc(db, 'users', userId));
const userPosts = await getDocs(
query(collection(db, 'posts'), where('userId', '==', userId))
);
return {
profile: userData.data(),
posts: userPosts.docs.map(doc => doc.data()),
securityEvents: await getUserSecurityEvents(userId),
exportedAt: new Date().toISOString(),
};
}
// Data deletion for GDPR requests
export async function deleteUserData(userId: string) {
const batch = writeBatch(db);
// Delete user document
batch.delete(doc(db, 'users', userId));
// Delete user posts
const userPosts = await getDocs(
query(collection(db, 'posts'), where('userId', '==', userId))
);
userPosts.docs.forEach(doc => batch.delete(doc.ref));
// Anonymize security events (keep for security monitoring)
const securityEvents = await getDocs(
query(collection(db, 'security_events'), where('userId', '==', userId))
);
securityEvents.docs.forEach(doc => {
batch.update(doc.ref, {
userId: '[DELETED]',
email: '[DELETED]',
anonymized: true
});
});
await batch.commit();
// Delete from Firebase Auth
await admin.auth().deleteUser(userId);
}
Testing Security
Security Test Suite
// tests/security/auth.test.ts
describe('Authentication Security', () => {
test('should reject weak passwords', () => {
const weakPasswords = ['123456', 'password', 'qwerty'];
weakPasswords.forEach(password => {
const result = validatePassword(password);
expect(result.isValid).toBe(false);
});
});
test('should prevent account enumeration', async () => {
const nonExistentEmail = 'nonexistent@example.com';
// Both existing and non-existent emails should return same response
const response1 = await initiatePasswordReset(nonExistentEmail);
const response2 = await initiatePasswordReset('existing@example.com');
expect(response1.status).toBe(200);
expect(response2.status).toBe(200);
expect(response1.message).toBe(response2.message);
});
test('should enforce rate limiting', async () => {
const requests = Array(10).fill(null).map(() =>
fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com', password: 'wrong' })
})
);
const responses = await Promise.all(requests);
const rateLimitedResponses = responses.filter(r => r.status === 429);
expect(rateLimitedResponses.length).toBeGreaterThan(0);
});
});
Conclusion
Security is not a feature you add at the end—it must be built into every layer of your authentication system. This starter kit provides a solid foundation with:
- ✅ Email verification enforcement
- ✅ Secure session management with automatic refresh
- ✅ Rate limiting protection
- ✅ Security event logging for monitoring
- ✅ Privacy controls for user data protection
- ✅ GDPR compliance utilities
Remember that security is an ongoing process. Regularly review your authentication flows, monitor for suspicious activity, and stay updated with the latest security best practices.
Next steps:
- Enable multi-factor authentication for sensitive accounts
- Implement device fingerprinting for anomaly detection
- Set up security monitoring dashboards
- Conduct regular security audits and penetration testing
Stay secure! 🔒