How to Add Secure Authentication to Next.js

How-To Guide

How to Add Secure Authentication to Next.js

Using NextAuth.js (Auth.js) for App Router

TL;DR

TL;DR

NextAuth.js handles the auth mechanics for you. The mistakes happen after: skipping NEXTAUTH_SECRET in production (sessions won't be secure without it), guarding routes with middleware but forgetting to verify sessions in API routes, and confusing authentication with authorization. Knowing who someone is doesn't mean they can access whatever they're requesting.

Prerequisites

You'll need a Next.js 13+ app with App Router. This guide uses NextAuth.js v4 (also known as Auth.js).

Step-by-Step Guide

1

Install NextAuth.js

npm install next-auth
2

Create the auth configuration

Create app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { NextAuthOptions } from 'next-auth';

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      // Add user ID to session
      if (session.user) {
        session.user.id = token.sub!;
      }
      return session;
    },
  },
};

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

Set up environment variables

Add to .env.local:

# Generate with: openssl rand -base64 32
NEXTAUTH_SECRET=your-super-secret-key-here
NEXTAUTH_URL=http://localhost:3000

# OAuth provider credentials
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret

Critical: NEXTAUTH_SECRET must be set in production. Without it, your sessions are not secure.

4

Protect routes with middleware

Create middleware.ts in your project root:

import { withAuth } from 'next-auth/middleware';

export default withAuth({
  callbacks: {
    authorized: ({ token }) => !!token,
  },
});

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/api/protected/:path*'],
};
5

Get session in Server Components

import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { redirect } from 'next/navigation';

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

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

  return (
    <div>
      <h1>Welcome, {session.user?.name}</h1>
    </div>
  );
}
6

Protect API routes

import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const session = await getServerSession(authOptions);

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Now check authorization (does this user own this resource?)
  const userId = session.user.id;

  // Fetch data scoped to this user
  const data = await db.posts.findMany({ where: { authorId: userId } });

  return NextResponse.json(data);
}
7

Add session provider for client components

Create app/providers.tsx:

'use client';

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

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

Wrap your app in app/layout.tsx:

import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Common Security Mistakes

  • Missing NEXTAUTH_SECRET in production: sessions won't be secure without it, full stop
  • Only checking auth on the frontend: API routes are equally exposed and need their own checks
  • Forgetting authorization: auth proves who someone is, not what they're allowed to do
  • Exposing session data: anything in the JWT reaches the client, so keep sensitive fields server-side

Testing Your Auth

  1. Access /dashboard without logging in. It should redirect, not load.
  2. Call an API route with no session. You should get a 401 back.
  3. Request another user's data by ID. It should be denied, not silently return empty.
  4. Open DevTools after sign-in and check what's in the JWT. Nothing sensitive should be there.
How-To Guides

How to Add Secure Authentication to Next.js