ProgrUmar Logo
Module 5: Authentication & Authorization

Auth.js v5 with Next.js

Duration: 25 mins

Auth.js v5 Setup

Auth.js v5 is fully compatible with the App Router and Edge Runtime. Configure providers in auth.ts, export the { handlers, signIn, signOut, auth } helpers, and protect routes with a middleware.ts file.

Auth.js v5 with Next.js

Auth.js v5 (formerly NextAuth.js) is a major rewrite designed explicitly for the Next.js App Router and React Server Components. The primary enhancement in v5 is native compatibility with Edge runtimes and Next.js Middleware. In this lesson, we will build a complete authentication system using Auth.js v5, integrating both OAuth providers (like GitHub) and credential-based database logins, and learn how to extract sessions on both client and server contexts securely.


1. Designing the Auth.js v5 Directory Structure

To satisfy Edge compatibility, Auth.js v5 recommends splitting configuration properties into two files. This is because database adapters (like Prisma) cannot compile on Edge routers, whereas the core routing and OAuth policies must run there.

Here is the standard file structure:

src/
├── auth.config.ts      ← OAuth settings, route permissions (Edge compatible)
├── auth.ts             ← Database adapter configuration, password checks (Node.js)
├── middleware.ts       ← Global route protection (Edge runtime)
└── app/
    └── api/
        └── auth/
            └── [...nextauth]/
                └── route.ts  ← Auth API endpoints (GET & POST handlers)

2. Initializing auth.config.ts

The auth.config.ts file stores properties that do not depend on database clients. This config handles core provider configurations and authentication callbacks:

// auth.config.ts
import type { NextAuthConfig } from 'next-auth';
import GitHub from 'next-auth/providers/github';

export const authConfig = {
  pages: {
    signIn: '/login', // Redirect path for custom sign-in page
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      }
      return true;
    },
  },
  providers: [
    GitHub, // GitHub OAuth config automatically reads GITHUB_ID/GITHUB_SECRET env vars
  ],
} satisfies NextAuthConfig;

3. Defining the Main Node.js Config (auth.ts)

The main auth.ts file extends the Edge-compatible configurations, adding database adapters and credential verification rules. This is where we run database queries using Drizzle, Prisma, or custom clients:

// auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { z } from 'zod';

// Replace with your real DB user retrieval query
async function getUser(email: string) {
  try {
    const user = await fetch('https://api.progrumar.com/users?email=' + email).then(r => r.json());
    return user;
  } catch (error) {
    throw new Error('Failed to fetch user.');
  }
}

export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    ...authConfig.providers,
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);

        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;
          
          // Verify user password hash (using bcrypt or similar)
          const passwordsMatch = password === user.password; // Replace with proper compare function
          if (passwordsMatch) return user;
        }

        return null; // Invalid credentials
      },
    }),
  ],
});

4. Mounting the API Route Handlers

Next, we mount the authentication routing hooks inside the App Router. The API file must export the GET and POST handlers generated by the NextAuth init call in auth.ts.

// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';

export const { GET, POST } = handlers;

5. Session Extraction in React Server Components

To inspect user profiles inside async Server Components, await the exported auth() function directly. The session is returned as a plain object, sent directly from server memory:

// app/dashboard/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();

  if (!session?.user) {
    redirect('/login');
  }

  return (
    <main className="p-8">
      <h1>Welcome back, {session.user.name}</h1>
      <p>Logged in as: {session.user.email}</p>
    </main>
  );
}

6. Accessing Session inside Client Components

If you need to access session states inside Client Components (for instance, to toggle navigation links dynamically in your header), wrap your layouts in a SessionProvider and use the useSession Hook.

// app/providers.tsx
'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}

Now retrieve session parameters in child Client Components:

// components/UserMenu.tsx
'use client';

import { useSession } from 'next-auth/react';

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

  if (status === 'loading') return <p>Loading...</p>;
  if (!session) return <button>Sign In</button>;

  return <span>Profile: {session.user?.email}</span>;
}

7. Triggering Sign-In & Sign-Out with Server Actions

Instead of rendering legacy HTTP anchor tags that redirect users away to separate login pipelines, Auth.js v5 lets you execute sign-in and sign-out triggers natively via Server Actions:

// components/LogoutButton.tsx
import { signOut } from '@/auth';

export default function LogoutButton() {
  return (
    <form
      action={async () => {
        'use server';
        await signOut({ redirectTo: '/' });
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  );
}

8. Adding OAuth Sign-In Buttons

You can build OAuth actions similarly by passing the provider name (like 'github' or 'google') directly to the signIn function:

// components/LoginButtons.tsx
import { signIn } from '@/auth';

export default function LoginButtons() {
  return (
    <div className="space-y-4">
      <form
        action={async () => {
          'use server';
          await signIn('github', { redirectTo: '/dashboard' });
        }}
      >
        <button type="submit">Sign In with GitHub</button>
      </form>
    </div>
  );
}

9. Common Gotchas

  • Database Models Mismatch: When using database adapters (like PrismaAdapter), ensure your relational database schema has the exact table schemas (User, Account, Session, VerificationToken) required by Auth.js. Missing a column like emailVerified will prevent users from registering.
  • Missing NEXTAUTH_SECRET: In production environments, Auth.js will crash immediately if the NEXTAUTH_SECRET environment variable is missing or blank. Use openssl rand -base64 32 to generate a secure secret.
  • Cookies Overwrite on Subdomains: If your application shares authentication states between different subdomain layouts, configure custom cookies domains inside the root config block.

Key Takeaways

  • Auth.js v5 splits configurations into auth.ts and auth.config.ts to ensure compatibility with Edge runtimes.
  • Access sessions on the server side using the async auth() function directly (zero client load).
  • Access sessions on the client side using SessionProvider and the useSession() hook.
  • Execute logins and logouts using native Server Actions to bypass legacy redirection scripts.
  • Always verify environment variables (secrets and client keys) are present on deployment.

Configuring your providers handles database queries and identity validations. But managing route protections per-page can lead to redundant checks. In the next lesson, we will cover Protecting Routes with Middleware, learning how to write central routing rules to redirect anonymous users globally.

Chat with us