Skip to main content
This Quickstart is currently in Beta. We’d love to hear your feedback!
Prerequisites: Before you begin, ensure you have the following installed:

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- and claim-based authorization.
1

Create a new project

Create a new directory for your Express API and initialize a Node.js project.Update your package.json to use ES modules and add start scripts:
{
  "name": "auth0-express-api",
  "version": "1.0.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  }
}
2

Install the SDK

Install @auth0/auth0-express-api along with express and dotenv:
npm install @auth0/auth0-express-api@beta express dotenv
3

Setup your Auth0 API

You need to create a new API on your Auth0 tenant and configure your environment variables.
Run the following command in your project root to create an Auth0 API:After creation, copy the Identifier and your Domain values, then create your .env file:Replace YOUR_API_IDENTIFIER with the identifier you used above (e.g., https://my-express-api.example.com).
4

Configure the JWT middleware

Register createAuth0Api() on your Express application to configure JWT validation. Then add public and protected routes.
server.js
import 'dotenv/config';
import express from 'express';
import { createAuth0Api, requiresAuth } from '@auth0/auth0-express-api';

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

app.use(express.json());
app.use(createAuth0Api());

// Public route — no token required
app.get('/api/public', (req, res) => {
  res.json({
    message: 'Hello from a public endpoint! No authentication required.',
    timestamp: new Date().toISOString(),
  });
});

// Protected route — requires a valid access token
app.get('/api/private', requiresAuth(), (req, res) => {
  res.json({
    message: 'Hello from a protected endpoint! You are authenticated.',
    sub: req.auth0.user?.sub,
    timestamp: new Date().toISOString(),
  });
});

app.listen(port, () => {
  console.log(`API server running at http://localhost:${port}`);
});
What this does:
  • createAuth0Api() reads AUTH0_DOMAIN and AUTH0_AUDIENCE from environment variables automatically
  • requiresAuth() validates the Authorization: Bearer <token> header on each request
  • req.auth0.user contains the decoded JWT claims for authenticated requests — sub is the user’s unique identifier
5

Protect a route with a required scope

Beyond requiring a valid token, you can require a specific scope. Pass a scopes option to requiresAuth() — the SDK returns 403 insufficient_scope if the token is missing the scope.
server.js
// Requires the "read:messages" scope
app.get('/api/messages', requiresAuth({ scopes: ['read:messages'] }), (req, res) => {
  res.json({ messages: ['Hello!', 'World!'] });
});
Define the scope in your API’s Permissions tab (see Advanced Usage) and request it when obtaining the access token. For matching multiple scopes or authorizing on custom claims, the SDK also provides scopesInclude, claimEquals, claimIncludes, and claimCheck — covered in Advanced Usage.
6

Run your API

Start the development server:
npm run dev
Your API is now running at http://localhost:3001.
7

Test your API

Test the public endpoint (no token required):
curl http://localhost:3001/api/public
Expected response:
{
  "message": "Hello from a public endpoint! No authentication required.",
  "timestamp": "2026-06-22T12:00:00.000Z"
}
To call the protected endpoint, you need an access token:
  1. Go to Auth0 DashboardApplications > APIs
  2. Select your API → Test tab
  3. Copy the generated access token
Test the protected endpoint:
curl http://localhost:3001/api/private \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Expected response:
{
  "message": "Hello from a protected endpoint! You are authenticated.",
  "sub": "auth0|abc123...",
  "timestamp": "2026-06-22T12:00:00.000Z"
}
CheckpointYou should now have a protected API. Your API:
  1. Accepts requests to public endpoints without a token
  2. Returns the protected response when a valid access token is provided
  3. Validates JWTs against your Auth0 domain and audience
  4. Exposes decoded token claims via req.auth0.user

Advanced Usage

Use scopesInclude when a route should accept any one of several scopes, or require several at once. By default it matches any of the listed scopes; pass { match: 'all' } to require all of them. Scopes can be passed as an array or a space-separated string — these examples use arrays.
server.js
import { scopesInclude } from '@auth0/auth0-express-api';

// Requires ANY of these scopes
app.get('/api/feed', requiresAuth(), scopesInclude(['read:feed', 'read:admin']), (req, res) => {
  res.json({ feed: [] });
});

// Requires ALL of these scopes
app.get('/api/admin/edit', requiresAuth(), scopesInclude(['read:admin', 'write:admin'], { match: 'all' }), (req, res) => {
  res.json({ message: 'Admin editor access granted.' });
});
When authorization depends on claims other than scope, use claimEquals, claimIncludes, or claimCheck. Each runs after requiresAuth() and returns 401 invalid_token if the claim requirement isn’t met.
server.js
import { claimEquals, claimIncludes, claimCheck } from '@auth0/auth0-express-api';

// claimEquals — claim must equal an exact value
app.get('/api/admin', requiresAuth(), claimEquals('isAdmin', true), (req, res) => {
  res.json({ message: 'Admin access granted.' });
});

// claimIncludes — array claim must contain all listed values
app.get('/api/editor', requiresAuth(), claimIncludes('roles', ['admin', 'editor']), (req, res) => {
  res.json({ message: 'Editor access granted.' });
});

// claimCheck — custom logic over the decoded token
app.get('/api/premium', requiresAuth(), claimCheck(
  (req, token) => token.tier === 'premium' || token.roles?.includes('admin'),
  { errorMessage: 'Premium tier or admin role required' }
), (req, res) => {
  res.json({ message: 'Premium content access granted.' });
});
If you’re using TypeScript, augment the Token interface to get type-safe access to custom claims:
server.ts
import '@auth0/auth0-express-api';

declare module '@auth0/auth0-express-api' {
  interface Token {
    tier: 'free' | 'premium';
    roles: string[];
    'https://myapp.com/org_id': string;
  }
}
Install type support:
npm install -D typescript @types/express @types/node
Enable CORS to allow your web application to call the API:
npm install cors
server.js
import cors from 'cors';

app.use(cors({
  origin: ['http://localhost:3000', 'http://localhost:5173'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  exposedHeaders: ['WWW-Authenticate'],
}));

app.use(createAuth0Api());
For production, specify exact allowed origins instead of wildcards.
To use scope-based authorization, first define the permissions on your API:
  1. Go to Auth0 DashboardApplications > APIs → your API
  2. Navigate to the Permissions tab
  3. Add permissions like read:messages, write:messages, read:admin
  4. Click Save
Your client application must then request these scopes when obtaining an access token. If a token lacks the required scope, the API returns 403 Forbidden.

Troubleshooting

Cause: The Authorization header is missing or malformed, so no bearer token could be extracted. Per RFC 6750, the SDK returns 401 with a bare WWW-Authenticate: Bearer header and no error body in this case. This is distinct from a token that is present but invalid or expired, which returns 401 with an invalid_token error and a JSON body (see below).Fix:
  1. Ensure the header is present: Authorization: Bearer YOUR_TOKEN
  2. Verify “Bearer” (with a capital B and a space) precedes the token
Cause: The token was not issued for this API, or the domain/audience values don’t match.Fix:
  1. Decode your token at jwt.io
  2. Check iss matches https://{yourDomain}/ (note the trailing slash)
  3. Check aud matches your AUTH0_AUDIENCE exactly
  4. Make sure you’re using an access token, not an ID token — access tokens are obtained with the audience parameter
Cause: The token does not include the required scope.Fix:
  1. Verify the scope is defined in your API’s Permissions tab in the Auth0 Dashboard
  2. Ensure the client is requesting the scope when obtaining the access token
  3. Decode the token at jwt.io and check the scope claim
Cause: dotenv is not configured, or variable names are wrong.Fix:
  1. Ensure import 'dotenv/config' is the first import in your entry file
  2. Verify .env contains AUTH0_DOMAIN and AUTH0_AUDIENCE
  3. Debug:
console.log({
  domain: !!process.env.AUTH0_DOMAIN,
  audience: !!process.env.AUTH0_AUDIENCE,
});
Cause: The @auth0/auth0-express-api SDK uses ES modules.Fix: Add "type": "module" to your package.json:📁 package.json
{
  "type": "module"
}
Or rename your server file to server.mjs.

Next Steps


Resources