intermediate Step 12 of 15

Authentication Patterns

Next.js Development

Authentication Patterns

Authentication is a critical requirement for most web applications, and Next.js provides several integration points for implementing auth: middleware for route protection, Server Components for accessing session data, API routes for auth endpoints, and client components for login forms. The most popular approach is using NextAuth.js (now Auth.js), a complete authentication solution that supports dozens of providers. Understanding authentication patterns in Next.js means knowing how to protect routes, manage sessions across server and client, and handle the auth lifecycle from login to logout.

Session-Based Auth with Cookies

A common pattern is storing a session token in an HTTP-only cookie, validating it in middleware and Server Components.

// lib/auth.ts — authentication utilities
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

interface Session {
  user: User;
  expires: string;
}

export async function getSession(): Promise<Session | null> {
  const cookieStore = await cookies();
  const token = cookieStore.get("session-token")?.value;

  if (!token) return null;

  try {
    const res = await fetch(`${process.env.API_URL}/auth/session`, {
      headers: { Authorization: `Bearer ${token}` },
      cache: "no-store",
    });

    if (!res.ok) return null;
    return res.json();
  } catch {
    return null;
  }
}

export async function requireAuth(): Promise<Session> {
  const session = await getSession();
  if (!session) {
    redirect("/login");
  }
  return session;
}

Login API Route

// app/api/auth/login/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { email, password } = await request.json();

  // Validate credentials against your backend or database
  const res = await fetch(`${process.env.API_URL}/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });

  if (!res.ok) {
    const error = await res.json();
    return NextResponse.json(
      { error: error.message || "Invalid credentials" },
      { status: 401 }
    );
  }

  const { token, user } = await res.json();

  // Set HTTP-only cookie with the session token
  const response = NextResponse.json({ user });
  response.cookies.set("session-token", token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 7 days
    path: "/",
  });

  return response;
}

// app/api/auth/logout/route.ts
export async function POST() {
  const response = NextResponse.json({ success: true });
  response.cookies.delete("session-token");
  return response;
}

Protected Server Component

// app/dashboard/page.tsx
import { requireAuth } from "@/lib/auth";

export default async function DashboardPage() {
  // Redirects to /login if not authenticated
  const session = await requireAuth();

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {session.user.name}!</p>
      <p>Role: {session.user.role}</p>
    </div>
  );
}

Login Form (Client Component)

// components/LoginForm.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError("");

    try {
      const res = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });

      if (!res.ok) {
        const data = await res.json();
        setError(data.error || "Login failed");
        return;
      }

      router.push("/dashboard");
      router.refresh(); // Refresh server components to reflect auth state
    } catch {
      setError("Network error. Please try again.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <p className="error">{error}</p>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? "Signing in..." : "Sign In"}
      </button>
    </form>
  );
}

Using NextAuth.js

// NextAuth.js (Auth.js) provides a complete auth solution
// npm install next-auth

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";

const handler = NextAuth({
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        // Validate against your database
        const user = await validateUser(credentials);
        return user || null;
      },
    }),
  ],
  pages: { signIn: "/login" },
});

export { handler as GET, handler as POST };
Tip: Always use HTTP-only cookies for session tokens — they cannot be accessed by JavaScript, protecting against XSS attacks. Combine cookie-based auth with middleware for route protection and Server Components for data fetching with session context.

Key Takeaways

  • Store session tokens in HTTP-only cookies for security against XSS attacks.
  • Use middleware for route protection and Server Components for session-aware data fetching.
  • Create auth utility functions (getSession, requireAuth) for reuse across pages.
  • Call router.refresh() after login/logout to update Server Components with new auth state.
  • NextAuth.js provides a complete solution with OAuth providers, session management, and database adapters.