Building in PublicRead Blog
Mastering Authentication in Next.js 15: The Senior Engineer's Guide

Mastering Authentication in Next.js 15: The Senior Engineer's Guide

D
Dev.Log
30 min read
0 views
Share:

1. The Architecture: "One Config to Rule Them All"

In NextAuth v4, configuration was often trapped inside an API route. In v5, we lift the config into a standalone auth.ts file. This allows us to use the same logic in Middleware, Server Actions, and API routes.

Step 1: Initialize the Auth Engine

Create a file at your root (or /src) called auth.ts. This is the brain of your auth system.

TypeScript

// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { 
  handlers, 
  auth, 
  signIn, 
  signOut 
} = NextAuth({
  providers: [GitHub],
  // Senior Tip: Always use the 'jwt' strategy for better scalability 
  // in serverless environments.
  session: { strategy: "jwt" },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      }
      return true;
    },
  },
});

2. The Route Handler

Next, we need to expose the authentication API. Thanks to our setup above, the route handler is now just two lines of code.

TypeScript

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

3. Protecting Routes: Middleware vs. Server Components

As a senior dev, I recommend Middleware for global "gates" and Server Components for granular data protection.

The Middleware Gate

This prevents unauthenticated users from even hitting your server components for protected routes.

TypeScript

// middleware.ts
export { auth as middleware } from "@/auth";

export const config = {
  // Protect everything except public assets and login
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

The Server Component Check

Inside a page, you can access the session without any "useEffect" or "loading" states. It’s instant and secure.

TypeScript

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

export default async function Dashboard() {
  const session = await auth();

  if (!session) return <div>Please log in</div>;

  return (
    <main>
      <h1>Welcome back, {session.user?.name}</h1>
      {/* Personalized content rendered on the server */}
    </main>
  );
}

4. The Client Side: Interactivity

While we prefer server-side checks, you still need buttons. Use the exported signIn and signOut functions.

TypeScript

// components/AuthButtons.tsx
import { signIn, signOut } from "@/auth";

export function LoginButton() {
  return (
    <form action={async () => {
      "use server";
      await signIn("github");
    }}>
      <button type="submit">Sign in with GitHub</button>
    </form>
  );
}

Senior Engineering Checklist

  1. Secret Management: Ensure AUTH_SECRET is set in your .env. Without it, your production build will fail.
  2. Type Safety: If you're adding custom fields (like role) to the user object, remember to extend the Session interface in a next-auth.d.ts file.
  3. Edge Compatibility: Auth.js v5 is built to run on the Edge. Avoid importing heavy Node-only libraries in your main auth.ts if you plan to use Edge Runtime.