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.
Create a new project
Create a new Next.js project for this Quickstartnpx create-next-app@latest auth0-nextjs --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --yesOpen the project
cd auth0-nextjsInstall the Auth0 Next.js SDK
npm install @auth0/nextjs-auth0Create 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.tsxWindows
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.tsxSetup 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:
- Check if you’re authenticated (and prompt for login if needed)
- Create an Auth0 Regular Web Application configured for
http://localhost:3000 - Generate a
.envfile withAUTH0_DOMAIN,AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRET,AUTH0_SECRET, andAPP_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:
- Head to the Auth0 Dashboard
- Click on Applications > Applications > Create Application
- In the popup, enter a name for your app, select
Regular Web Applicationas the app type and click Create - Switch to the Settings tab on the Application Details page
- Replace
YOUR_AUTH0_APP_DOMAIN,YOUR_AUTH0_APP_CLIENT_ID, andYOUR_AUTH0_APP_CLIENT_SECRETin the.env.localfile 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.
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();Add Proxy
Add the proxy code tosrc/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:
/auth/login- Login route/auth/logout- Logout route/auth/callback- Callback route/auth/profile- User profile route/auth/access-token- Access token route/auth/backchannel-logout- Backchannel logout route
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> </> ); }Update your main page
Replace
src/app/page.tsxwith: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
Update layout with Auth0Provider
Update
src/app/layout.tsxto load the Inter font and wrap your app withAuth0Provider: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.
- Configure Tailwind CSS
Replace the contents of src/app/globals.css with:
src/app/globals.css
@import "tailwindcss";
- 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:
Generate a new secret using:
openssl rand -hex 32Update your
.env.localfile:AUTH0_SECRET=<your-new-64-character-hex-string>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
- Chrome/Edge: Press
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:
- Proxy location: Ensure
src/proxy.tsexists in the correct location. - Proxy code: Verify the proxy matches the code in Step 6.
- Restart server: After creating the proxy file, restart the dev server.
- 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:
- Verify files exist: Check that all files from Step 3 were created.
- Check paths: Ensure components are in
src/components/directory. - Restart TypeScript: Press
Cmd+Shift+P(Mac) orCtrl+Shift+P(Windows) and run “TypeScript: Restart TS Server”. - 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:
- The
proxy.tsfile is in the correct location (project root, or insidesrc/if using asrc/directory). - The proxy is properly configured with the matcher pattern shown in Step 6.
- 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:
- Verify token is being passed: Check browser DevTools Network tab to confirm the token is included in requests.
- Check token format: Ensure you’re passing the
accessToken, not theidToken. - Verify backend configuration: Confirm your backend has the correct Auth0 domain and client ID.
- Check audience: If using an Auth0 API, ensure the
AUTH0_AUDIENCEis set and matches your API identifier. - Inspect token claims: Decode your JWT at jwt.io to verify it contains the expected claims.