Your Image Alt Text
Jazzed Technology Blog

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:

  1. Single Token Philosophy: Generate one token that works for both frontend and API authentication
  2. Audience-Specific Access: Ensure tokens are only valid for your specific application
  3. Stateless Authentication: No need to store session data on your server
  4. 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:

  1. Create an Auth0 Account at auth0.com if you don't have one

  2. Create a Single Page Application:

    • Go to Applications → Create Application
    • Select "Single Page Web Applications"
    • Name it "NextJS Sample App"
  3. 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
  4. Create an API:

    • Go to APIs → Create API
    • Name: "NextJS Sample API"
    • Identifier (audience): https://api.nextjs-sample.com
    • Signing Algorithm: RS256
    • Save
  5. Note your credentials:

    • Domain (e.g., dev-abc123.us.auth0.com)
    • Client ID (from your Application settings)
    • API Audience (https://api.nextjs-sample.com)

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

  1. Token Audience: Always specify the audience when requesting tokens to ensure they're valid only for your API
  2. JWKS Verification: Using JSON Web Key Sets allows token verification without storing secrets
  3. RS256 Algorithm: Auth0 uses RS256 (asymmetric) by default, which is more secure than HS256
  4. Token Storage: The Auth0 SDK handles token storage securely in memory and browser storage
  5. CSRF Protection: The Auth0 SDK includes built-in protections against CSRF attacks

Troubleshooting

Common Issues and Solutions

  1. "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
  2. Login redirects in a loop:

    • Verify your callback URL is correctly set in Auth0 and your application
    • Check for errors in browser console
  3. 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:

  1. Role-Based Access Control:

    • Configure roles and permissions in Auth0
    • Use scopes to limit API access
  2. Refresh Tokens:

    • Enable refresh token rotation in Auth0
    • Implement token refresh in your application
  3. 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.

Toggle Theme:

Our mission is to deliver high-quality web design, SEO, and IT support services in Vancouver, tailored to the unique needs of our clients. We aim to be your trusted partner, providing exceptional customer service that exceeds your expectations.

© 2023 Jazzed Technology | Vancouver Web Design, SEO & IT Support Company. All rights reserved.