← Back to blog
·6 min read·By Utkarsh Singh

How to Add Authentication to Your Next.js App Router Project (2026)

A complete guide to setting up authentication in Next.js App Router using NextAuth — Google OAuth, magic links, protected routes, and JWT sessions.

nextjsauthenticationnextauthapp-router

How to Add Authentication to Your Next.js App Router Project (2026)

Authentication is one of those things that sounds simple until you're actually doing it. Then you're four hours deep into JWT configuration, wondering why your session doesn't persist across page refreshes, and questioning your life choices.

This guide covers everything you need to set up production-ready authentication in a Next.js App Router project — Google OAuth, magic links, protected routes, and session handling. No hand-waving. No "left as an exercise for the reader."


What We're Building

By the end of this guide you'll have:

We're using NextAuth v5 (Auth.js), which is the current standard for Next.js App Router authentication in 2026.


Prerequisites


Step 1: Install NextAuth

npm install next-auth@beta
npm install @auth/prisma-adapter

NextAuth v5 is the App Router-native version. If you're on v4, the configuration is significantly different — this guide covers v5 only.


Step 2: Set Up Environment Variables

Create or update your .env.local:

# NextAuth
AUTH_SECRET=your-secret-here # generate with: openssl rand -base64 32
AUTH_URL=http://localhost:3000

# Google OAuth
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret

# Email (for magic links)
AUTH_RESEND_KEY=your-resend-api-key

# Database
DATABASE_URL=your-postgresql-connection-string

Generate AUTH_SECRET with:

openssl rand -base64 32

Never commit this to version control.


Step 3: Configure NextAuth

Create auth.ts at the root of your project:

import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import Resend from "next-auth/providers/resend";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
      allowDangerousEmailAccountLinking: true,
    }),
    Resend({
      apiKey: process.env.AUTH_RESEND_KEY!,
      from: "noreply@yourdomain.com",
    }),
  ],
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      if (token) {
        session.user.id = token.id as string;
      }
      return session;
    },
  },
  pages: {
    signIn: "/login",
    error: "/auth/error",
  },
});

The allowDangerousEmailAccountLinking: true on Google is intentional — it allows a user who signed up with a magic link to later link their Google account without creating a duplicate. Without this, your users will hit confusing errors.


Step 4: Add the Route Handler

Create app/api/auth/[...nextauth]/route.ts:

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

That's the entire file. NextAuth handles all the OAuth callbacks, magic link verification, and session management through this single route.


Step 5: Set Up the Prisma Schema

NextAuth requires specific database tables. Add this to your schema.prisma:

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
  identifier String
  token      String
  expires    DateTime

  @@unique([identifier, token])
}

Run migrations:

npx prisma migrate dev --name add-auth-tables

Step 6: Protect Routes with Middleware

Create middleware.ts at the root of your project:

import { auth } from "@/auth";
import { NextResponse } from "next/server";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");
  const isOnApi = req.nextUrl.pathname.startsWith("/api");

  if (isOnDashboard && !isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.url));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

This runs on every request and redirects unauthenticated users away from protected routes before the page even renders. Much more secure than client-side route guards.


Step 7: Access Session in Server Components

In any Server Component:

import { auth } from "@/auth";
import { redirect } from "next/navigation";

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

  if (!session?.user) {
    redirect("/login");
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
    </div>
  );
}

No useSession hook, no client component required. Just a direct async call.


Step 8: Build the Login Page

Create app/login/page.tsx:

import { signIn } from "@/auth";

export default function LoginPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-4 p-8">
        <h1 className="text-2xl font-bold">Sign in to your account</h1>

        {/* Google OAuth */}
        <form
          action={async () => {
            "use server";
            await signIn("google", { redirectTo: "/dashboard" });
          }}
        >
          <button type="submit" className="w-full rounded-md bg-white border px-4 py-2">
            Continue with Google
          </button>
        </form>

        {/* Magic Link */}
        <form
          action={async (formData: FormData) => {
            "use server";
            await signIn("resend", {
              email: formData.get("email") as string,
              redirectTo: "/dashboard",
            });
          }}
        >
          <input
            name="email"
            type="email"
            placeholder="you@example.com"
            className="w-full rounded-md border px-4 py-2"
          />
          <button type="submit" className="w-full rounded-md bg-black text-white px-4 py-2 mt-2">
            Send magic link
          </button>
        </form>
      </div>
    </div>
  );
}

Common Mistakes to Avoid

Not setting AUTH_URL in production. NextAuth needs to know the canonical URL of your app for OAuth redirects. Set it explicitly in your hosting environment variables — don't rely on auto-detection.

Using client-side session checks as the only protection. Middleware runs before the page renders. A client-side useSession check is a UI concern, not a security measure. Always protect sensitive routes in middleware or server components.

Forgetting cascade deletes. The Prisma schema above includes onDelete: Cascade on Account and Session. Without this, deleting a user leaves orphaned records that will cause database errors.

Not handling the email verification flow. Magic links require the user to check their email. Make sure your login page shows a confirmation state after submission so users know what to do next.


Testing Locally

For magic links locally, use Resend's test mode or point to a local SMTP server. Magic links won't work if your email provider isn't configured.

For Google OAuth locally, add http://localhost:3000/api/auth/callback/google to your Google Cloud Console's authorized redirect URIs.


The Honest Time Estimate

Done properly, this setup takes about 3–4 hours including:

If you'd rather skip all of it, ZeroDrag ships with this entire auth setup pre-configured — Google OAuth, magic links, account linking, protected routes, and server-side session helpers. It's the first thing that's ready when you clone the repo.


Written by Utkarsh Singh. Last updated March 2026.

Skip the setup. Ship your product.

ZeroDrag gives you auth, payments, database, email, and AI — pre-wired. Starter at $119, Pro at $169. One-time payment.

Get ZeroDrag →