Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs-staging.auth0-mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Protect Your Express.js API with JWT Authentication

This guide demonstrates how to add JWT authentication to your Express.js API using the express-oauth2-jwt-bearer SDK. You’ll validate access tokens issued by Auth0, protect API routes, and implement scope-based authorization.
Prerequisites: Before you begin, ensure you have:
  • Node.js 18 LTS or newer (supports ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0)
  • npm 8+ (or yarn/pnpm)
  • An Auth0 account (free tier available)
  • Basic familiarity with Express.js

Get Started

1. Create a new Express project

Create a new directory and initialize a Node.js project:
mkdir my-secure-api && cd my-secure-api
npm init -y

2. Install dependencies

Install Express, the authentication SDK, and dotenv for environment variable management:
npm install express express-oauth2-jwt-bearer dotenv
For TypeScript projects, also install type definitions:
npm install -D typescript @types/express @types/node

3. Configure your Auth0 API

You need an Auth0 API to issue access tokens for your Express application. Configure Auth0 using the CLI or manually via the Dashboard:
If you have the Auth0 CLI installed, run:
auth0 apis create \
  --name "My Express API" \
  --identifier "https://api.example.com" \
  --signing-alg "RS256"
After creation, note the Identifier value—this is your AUTH0_AUDIENCE.

4. Create environment configuration

Create a .env file in your project root with your Auth0 configuration:
# .env

# Your Auth0 tenant domain (without https:// prefix shown in Dashboard)
AUTH0_AUDIENCE=https://api.example.com
AUTH0_DOMAIN=your-tenant.us.auth0.com

# Server configuration
PORT=3000
ℹ️ Finding your Auth0 Domain: In the Auth0 Dashboard, your domain appears in the top-left corner or under SettingsGeneral. It typically looks like dev-abc123.us.auth0.com.
VariableDescriptionExample
AUTH0_DOMAINYour Auth0 tenant domaindev-abc123.us.auth0.com
AUTH0_AUDIENCEThe API Identifier you createdhttps://api.example.com

5. Create the server

Create server.js (or server.ts for TypeScript) with the following code:
// server.js
require('dotenv').config();
const express = require('express');
const { auth } = require('express-oauth2-jwt-bearer');

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

// 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! No authentication required.',
  });
});

// Protected route - requires valid JWT
app.get('/api/private', checkJwt, (req, res) => {
  res.json({
    message: 'Hello from a private endpoint!',
    user: req.auth.payload.sub,
  });
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  const status = err.status || 500;
  const message = err.message || 'Internal Server Error';
  
  res.status(status).json({
    error: err.code || 'unauthorized',
    message: status === 401 ? 'Authentication required' : message,
  });
});

app.listen(port, () => {
  console.log(`API server running on http://localhost:${port}`);
});

6. Run your API

Start the server:
node server.js
For TypeScript:
npx ts-node server.ts
ℹ️ Your API is now running at http://localhost:3000.
Checkpoint: Verify your API is protecting routes correctly: Test the public endpoint (should return 200):
curl http://localhost:3000/api/public
Test the private endpoint without a token (should return 401):
curl http://localhost:3000/api/private
You should receive:
  • Public endpoint: {"message":"Hello from a public endpoint!..."}
  • Private endpoint: {"error":"unauthorized","message":"Authentication required"}

7. Test with a valid token

To test the protected endpoint with a valid access 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:3000/api/private \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
You should receive a response like:
{
  "message": "Hello from a private endpoint!",
  "user": "auth0|abc123..."
}
Success! Your Express API is now protected with JWT authentication.

Add Scope-Based Authorization

Scopes allow fine-grained access control. You can require specific scopes for different endpoints.

1. Configure scopes in Auth0

  1. In the Auth0 Dashboard, go to ApplicationsAPIs → Your API
  2. Navigate to the Permissions tab
  3. Add the following scopes:
    • read:messages - Read messages
    • write:messages - Write messages
    • admin:access - Administrative access

2. Protect routes with scopes

Update your server to use scope-based authorization:
const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

// ... existing setup ...

// 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,
  });
});

3. Request tokens with scopes

When obtaining tokens, include the required scopes in the authorization request. The scopes granted will be included in the token’s scope claim.
⚠️ Important: If a request lacks the required scope, the API returns 403 Forbidden with an insufficient_scope error.

Add Claim Validation

Beyond scopes, you can validate custom claims in the JWT payload.

Validate specific claim values

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' });
  }
);

Troubleshooting


Advanced Usage


Complete Example

Here’s a complete Express API with all features:
// server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { 
  auth, 
  requiredScopes, 
  claimCheck,
  UnauthorizedError,
  InsufficientScopeError,
} = require('express-oauth2-jwt-bearer');

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

// Middleware
app.use(cors({
  origin: process.env.ALLOWED_ORIGIN || '*',
  exposedHeaders: ['WWW-Authenticate'],
}));
app.use(express.json());

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

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

app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

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

// Scope-protected endpoint
app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => {
  res.json({
    messages: [
      { id: 1, text: 'Hello from the API!' },
    ],
  });
});

// Admin endpoint with scope and claim check
app.get('/api/admin', 
  checkJwt, 
  requiredScopes('admin:access'),
  claimCheck((claims) => claims.role === 'admin'),
  (req, res) => {
    res.json({
      message: 'Admin access granted',
      userId: req.auth.payload.sub,
    });
  }
);

// Error handling
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  
  if (err instanceof InsufficientScopeError) {
    return res.status(403).json({
      error: 'insufficient_scope',
      message: 'Missing required permissions',
    });
  }
  
  if (err instanceof UnauthorizedError) {
    return res.status(err.status).set(err.headers).json({
      error: err.code || 'unauthorized',
      message: 'Authentication required',
    });
  }
  
  res.status(500).json({
    error: 'server_error',
    message: 'An unexpected error occurred',
  });
});

app.listen(port, () => {
  console.log(`🚀 API server running at http://localhost:${port}`);
  console.log(`   - Public:  http://localhost:${port}/api/public`);
  console.log(`   - Private: http://localhost:${port}/api/private`);
});

Next Steps

Now that your Express API is protected with JWT authentication, you can:
  • Add more protected routes with different scope requirements
  • Implement refresh token rotation for long-lived sessions
  • Add rate limiting to protect against abuse
  • Connect a frontend application that obtains tokens from Auth0
  • Enable DPoP for enhanced token security (proof-of-possession)

Useful Resources


Congratulations! You’ve successfully protected your Express.js API with JWT authentication using Auth0 and express-oauth2-jwt-bearer.