CodeWithSabir
HomeAIDevOpsNext.jsMobile DevelopmentWeb Development
CodeWithSabir
  • Home
  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development
  • About
  • Contact
CodeWithSabir

In-depth articles, tutorials, and guides on web development, React, Next.js, AI, and modern programming practices.

Topics

  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development

Company

  • About
  • Contact
  • Privacy Policy
  • Terms

© 2026 CodeWithSabir. All rights reserved.

Built with SabirSoft.com

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

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

Learn how to implement authentication in Next.js using NextAuth.js v5 — covering credentials, OAuth providers, JWT sessions, protected routes, and role-based access control.

Sabir KhaloufiSabir KhaloufiFebruary 15, 20264 min read

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

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

Add to .env.local:

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

Core Configuration

typescript
// 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' },
})
typescript
// 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:

typescript
// 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

typescript
// 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

tsx
// 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

tsx
// 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>
}
tsx
// 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:

tsx
// 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
#next.js#nextauth#authentication#oauth#jwt
Share:
Sabir Khaloufi — author photo

Written by

Sabir Khaloufi

Full-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.

On this page

  • Installation
  • Core Configuration
  • Extending the Session Type
  • Middleware for Route Protection
  • Login Form with Server Action
  • Getting the Session in Components
  • Common Mistakes
  • Key Takeaways

Related Articles

Next.js

How to Build a Fullstack App with Next.js 15: Complete Guide

A hands-on guide to building a fullstack application with Next.js 15 — covering App Router, Server Actions, database integration, authentication, and deployment.

Sabir KhaloufiFebruary 25, 20265 min read
Next.js

Next.js Server Components Explained: What They Are and Why They Matter

A clear explanation of React Server Components in Next.js — what problem they solve, how they differ from Client Components, and the patterns that make them powerful.

Sabir KhaloufiFebruary 20, 20264 min read
Next.js

Next.js Performance Optimization: 10 Techniques That Make a Real Difference

Practical Next.js performance optimizations covering image optimization, caching strategies, bundle analysis, lazy loading, and Core Web Vitals improvements that affect Google rankings.

Sabir KhaloufiFebruary 10, 20264 min read