Follow Us

CodeWithSabir

  • Contact Us
  • Privacy Policy
  • About
  • Terms & Conditions

All Rights Reserved © 2026

  • Light
  • Dark
Next.js

Authentication in Next.js with NextAuth.js v5: The Complete Setup

Sabir Soft
Sabir Lkhaloufi
  • February 15, 2026
  • 4 min read

Authentication in Next.js with NextAuth.js v5: The Complete Setup

Authentication is one of those things that looks simple and turns out to be a rabbit hole. Sessions, tokens, OAuth flows, CSRF protection, secure cookies — there's a lot to get right, and getting it wrong has security consequences.

NextAuth.js (now Auth.js) handles all of this for you. Version 5 was a significant rewrite that works seamlessly with Next.js App Router. Here's how to set it up correctly.

Installation

npm install next-auth@beta
# Generate a secret key
openssl rand -base64 32

Add to .env.local:

AUTH_SECRET=your-generated-secret
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret

Core Configuration

// auth.ts — in the project root
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Credentials from 'next-auth/providers/credentials'
import { db } from '@/lib/db'
import bcrypt from 'bcryptjs'
import { z } from 'zod'
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google,
    GitHub,
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const parsed = z.object({
          email: z.string().email(),
          password: z.string().min(1),
        }).safeParse(credentials)
 
        if (!parsed.success) return null
 
        const user = await db.user.findUnique({
          where: { email: parsed.data.email },
        })
 
        if (!user?.password) return null
 
        const valid = await bcrypt.compare(parsed.data.password, user.password)
        if (!valid) return null
 
        return { id: user.id, email: user.email, name: user.name, role: user.role }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = (user as any).role
      }
      return token
    },
    async session({ session, token }) {
      session.user.id = token.id as string
      session.user.role = token.role as string
      return session
    },
  },
  pages: {
    signIn: '/login',
    error: '/auth/error',
  },
  session: { strategy: 'jwt' },
})
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers

Extending the Session Type

NextAuth's default session type doesn't include custom fields. Extend it:

// types/next-auth.d.ts
import 'next-auth'
 
declare module 'next-auth' {
  interface Session {
    user: {
      id: string
      email: string
      name: string | null
      image: string | null
      role: string
    }
  }
}

Middleware for Route Protection

// middleware.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'
 
export default auth(req => {
  const { pathname } = req.nextUrl
  const isAuthenticated = !!req.auth
 
  // Public routes
  const publicPaths = ['/login', '/register', '/about', '/']
  const isPublic = publicPaths.some(path =>
    pathname === path || pathname.startsWith('/post/')
  )
 
  if (!isAuthenticated && !isPublic) {
    const loginUrl = new URL('/login', req.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }
 
  // Admin routes
  if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
    return NextResponse.redirect(new URL('/unauthorized', req.url))
  }
 
  return NextResponse.next()
})
 
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

Login Form with Server Action

// app/login/page.tsx
import { signIn } from '@/auth'
import { AuthError } from 'next-auth'
import { redirect } from 'next/navigation'
 
export default function LoginPage({
  searchParams,
}: {
  searchParams: { callbackUrl?: string; error?: string }
}) {
  async function handleLogin(formData: FormData) {
    'use server'
    try {
      await signIn('credentials', {
        email: formData.get('email'),
        password: formData.get('password'),
        redirectTo: searchParams.callbackUrl || '/dashboard',
      })
    } catch (error) {
      if (error instanceof AuthError) {
        redirect(`/login?error=${error.type}`)
      }
      throw error
    }
  }
 
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="w-full max-w-sm space-y-6">
        <h1 className="text-2xl font-bold text-center">Sign In</h1>
 
        {searchParams.error && (
          <div className="bg-red-50 text-red-600 p-3 rounded text-sm">
            {searchParams.error === 'CredentialsSignin'
              ? 'Invalid email or password'
              : 'Authentication failed'}
          </div>
        )}
 
        <form action={handleLogin} className="space-y-4">
          <input
            name="email"
            type="email"
            placeholder="Email"
            required
            className="w-full border rounded-lg px-4 py-2"
          />
          <input
            name="password"
            type="password"
            placeholder="Password"
            required
            className="w-full border rounded-lg px-4 py-2"
          />
          <button
            type="submit"
            className="w-full bg-blue-600 text-white py-2 rounded-lg font-medium"
          >
            Sign in
          </button>
        </form>
 
        <div className="space-y-2">
          <form action={async () => { 'use server'; await signIn('google', { redirectTo: '/dashboard' }) }}>
            <button type="submit" className="w-full border py-2 rounded-lg">
              Continue with Google
            </button>
          </form>
          <form action={async () => { 'use server'; await signIn('github', { redirectTo: '/dashboard' }) }}>
            <button type="submit" className="w-full border py-2 rounded-lg">
              Continue with GitHub
            </button>
          </form>
        </div>
      </div>
    </div>
  )
}

Getting the Session in Components

// Server Component
import { auth } from '@/auth'
 
export default async function Dashboard() {
  const session = await auth()
  if (!session) return null
 
  return <div>Welcome, {session.user.name}</div>
}
// Client Component
'use client'
import { useSession } from 'next-auth/react'
 
export default function UserMenu() {
  const { data: session, status } = useSession()
 
  if (status === 'loading') return <div>Loading...</div>
  if (!session) return <a href="/login">Sign in</a>
 
  return (
    <div>
      <img src={session.user.image ?? ''} alt={session.user.name ?? ''} />
      <span>{session.user.name}</span>
    </div>
  )
}

Don't forget to wrap your app in SessionProvider for client-side session access:

// app/layout.tsx
import { SessionProvider } from 'next-auth/react'
import { auth } from '@/auth'
 
export default async function RootLayout({ children }) {
  const session = await auth()
  return (
    <html>
      <body>
        <SessionProvider session={session}>
          {children}
        </SessionProvider>
      </body>
    </html>
  )
}

Common Mistakes

1. Not setting AUTH_SECRET. Without it, sessions are insecure and production will break.

2. Checking auth in every page instead of middleware. Use middleware for consistent protection. Per-page checks are error-prone.

3. Storing sensitive data in the JWT token. JWT tokens are base64 encoded, not encrypted by default. Don't store passwords, payment info, or anything sensitive.

4. Missing callbackUrl handling. Always preserve the intended destination through the login redirect so users land where they were going.

Key Takeaways

  • NextAuth.js v5 works natively with App Router — use auth() in Server Components and useSession() in Client Components
  • Middleware is the right place to protect routes — not individual pages
  • Extend session types with declare module 'next-auth' to get full TypeScript support
  • Use strategy: 'jwt' for edge-compatible sessions (no database required for session storage)
  • Always add callbackUrl to login redirects so users return to their intended page
Popular Blogs
Claude AI vs ChatGPT: An Honest Comparison for Developers
  • April 28, 2026
AI Tools Every Developer Should Be Using in 2026
  • April 20, 2026
Using the Claude API in Real Projects: A Practical Developer Guide
  • April 15, 2026
Prompt Engineering for Developers: Write Prompts That Actually Work
  • April 10, 2026
Categories
AIDevOpsNext.jsMobile DevelopmentWeb Development

Related Posts

Next.js
How to Build a Fullstack App with Next.js 15: Complete Guide
Sabir Khaloufi·Feb 25, 2026
Next.js
Next.js Server Components Explained: What They Are and Why They Matter
Sabir Khaloufi·Feb 20, 2026
Next.js
Next.js Performance Optimization: 10 Techniques That Make a Real Difference
Sabir Khaloufi·Feb 10, 2026