Skip to main content

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 auth0/agent-skills
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
npm init -y
Create the project structure:
touch index.js .env

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)`nCLIENT_ID=$($_.client_id)`nSECRET=$([guid]::NewGuid().ToString())`nBASE_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.

4. Configure the middleware

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:
npm run dev
Open your browser to http://localhost:3000.
CheckpointYou should now have a fully functional Auth0 login page. When you:
  1. Click “Login” - you’re redirected to Auth0’s Universal Login page
  2. Complete authentication - you’re redirected back to your app
  3. Visit “/profile” - you see your user information
  4. Click “Logout” - you’re logged out of both your app and Auth0

Advanced Usage

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
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',
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');
});
Claims like role must be added to your tokens via Auth0 Rules or Actions. Learn more about adding custom claims.
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

”Invalid state” error after login

Problem: State mismatch between the authentication request and callback.Solutions:
  1. Ensure you’re using HTTPS in production
  2. Check that cookies are being set correctly (not blocked by browser)
  3. 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));
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:
  1. Go to your Auth0 Dashboard → Applications → Your App → Settings
  2. Add http://localhost:3000 (or your production URL) to Allowed Callback URLs
  3. The URL must match exactly (including trailing slashes)

Environment variables not loading

Problem: Configuration values are undefined.Solution:
  1. Ensure require('dotenv').config() is at the top of your entry file
  2. Verify .env file is in the root directory
  3. 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