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
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
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.
- Go to the Auth0 Dashboard
- Navigate to Applications → APIs in the sidebar
- Click + Create API
- Fill in the following:
- Name: My Express API (or your preferred name)
- Identifier:
https://api.example.com (this becomes your AUTH0_AUDIENCE)
- Signing Algorithm: RS256 (recommended)
- Click Create
- Copy the Identifier value for use in your application
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 Settings → General. It typically looks like dev-abc123.us.auth0.com.
| Variable | Description | Example |
|---|
AUTH0_DOMAIN | Your Auth0 tenant domain | dev-abc123.us.auth0.com |
AUTH0_AUDIENCE | The API Identifier you created | https://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:
For TypeScript:
ℹ️ 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:
- Go to Auth0 Dashboard → Applications → APIs
- Select your API → Test tab
- 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.
- In the Auth0 Dashboard, go to Applications → APIs → Your API
- Navigate to the Permissions tab
- 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.