Authentication

NextAuth.js Authentication Guide

S

Sajan Acharya

Author

October 25, 2024
16 min read

Introduction

NextAuth.js is the standard authentication solution for Next.js applications. It handles OAuth providers (Google, GitHub, Facebook), email magic links, credentials-based authentication, and more. Built on open standards like OAuth 2.0 and OpenID Connect, it provides enterprise-grade security with minimal setup.

Why Choose NextAuth.js?

  • Zero-Configuration OAuth: Works out of the box with major providers (Google, GitHub, Facebook, etc.)
  • Type-Safe: Full TypeScript support with excellent IDE autocomplete
  • Flexible Sessions: Choose between JWT and database sessions based on your needs
  • Middleware Protection: Secure pages with a single line of code
  • Database Adapters: Works with PostgreSQL, MySQL, MongoDB, and more
  • Callbacks: Extend functionality with custom logic at various authentication stages

Installation & Setup

Install NextAuth.js in your Next.js project:

npm install next-auth

Generate a secure NEXTAUTH_SECRET for token signing:

npx auth secret

Core Configuration

Create the authentication API route at app/api/auth/[...nextauth]/route.ts:

import NextAuth, { type NextAuthOptions } from "next-auth"
import GithubProvider from "next-auth/providers/github"
import GoogleProvider from "next-auth/providers/google"
import CredentialsProvider from "next-auth/providers/credentials"

export const authOptions: NextAuthOptions = {
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID || "",
      clientSecret: process.env.GITHUB_SECRET || "",
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID || "",
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        // This is where you find the user from your database
        // Hash passwords with bcrypt or similar!
        const user = await db.user.findUnique({
          where: { email: credentials?.email },
        })
        
        if (user && await verifyPassword(credentials?.password, user.password)) {
          return user
        }
        
        return null
      },
    }),
  ],
  session: {
    strategy: "jwt", // or "database" for database sessions
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      return token
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
}

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

Environment Variables

Configure your .env.local file with provider credentials:

# NextAuth Configuration
NEXTAUTH_SECRET=generated_secret_from_npx_auth_secret
NEXTAUTH_URL=http://localhost:3000

# GitHub OAuth
GITHUB_ID=your_github_oauth_app_id
GITHUB_SECRET=your_github_oauth_secret

# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_google_client_secret

# Database (if using database sessions)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

Getting OAuth Credentials

GitHub: Go to Settings → Developer settings → OAuth Apps → Create New OAuth App

Google: Visit Google Cloud Console → Create Project → APIs & Services → Credentials → OAuth 2.0 Client ID

Authentication Strategies

JWT Sessions (Default)

JWTs are stateless and scale well for APIs and distributed systems:

// Pros: Stateless, scales well, works across subdomains
// Cons: Token is in localStorage (XSS vulnerability), requires refresh strategy

session: {
  strategy: "jwt",
  maxAge: 30 * 24 * 60 * 60, // 30 days
}

Database Sessions

Database sessions are more secure for sensitive applications:

// Pros: Secure, can revoke tokens, better for sensitive data
// Cons: Requires database, adds latency

session: {
  strategy: "database",
  maxAge: 24 * 60 * 60, // 24 hours
}

// Requires setting up a database adapter
import { PrismaAdapter } from "@next-auth/prisma-adapter"

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [ /* ... */ ],
}

Protecting Routes

Server-Side Protection with Middleware

Create middleware.ts at your project root to protect entire route groups:

import { withAuth } from "next-auth/middleware"
import { NextRequest } from "next/server"

export default withAuth(
  function middleware(req: NextRequest) {
    // Custom logic here
    console.log("Protected route accessed by:", req.nextauth.token?.email)
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token,
    },
  }
)

export const config = {
  matcher: [
    "/dashboard/:path*",
    "/admin/:path*",
    "/api/protected/:path*",
  ],
}

Component-Level Protection

Protect components with the SessionProvider and useSession hook:

import { SessionProvider } from "next-auth/react"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <SessionProvider>
          {children}
        </SessionProvider>
      </body>
    </html>
  )
}

Using Authentication in Components

Client-Side Session Access

"use client"

import { useSession, signIn, signOut } from "next-auth/react"

export default function UserMenu() {
  const { data: session, status } = useSession()

  if (status === "loading") {
    return <div>Loading...</div>
  }

  if (status === "unauthenticated") {
    return <button onClick={() => signIn()}>Sign In</button>
  }

  return (
    <div>
      <p>Welcome, {session?.user?.name}!</p>
      <p>Email: {session?.user?.email}</p>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  )
}

Server-Side Session Access

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"

