Skip to main content

Add Login to Your Express Application

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. :::note 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. :::

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: ::::tabs :::tab 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
:::note If you haven’t installed the Auth0 CLI yet, run:
brew tap auth0/auth0-cli && brew install auth0
Then authenticate with auth0 login. ::: ::: :::tab
  1. Go to the Auth0 Dashboard
  2. Navigate to ApplicationsCreate Application
  3. Enter a name for your application (e.g., “My Express App”)
  4. Select Regular Web Applications and click Create
  5. In the Settings tab, configure the following:
SettingValue
Allowed Callback URLshttp://localhost:3000
Allowed Logout URLshttp://localhost:3000
  1. Scroll down and click Save Changes
  2. 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
:::warning 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:
openssl rand -hex 32
Copy the output and use it as the SECRET value in your .env file. ::: ::::

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. :::checkpoint ✓ Checkpoint You 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

:::details[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
::: :::details[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' });
  }
});
:::note To get refresh tokens, add offline_access to your scope:
scope: 'openid profile email offline_access read:data',
::: ::: :::details[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');
});
:::note Claims like role must be added to your tokens via Auth0 Rules or Actions. Learn more about adding custom claims. ::: ::: :::details[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 :::
:::details[Error Handling] 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

:::details[Common Issues and Solutions]

“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