Skip to main content

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-quickstart --skill auth0-fastify
Then ask your AI assistant:
Add Auth0 authentication to my Fastify app
Your AI assistant will automatically create your Auth0 application, fetch credentials, install @auth0/auth0-fastify, configure the plugin, and create all necessary routes and views. Full agent skills documentation →
Prerequisites: Before you begin, ensure you have the following installed:Verify installation: node --version && npm --versionFastify Version Compatibility: This quickstart works with Fastify 5.x and newer.

Get Started

This quickstart demonstrates how to add Auth0 authentication to a Fastify application. You’ll build a secure web app with login, logout, and user profile features using the Auth0 Fastify SDK.
1

Create a new project

Create a new directory for your Fastify application and initialize a Node.js project.
mkdir auth0-fastify && cd auth0-fastify
Initialize the project
npm init -y
Create the project structure
touch server.js .env
2

Install the Auth0 Fastify SDK

Install the required dependencies
npm install @auth0/auth0-fastify fastify dotenv @fastify/view ejs
We’re using @fastify/view with ejs for server-side rendering. You can use any template engine supported by Fastify.
Update your package.json to add start scripts:
package.json
{
  "name": "auth0-fastify",
  "version": "1.0.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "@auth0/auth0-fastify": "^1.2.0",
    "@fastify/view": "^10.0.0",
    "dotenv": "^16.3.1",
    "ejs": "^3.1.9",
    "fastify": "^5.0.0"
  }
}
3

Setup your Auth0 App

Next, you need to create a new application 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:
Verify your .env file exists: cat .env (Mac/Linux) or type .env (Windows)
4

Configure the Auth0 plugin

Create your Fastify server and register the Auth0 plugin:
server.js
import 'dotenv/config';
import Fastify from 'fastify';
import fastifyView from '@fastify/view';
import fastifyAuth0 from '@auth0/auth0-fastify';
import ejs from 'ejs';

const fastify = Fastify({ logger: true });
const port = process.env.PORT || 3000;

// Register view engine
await fastify.register(fastifyView, {
  engine: { ejs },
  root: './views',
});

// Register Auth0 plugin
await fastify.register(fastifyAuth0, {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  appBaseUrl: process.env.APP_BASE_URL,
  sessionSecret: process.env.SESSION_SECRET,
});

// Start server
fastify.listen({ port }, (err) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
  fastify.log.info(`Server running at http://localhost:${port}`);
});
What this does:
  • Registers the view engine for rendering HTML templates
  • Configures the Auth0 plugin with your credentials
  • Automatically creates routes at /auth/login, /auth/logout, and /auth/callback
  • Handles session management with encrypted cookies
5

Create view templates

