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 API authentication automatically in minutes using agent skills.Install:
npx skills add auth0/agent-skills --skill auth0-quickstart --skill auth0-fastify-api
Then ask your AI assistant:
Add Auth0 JWT authentication to my Fastify API
Your AI assistant will automatically create your Auth0 API, fetch credentials, install @auth0/auth0-fastify-api, configure the plugin, and protect your API endpoints with JWT validation. 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 protect Fastify API endpoints using JWT access tokens. You’ll build a secure API that validates Auth0 access tokens and grants access to protected resources.
1

Create a new project

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

Install the Auth0 Fastify API SDK

Install the required dependencies
npm install @auth0/auth0-fastify-api fastify dotenv
Update your package.json to add start scripts:
package.json
{
  "name": "auth0-fastify-api",
  "version": "1.0.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "@auth0/auth0-fastify-api": "^1.2.0",
    "dotenv": "^16.3.1",
    "fastify": "^5.0.0"
  }
}
3

Setup your Auth0 API

Next, you need to create a new API on your Auth0 tenant and add the environment variables to your project.You have two options to set up your Auth0 API: use a CLI command or configure manually via the Dashboard:
Run the following command in your project’s root directory to create an Auth0 API:
# Install Auth0 CLI (if not already installed)
brew tap auth0/auth0-cli && brew install auth0

# Create Auth0 API
auth0 apis create \
  --name "My Fastify API" \
  --identifier https://my-fastify-api.example.com
After creation, copy the Identifier and your Domain values, then create your .env file:
.env
AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
AUTH0_AUDIENCE=YOUR_API_IDENTIFIER
This command will:
  1. Check if you’re authenticated (and prompt for login if needed)
  2. Create an Auth0 API with the specified identifier
  3. Display the API details including the domain and identifier
Verify your .env file exists: cat .env (Mac/Linux) or type .env (Windows)
4

Configure the Auth0 API plugin

Create your Fastify server and register the Auth0 API plugin:
server.js
import 'dotenv/config';
import Fastify from 'fastify';
import fastifyAuth0Api from '@auth0/auth0-fastify-api';

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

// Register Auth0 API plugin
await fastify.register(fastifyAuth0Api, {
  domain: process.env.AUTH0_DOMAIN,
  audience: process.env.AUTH0_AUDIENCE,
});

// Start server
fastify.listen({ port }, (err) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
  fastify.log.info(`API server running at http://localhost:${port}`);
});
What this does:
  • Registers the Auth0 API plugin with your Auth0 domain and API audience
  • Configures JWT validation for incoming requests
  • Makes the requireAuth() preHandler available for protecting routes
5

Create API routes

Add public and protected routes to your server.js:
server.js
import 'dotenv/config';
import Fastify from 'fastify';
import fastifyAuth0Api from '@auth0/auth0-fastify-api';

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

// Register Auth0 API plugin
await fastify.register(fastifyAuth0Api, {
  domain: process.env.AUTH0_DOMAIN,
  audience: process.env.AUTH0_AUDIENCE,
});

// Public route - no authentication required
fastify.get('/api/public', async (request, reply) => {
  return {
    message: 'Hello from a public endpoint! You don\'t need to be authenticated to see this.',
    timestamp: new Date().toISOString(),
  };
});

// Protected route - requires valid access token
fastify.get('/api/private', {
  preHandler: fastify.requireAuth()
}, async (request, reply) => {
  return {
    message: 'Hello from a protected endpoint! You successfully authenticated.',
    user: request.user.sub,
    timestamp: new Date().toISOString(),
  };
});

// Protected route - returns user information from token
fastify.get('/api/profile', {
  preHandler: fastify.requireAuth()
}, async (request, reply) => {
  return {
    message: 'Your user profile from the access token',
    profile: request.user,
  };
});

// Start server
fastify.listen({ port }, (err) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
  fastify.log.info(`API server running at http://localhost:${port}`);
});
Key points:
  • Public routes don’t require authentication
  • Protected routes use preHandler: fastify.requireAuth() to require a valid JWT
  • request.user contains the decoded JWT claims for authenticated requests
  • The sub claim contains the user’s unique identifier
