Auth0 Authentication for Next.js Applications: A Complete Guide
By Jazz Grewal | Published June 3, 2025
In this guide, I'll walk you through setting up a robust authentication system for your Next.js application using Auth0. We'll implement the pattern where a single token securely authenticates both frontend and backend requests.
Authentication Architecture Overview
Our architecture follows these key principles:
- Single Token Philosophy: Generate one token that works for both frontend and API authentication
- Audience-Specific Access: Ensure tokens are only valid for your specific application
- Stateless Authentication: No need to store session data on your server
- Strong Security: Industry-standard JWT verification with RS256 asymmetric encryption
Step-by-Step Implementation Guide
Step 1: Set Up Auth0 Application and API
First, let's configure Auth0:
-
Create an Auth0 Account at auth0.com if you don't have one
-
Create a Single Page Application:
- Go to Applications → Create Application
- Select "Single Page Web Applications"
- Name it "NextJS Sample App"
-
Configure Application Settings:
- In Application Settings, add Allowed Callback URLs:
http://localhost:3000/callback, http://localhost:3000
- Add Allowed Logout URLs:
http://localhost:3000
- Add Allowed Web Origins:
http://localhost:3000
- Save changes
- In Application Settings, add Allowed Callback URLs:
-
Create an API:
- Go to APIs → Create API
- Name: "NextJS Sample API"
- Identifier (audience):
https://api.nextjs-sample.com
- Signing Algorithm: RS256
- Save
-
Note your credentials:
- Domain (e.g.,
dev-abc123.us.auth0.com
) - Client ID (from your Application settings)
- API Audience (
https://api.nextjs-sample.com
)
- Domain (e.g.,
Step 2: Set Up Your Next.js Project
# Create a new Next.js application
npx create-next-app@latest auth0-nextjs-sample
cd auth0-nextjs-sample
# Install Auth0 dependencies
npm install @auth0/auth0-react jose
Step 3: Configure Environment Variables
Create a .env.local
file in your project root:
# Auth0 Configuration
NEXT_PUBLIC_AUTH0_DOMAIN=dev-abc123.us.auth0.com
NEXT_PUBLIC_AUTH0_CLIENT_ID=your-client-id
NEXT_PUBLIC_AUTH0_REDIRECT_URI=http://localhost:3000/callback
NEXT_PUBLIC_AUTH0_AUDIENCE=https://api.nextjs-sample.com
# Backend Auth0 Configuration
AUTH0_DOMAIN=dev-abc123.us.auth0.com
AUTH0_AUDIENCE=https://api.nextjs-sample.com
Step 4: Create Auth0 Provider Component
Create a file at src/components/auth/auth-provider.tsx
:
'use client';
import { ReactNode } from 'react';
import { Auth0Provider } from '@auth0/auth0-react';
import { useRouter } from 'next/navigation';
interface Auth0ProviderProps {
children: ReactNode;
}
export function Auth0ProviderWithNavigation({ children }: Auth0ProviderProps) {
const router = useRouter();
const domain = process.env.NEXT_PUBLIC_AUTH0_DOMAIN || '';
const clientId = process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID || '';
const redirectUri = process.env.NEXT_PUBLIC_AUTH0_REDIRECT_URI || '';
const audience = process.env.NEXT_PUBLIC_AUTH0_AUDIENCE || '';
if (!(domain && clientId && redirectUri && audience)) {
console.error('Missing Auth0 configuration');
return <>{children}</>;
}
const onRedirectCallback = (appState: any) => {
router.push(appState?.returnTo || '/dashboard');
};
return (
<Auth0Provider
domain={domain}
clientId={clientId}
authorizationParams={{
redirect_uri: redirectUri,
audience: audience, // This is the critical part!
}}
onRedirectCallback={onRedirectCallback}
>
{children}
</Auth0Provider>
);
}
Step 5: Create a Custom Auth Hook
Create a file at src/hooks/useAuth.ts
:
'use client';
import { useAuth0 } from "@auth0/auth0-react";
import { useEffect, useState } from "react";
export function useAuth() {
const {
isAuthenticated,
isLoading,
loginWithRedirect,
logout,
user,
getAccessTokenSilently,
} = useAuth0();
const [token, setToken] = useState<string | null>(null);
const [tokenLoading, setTokenLoading] = useState(false);
// Fetch token with audience when authenticated
useEffect(() => {
const getToken = async () => {
if (isAuthenticated) {
try {
setTokenLoading(true);
const accessToken = await getAccessTokenSilently({
authorizationParams: {
audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE,
},
});
setToken(accessToken);
} catch (error) {
console.error("Error getting access token", error);
} finally {
setTokenLoading(false);
}
}
};
getToken();
}, [isAuthenticated, getAccessTokenSilently]);
const login = () => {
loginWithRedirect({
appState: {
returnTo: window.location.pathname,
},
});
};
const logoutUser = () => {
logout({
logoutParams: {
returnTo: window.location.origin,
},
});
};
return {
isAuthenticated,
isLoading: isLoading || tokenLoading,
login,
logout: logoutUser,
user,
token,
};
}
Step 6: Create a Protected Route Component
Create a file at src/components/auth/protected-route.tsx
:
'use client';
import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { ReactNode } from 'react';
interface ProtectedRouteProps {
children: ReactNode;
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated, isLoading, login } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
login();
}
}, [isLoading, isAuthenticated, login]);
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
);
}
return isAuthenticated ? <>{children}</> : null;
}
Step 7: Set Up the Root Layout
Update your src/app/layout.tsx
:
import './globals.css';
import { Auth0ProviderWithNavigation } from '@/components/auth/auth-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Auth0ProviderWithNavigation>
{children}
</Auth0ProviderWithNavigation>
</body>
</html>
);
}
Step 8: Create a Protected Page
Create a file at src/app/dashboard/page.tsx
:
'use client';
import { ProtectedRoute } from '@/components/auth/protected-route';
import { useAuth } from '@/hooks/useAuth';
export default function Dashboard() {
const { user, logout } = useAuth();
return (
<ProtectedRoute>
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
<div className="bg-white shadow rounded p-4 mb-4">
<h2 className="text-xl mb-2">User Profile</h2>
<pre className="bg-gray-100 p-2 rounded">
{JSON.stringify(user, null, 2)}
</pre>
</div>
<button
onClick={() => logout()}
className="bg-red-500 text-white px-4 py-2 rounded"
>
Log Out
</button>
</div>
</ProtectedRoute>
);
}
Step 9: Create Backend Authentication Middleware
Create a file at src/lib/auth/middleware.ts
:
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
// Create a JWKS client using Auth0's JWKS endpoint
const domain = process.env.AUTH0_DOMAIN || "";
const JWKS = createRemoteJWKSet(
new URL(`https://${domain}/.well-known/jwks.json`)
);
export async function verifyAuth(req: NextRequest) {
const authHeader = req.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return {
isAuthenticated: false,
error: "Missing or invalid authorization header"
};
}
// Extract the token
const token = authHeader.substring(7);
try {
// Verify the token using Auth0's JWKS
const { payload } = await jwtVerify(token, JWKS, {
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://${domain}/`,
});
return {
isAuthenticated: true,
user: payload.sub,
permissions: payload.permissions || [],
};
} catch (error) {
console.error("Token verification failed:", error);
return {
isAuthenticated: false,
error: "Invalid token"
};
}
}
Step 10: Create a Protected API Route
Create a file at src/app/api/user/route.ts
:
import { verifyAuth } from '@/lib/auth/middleware';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const auth = await verifyAuth(req);
if (!auth.isAuthenticated) {
return NextResponse.json({ error: auth.error }, { status: 401 });
}
// Here you can fetch user data from your database
// using the auth.user identifier
return NextResponse.json({
message: "You are authenticated!",
userId: auth.user,
permissions: auth.permissions,
});
}
Step 11: Create an API Service
Create a file at src/lib/api/api-service.ts
:
import { useAuth } from '@/hooks/useAuth';
export function useApiService() {
const { token } = useAuth();
const fetchApi = async (endpoint: string, options: RequestInit = {}) => {
if (!token) {
throw new Error("No token available");
}
const headers = {
...options.headers,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
};
const response = await fetch(`/api/${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return response.json();
};
return { fetchApi };
}
Step 12: Create a Component that Uses the API
Create a file at src/components/user-data.tsx
:
'use client';
import { useApiService } from '@/lib/api/api-service';
import { useState, useEffect } from 'react';
export function UserData() {
const { fetchApi } = useApiService();
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const getUserData = async () => {
try {
setLoading(true);
const data = await fetchApi('user');
setUserData(data);
setError(null);
} catch (err) {
setError('Failed to fetch user data');
console.error(err);
} finally {
setLoading(false);
}
};
getUserData();
}, [fetchApi]);
if (loading) return <div>Loading user data...</div>;
if (error) return <div className="text-red-500">{error}</div>;
return (
<div className="mt-4 p-4 border rounded">
<h2 className="text-lg font-semibold mb-2">API Response</h2>
<pre className="bg-gray-100 p-2 rounded text-sm overflow-auto">
{JSON.stringify(userData, null, 2)}
</pre>
</div>
);
}
Update your dashboard page to include this component:
// In src/app/dashboard/page.tsx
import { UserData } from '@/components/user-data';
// Inside your component's return statement:
<div className="mt-4">
<h2 className="text-xl mb-2">API Data</h2>
<UserData />
</div>
Step 13: Run Your Application
npm run dev
Visit http://localhost:3000/dashboard to see the protected page in action!
Key Security Considerations
- Token Audience: Always specify the audience when requesting tokens to ensure they're valid only for your API
- JWKS Verification: Using JSON Web Key Sets allows token verification without storing secrets
- RS256 Algorithm: Auth0 uses RS256 (asymmetric) by default, which is more secure than HS256
- Token Storage: The Auth0 SDK handles token storage securely in memory and browser storage
- CSRF Protection: The Auth0 SDK includes built-in protections against CSRF attacks
Troubleshooting
Common Issues and Solutions
-
"Invalid audience" error:
- Ensure the audience in your Auth0Provider matches the API identifier in Auth0
- Check that you're requesting tokens with the audience parameter
-
Login redirects in a loop:
- Verify your callback URL is correctly set in Auth0 and your application
- Check for errors in browser console
-
API returns 401 Unauthorized:
- Confirm your token includes the correct audience
- Check that API verification uses the same audience value
- Ensure your Auth0 domain is correct in both frontend and backend
Advanced Features
Once you have the basic setup working, you can add these advanced features:
-
Role-Based Access Control:
- Configure roles and permissions in Auth0
- Use scopes to limit API access
-
Refresh Tokens:
- Enable refresh token rotation in Auth0
- Implement token refresh in your application
-
Multi-Factor Authentication:
- Enable MFA in Auth0
- Configure MFA policies
This authentication architecture provides a robust, secure foundation for your Next.js applications. By using audience-specific tokens verified with JWKS, you create a seamless authentication experience while maintaining strong security standards. The implementation follows best practices from production applications and can scale as your application grows.
For a complete working example, constact us at Jazzed Technology.