Lesson 31 of 5028 min read

Next.js Authentication Tutorial with Auth.js and NextAuth.js

Learn how to add authentication to a Next.js application using Auth.js (NextAuth.js), covering providers, sessions, JWTs, and protecting routes.

Author: CodersNexus

Next.js Authentication Tutorial with Auth.js and NextAuth.js

Nearly every real application needs to know who's using it — letting users sign up, log in, and access content or actions specific to their account. Building this from scratch is deceptively complex: securely hashing passwords, managing sessions or tokens, handling third-party login providers like Google or GitHub, and protecting specific routes from unauthenticated access, all correctly and securely, is a substantial undertaking prone to serious security mistakes if done casually.

Auth.js (the framework-agnostic evolution of the popular NextAuth.js library) solves this comprehensively for Next.js applications, providing a battle-tested, secure foundation for authentication with minimal setup. This lesson covers configuring Auth.js with both a third-party provider (like GitHub) and credentials-based login, understanding how sessions and JWTs work under the hood, and protecting specific routes so only authenticated users can access them.

Learning Objectives

  • Install and configure Auth.js in a Next.js App Router project.
  • Set up an authentication provider, such as a third-party OAuth provider or credentials-based login.
  • Understand the difference between session-based and JWT-based authentication strategies.
  • Access the current user's session inside Server Components, Client Components, and Route Handlers.
  • Protect specific routes so only authenticated users can access them.

Core Definitions

  • Auth.js (NextAuth.js): An open-source authentication library for JavaScript frameworks, including Next.js, providing built-in support for OAuth providers, credentials-based login, session management, and route protection.
  • Provider: A specific authentication method configured in Auth.js, such as a third-party OAuth service (Google, GitHub) or a custom credentials-based (email/password) login.
  • Session: A record of a currently logged-in user, allowing an application to recognize and identify that user across multiple requests without requiring them to log in again on every page.
  • JWT (JSON Web Token): A compact, digitally signed token encoding a user's identity and other claims, which can be verified without a database lookup, commonly used as an alternative to database-stored sessions.
  • Middleware-based route protection: Using Next.js middleware (running before a request reaches a page) to check authentication status and redirect unauthenticated users away from protected routes.

How Next.js Authentication Auth.js Actually Works

Setting up Auth.js begins with installing the library and creating a central configuration file (conventionally auth.ts or auth.config.ts) where you define your authentication providers. A provider represents one specific way a user can log in: adding a GitHub provider (`GitHub({ clientId: ..., clientSecret: ... })`) lets users authenticate using their existing GitHub account via OAuth, redirecting them to GitHub, then back to your app with proof of their identity, all handled by Auth.js. A Credentials provider instead lets you implement your own email/password login flow, where Auth.js calls a function you provide to verify the submitted credentials against your own database (using a securely hashed password comparison, never storing or comparing plain-text passwords).

Once a user successfully authenticates through either method, Auth.js needs a strategy for remembering that they're logged in across subsequent requests — this is the difference between session-based and JWT-based strategies. A database session strategy stores session information (like which user is logged in) directly in your database, with the browser holding only a reference token to look that record up; this makes sessions easy to instantly revoke (simply delete the database record) but requires a database lookup on every authenticated request. A JWT strategy instead encodes the user's identity directly into a signed token stored in the browser, which can be verified purely through cryptographic signature checking, without any database lookup — faster and simpler to scale, but slightly harder to instantly revoke a specific session before its token naturally expires, since the token itself carries the necessary information independent of any database record.

Once configured, Auth.js exposes the current session through several different mechanisms depending on where you need it: a server-side `auth()` function (callable directly inside async Server Components, Server Actions, and Route Handlers) returns the current session, including the logged-in user's details, directly usable in server-rendered content or server-side logic. For Client Components needing reactive access to session state, Auth.js provides a `useSession()` hook (requiring the app to be wrapped in a SessionProvider), returning the current session along with a loading status.

Protecting specific routes so only authenticated users can access them is commonly handled via Next.js middleware — a special middleware.ts file at your project's root that runs before a request reaches its destination page, letting you check the current session's authentication status and redirect unauthenticated users to a login page before they ever see the protected content, rather than relying solely on client-side checks after the page has already started rendering (which the earlier evenhandedness around client-side validation from Lesson 3.4 would flag as insufficient for genuine security). This middleware-based approach ensures protected routes are enforced consistently and securely at the framework level, regardless of how a request arrives.

Visualizing Next.js Authentication Auth.js

{"heading":"Visualizing Next.js Authentication Auth.js","description":"Visualize the authentication flow and session strategies:\n\nLOGIN FLOW (GitHub provider example):\n[User clicks 'Login with GitHub'] --> [Redirected to GitHub] --> [User approves]\n --> [Redirected back to your app with proof of identity] --> [Auth.js creates a session/JWT]\n\nSESSION STRATEGY COMPARISON:\nDatabase sessions: [Browser holds a reference token] --> [Every request: DB lookup for session data] --> [Instantly revocable by deleting the DB record]\nJWT sessions: [Browser holds a signed token with identity encoded IN it] --> [Every request: verify signature, NO DB lookup] --> [Faster, but harder to instantly revoke before expiry]\n\nROUTE PROTECTION:\n[Request to /dashboard] --> [middleware.ts checks session BEFORE reaching the page] --> Authenticated? → allow through | Not authenticated? → redirect to /login"}

Next.js Practical Example