Create a views directory and add template files:
Mac/Linux
mkdir views && touch views/home.ejs views/profile.ejs
Windows
New-Item -ItemType Directory -Path views
New-Item -ItemType File -Path views/home.ejs
New-Item -ItemType File -Path views/profile.ejs
Create the home page template:
views/home.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Auth0 Fastify Quickstart</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      margin: 0;
      padding: 2rem;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .container {
      background: white;
      border-radius: 20px;
      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
      padding: 3rem;
      max-width: 500px;
      width: 100%;
      text-align: center;
    }
    h1 {
      color: #2d3748;
      font-size: 2.5rem;
      margin-bottom: 1rem;
    }
    .status {
      padding: 1rem;
      border-radius: 10px;
      margin: 1.5rem 0;
      font-size: 1.1rem;
    }
    .logged-in {
      background: #d4edda;
      color: #155724;
    }
    .logged-out {
      background: #f8d7da;
      color: #721c24;
    }
    .button {
      display: inline-block;
      padding: 1rem 2rem;
      margin: 0.5rem;
      border-radius: 10px;
      text-decoration: none;
      font-weight: 600;
      transition: all 0.3s;
    }
    .button-primary {
      background: #667eea;
      color: white;
    }
    .button-primary:hover {
      background: #5568d3;
      transform: translateY(-2px);
    }
    .button-secondary {
      background: #e53e3e;
      color: white;
    }
    .button-secondary:hover {
      background: #c53030;
      transform: translateY(-2px);
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>🚀 Auth0 Fastify</h1>
    <div class="status <%= isAuthenticated ? 'logged-in' : 'logged-out' %>">
      <%= isAuthenticated ? '✓ You are logged in' : '✗ You are logged out' %>
    </div>
    <div>
      <% if (isAuthenticated) { %>
        <a href="/profile" class="button button-primary">View Profile</a>
        <a href="/auth/logout" class="button button-secondary">Logout</a>
      <% } else { %>
        <a href="/auth/login" class="button button-primary">Login</a>
      <% } %>
    </div>
  </div>
</body>
</html>
Create the profile page template:
views/profile.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Profile - Auth0 Fastify</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      margin: 0;
      padding: 2rem;
      min-height: 100vh;
    }
    .container {
      background: white;
      border-radius: 20px;
      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
      padding: 3rem;
      max-width: 700px;
      margin: 0 auto;
    }
    h1 {
      color: #2d3748;
      margin-bottom: 2rem;
    }
    .profile-card {
      display: flex;
      align-items: center;
      gap: 2rem;
      padding: 2rem;
      background: #f7fafc;
      border-radius: 15px;
      margin-bottom: 2rem;
    }
    .profile-picture {
      width: 100px;
      height: 100px;
      border-radius: 50%;
      object-fit: cover;
      border: 3px solid #667eea;
    }
    .profile-info h2 {
      margin: 0 0 0.5rem 0;
      color: #2d3748;
    }
    .profile-info p {
      margin: 0;
      color: #718096;
    }
    .user-data {
      background: #f7fafc;
      padding: 1.5rem;
      border-radius: 10px;
      overflow-x: auto;
    }
    pre {
      margin: 0;
      white-space: pre-wrap;
      word-wrap: break-word;
    }
    .button {
      display: inline-block;
      padding: 0.75rem 1.5rem;
      margin-right: 1rem;
      border-radius: 10px;
      text-decoration: none;
      font-weight: 600;
      transition: all 0.3s;
    }
    .button-primary {
      background: #667eea;
      color: white;
    }
    .button-primary:hover {
      background: #5568d3;
    }
    .button-secondary {
      background: #e53e3e;
      color: white;
    }
    .button-secondary:hover {
      background: #c53030;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>User Profile</h1>
    <div class="profile-card">
      <img src="<%= user.picture || 'https://via.placeholder.com/100' %>" alt="Profile" class="profile-picture">
      <div class="profile-info">
        <h2><%= user.name || user.nickname || 'User' %></h2>
        <p><strong>Email:</strong> <%= user.email || 'N/A' %></p>
      </div>
    </div>
    <h3>Full User Object</h3>
    <div class="user-data">
      <pre><%= JSON.stringify(user, null, 2) %></pre>
    </div>
    <div style="margin-top: 2rem;">
      <a href="/" class="button button-primary">← Back to Home</a>
      <a href="/auth/logout" class="button button-secondary">Logout</a>
    </div>
  </div>
</body>
</html>
6

Create routes

Add routes to your server.js file:
server.js
import 'dotenv/config';
import Fastify from 'fastify';
import fastifyView from '@fastify/view';
import fastifyAuth0 from '@auth0/auth0-fastify';
import ejs from 'ejs';

const fastify = Fastify({ logger: true });
const port = process.env.PORT || 3000;

// Register view engine
await fastify.register(fastifyView, {
  engine: { ejs },
  root: './views',
});

// Register Auth0 plugin
await fastify.register(fastifyAuth0, {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  appBaseUrl: process.env.APP_BASE_URL,
  sessionSecret: process.env.SESSION_SECRET,
});

// Home route - public
fastify.get('/', async (request, reply) => {
  const session = await fastify.auth0Client.getSession({ request, reply });
  return reply.view('views/home.ejs', {
    isAuthenticated: !!session,
  });
});

// Profile route - protected
fastify.get('/profile', {
  preHandler: async (request, reply) => {
    const session = await fastify.auth0Client.getSession({ request, reply });
    if (!session) {
      return reply.redirect('/auth/login');
    }
  }
}, async (request, reply) => {
  const user = await fastify.auth0Client.getUser({ request, reply });
  return reply.view('views/profile.ejs', { user });
});

// Start server
fastify.listen({ port }, (err) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
  fastify.log.info(`Server running at http://localhost:${port}`);
});
Key points:
  • The home route checks authentication status and passes it to the template
  • The profile route uses a preHandler to protect the route
  • getSession() returns the user’s session or null if not authenticated
  • getUser() returns the authenticated user’s profile information
7

Run your app

Start the development server:
npm run dev
Open your browser to http://localhost:3000.
The --watch flag in Node.js 20+ automatically restarts the server when files change.
CheckpointYou should now have a fully functional Auth0 login page. When you:
  1. Click “Login” - you’re redirected to Auth0’s Universal Login page
  2. Complete authentication - you’re redirected back to your app
  3. Visit “/profile” - you see your user information
  4. Click “Logout” - you’re logged out of both your app and Auth0

Advanced Usage

