Add Login to Your Next.js Application - Auth0 Docs

Use AI to integrate Auth0

If you use an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you can add Auth0 authentication automatically in minutes using agent skills.Install:

npx skills add auth0/agent-skills --skill auth0

Then ask your AI assistant:

Add Auth0 authentication to my Next.js app

Your AI assistant will automatically create your Auth0 application, fetch credentials, install @auth0/nextjs-auth0, create API routes, and set up environment variables. Full agent skills documentation →

Prerequisites: Before you begin, ensure you have the following installed:

Verify installation: node --version && npm --version

Get Started

This quickstart demonstrates how to add Auth0 authentication to a Next.js 16 application. You’ll build a full-stack web application with server-side rendering, secure login functionality, and protected routes using the Auth0 Next.js SDK.

  1. Create a new project
    Create a new Next.js project for this Quickstart

    npx create-next-app@latest auth0-nextjs --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --yes
    

    Open the project

    cd auth0-nextjs
    
  2. Install the Auth0 Next.js SDK

    npm install @auth0/nextjs-auth0
    
  3. Create project files

    Create all necessary directories and files for Auth0 integration:

    Mac/Linux

    mkdir -p src/lib src/components && touch src/lib/auth0.ts src/proxy.ts src/components/LoginButton.tsx src/components/LogoutButton.tsx src/components/Profile.tsx
    

    Windows

    New-Item -ItemType Directory -Force -Path src/lib, src/components
    New-Item -ItemType File -Path src/lib/auth0.ts, src/proxy.ts, src/components/LoginButton.tsx, src/components/LogoutButton.tsx, src/components/Profile.tsx
    
  4. Setup your Auth0 App

    Next up, you need to create a new app on your Auth0 tenant and add the environment variables to your project. You have three options to set up your Auth0 app: use the Quick Setup tool (recommended), run a CLI command, or configure manually via the Dashboard:

    • Quick Setup (recommended)
    • CLI
    • Dashboard

Create an Auth0 App and copy the pre-filled .env file with the right configuration values.

Log into create the app

.env.local

AUTH0_DOMAIN={yourDomain}
AUTH0_CLIENT_ID={yourClientId}
AUTH0_CLIENT_SECRET={yourClientSecret}
AUTH0_SECRET=GaHLyp0agjpDkTuB5q73dVpkOeaRPsMW
APP_BASE_URL=http://localhost:3000

Run the following command in your project’s root directory to create an Auth0 app and generate a .env file:

Mac

# Install Auth0 CLI (if not already installed)
brew tap auth0/auth0-cli && brew install auth0

# Set up Auth0 app and generate .env file
auth0 qs setup --app --type regular --framework nextjs --name "My App" --port 3000

Windows

# Install Auth0 CLI (if not already installed)
scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git
scoop install auth0

# Set up Auth0 app and generate .env file
auth0 qs setup --app --type regular --framework nextjs --name "My App" --port 3000

This command will:

  1. Check if you’re authenticated (and prompt for login if needed)
  2. Create an Auth0 Regular Web Application configured for http://localhost:3000
  3. Generate a .env file with AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_SECRET, and APP_BASE_URL

Create a .env.local file on your project’s root directory:

.env.local

AUTH0_DOMAIN=YOUR_AUTH0_APP_DOMAIN
AUTH0_CLIENT_ID=YOUR_AUTH0_APP_CLIENT_ID
AUTH0_CLIENT_SECRET=YOUR_AUTH0_APP_CLIENT_SECRET
AUTH0_SECRET=YOUR_LONG_RANDOM_SECRET_HERE
APP_BASE_URL=http://localhost:3000

Generate a secure AUTH0_SECRET:

openssl rand -hex 32

Copy the output and replace YOUR_LONG_RANDOM_SECRET_HERE in .env.local. This must be exactly 64 hexadecimal characters. Then configure your Auth0 application:

  1. Head to the Auth0 Dashboard
  2. Click on Applications > Applications > Create Application
  3. In the popup, enter a name for your app, select Regular Web Application as the app type and click Create
  4. Switch to the Settings tab on the Application Details page
  5. Replace YOUR_AUTH0_APP_DOMAIN, YOUR_AUTH0_APP_CLIENT_ID, and YOUR_AUTH0_APP_CLIENT_SECRET in the .env.local file with the Domain, Client ID, and Client Secret values from the dashboard