// auth.ts — central Auth.js configuration
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { prisma } from '@/lib/prisma';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Credentials({
      credentials: { email: {}, password: {} },
      authorize: async (credentials) => {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user) return null;

        const isValid = await bcrypt.compare(
          credentials.password as string,
          user.hashedPassword
        );
        return isValid ? user : null;
      },
    }),
  ],
  session: { strategy: 'jwt' },
});

// app/api/auth/[...nextauth]/route.ts — wires Auth.js into a Route Handler
import { handlers } from '@/auth';
export const { GET, POST } = handlers;

// app/dashboard/page.tsx — reading the session server-side
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();
  if (!session) redirect('/login');

  return <h1>Welcome, {session.user?.name}</h1>;
}

// middleware.ts — protecting a whole section of routes
import { auth } from '@/auth';

export default auth((req) => {
  if (!req.auth && req.nextUrl.pathname.startsWith('/dashboard')) {
    return Response.redirect(new URL('/login', req.nextUrl));
  }
});

export const config = { matcher: ['/dashboard/:path*'] };

auth.ts configures two providers: GitHub for one-click OAuth login, and Credentials for email/password login, where the authorize function looks up the user in the database via Prisma and securely compares the submitted password against a bcrypt-hashed stored value — never comparing or storing plain-text passwords. The catch-all app/api/auth/[...nextauth]/route.ts file wires Auth.js's generated handlers into an actual Route Handler, exactly the pattern from Lesson 4.5/5.3, giving Auth.js the actual API endpoints it needs to handle the login flow. DashboardPage demonstrates server-side session checking using the auth() function directly inside an async Server Component, redirecting unauthenticated visitors before any protected content renders. middleware.ts provides an additional, broader layer of protection at the framework level, using a matcher config to apply this authentication check to every route under /dashboard, redirecting unauthenticated requests to /login before they ever reach the actual page component.

Real-World Examples: How Companies Use Next.js Authentication Auth.js

  • SaaS products commonly offer both a 'Sign in with Google' OAuth provider for convenience and a traditional email/password Credentials provider for users who prefer not to link a third-party account.
  • Developer tools and open-source-adjacent products frequently use a GitHub provider specifically, since their target audience (developers) already has GitHub accounts, reducing sign-up friction significantly.
  • Financial and healthcare applications often favor JWT-based sessions combined with short expiration times and refresh mechanisms, balancing performance with the ability to limit how long a compromised token remains valid.
  • Enterprise internal tools frequently configure Auth.js with a corporate SSO (Single Sign-On) provider, letting employees log in using their existing company credentials rather than creating separate application-specific accounts.
  • E-commerce platforms use middleware-based route protection extensively to guard authenticated-only sections like order history, account settings, and checkout, ensuring these are never accessible without a valid session regardless of how a URL is reached.

Common Mistakes to Avoid

  • Storing or comparing plain-text passwords instead of using a secure hashing library like bcrypt within a Credentials provider's authorize function.
  • Relying solely on client-side checks (like conditionally hiding a link) to 'protect' a route, without genuine server-side or middleware-based enforcement.
  • Forgetting to wrap the application in a SessionProvider when using the useSession() hook in Client Components, causing it to fail.
  • Choosing a JWT strategy without considering that instantly revoking a specific compromised session is harder compared to a database session strategy.
  • Hardcoding OAuth provider credentials (client ID and secret) directly in code instead of using environment variables, exactly the practice emphasized in Lesson 5.1 and 5.7.

Interview Notes

  • Auth.js (NextAuth.js) provides built-in support for OAuth providers, credentials-based login, session management, and route protection.
  • Database sessions require a lookup per request but allow instant revocation; JWT sessions avoid a database lookup but are harder to instantly revoke before expiry.
  • The auth() function reads the current session server-side (Server Components, Server Actions, Route Handlers); useSession() reads it reactively in Client Components.
  • middleware.ts provides framework-level route protection, checking authentication before a request reaches its destination page.
  • Credentials providers must securely hash and compare passwords (e.g., with bcrypt), never storing or comparing plain text.

Key Takeaways

  • Authentication is complex and security-critical enough that using a battle-tested library like Auth.js is almost always preferable to building it from scratch.
  • Choosing between database sessions and JWTs is a meaningful architectural tradeoff between revocation control and per-request performance.
  • Genuine route protection requires server-side or middleware-based enforcement, not merely client-side UI hiding, echoing the client/server validation principle from earlier in the course.
  • Auth.js's consistent API across Server Components, Client Components, and Route Handlers makes session access predictable throughout an entire application.

Summary

Auth.js (the evolution of NextAuth.js) provides a secure, battle-tested foundation for authentication in Next.js applications, configured through a central file defining one or more providers — such as a third-party OAuth service like GitHub, or a custom Credentials provider for email/password login, which must securely hash and compare passwords rather than handling them in plain text. Once configured, Auth.js manages either database-backed sessions (requiring a per-request lookup but allowing instant revocation) or JWT-based sessions (avoiding a database lookup via cryptographic signature verification, at the cost of harder instant revocation). The current session is accessible server-side via the auth() function inside Server Components, Server Actions, and Route Handlers, and reactively in Client Components via the useSession() hook. Genuine route protection is enforced through a middleware.ts file, checking authentication status and redirecting unauthenticated requests before they ever reach a protected page — a framework-level enforcement mechanism far more reliable than client-side-only checks.