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-express-api
Then ask your AI assistant:
Add Auth0 JWT authentication to my Express API
Your AI assistant will automatically create your Auth0 API, fetch credentials, install express-oauth2-jwt-bearer, configure the JWT middleware, and protect your API endpoints with token validation. Full agent skills documentation →
Prerequisites: Before you begin, ensure you have the following installed:
  • Node.js 18 LTS or newer (supports ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0)
  • npm 8+ or yarn 1.22+ or pnpm 8+
Verify installation: node --version && npm --versionExpress Version Compatibility: This quickstart works with Express 4.x and Express 5.x.

Get Started

This quickstart demonstrates how to protect Express.js API endpoints using JWT access tokens. You’ll build a secure API that validates Auth0 access tokens, protects routes, and implements scope-based authorization.
1

Create a new project

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

Install the express-oauth2-jwt-bearer SDK

Install the required dependencies
npm install express express-oauth2-jwt-bearer dotenv
Update your package.json to add start scripts:
package.json
{
  "name": "auth0-express-api",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "dotenv": "^16.3.1",
    "express": "^4.21.0",
    "express-oauth2-jwt-bearer": "^1.6.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 Express API" \
  --identifier https://my-express-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 JWT middleware

Create your Express server and configure JWT validation:
server.js
require('dotenv').config();
const express = require('express');
const { auth } = require('express-oauth2-jwt-bearer');

const app = express();
const port = process.env.PORT || 3001;

// Configure JWT validation middleware
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

// Start server
app.listen(port, () => {
  console.log(`API server running at http://localhost:${port}`);
});
What this does:
  • Creates JWT validation middleware using your Auth0 domain and API audience
  • Validates the iss and aud claims on incoming access tokens
  • Makes checkJwt available for protecting individual routes
5

Create API routes

Add public and protected routes to your server.js:
server.js
require('dotenv').config();
const express = require('express');
const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

const app = express();
const port = process.env.PORT || 3001;

// Configure JWT validation middleware
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

// Public route - no authentication required
app.get('/api/public', (req, res) => {
  res.json({
    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
app.get('/api/private', checkJwt, (req, res) => {
  res.json({
    message: 'Hello from a protected endpoint! You successfully authenticated.',
    user: req.auth.payload.sub,
    timestamp: new Date().toISOString(),
  });
});

// Protected route with scope - requires 'read:messages' scope
app.get('/api/private-scoped', checkJwt, requiredScopes('read:messages'), (req, res) => {
  res.json({
    message: 'Hello from a scoped endpoint! You have the required permission.',
    user: req.auth.payload.sub,
    scope: req.auth.payload.scope,
    timestamp: new Date().toISOString(),
  });
});

// Error handling middleware
app.use((err, req, res, next) => {
  const status = err.status || 500;
  const message = err.message || 'Internal Server Error';

  res.status(status).json({
    error: err.code || 'server_error',
    message: status === 401 ? 'Authentication required' : message,
  });
});

// Start server
app.listen(port, () => {
  console.log(`API server running at http://localhost:${port}`);
});
Key points:
  • Public routes don’t require authentication
  • Protected routes use the checkJwt middleware to require a valid JWT
  • Scoped routes use requiredScopes() to require specific permissions in the token
  • req.auth.payload 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 18+ 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": "Authentication required"
}
To test with a valid token:
  1. Go to Auth0 DashboardApplicationsAPIs
  2. Select your API → Test tab
  3. Copy the generated access token
Test your protected endpoint:
curl http://localhost:3001/api/private \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
You should see:
{
  "message": "Hello from a protected endpoint! You successfully authenticated.",
  "user": "auth0|abc123...",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
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 req.auth.payload

Advanced Usage

Scopes allow fine-grained access control. You can require specific scopes for different endpoints.Configure scopes in Auth0:
  1. In the Auth0 Dashboard, go to ApplicationsAPIs → Your API
  2. Navigate to the Permissions tab
  3. Add permissions like read:messages, write:messages, admin:access
Protect routes with scopes:
server.js
const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

// Requires 'read:messages' scope
app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => {
  res.json({
    messages: [
      { id: 1, text: 'Hello!' },
      { id: 2, text: 'World!' },
    ],
  });
});

// Requires 'admin:access' scope
app.get('/api/admin', checkJwt, requiredScopes('admin:access'), (req, res) => {
  res.json({
    message: 'Admin access granted',
    userId: req.auth.payload.sub,
  });
});
If a request lacks the required scope, the API returns 403 Forbidden with an insufficient_scope error. Ensure the client application requests the correct scopes when obtaining an access token.
Beyond scopes, you can validate custom claims in the JWT payload:
server.js
const { auth, claimEquals, claimIncludes, claimCheck } = require('express-oauth2-jwt-bearer');

// Require exact claim value
app.get('/api/org/:orgId',
  checkJwt,
  claimEquals('org_id', 'org_123'),
  (req, res) => {
    res.json({ message: 'Organization access granted' });
  }
);

// Require claim to include all specified values
app.get('/api/roles',
  checkJwt,
  claimIncludes('roles', 'editor', 'viewer'),
  (req, res) => {
    res.json({ message: 'Role check passed' });
  }
);

// Custom claim validation logic
app.get('/api/premium',
  checkJwt,
  claimCheck((claims) => {
    return claims.subscription === 'premium' && claims.verified === true;
  }),
  (req, res) => {
    res.json({ message: 'Premium feature access granted' });
  }
);
Custom claims must use namespaced URLs (e.g., https://myapp.com/roles) unless they’re standard OIDC claims. Learn more about custom claims.
Allow both authenticated and anonymous access to the same route:
server.js
const optionalAuth = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
  authRequired: false,
});

app.get('/api/feed', optionalAuth, (req, res) => {
  if (req.auth) {
    res.json({
      message: `Welcome back, ${req.auth.payload.sub}!`,
      personalizedContent: true,
    });
  } else {
    res.json({
      message: 'Welcome, guest!',
      personalizedContent: false,
    });
  }
});
Enable CORS to allow requests from web applications:
npm install cors
server.js
const cors = require('cors');

app.use(cors({
  origin: ['http://localhost:3000', 'http://localhost:5173'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  exposedHeaders: ['WWW-Authenticate'],
}));
For production, specify exact origins:
server.js
app.use(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
const { UnauthorizedError, InvalidTokenError, InsufficientScopeError } = require('express-oauth2-jwt-bearer');

app.use((err, req, res, next) => {
  if (err instanceof InsufficientScopeError) {
    return res.status(403).json({
      error: 'forbidden',
      message: 'You do not have permission to access this resource',
      required_scopes: err.requiredScopes,
    });
  }

  if (err instanceof InvalidTokenError) {
    return res.status(401).json({
      error: 'invalid_token',
      message: 'The provided token is invalid or expired',
    });
  }

  if (err instanceof UnauthorizedError) {
    return res.status(401).set(err.headers).json({
      error: 'unauthorized',
      message: 'Authentication required',
    });
  }

  next(err);
});
For TypeScript projects, install type definitions and configure your project:
npm install -D typescript @types/express @types/node
Create server.ts:
server.ts
import 'dotenv/config';
import express, { Request, Response, NextFunction } from 'express';
import { auth, requiredScopes, UnauthorizedError } from 'express-oauth2-jwt-bearer';

const app = express();
const port = process.env.PORT || 3001;

const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

app.get('/api/public', (req: Request, res: Response) => {
  res.json({ message: 'Public endpoint - no authentication required' });
});

app.get('/api/private', checkJwt, (req: Request, res: Response) => {
  res.json({
    message: 'Private endpoint',
    user: req.auth?.payload.sub,
  });
});

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof UnauthorizedError) {
    res.status(err.status).set(err.headers).json({
      error: err.code || 'unauthorized',
      message: 'Authentication required',
    });
  } else {
    res.status(500).json({
      error: 'server_error',
      message: 'Internal Server Error',
    });
  }
});

app.listen(port, () => {
  console.log(`API server running at http://localhost:${port}`);
});
Add a tsconfig.json:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["*.ts"]
}
Run with: npx ts-node server.ts

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)