6

Run your API

Start the development server:
npm run dev
Your API is now running at http://localhost:3001.
The --watch flag in Node.js 20+ automatically restarts the server when files change.
7

Test your API

Test the public endpoint (no authentication required):
curl http://localhost:3001/api/public
You should see:
{
  "message": "Hello from a public endpoint! You don't need to be authenticated to see this.",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
Test the protected endpoint without a token (should fail):
curl http://localhost:3001/api/private
You should see a 401 Unauthorized error:
{
  "error": "Unauthorized",
  "message": "No authorization token was found"
}
To test with a valid token, you need to:
  1. Create a client application (web or mobile app) that authenticates users
  2. Configure the client to request an access token for your API (using the audience parameter)
  3. Use that access token in the Authorization header
Example with a token:
curl http://localhost:3001/api/private \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
CheckpointYou should now have a protected API. Your API:
  1. Accepts requests to public endpoints without authentication
  2. Rejects requests to protected endpoints without a valid token
  3. Validates JWT tokens against your Auth0 domain and audience
  4. Provides user information from the token claims via request.user

Advanced Usage

Extend the Token interface to add type safety for custom claims in your access tokens:
server.ts
import '@auth0/auth0-fastify-api';

// Extend the Token interface with your custom claims
declare module '@auth0/auth0-fastify-api' {
  interface Token {
    sub: string;
    permissions?: string[];
    'https://myapp.com/roles'?: string[];
    email?: string;
    email_verified?: boolean;
  }
}
Now TypeScript will recognize your custom claims:
server.ts
fastify.get('/api/profile', {
  preHandler: fastify.requireAuth()
}, async (request, reply) => {
  // TypeScript knows about these properties
  const userRoles = request.user['https://myapp.com/roles']; // string[] | undefined
  const permissions = request.user.permissions; // string[] | undefined
  const email = request.user.email; // string | undefined

  return {
    userId: request.user.sub,
    roles: userRoles || [],
    permissions: permissions || [],
    email: email,
  };
});
Custom claims must use namespaced URLs (e.g., https://myapp.com/roles) unless they’re standard OIDC claims. Learn more about custom claims.
Check for specific permissions in the access token:
server.js
// Middleware to check for specific permission
function requirePermission(permission) {
  return async (request, reply) => {
    const permissions = request.user.permissions || [];

    if (!permissions.includes(permission)) {
      return reply.status(403).send({
        error: 'Forbidden',
        message: `Missing required permission: ${permission}`
      });
    }
  };
}

// Route requiring 'read:messages' permission
fastify.get('/api/messages', {
  preHandler: [
    fastify.requireAuth(),
    requirePermission('read:messages')
  ]
}, async (request, reply) => {
  return {
    messages: ['Message 1', 'Message 2', 'Message 3']
  };
});

// Route requiring 'write:messages' permission
fastify.post('/api/messages', {
  preHandler: [
    fastify.requireAuth(),
    requirePermission('write:messages')
  ]
}, async (request, reply) => {
  return {
    message: 'Message created successfully',
    id: 'msg_123'
  };
});
Permissions must be configured in your Auth0 API settings and granted to clients. Learn more about API permissions.
Implement role-based access control using custom claims:
server.js
// Middleware to check for specific role
function requireRole(role) {
  return async (request, reply) => {
    const roles = request.user['https://myapp.com/roles'] || [];

    if (!roles.includes(role)) {
      return reply.status(403).send({
        error: 'Forbidden',
        message: `Missing required role: ${role}`
      });
    }
  };
}

// Admin-only route
fastify.get('/api/admin/users', {
  preHandler: [
    fastify.requireAuth(),
    requireRole('admin')
  ]
}, async (request, reply) => {
  return {
    users: [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' }
    ]
  };
});

// Manager or admin route
function requireAnyRole(...roles) {
  return async (request, reply) => {
    const userRoles = request.user['https://myapp.com/roles'] || [];
    const hasRole = roles.some(role => userRoles.includes(role));

    if (!hasRole) {
      return reply.status(403).send({
        error: 'Forbidden',
        message: `Missing required role. Need one of: ${roles.join(', ')}`
      });
    }
  };
}

fastify.get('/api/reports', {
  preHandler: [
    fastify.requireAuth(),
    requireAnyRole('admin', 'manager')
  ]
}, async (request, reply) => {
  return { reports: [] };
});
Roles must be added to tokens using Auth0 Actions. Learn how to add roles to tokens.
Enable CORS to allow requests from web applications:
npm install @fastify/cors
server.js
import cors from '@fastify/cors';

await fastify.register(cors, {
  origin: ['http://localhost:3000', 'http://localhost:5173'], // Your web app URLs
  credentials: true,
});
For production, specify exact origins:
server.js
await fastify.register(cors, {
  origin: [
    'https://myapp.com',
    'https://www.myapp.com'
  ],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
});
Add comprehensive error handling for authentication errors:
server.js
// Custom error handler
fastify.setErrorHandler((error, request, reply) => {
  fastify.log.error(error);

  // Handle JWT validation errors
  if (error.statusCode === 401) {
    return reply.status(401).send({
      error: 'Unauthorized',
      message: error.message || 'Invalid or missing access token',
      code: 'UNAUTHORIZED'
    });
  }

  // Handle permission/role errors
  if (error.statusCode === 403) {
    return reply.status(403).send({
      error: 'Forbidden',
      message: error.message || 'Insufficient permissions',
      code: 'FORBIDDEN'
    });
  }

  // Handle other errors
  return reply.status(error.statusCode || 500).send({
    error: 'Internal Server Error',
    message: 'An unexpected error occurred',
    code: 'INTERNAL_ERROR'
  });
});

// Not found handler
fastify.setNotFoundHandler((request, reply) => {
  return reply.status(404).send({
    error: 'Not Found',
    message: `Route ${request.method} ${request.url} not found`,
    code: 'NOT_FOUND'
  });
});
Protect your API from abuse with rate limiting:
npm install @fastify/rate-limit
server.js
import rateLimit from '@fastify/rate-limit';

await fastify.register(rateLimit, {
  max: 100, // Maximum requests
  timeWindow: '1 minute', // Time window
  errorResponseBuilder: (request, context) => {
    return {
      error: 'Too Many Requests',
      message: `Rate limit exceeded. Try again in ${context.after}`,
      retryAfter: context.after
    };
  }
});

// Apply stricter limits to specific routes
fastify.get('/api/expensive-operation', {
  preHandler: fastify.requireAuth(),
  config: {
    rateLimit: {
      max: 10,
      timeWindow: '1 minute'
    }
  }
}, async (request, reply) => {
  return { result: 'expensive operation result' };
});

Troubleshooting

”No authorization token was found”

Problem: The API cannot find the access token in the request.Solutions:
  1. Ensure the Authorization header is present: Authorization: Bearer YOUR_TOKEN
  2. Check that “Bearer” is included before the token
  3. Verify the token is not expired

”Invalid token” or “jwt malformed”

Problem: The token format is invalid.Solutions:
  1. Ensure you’re using an access token, not an ID token
  2. The token should be obtained with your API’s audience parameter
  3. Check that the token is a valid JWT (should have three parts separated by dots)

“Invalid signature”

Problem: The token signature doesn’t match.Solutions:
  1. Verify AUTH0_DOMAIN matches the domain that issued the token
  2. Ensure you’re using RS256 signing algorithm (default)
  3. Check that the token hasn’t been modified

”Invalid audience”

Problem: The token’s audience doesn’t match your API.Solution: The client application must request a token with the correct audience:
// In your client app
const token = await getAccessTokenSilently({
  authorizationParams: {
    audience: 'https://my-fastify-api.example.com' // Must match your API identifier
  }
});

CORS errors in browser

Problem: Browser blocks API requests due to CORS policy.Solution: Install and configure @fastify/cors:
npm install @fastify/cors
import cors from '@fastify/cors';

await fastify.register(cors, {
  origin: 'http://localhost:3000', // Your frontend URL
  credentials: true
});

Next Steps

Now that you have a protected API, consider exploring:

Resources