Critical: If you change the AUTH0_SECRET after the app has been running, you must clear your browser cookies for localhost. Old session cookies encrypted with the previous secret will cause a JWEDecryptionFailed error.

Finally, on the Settings tab of your Application Details page, configure the following URLs:

Allowed Callback URLs:

http://localhost:3000/auth/callback

Allowed Logout URLs:

http://localhost:3000

Allowed Web Origins:

http://localhost:3000

Allowed Callback URLs are a critical security measure to ensure users are safely returned to your application after authentication. Without a matching URL, the login process will fail, and users will be blocked by an Auth0 error page instead of accessing your app.

Allowed Logout URLs are essential for providing a seamless user experience upon signing out. Without a matching URL, users will not be redirected back to your application after logout and will instead be left on a generic Auth0 page.

Allowed Web Origins is critical for silent authentication. Without it, users will be logged out when they refresh the page or return to your app later.

  1. Create the Auth0 configuration

    Add the Auth0 client code to src/lib/auth0.ts:

    src/lib/auth0.ts

    import { Auth0Client } from '@auth0/nextjs-auth0/server';
    
    export const auth0 = new Auth0Client();
    
  2. Add Proxy
    Add the proxy code to src/proxy.ts:

    src/proxy.ts

    import { auth0 } from "./lib/auth0";
    
    export async function proxy(request: Request) {
      return await auth0.middleware(request);
    }
    
    export const config = {
      matcher: [\
        /*\
         * Match all request paths except for the ones starting with:\
         * - _next/static (static files)\
         * - _next/image (image optimization files)\
         * - favicon.ico, sitemap.xml, robots.txt (metadata files)\
         */\
        "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",\
      ],
    };
    

Since we’re using a src/ directory, the proxy.ts file is created inside src/. If you’re not using a src/ directory, create it in the project root instead.