Unexpected “iss” or “aud” value

Problem: The issuer or audience in the token doesn’t match your configuration.Solutions:
  1. Decode your token at jwt.io
  2. Check the iss claim matches https://YOUR_AUTH0_DOMAIN/ (note the trailing slash)
  3. Check the aud claim matches your AUTH0_AUDIENCE exactly
  4. Verify your .env values:
AUTH0_DOMAIN=dev-abc123.us.auth0.com
AUTH0_AUDIENCE=https://my-express-api.example.com

“You must provide an issuerBaseURL” or “audience is required”

Problem: Environment variables are not being loaded.Solutions:
  1. Ensure .env file exists in your project root
  2. Verify dotenv is installed: npm install dotenv
  3. Add require('dotenv').config() at the very top of your server file
  4. Check variable names match exactly (case-sensitive)

401 Unauthorized on all requests

Possible causes:
  • Token is expired
  • Audience doesn’t match
  • Issuer doesn’t match
Debug steps:
  1. Decode your token at jwt.io
  2. Check the exp claim hasn’t passed
  3. Verify aud claim matches your AUTH0_AUDIENCE exactly
  4. Verify iss claim is https://{AUTH0_DOMAIN}/
  5. Ensure Authorization header format is Bearer YOUR_TOKEN (with space)

403 Forbidden with “insufficient_scope”

Problem: The token doesn’t have the required scopes.Solutions:
  1. Verify the scopes are defined in your Auth0 API (Dashboard → ApplicationsAPIsPermissions)
  2. Request the scopes when obtaining the token
  3. Check the token’s scope claim includes the required scopes

CORS errors in browser

Problem: Browser blocks API requests due to CORS policy.Solution: Install and configure cors:
npm install cors
const cors = require('cors');

app.use(cors({
  origin: 'http://localhost:3000',
}));

Next Steps

Now that you have a protected API, consider exploring:

Resources