DEV Community

TJ Coding
TJ Coding

Posted on

Full-Stack Security Architecture

Security is not a plugin you add at the end; it is an architectural requirement. In a modern Next.js application, security responsibilities are split between the Edge (Middleware), the Configuration (next.config.js), and the Server Runtime (Server Actions).

This guide details a cohesive security strategy using Next.js, Zod (validation), and Bcrypt (cryptography).


I. The Security Checklist

Before implementing code, ensure your infrastructure meets these baselines:

  • [ ] SSL/TLS: Enforce HTTPS strictly (HSTS).
  • [ ] Content Security Policy (CSP): Restrict data sources to trusted domains.
  • [ ] Database Security: Never concatenate SQL; use ORMs (Prisma/Drizzle) or parameterized queries.
  • [ ] Input Sanitization: Validate data types and length on the server.
  • [ ] Secure Dependencies: regularly run npm audit.

II. The First Line of Defense: Headers & Config

In Next.js, security headers are injected at the build time configuration. This ensures that every response—static or dynamic—carries the necessary protection instructions for the browser.

File: next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        // Apply these headers to all routes in your application
        source: '/:path*',
        headers: [
          {
            key: 'X-DNS-Prefetch-Control',
            value: 'on',
          },
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=63072000; includeSubDomains; preload',
          },
          {
            key: 'X-Frame-Options',
            value: 'SAMEORIGIN', // Prevents clickjacking
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff', // Prevents MIME-sniffing
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;
Enter fullscreen mode Exit fullscreen mode

III. Authentication & Cryptography

Handling passwords requires strict adherence to one rule: never store them in plain text. In the Node.js/Next.js runtime, we use bcrypt (or bcryptjs) to handle one-way hashing.

Installation: npm install bcryptjs

1. The Hashing Utility

Create a dedicated utility file for crypto operations. This ensures you can rotate algorithms easily in the future.

File: lib/auth-utils.ts

import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12; 

/**
 * Hashes a plain text password.
 * Usage: Before saving a new user to the DB.
 */
export async function hashPassword(password: string): Promise<string> {
  return await bcrypt.hash(password, SALT_ROUNDS);
}

/**
 * Verifies a plain text password against a stored hash.
 * Usage: During the login process.
 */
export async function verifyPassword(
  plainText: string, 
  hashed: string
): Promise<boolean> {
  return await bcrypt.compare(plainText, hashed);
}
Enter fullscreen mode Exit fullscreen mode

IV. Secure Input Handling (Server Actions)

In the Next.js App Router, Server Actions are publicly accessible HTTP endpoints. You must validate data structure and types before processing logic. In We use Zod for schema validation.

Installation: npm install zod

File: app/actions/auth-actions.ts

'use server'

import { z } from 'zod';
import { hashPassword } from '@/lib/auth-utils';
import { db } from '@/lib/db'; // Hypothetical DB connection
import { redirect } from 'next/navigation';

// 1. Define the Strict Schema
const SignUpSchema = z.object({
  email: z.string().email({ message: "Invalid email format" }),
  password: z.string()
    .min(8, { message: "Password must be at least 8 characters" })
    .regex(/[A-Z]/, { message: "Must contain one uppercase letter" })
    .regex(/[0-9]/, { message: "Must contain one number" }),
  role: z.enum(['user', 'admin']).default('user'), // Enforce allowed values
});

export async function registerUser(prevState: any, formData: FormData) {

  // 2. Parse and Validate Input
  const result = SignUpSchema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
    role: 'user', // Force role to user prevents Mass Assignment vulnerabilities
  });

  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  const { email, password } = result.data;

  // 3. Securely Hash Password
  const hashedPassword = await hashPassword(password);

  // 4. Database Interaction (Example with Prisma/Drizzle)
  try {
    await db.user.create({
      data: {
        email,
        password: hashedPassword,
      },
    });
  } catch (e) {
    return { error: "User already exists" };
  }

  redirect('/login');
}
Enter fullscreen mode Exit fullscreen mode

V. Session Management & Cookies

When managing sessions manually or via libraries, configuring the cookie attributes is vital to prevent Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF).

File: lib/session.ts

import { cookies } from 'next/headers';

export async function createSession(userId: string) {
  // Set the expiration time (e.g., 7 days)
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
  const sessionToken = "generated_jwt_or_session_id"; // Replace with actual token logic

  const cookieStore = await cookies();

  cookieStore.set('session', sessionToken, {
    httpOnly: true,    // CRITICAL: Prevents JavaScript access (stops XSS theft)
    secure: process.env.NODE_ENV === 'production', // Only send over HTTPS
    sameSite: 'lax',   // Protects against CSRF while allowing normal navigation
    expires: expiresAt,
    path: '/',
  });
}
Enter fullscreen mode Exit fullscreen mode

VI. Output Sanitization (XSS Prevention)

Next.js automatically sanitizes data passed to JSX components. However, if you must render HTML stored in a database (e.g., a blog post body), you bypass this protection.

You must use a sanitizer library like isomorphic-dompurify before rendering raw HTML.

Installation: npm install isomorphic-dompurify

File: components/SafeHTML.tsx

import DOMPurify from 'isomorphic-dompurify';

interface Props {
  htmlContent: string;
}

export default function SafeHTML({ htmlContent }: Props) {
  // Cleanses the HTML of scripts, iframes, and event handlers (e.g., onclick)
  const sanitizedHTML = DOMPurify.sanitize(htmlContent);

  // Now safe to render
  return (
    <div 
      className="prose" 
      dangerouslySetInnerHTML={{ __html: sanitizedHTML }} 
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
cyber8080 profile image
Cyber Safety Zone

This is a really thoughtful breakdown of full-stack security architecture. I love how you emphasize that security isn’t an afterthought but a core architectural concern — especially by layering defenses across the edge (middleware), config, and server runtime. Your use of Next.js with Zod for validation and bcrypt for hashing is concrete and practical. The checklist for SSL/TLS, CSP, secure dependencies, and input sanitization is super helpful for any dev team looking to build securely from day one. Thanks for putting this together — much more devs need to think in terms of defense-in-depth like this! 🔐