To call external APIs that require an access token, configure the SDK with an audience:
server.js
await fastify.register(fastifyAuth0, {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  appBaseUrl: process.env.APP_BASE_URL,
  sessionSecret: process.env.SESSION_SECRET,
  audience: process.env.AUTH0_AUDIENCE, // Add this
});
Add to your .env file:
.env
AUTH0_AUDIENCE=https://your-api.example.com
Then retrieve and use the access token:
server.js
fastify.get('/api-data', {
  preHandler: async (request, reply) => {
    const session = await fastify.auth0Client.getSession({ request, reply });
    if (!session) {
      return reply.redirect('/auth/login');
    }
  }
}, async (request, reply) => {
  try {
    const { accessToken } = await fastify.auth0Client.getAccessToken({ request, reply });

    // Call your protected API
    const response = await fetch('https://your-api.example.com/data', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    const data = await response.json();
    return data;
  } catch (error) {
    fastify.log.error('API call failed:', error);
    return reply.status(500).send({ error: 'Failed to fetch data' });
  }
});
By default, Auth0 routes are mounted at /auth/*. You can disable auto-mounting and create custom routes:
server.js
await fastify.register(fastifyAuth0, {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  appBaseUrl: process.env.APP_BASE_URL,
  sessionSecret: process.env.SESSION_SECRET,
  mountRoutes: false, // Disable auto-mounting
});

// Custom login route
fastify.get('/custom-login', async (request, reply) => {
  const authorizationUrl = await fastify.auth0Client.startInteractiveLogin(
    {
      authorizationParams: {
        redirect_uri: `${process.env.APP_BASE_URL}/custom-callback`
      }
    },
    { request, reply }
  );
  return reply.redirect(authorizationUrl.href);
});

// Custom callback route
fastify.get('/custom-callback', async (request, reply) => {
  await fastify.auth0Client.completeInteractiveLogin(
    new URL(request.url, process.env.APP_BASE_URL),
    { request, reply }
  );
  return reply.redirect('/');
});

// Custom logout route
fastify.get('/custom-logout', async (request, reply) => {
  const logoutUrl = await fastify.auth0Client.logout(
    { returnTo: process.env.APP_BASE_URL },
    { request, reply }
  );
  return reply.redirect(logoutUrl.href);
});
Remember to update your Allowed Callback URLs in the Auth0 Dashboard to include your custom callback URL.
Enable users to link multiple authentication providers to a single account:
server.js
await fastify.register(fastifyAuth0, {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  appBaseUrl: process.env.APP_BASE_URL,
  sessionSecret: process.env.SESSION_SECRET,
  mountConnectRoutes: true, // Enable account linking routes
});
This automatically creates the following routes:
  • /auth/connect - Link a new provider
  • /auth/connect/callback - Handle the linking callback
  • /auth/unconnect - Unlink a provider
  • /auth/unconnect/callback - Handle the unlinking callback
Add linking buttons to your profile page:
views/profile.ejs
<div>
  <a href="/auth/connect?connection=google-oauth2">Link Google Account</a>
  <a href="/auth/unconnect?connection=google-oauth2">Unlink Google Account</a>
</div>
Convert your project to TypeScript for better type safety:
npm install --save-dev typescript @types/node tsx
Create a tsconfig.json:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
Rename server.js to server.ts and add types:
server.ts
import 'dotenv/config';
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import fastifyView from '@fastify/view';
import fastifyAuth0 from '@auth0/auth0-fastify';
import ejs from 'ejs';

const fastify = Fastify({ logger: true });
const port = process.env.PORT || 3000;

await fastify.register(fastifyView, {
  engine: { ejs },
  root: './views',
});

await fastify.register(fastifyAuth0, {
  domain: process.env.AUTH0_DOMAIN!,
  clientId: process.env.AUTH0_CLIENT_ID!,
  clientSecret: process.env.AUTH0_CLIENT_SECRET!,
  appBaseUrl: process.env.APP_BASE_URL!,
  sessionSecret: process.env.SESSION_SECRET!,
});

fastify.get('/', async (request: FastifyRequest, reply: FastifyReply) => {
  const session = await fastify.auth0Client.getSession({ request, reply });
  return reply.view('views/home.ejs', {
    isAuthenticated: !!session,
  });
});

fastify.listen({ port: Number(port) });
Update package.json:
package.json
{
  "scripts": {
    "dev": "tsx watch server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

Troubleshooting

”Invalid state” error after login

Problem: State mismatch between the authentication request and callback.Solutions:
  1. Ensure cookies are being set correctly (not blocked by browser)
  2. Verify callback URL matches exactly in Auth0 Dashboard (including /auth/callback)
  3. Check that SESSION_SECRET is set and at least 64 characters long

”session is undefined” error

Problem: Unable to retrieve session data.Solution: Ensure the Auth0 plugin is registered before accessing session methods:
// ✅ Correct order
await fastify.register(fastifyAuth0, { ... });
fastify.get('/profile', async (request, reply) => {
  const session = await fastify.auth0Client.getSession({ request, reply });
});

// ❌ Wrong - plugin not awaited
fastify.register(fastifyAuth0, { ... }); // Missing await
fastify.get('/profile', async (request, reply) => { ... });

Callback URL mismatch

Problem: “Callback URL mismatch” error from Auth0.Solution:
  1. Go to your Auth0 Dashboard → Applications → Your App → Settings
  2. Add http://localhost:3000/auth/callback to Allowed Callback URLs
  3. The URL must match exactly (including the /auth/callback path)

Environment variables not loading

Problem: Configuration values are undefined.Solution:
  1. Ensure import 'dotenv/config' is at the top of your entry file
  2. Verify .env file is in the root directory
  3. Check for typos in variable names
// Debug: Log config values (remove in production!)
console.log('Config check:', {
  hasDomain: !!process.env.AUTH0_DOMAIN,
  hasClientID: !!process.env.AUTH0_CLIENT_ID,
  hasSecret: !!process.env.SESSION_SECRET,
});

Next Steps

Now that you have authentication working, consider exploring:

Resources