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:
You need to create a new API on your Auth0 tenant and configure your environment variables.
CLI
Dashboard
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).
Set the Identifier — this is your API audience (e.g., https://my-express-api.example.com). It doesn’t need to be a real URL.
Keep Signing Algorithm as RS256
Click Create
Copy the Domain from your tenant and the Identifier from API Settings
Create your .env file:
Replace YOUR_AUTH0_DOMAIN with your Auth0 tenant domain (e.g., dev-abc123.us.auth0.com) and YOUR_API_IDENTIFIER with the API identifier you set above.
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 requiredapp.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 tokenapp.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.
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.
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 scopesapp.get('/api/feed', requiresAuth(), scopesInclude(['read:feed', 'read:admin']), (req, res) => { res.json({ feed: [] });});// Requires ALL of these scopesapp.get('/api/admin/edit', requiresAuth(), scopesInclude(['read:admin', 'write:admin'], { match: 'all' }), (req, res) => { res.json({ message: 'Admin editor access granted.' });});
Authorizing on custom claims
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 valueapp.get('/api/admin', requiresAuth(), claimEquals('isAdmin', true), (req, res) => { res.json({ message: 'Admin access granted.' });});// claimIncludes — array claim must contain all listed valuesapp.get('/api/editor', requiresAuth(), claimIncludes('roles', ['admin', 'editor']), (req, res) => { res.json({ message: 'Editor access granted.' });});// claimCheck — custom logic over the decoded tokenapp.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.' });});
Defining custom token claims with TypeScript
If you’re using TypeScript, augment the Token interface to get type-safe access to custom claims:
Add permissions like read:messages, write:messages, read:admin
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.
401 with an empty body and a bare 'WWW-Authenticate: Bearer' header
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:
Ensure the header is present: Authorization: Bearer YOUR_TOKEN
Verify “Bearer” (with a capital B and a space) precedes the token
'Invalid token' or audience/issuer mismatch (401)
Cause: The token was not issued for this API, or the domain/audience values don’t match.Fix: