Programming Intermediate 14 min

How to Add Authentication to a Next.js App with Auth.js

Auth.js (formerly NextAuth.js) v5 is the standard way to add authentication to Next.js App Router projects. It handles the OAuth dance, session management, CSRF protection, and token rotation for you — the boilerplate you would otherwise write yourself.

This guide walks through the full setup using either Google OAuth (social login) or a Credentials provider (email + password). You will protect server components, add client-side session access with useSession(), and lock entire route groups with middleware — all without a third-party auth service.

Step-by-step

  1. 1

    Install Auth.js v5

    Auth.js v5 is published as next-auth@beta. Install it and generate a secret — this secret signs the session token and must stay out of version control.

    bash
    npm install next-auth@beta
    
    # Generate a random AUTH_SECRET and write it to .env.local
    npx auth secret
    # Adds AUTH_SECRET="<random>" to .env.local automatically
  2. 2

    Create the auth.ts config file

    Create auth.ts at the project root (next to package.json). This file is the single source of truth — it exports auth, signIn, signOut, and handlers. Everything else imports from here, not from next-auth directly.

    typescript
    // auth.ts
    import NextAuth from "next-auth"
    import Google from "next-auth/providers/google"
    
    export const { handlers, signIn, signOut, auth } = NextAuth({
      providers: [
        Google({
          clientId: process.env.AUTH_GOOGLE_ID,
          clientSecret: process.env.AUTH_GOOGLE_SECRET,
        }),
      ],
    })
  3. 3

    Wire up the API route handler

    Auth.js needs a catch-all route at /api/auth/[...nextauth] to handle OAuth callbacks, sign-in, sign-out, and session endpoints. Create the file and re-export the handlers from your config.

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

    Set the required environment variables

    Add these to .env.local. For Google, get the client ID and secret from the Google Cloud Console under APIs & Services → Credentials → OAuth 2.0 Client IDs. Set the authorised redirect URI to http://localhost:3000/api/auth/callback/google for development (add your production URL separately).

    bash
    # .env.local
    AUTH_SECRET="generated-by-npx-auth-secret"
    AUTH_GOOGLE_ID="your-google-client-id.apps.googleusercontent.com"
    AUTH_GOOGLE_SECRET="your-google-client-secret"
    
    # NEXTAUTH_URL is not required in v5 — Auth.js infers it from the request
  5. 5

    Protect server components with auth()

    In any Server Component or Server Action, call auth() to get the session. It returns null when the user is not signed in. Redirect or render a gate accordingly.

    typescript
    // app/dashboard/page.tsx
    import { auth } from "@/auth"
    import { redirect } from "next/navigation"
    
    export default async function DashboardPage() {
      const session = await auth()
    
      if (!session) {
        redirect("/api/auth/signin")
      }
    
      return (
        <main>
          <h1>Welcome, {session.user?.name}</h1>
        </main>
      )
    }
  6. 6

    Add SessionProvider for client components

    Client components cannot call auth() — they use the useSession() hook instead. Wrap your root layout with SessionProvider so the hook works anywhere in the client tree.

    typescript
    // app/layout.tsx
    import { SessionProvider } from "next-auth/react"
    import { auth } from "@/auth"
    
    export default async function RootLayout({ children }: { children: React.ReactNode }) {
      const session = await auth()
      return (
        <html>
          <body>
            <SessionProvider session={session}>
              {children}
            </SessionProvider>
          </body>
        </html>
      )
    }
    
    // In any client component:
    "use client"
    import { useSession } from "next-auth/react"
    
    export function UserAvatar() {
      const { data: session, status } = useSession()
      if (status === "loading") return <span>Loading…</span>
      if (!session) return <a href="/api/auth/signin">Sign in</a>
      return <img src={session.user?.image ?? ""} alt={session.user?.name ?? ""} />
    }
  7. 7

    Add sign-in and sign-out buttons

    Import signIn and signOut from your auth.ts file and call them from Server Actions inside a <form>. This avoids exposing credentials to the client and works without JavaScript.

    typescript
    // components/auth-buttons.tsx
    import { signIn, signOut } from "@/auth"
    
    export function SignInButton() {
      return (
        <form action={async () => { "use server"; await signIn("google") }}>
          <button type="submit">Sign in with Google</button>
        </form>
      )
    }
    
    export function SignOutButton() {
      return (
        <form action={async () => { "use server"; await signOut() }}>
          <button type="submit">Sign out</button>
        </form>
      )
    }
  8. 8

    Protect routes with middleware

    Create a middleware.ts file at the project root. Export the auth function as the middleware — it intercepts every request that matches the matcher config and redirects unauthenticated users to the sign-in page before the route handler even runs.

    typescript
    // middleware.ts
    export { auth as middleware } from "@/auth"
    
    export const config = {
      matcher: [
        // Protect everything under /dashboard and /account
        "/dashboard/:path*",
        "/account/:path*",
        // Skip static files and API routes that handle auth themselves
        "/((?!api|_next/static|_next/image|favicon.ico).*)",
      ],
    }
  9. 9

    Use the Credentials provider for email/password

    If you need email + password login instead of (or alongside) OAuth, add the Credentials provider to your auth.ts. Compare the password against a hashed value from your database — never store plaintext passwords.

    typescript
    // auth.ts (Credentials provider addition)
    import Credentials from "next-auth/providers/credentials"
    import bcrypt from "bcryptjs"
    import { db } from "@/lib/db" // your DB client
    
    export const { handlers, signIn, signOut, auth } = NextAuth({
      providers: [
        Credentials({
          credentials: {
            email: { label: "Email", type: "email" },
            password: { label: "Password", type: "password" },
          },
          async authorize(credentials) {
            if (!credentials?.email || !credentials?.password) return null
    
            const user = await db.user.findUnique({
              where: { email: credentials.email as string },
            })
            if (!user) return null
    
            const passwordMatch = await bcrypt.compare(
              credentials.password as string,
              user.passwordHash
            )
            return passwordMatch ? user : null
          },
        }),
      ],
    })

Tips & gotchas

  • Use JWT sessions (the default) for stateless APIs. Switch to database sessions only when you need to invalidate sessions server-side (e.g. account bans).
  • Extend the session object with custom fields (user ID, role) using the `callbacks.session` and `callbacks.jwt` hooks in `auth.ts` — this avoids extra DB round-trips on every request.
  • In the Google Cloud Console, always add both your development (`localhost`) and production callback URLs to the authorised redirect URIs list before going live.
  • Never call `signIn()` or `signOut()` from a Client Component directly — wrap them in a Server Action or use the built-in `/api/auth/signin` URL.

Wrapping up

Auth.js v5 cuts the authentication boilerplate down to a config file, a single API route, and a middleware export. Once it is wired up, adding new providers is a one-liner, and protecting new routes means adding a path to the middleware matcher. The combination of server-side auth() and client-side useSession() covers every rendering pattern the App Router offers.

#Next.js #Auth #NextAuth
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.