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.
Use AI to integrate Auth0
If you use an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you can add Auth0 authentication automatically in minutes using agent skills . Install: npx skills add https://github.com/auth0/agent-skills --skill auth0-express
Then ask your AI assistant: Add Auth0 authentication to my Express app
Your AI assistant will automatically create your Auth0 application, fetch credentials, install express-openid-connect, configure the middleware, and set up your routes. Full agent skills documentation →
Prerequisites: Before you begin, ensure you have the following installed:
Node.js 18 LTS or newer
npm 10+ or yarn 1.22+
jq - Required for Auth0 CLI setup (optional)
Express Version Compatibility: This quickstart works with Express 4.17.0 and newer.
Get Started
This guide demonstrates how to integrate Auth0, add authentication, and display user profile information in an Express.js web application using the express-openid-connect SDK.
1. Create a new project
Create a new directory for your Express application and initialize a Node.js project.
mkdir auth0-express && cd auth0-express
Create the project structure:
2. Install the Auth0 Express SDK
Install express-openid-connect along with Express and dotenv for environment variable management.
npm install express express-openid-connect dotenv
For development, install nodemon to automatically restart your server on file changes:
npm install --save-dev nodemon
Update your package.json to add start scripts:
📁 package.json
{
"name" : "auth0-express" ,
"version" : "1.0.0" ,
"main" : "index.js" ,
"scripts" : {
"start" : "node index.js" ,
"dev" : "nodemon index.js"
},
"dependencies" : {
"dotenv" : "^16.3.1" ,
"express" : "^4.18.2" ,
"express-openid-connect" : "^2.17.1"
},
"devDependencies" : {
"nodemon" : "^3.0.2"
}
}
3. Setup your Auth0 App
Next, you need to create a new application on your Auth0 tenant and add the environment variables to your project.
You can choose to do this automatically by running a CLI command or manually via the Dashboard:
Run the following shell command in your project’s root directory to create an Auth0 application and generate your .env file: macOS / Linux: AUTH0_APP_NAME = "My Express App" && \
auth0 apps create -n "${ AUTH0_APP_NAME }" -t regular \
--callbacks http://localhost:3000 \
--logout-urls http://localhost:3000 \
--json | jq -r '"ISSUER_BASE_URL=https://\(.domain)\nCLIENT_ID=\(.client_id)\nSECRET=' $( openssl rand -hex 32 ) '\nBASE_URL=http://localhost:3000"' > .env
Windows (PowerShell): $appName = "My Express App"
auth0 apps create - n $appName - t regular `
-- callbacks http: // localhost: 3000 `
-- logout - urls http: // localhost: 3000 `
-- json | ConvertFrom-Json | ForEach-Object {
"ISSUER_BASE_URL=https:// $( $_ .domain ) `n CLIENT_ID= $( $_ .client_id ) `n SECRET= $( [ guid ]::NewGuid().ToString() ) `n BASE_URL=http://localhost:3000"
} | Out-File .env - Encoding utf8
If you haven’t installed the Auth0 CLI yet, run: brew tap auth0/auth0-cli && brew install auth0
Then authenticate with auth0 login.
Go to the Auth0 Dashboard
Navigate to Applications → Create Application
Enter a name for your application (e.g., “My Express App”)
Select Regular Web Applications and click Create
In the Settings tab, configure the following:
Setting Value Allowed Callback URLs http://localhost:3000Allowed Logout URLs http://localhost:3000
Scroll down and click Save Changes
Copy the Domain and Client ID values from the Basic Information section
Create your .env file with the following values: 📁 .env ISSUER_BASE_URL = https://YOUR_AUTH0_DOMAIN
CLIENT_ID = YOUR_CLIENT_ID
SECRET = use-a-long-random-string-at-least-32-characters
BASE_URL = http://localhost:3000
Replace YOUR_AUTH0_DOMAIN with your Auth0 tenant domain (e.g., dev-abc123.us.auth0.com) and YOUR_CLIENT_ID with your application’s Client ID from the dashboard.
Generate a secure secret for session encryption: Copy the output and use it as the SECRET value in your .env file.
Add the Auth0 middleware to your Express application. The auth() middleware handles session management and automatically creates /login, /logout, and /callback routes.
📁 index.js
require ( 'dotenv' ). config ();
const express = require ( 'express' );
const { auth } = require ( 'express-openid-connect' );
const app = express ();
const port = process . env . PORT || 3000 ;
// Auth0 configuration
const config = {
authRequired: false , // Allow public routes
auth0Logout: true , // Use Auth0 logout endpoint
secret: process . env . SECRET ,
baseURL: process . env . BASE_URL ,
clientID: process . env . CLIENT_ID ,
issuerBaseURL: process . env . ISSUER_BASE_URL ,
};
// Apply the auth middleware
app . use ( auth ( config ));
// Home route - public
app . get ( '/' , ( req , res ) => {
res . send ( req . oidc . isAuthenticated () ? 'Logged in' : 'Logged out' );
});
app . listen ( port , () => {
console . log ( `Server running at http://localhost: ${ port } ` );
});
What this does:
authRequired: false allows both authenticated and unauthenticated users to access routes by default
auth0Logout: true ensures users are logged out from Auth0 as well as your app
The middleware automatically provides routes at /login, /logout, and /callback
User session is stored in an encrypted cookie
5. Create login, logout, and profile routes
Now add routes to display login/logout links and a protected profile page.
📁 index.js
require ( 'dotenv' ). config ();
const express = require ( 'express' );
const { auth , requiresAuth } = require ( 'express-openid-connect' );
const app = express ();
const port = process . env . PORT || 3000 ;
// Auth0 configuration
const config = {
authRequired: false ,
auth0Logout: true ,
secret: process . env . SECRET ,
baseURL: process . env . BASE_URL ,
clientID: process . env . CLIENT_ID ,
issuerBaseURL: process . env . ISSUER_BASE_URL ,
};
// Apply the auth middleware
app . use ( auth ( config ));
// Home route - shows login/logout status
app . get ( '/' , ( req , res ) => {
const isAuthenticated = req . oidc . isAuthenticated ();
res . send ( `
<html>
<head>
<title>Auth0 Express Quickstart</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; }
a { color: #0066cc; text-decoration: none; margin-right: 1rem; }
a:hover { text-decoration: underline; }
.status { padding: 1rem; border-radius: 4px; margin: 1rem 0; }
.logged-in { background: #d4edda; color: #155724; }
.logged-out { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>Auth0 Express Quickstart</h1>
<div class="status ${ isAuthenticated ? 'logged-in' : 'logged-out' } ">
${ isAuthenticated ? '✓ You are logged in' : '✗ You are logged out' }
</div>
<nav>
${ isAuthenticated
? '<a href="/profile">Profile</a> | <a href="/logout">Logout</a>'
: '<a href="/login">Login</a>' }
</nav>
</body>
</html>
` );
});
// Protected profile route - requires authentication
app . get ( '/profile' , requiresAuth (), ( req , res ) => {
const user = req . oidc . user ;
res . send ( `
<html>
<head>
<title>Profile - Auth0 Express</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; }
a { color: #0066cc; text-decoration: none; }
img { border-radius: 50%; }
pre { background: #f4f4f4; padding: 1rem; border-radius: 4px; overflow-x: auto; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 1.5rem; margin: 1rem 0; }
</style>
</head>
<body>
<h1>User Profile</h1>
<div class="card">
${ user . picture ? `<img src=" ${ user . picture } " alt="Profile" width="80" />` : '' }
<h2> ${ user . name || user . nickname || 'User' } </h2>
<p><strong>Email:</strong> ${ user . email || 'N/A' } </p>
</div>
<h3>Full User Object</h3>
<pre> ${ JSON . stringify ( user , null , 2 ) } </pre>
<nav>
<a href="/">← Back to Home</a> | <a href="/logout">Logout</a>
</nav>
</body>
</html>
` );
});
app . listen ( port , () => {
console . log ( `Server running at http://localhost: ${ port } ` );
});
Key points:
requiresAuth() middleware protects the /profile route - unauthenticated users are redirected to login
req.oidc.user contains the authenticated user’s profile information
req.oidc.isAuthenticated() returns a boolean indicating login status
Login and logout routes (/login, /logout) are automatically created by the auth() middleware
6. Run your app
Start the development server:
Open your browser to http://localhost:3000 .
Checkpoint You should now have a fully functional Auth0 login page. When you:
Click “Login” - you’re redirected to Auth0’s Universal Login page
Complete authentication - you’re redirected back to your app
Visit “/profile” - you see your user information
Click “Logout” - you’re logged out of both your app and Auth0
Advanced Usage
Protecting Specific Routes with requiresAuth()
Use the requiresAuth() middleware to protect individual routes that require authentication: const { auth , requiresAuth } = require ( 'express-openid-connect' );
app . use ( auth ({ authRequired: false }));
// Public route
app . get ( '/' , ( req , res ) => {
res . send ( 'Welcome! This is public.' );
});
// Protected routes
app . get ( '/dashboard' , requiresAuth (), ( req , res ) => {
res . send ( `Hello ${ req . oidc . user . name } , welcome to your dashboard!` );
});
app . get ( '/settings' , requiresAuth (), ( req , res ) => {
res . send ( 'Settings page - only for authenticated users' );
});
You can also protect all routes under a specific path using Express Router: const protectedRouter = express . Router ();
// All routes in this router require authentication
protectedRouter . use ( requiresAuth ());
protectedRouter . get ( '/dashboard' , ( req , res ) => {
res . send ( 'Protected dashboard' );
});
protectedRouter . get ( '/settings' , ( req , res ) => {
res . send ( 'Protected settings' );
});
app . use ( '/app' , protectedRouter );
// Routes: /app/dashboard, /app/settings are all protected
Calling Protected APIs with Access Tokens
To call external APIs that require an access token, configure the SDK to request one: 📁 index.js (updated configuration) const config = {
authRequired: false ,
auth0Logout: true ,
secret: process . env . SECRET ,
baseURL: process . env . BASE_URL ,
clientID: process . env . CLIENT_ID ,
issuerBaseURL: process . env . ISSUER_BASE_URL ,
clientSecret: process . env . CLIENT_SECRET , // Required for code flow
authorizationParams: {
response_type: 'code' ,
audience: process . env . API_AUDIENCE , // Your API identifier
scope: 'openid profile email read:data' ,
},
};
Add these to your .env file: CLIENT_SECRET = your_client_secret_from_dashboard
API_AUDIENCE = https://your-api.example.com
Then use the access token to call your API: app . get ( '/api-data' , requiresAuth (), async ( req , res ) => {
try {
let { token_type , access_token , isExpired , refresh } = req . oidc . accessToken ;
// Refresh the token if expired
if ( isExpired ()) {
const refreshed = await refresh ();
access_token = refreshed . access_token ;
}
// Call your protected API
const response = await fetch ( 'https://your-api.example.com/data' , {
headers: {
Authorization: ` ${ token_type } ${ access_token } ` ,
},
});
const data = await response . json ();
res . json ( data );
} catch ( error ) {
console . error ( 'API call failed:' , error );
res . status ( 500 ). json ({ error: 'Failed to fetch data' });
}
});
To get refresh tokens, add offline_access to your scope: scope : 'openid profile email offline_access read:data' ,
Using Claim-Based Authorization
Protect routes based on user claims (roles, permissions, etc.): const { auth , requiresAuth , claimEquals , claimIncludes , claimCheck } = require ( 'express-openid-connect' );
app . use ( auth ({ authRequired: false }));
// Only users with role = 'admin'
app . get ( '/admin' , claimEquals ( 'role' , 'admin' ), ( req , res ) => {
res . send ( 'Admin dashboard' );
});
// Users whose roles array includes 'editor'
app . get ( '/editor' , claimIncludes ( 'roles' , 'editor' ), ( req , res ) => {
res . send ( 'Editor dashboard' );
});
// Custom claim check with logic
app . get ( '/premium' , claimCheck (( req , claims ) => {
return claims . subscription === 'premium' || claims . role === 'admin' ;
}), ( req , res ) => {
res . send ( 'Premium content' );
});
Custom Session Store (Redis)
For production environments or when running multiple server instances, use a custom session store: npm install redis connect-redis
const { auth } = require ( 'express-openid-connect' );
const { createClient } = require ( 'redis' );
const RedisStore = require ( 'connect-redis' ). default ;
// Create Redis client
const redisClient = createClient ({
url: process . env . REDIS_URL || 'redis://localhost:6379' ,
});
redisClient . connect (). catch ( console . error );
const config = {
authRequired: false ,
auth0Logout: true ,
secret: process . env . SECRET ,
baseURL: process . env . BASE_URL ,
clientID: process . env . CLIENT_ID ,
issuerBaseURL: process . env . ISSUER_BASE_URL ,
session: {
store: new RedisStore ({ client: redisClient }),
},
};
app . use ( auth ( config ));
When to use a custom session store:
Running multiple server instances (load balancing)
Session data exceeds cookie size limits (~4KB)
Need session persistence across server restarts
Using back-channel logout
Add proper error handling for authentication errors: const { auth } = require ( 'express-openid-connect' );
app . use ( auth ({
authRequired: false ,
auth0Logout: true ,
secret: process . env . SECRET ,
baseURL: process . env . BASE_URL ,
clientID: process . env . CLIENT_ID ,
issuerBaseURL: process . env . ISSUER_BASE_URL ,
errorOnRequiredAuth: true , // Return 401 instead of redirecting for API routes
}));
// Custom error handler
app . use (( err , req , res , next ) => {
// Handle authentication errors
if ( err . statusCode === 401 ) {
// For API requests, return JSON
if ( req . accepts ( 'json' )) {
return res . status ( 401 ). json ({
error: 'Authentication required' ,
login_url: '/login' ,
});
}
// For browser requests, redirect to login
return res . redirect ( '/login' );
}
// Log the error (don't expose details to client)
console . error ( 'Application error:' , err . message );
res . status ( err . statusCode || 500 ). json ({
error: 'An unexpected error occurred' ,
});
});
Troubleshooting
Common Issues and Solutions
”Invalid state” error after login Problem: State mismatch between the authentication request and callback.Solutions:
Ensure you’re using HTTPS in production
Check that cookies are being set correctly (not blocked by browser)
Verify callback URL matches exactly in Auth0 Dashboard
”req.oidc is undefined” Problem: The auth() middleware is not applied before accessing req.oidc.Solution: Ensure app.use(auth(config)) is called before any route that accesses req.oidc:// ✅ Correct order
app . use ( auth ( config ));
app . get ( '/profile' , requiresAuth (), ( req , res ) => { ... });
// ❌ Wrong order
app . get ( '/profile' , requiresAuth (), ( req , res ) => { ... });
app . use ( auth ( config ));
Session too large / Cookie errors Problem: User session data exceeds cookie size limits.Solution: Use a custom session store like Redis:session : {
store : new RedisStore ({ client: redisClient }),
}
Callback URL mismatch Problem: “Callback URL mismatch” error from Auth0.Solution:
Go to your Auth0 Dashboard → Applications → Your App → Settings
Add http://localhost:3000 (or your production URL) to Allowed Callback URLs
The URL must match exactly (including trailing slashes)
Environment variables not loading Problem: Configuration values are undefined.Solution:
Ensure require('dotenv').config() is at the top of your entry file
Verify .env file is in the root directory
Check for typos in variable names
// Debug: Log config values (remove in production!)
console . log ( 'Config check:' , {
hasSecret: !! process . env . SECRET ,
hasClientID: !! process . env . CLIENT_ID ,
issuerBaseURL: process . env . ISSUER_BASE_URL ,
});
Next Steps
Now that you have authentication working, consider exploring:
Resources