export default async function Dashboard() {
  const session = await getServerSession(authOptions)

  if (!session) {
    redirect("/api/auth/signin")
  }

  return (
    <div>
      <h1>Dashboard for {session.user?.email}</h1>
      {/* Protected content */}
    </div>
  )
}

Advanced Features

Role-Based Access Control (RBAC)

Extend the session with user roles for fine-grained access control:

// Extend NextAuth types
declare module "next-auth" {
  interface Session {
    user: {
      id: string
      email: string
      name: string
      role: "admin" | "user"
    }
  }
}

// In callbacks
callbacks: {
  async jwt({ token, user }) {
    if (user) {
      token.role = user.role
    }
    return token
  },
  async session({ session, token }) {
    if (session.user) {
      session.user.role = token.role as string
    }
    return session
  },
}

// Use in components
function AdminPanel() {
  const { data: session } = useSession()
  
  if (session?.user?.role !== "admin") {
    return <div>Access denied</div>
  }
  
  return <div>Admin controls here</div>
}

Custom Sign-In Page

Create a custom authentication UI at app/auth/signin/page.tsx:

import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"

export default function SignIn() {
  const router = useRouter()

  return (
    <div style={{ maxWidth: "400px", margin: "50px auto" }}>
      <h1>Sign In</h1>
      
      <button onClick={() => signIn("github")}>
        Sign in with GitHub
      </button>
      
      <button onClick={() => signIn("google")}>
        Sign in with Google
      </button>
      
      <button onClick={() => signIn("credentials", { 
        email: "user@example.com",
        password: "password"
      })}>
        Sign in with Email
      </button>
    </div>
  )
}

Email Provider with Magic Links

Send magic links via email without passwords:

import EmailProvider from "next-auth/providers/email"
import { resend } from "resend"

providers: [
  EmailProvider({
    server: {
      host: process.env.EMAIL_SERVER_HOST,
      port: process.env.EMAIL_SERVER_PORT,
      auth: {
        user: process.env.EMAIL_SERVER_USER,
        pass: process.env.EMAIL_SERVER_PASSWORD,
      },
    },
    from: process.env.EMAIL_FROM,
  }),
]

Security Best Practices

  • HTTPS Only in Production: Set NEXTAUTH_URL to HTTPS in production.
  • Secure Secrets: Use strong, random NEXTAUTH_SECRET. Regenerate with npx auth secret.
  • Hash Passwords: Never store plain text passwords. Use bcrypt, argon2, or similar.
  • Validate Inputs: Always validate email and password on the server side.
  • Rate Limit: Implement rate limiting on signin/signup endpoints.
  • CSRF Protection: NextAuth includes CSRF tokens by default.
  • Secure Cookies: In production, cookies are automatically httpOnly and secure.
  • Revoke Sessions: Implement logout that invalidates the session.

Common Integration Patterns

Redirect After Sign-In

signIn("github", { 
  callbackUrl: "/dashboard"
})

Check Auth in API Routes

import { getServerSession } from "next-auth/next"

export async function POST(req: Request) {
  const session = await getServerSession()
  
  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 })
  }
  
  // Process authenticated request
}

Refresh Token Strategy

// In JWT callback, handle token refresh
callbacks: {
  async jwt({ token, account }) {
    if (account?.expires_at) {
      token.expiresAt = account.expires_at * 1000
    }
    
    // Check if token is expired
    if (Date.now() > (token.expiresAt || 0)) {
      return refreshAccessToken(token)
    }
    
    return token
  },
}

Troubleshooting

Session not persisting: Ensure SessionProvider wraps your app and NEXTAUTH_SECRET is set.

OAuth callback fails: Check redirect URIs match exactly in your OAuth app configuration.

CSRF token mismatch: Clear cookies and cache, ensure NEXTAUTH_URL is correct.

Type errors with session: Extend NextAuth types as shown in the RBAC section.

Conclusion

NextAuth.js provides a secure, flexible, and production-ready authentication solution for Next.js applications. Whether you need simple OAuth authentication or complex multi-provider setups with role-based access control, NextAuth.js scales with your requirements. Its combination of zero-configuration simplicity and powerful customization options makes it the ideal choice for modern web applications.

Tags

#Next.js#Authentication#Security#NextAuth.js#OAuth

Share this article

About the Author

S

Sajan Acharya

Expert Writer & Developer

Sajan Acharya is an experienced software engineer and technology writer passionate about helping developers master modern web technologies. With years of professional experience in full-stack development, system design, and best practices, they bring real-world insights to every article.

Specializing in Next.js, TypeScript, Node.js, databases, and web performance optimization. Follow for more in-depth technical content.

Stay Updated

Get the latest articles delivered to your inbox