This proxy automatically mounts the following authentication routes:

  1. Create Login, Logout and Profile Components

    Add the component code to the files created in Step 3:

    src/components/LoginButton.tsx

    "use client";
    
    export default function LoginButton() {
      return (
        <a
          href="/auth/login"
          className="w-full text-center inline-block px-6 py-3 bg-gradient-to-b from-[#2d2d42] to-[#161620] hover:opacity-90 text-white font-medium rounded-full text-[14px] transition-opacity"
        >
          Login
        </a>
      );
    }
    

    src/components/LogoutButton.tsx

    "use client";
    
    export default function LogoutButton() {
      return (
        <a
          href="/auth/logout"
          className="w-full text-center inline-block px-6 py-3 bg-[#f0f0f0] hover:bg-gray-200 text-gray-600 font-medium rounded-full text-[14px] transition-colors"
        >
          Logout
        </a>
      );
    }
    

    src/components/Profile.tsx

    "use client";
    
    import { useUser } from "@auth0/nextjs-auth0/client";
    
    function getInitials(name?: string | null, email?: string | null): string {
      if (name) {
        const parts = name.trim().split(" ");
        return parts.length >= 2
          ? `${parts[0][0]}${parts[1][0]}`.toUpperCase()
          : parts[0].slice(0, 2).toUpperCase();
      }
      if (email) return email.slice(0, 2).toUpperCase();
      return "U";
    }
    
    export default function Profile() {
      const { user, isLoading } = useUser();
    
      if (isLoading) return <p className="text-xs text-gray-500">Loading...</p>;
      if (!user) return null;
    
      return (
        <>
          <div className="flex items-center gap-2 text-green-500 text-[13px] font-medium">
            <span className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center shrink-0">
              <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
                <path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            </span>
            Successfully authenticated
          </div>
          <div className="flex items-center gap-2 bg-gray-100 rounded-full py-1.5 pl-1.5 pr-4 text-[12px] text-gray-700 max-w-full">
            <span className="w-7 h-7 bg-gradient-to-b from-[#2d2d42] to-[#161620] rounded-full flex items-center justify-center text-white text-[10px] font-semibold shrink-0">
              {getInitials(user.name, user.email)}
            </span>
            <span className="truncate">{user.email}</span>
          </div>
        </>
      );
    }
    
  2. Update your main page

    Replace src/app/page.tsx with:

    src/app/page.tsx

    import { auth0 } from "@/lib/auth0";
    import LoginButton from "@/components/LoginButton";
    import LogoutButton from "@/components/LogoutButton";
    import Profile from "@/components/Profile";
    
    export default async function Home() {
      const session = await auth0.getSession();
      const user = session?.user;
    
      return (
        <main className="min-h-screen bg-[#efefef] flex flex-col items-center justify-center gap-4 px-6 py-12">
          <div className="bg-white rounded-[28px] shadow-[0_4px_32px_rgba(0,0,0,0.08)] px-12 py-14 flex flex-col items-center gap-4 w-[360px]">
            <svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg" className="mb-1">
              <g filter="url(#filter0_di)">
                <rect x="2" y="2" width="64" height="64" rx="16" fill="url(#paint0_linear)"/>
                <rect x="2.5" y="2.5" width="63" height="63" rx="15.5" stroke="#252525"/>
                <path d="M34.0002 18C25.1572 18 18 25.1669 18 34C18 42.8432 25.1672 50 34.0002 50C42.8333 50 50 42.8331 50 34C50 25.1669 42.8433 18 34.0002 18ZM43.9172 43.8971C43.9172 43.9071 43.9069 43.9072 43.9069 43.9172C43.9069 43.9172 43.8969 43.9272 43.8868 43.9272C43.144 44.65 41.9796 44.7303 41.0662 44.2585L40.0228 43.7265C36.2487 41.7792 31.7619 41.7792 27.9777 43.7265L26.9338 44.2585C26.0103 44.7303 24.8459 44.65 24.1132 43.9272C24.1132 43.9272 24.1031 43.9172 24.0931 43.9172C24.0931 43.9172 24.0828 43.9071 24.0828 43.8971C23.3601 43.1543 23.2797 41.9899 23.7515 41.0765L24.2837 40.0326C26.231 36.2585 26.231 31.7717 24.2837 27.9975L23.7515 26.9536C23.2797 26.0302 23.3601 24.8657 24.0828 24.133C24.0828 24.1229 24.0931 24.123 24.0931 24.123C24.0931 24.123 24.1031 24.1129 24.1132 24.1129C24.856 23.3902 26.0204 23.3099 26.9338 23.7817L27.9777 24.3137C31.7518 26.261 36.2386 26.261 40.0228 24.3137L41.0662 23.7817C41.9897 23.3099 43.1541 23.3902 43.8868 24.1129C43.8868 24.1129 43.8969 24.123 43.9069 24.123L43.9172 24.133C44.6399 24.8758 44.7203 26.0402 44.2485 26.9536L43.7163 27.9975C41.769 31.7717 41.769 36.2585 43.7163 40.0326L44.2485 41.0765C44.7203 41.9899 44.6499 43.1543 43.9172 43.8971Z" fill="white"/>
              </g>
              <defs>
                <filter id="filter0_di" x="0" y="0" width="68" height="68" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
                  <feFlood floodOpacity="0" result="BackgroundImageFix"/>
                  <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
                  <feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect1_dropShadow"/>
                  <feOffset/>
                  <feComposite in2="hardAlpha" operator="out"/>
                  <feColorMatrix type="matrix" values="0 0 0 0 0.117647 0 0 0 0 0.129412 0 0 0 0 0.164706 0 0 0 0.12 0"/>
                  <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
                  <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
                  <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
                  <feOffset dy="-1"/>
                  <feGaussianBlur stdDeviation="0.5"/>
                  <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
                  <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
                  <feBlend mode="normal" in2="shape" result="effect2_innerShadow"/>
                </filter>
                <linearGradient id="paint0_linear" x1="34" y1="2" x2="34" y2="66" gradientUnits="userSpaceOnUse">
                  <stop/>
                  <stop offset="1" stopColor="#677190"/>
                </linearGradient>
              </defs>
            </svg>
    

{user ? ( <>

Your account

</> ) : ( <>

Welcome to Sample0

Get started by logging in to your account

</> )}

Powered by
); } ```
  1. Update layout with Auth0Provider

    Update src/app/layout.tsx to load the Inter font and wrap your app with Auth0Provider:

    src/app/layout.tsx

    import { Inter } from "next/font/google";
    import type { Metadata } from "next";
    import { Auth0Provider } from "@auth0/nextjs-auth0/client";
    import "./globals.css";
    
    const inter = Inter({
      subsets: ["latin"],
      weight: ["300", "400", "500", "600", "700"],
    });
    
    export const metadata: Metadata = {
      title: "Auth0 Next.js App",
      description: "Next.js app with Auth0 authentication",
    };
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <Auth0Provider>{children}</Auth0Provider>
          </body>
        </html>
      );
    }
    

In v4, the Auth0Provider is optional. You only need it if you want to pass an initial user during server rendering to be available to the useUser() hook.

  1. Configure Tailwind CSS

Replace the contents of src/app/globals.css with:

src/app/globals.css

@import "tailwindcss";
  1. Run your app
npm run dev

Your app will be available at http://localhost:3000. The Auth0 SDK v4 automatically mounts authentication routes at /auth/* (not /api/auth/* like in v3). If port 3000 is in use, run: npm run dev -- --port 3001 and update your Auth0 app’s callback URLs to http://localhost:3001

Checkpoint You should now have a fully functional Auth0 login page running on your localhost

Troubleshooting

JWEDecryptionFailed Error

If you see a JWEDecryptionFailed: decryption operation failed error, this is caused by either an invalid AUTH0_SECRET or an old session cookie encrypted with a different secret.

Solution:

  1. Generate a new secret using:

    openssl rand -hex 32
    
  2. Update your .env.local file:

    AUTH0_SECRET=<your-new-64-character-hex-string>
    
  3. Clear your browser cookies for localhost:3000:

    • Chrome/Edge: Press F12 → Application tab → Cookies → Delete all cookies for localhost
    • Firefox: Press F12 → Storage tab → Cookies → Delete all cookies for localhost
    • Safari: Develop menu → Show Web Inspector → Storage tab → Cookies → Delete all
  4. Restart your dev server:

    npm run dev
    

The secret must be exactly 32 bytes (64 hexadecimal characters). The error occurs when the app tries to decrypt an existing session cookie that was encrypted with a different secret.

404 Error on /auth/login

If clicking login takes you to a 404 page, check these common issues:

  1. Proxy location: Ensure src/proxy.ts exists in the correct location.
  2. Proxy code: Verify the proxy matches the code in Step 6.
  3. Restart server: After creating the proxy file, restart the dev server.
  4. Check imports: Make sure import { auth0 } from "./lib/auth0" path is correct.

Module Not Found Errors

If you see “Cannot find module '@/components/LoginButton'” or similar errors:

  1. Verify files exist: Check that all files from Step 3 were created.
  2. Check paths: Ensure components are in src/components/ directory.
  3. Restart TypeScript: Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows) and run “TypeScript: Restart TS Server”.
  4. Verify imports: Make sure you’re using @/components/* (not ~/components/*).

Advanced Usage

Important v4 Changes

This quickstart uses Auth0 Next.js SDK v4, which has significant changes from v3:

  • No dynamic route handlers needed - Authentication routes are auto-mounted by the proxy
  • Simplified client setup - new Auth0Client() reads environment variables automatically
  • New route paths - Routes are at /auth/* instead of /api/auth/*
  • Required proxy - All authentication functionality goes through proxy.ts
  • Use <a> tags - Navigation must use <a href="/auth/login"> instead of buttons with onClick

Authentication Routes

The SDK automatically mounts these routes via the proxy:

Route Purpose
/auth/login Initiate login
/auth/logout Logout user
/auth/callback Handle Auth0 callback
/auth/profile Get user profile
/auth/access-token Get access token
/auth/backchannel-logout Handle backchannel logout

If you’re experiencing 404 errors on these routes, ensure that:

  1. The proxy.ts file is in the correct location (project root, or inside src/ if using a src/ directory).
  2. The proxy is properly configured with the matcher pattern shown in Step 6.
  3. The development server was restarted after creating the proxy file.

Server-Side Authentication

The Auth0 Next.js SDK v4 supports both App Router and Pages Router patterns. Here are some common server-side patterns:

  • App Router - Server Component
  • App Router - API Route
  • Pages Router - Page

app/protected/page.tsx

import { auth0 } from "@/lib/auth0";
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
  const session = await auth0.getSession();

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

return (
    <div>
      <h1>Protected Content</h1>
      <p>Welcome, {session.user.name}!</p>
    </div>
  );
}

app/api/protected/route.ts

import { auth0 } from "@/lib/auth0";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await auth0.getSession();

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

return NextResponse.json({
    message: "This is a protected API route",
    user: session.user
  });
}

pages/protected.tsx

import { auth0 } from "@/lib/auth0";
import { GetServerSideProps } from "next";

export default function ProtectedPage({ user }: { user: any }) {
  return (
    <div>
      <h1>Protected Content</h1>
      <p>Welcome, {user.name}!</p>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = auth0.withPageAuthRequired();

Client-Side Authentication

For client-side authentication state, use the useUser hook:

components/UserProfile.tsx

"use client";

import { useUser } from "@auth0/nextjs-auth0/client";

export default function UserProfile() {
  const { user, error, isLoading } = useUser();

if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>Not logged in</div>;

return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <img src={user.picture} alt="Profile" referrerPolicy="no-referrer" />
    </div>
  );
}

Protecting API Routes

For API route protection, use the withApiAuthRequired method:

app/api/protected/route.ts

import { auth0 } from "@auth0/nextjs-auth0";

export const GET = auth0.withApiAuthRequired(async function handler() {
  const session = await auth0.getSession();

return Response.json({
    message: "This is a protected API route",
    user: session?.user
  });
});

Using Auth0 Tokens with Third-Party Backends

If you’re using a third-party backend service (like Convex, Supabase, or Firebase) that requires Auth0 authentication tokens, you’ll need to pass the access token from your Next.js app to the backend client.

Getting the Access Token

Server-side (App Router):

app/api/token/route.ts

import { auth0 } from "@/lib/auth0";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await auth0.getSession();

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

const accessToken = session.accessToken;
  return NextResponse.json({ accessToken });
}

Client-side:

lib/convex-client.ts

"use client";

import { ConvexProviderWithAuth0 } from "convex/react-auth0";
import { ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
  return (
    <ConvexProviderWithAuth0
      client={convex}
      useAuth0={() => ({
        // Fetch the access token from your Next.js API route
        getAccessToken: async () => {
          const response = await fetch("/api/token");
          const { accessToken } = await response.json();
          return accessToken;
        },
      })}
    >
      {children}
    </ConvexProviderWithAuth0>
  );
}
}

Configuring Your Backend

Most third-party services need your Auth0 domain and audience to verify tokens. In your backend configuration:

convex/auth.config.ts

export default {
  providers: [\
    {\
      domain: process.env.AUTH0_DOMAIN,\
      applicationID: process.env.AUTH0_CLIENT_ID,\
    },\
  ],
};

Ensure your Auth0 application is configured with an API audience if your backend requires it. You can set this in the Auth0 Dashboard under Applications → APIs, or add AUTH0_AUDIENCE to your .env.local and configure the SDK accordingly.

Troubleshooting Token Issues

If ctx.auth.getUserIdentity() returns null in your backend:

  1. Verify token is being passed: Check browser DevTools Network tab to confirm the token is included in requests.
  2. Check token format: Ensure you’re passing the accessToken, not the idToken.
  3. Verify backend configuration: Confirm your backend has the correct Auth0 domain and client ID.
  4. Check audience: If using an Auth0 API, ensure the AUTH0_AUDIENCE is set and matches your API identifier.
  5. Inspect token claims: Decode your JWT at jwt.io to verify it contains the expected claims.