Implement Single Sign-On (SSO) with Auth0

How to Implement Single Sign-On (SSO) with Auth0 in Your Custom SaaS

The Problem

Building authentication from scratch is a security minefield. You need to handle password hashing, session management, token validation, password resets, multi-factor authentication, and comply with regulations like GDPR and SOC 2. For B2B SaaS, the complexity multiplies: enterprise clients demand SSO via SAML or OIDC, role-based access control (RBAC), and integration with their identity providers like Okta, Azure AD, or Google Workspace. Rolling your own auth means maintaining this infrastructure, patching vulnerabilities, and explaining to enterprise buyers why they should trust your homegrown security. Auth0 solves this by providing production-ready SSO with SAML/OIDC support, social logins, MFA, user management APIs, and compliance certifications out-of-the-box. This integration gives you enterprise-grade authentication in hours, not months, while maintaining flexibility to customize flows and branding.

Tech Stack & Prerequisites

  • Node.js v20+ and npm/pnpm
  • Express.js v4.18+ or Next.js 14+
  • Auth0 Account (free tier available)
  • express-openid-connect v2.17+ (for Express) or @auth0/nextjs-auth0 v3.5+ (for Next.js)
  • dotenv for environment variables
  • jsonwebtoken v9+ for token verification
  • axios or node-fetch for Auth0 Management API calls
  • PostgreSQL or MongoDB (optional, for user metadata storage)
  • Basic understanding of OAuth2/OIDC and JWT tokens

Step-by-Step Implementation

Step 1: Setup

We’ll implement both Express.js and Next.js examples. Choose your stack:

Option A: Express.js Setup

bash
mkdir auth0-sso-express
cd auth0-sso-express
npm init -y
npm install express express-openid-connect dotenv express-session
npm install -D typescript @types/node @types/express tsx nodemon

Option B: Next.js Setup

bash
npx create-next-app@latest auth0-sso-nextjs --typescript --app --tailwind
cd auth0-sso-nextjs
npm install @auth0/nextjs-auth0

For this tutorial, we’ll focus on Express.js with detailed implementation, then show Next.js equivalents.

Initialize TypeScript:

bash
npx tsc --init

Update tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Create project structure:

bash
mkdir src
touch src/index.ts src/config.ts src/auth.ts src/middleware.ts src/routes.ts

Step 2: Configuration

2.1: Set Up Auth0 Tenant

  1. Sign up at auth0.com
  2. Create a new tenant (e.g., yourcompany-dev.auth0.com)
  3. Go to ApplicationsCreate Application
  4. Choose “Regular Web Application”
  5. Select “Express” as technology
  6. Configure settings:
    • Allowed Callback URLs: http://localhost:3000/callback
    • Allowed Logout URLs: http://localhost:3000
    • Allowed Web Origins: http://localhost:3000
  7. Note your Domain, Client ID, and Client Secret

2.2: Configure Environment Variables

Create .env file:

bash
# .env
PORT=3000

# Auth0 Configuration
AUTH0_ISSUER_BASE_URL=https://yourcompany-dev.auth0.com
AUTH0_CLIENT_ID=your_client_id_here
AUTH0_CLIENT_SECRET=your_client_secret_here
AUTH0_BASE_URL=http://localhost:3000
AUTH0_SECRET=your_long_random_string_for_session_encryption

# Auth0 Management API (for advanced features)
AUTH0_DOMAIN=yourcompany-dev.auth0.com
AUTH0_MANAGEMENT_CLIENT_ID=management_client_id
AUTH0_MANAGEMENT_CLIENT_SECRET=management_client_secret
AUTH0_AUDIENCE=https://yourcompany-dev.auth0.com/api/v2/

# Database (optional)
DATABASE_URL=postgresql://user:password@localhost:5432/auth_demo

Generate a secure secret:

bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Add to .gitignore:

bash
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
echo "dist/" >> .gitignore

Create src/config.ts:

typescript
// src/config.ts
import dotenv from 'dotenv';

dotenv.config();

export const config = {
  port: process.env.PORT || 3000,
  auth0: {
    issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL || '',
    clientID: process.env.AUTH0_CLIENT_ID || '',
    clientSecret: process.env.AUTH0_CLIENT_SECRET || '',
    baseURL: process.env.AUTH0_BASE_URL || '',
    secret: process.env.AUTH0_SECRET || '',
    audience: process.env.AUTH0_AUDIENCE,
    domain: process.env.AUTH0_DOMAIN || '',
  },
  management: {
    clientId: process.env.AUTH0_MANAGEMENT_CLIENT_ID,
    clientSecret: process.env.AUTH0_MANAGEMENT_CLIENT_SECRET,
  },
} as const;

// Validate required environment variables
const requiredEnvVars = [
  'AUTH0_ISSUER_BASE_URL',
  'AUTH0_CLIENT_ID',
  'AUTH0_CLIENT_SECRET',
  'AUTH0_BASE_URL',
  'AUTH0_SECRET',
];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
}

Step 3: Core Logic

3.1: Auth0 Authentication Setup

Create src/auth.ts:

typescript
// src/auth.ts
import { auth, ConfigParams, requiresAuth } from 'express-openid-connect';
import { config } from './config';

/**
 * Auth0 configuration for express-openid-connect
 */
export const authConfig: ConfigParams = {
  authRequired: false, // Don't require auth on all routes by default
  auth0Logout: true, // Enable Auth0 logout
  issuerBaseURL: config.auth0.issuerBaseURL,
  baseURL: config.auth0.baseURL,
  clientID: config.auth0.clientID,
  clientSecret: config.auth0.clientSecret,
  secret: config.auth0.secret,
  idpLogout: true, // Logout from Auth0 when logging out
  authorizationParams: {
    response_type: 'code',
    scope: 'openid profile email', // Request user info
    audience: config.auth0.audience, // For API access tokens
  },
  routes: {
    login: '/login',
    logout: '/logout',
    callback: '/callback',
  },
  session: {
    name: 'auth_session',
    rollingDuration: 86400, // 24 hours in seconds
    absoluteDuration: 604800, // 7 days in seconds
  },
};

/**
 * Middleware export for protecting routes
 */
export { requiresAuth };

/**
 * Middleware to make user available in all templates/responses
 */
export const attachUser = (req: any, res: any, next: any) => {
  res.locals.user = req.oidc?.user || null;
  res.locals.isAuthenticated = req.oidc?.isAuthenticated() || false;
  next();
};

3.2: Auth0 Management API Client

Create src/management.ts:

typescript
// src/management.ts
import axios from 'axios';
import { config } from './config';

/**
 * Auth0 Management API client for advanced user operations
 */
export class Auth0Management {
  private accessToken: string | null = null;
  private tokenExpiry: number = 0;

  /**
   * Get Management API access token
   */
  private async getAccessToken(): Promise<string> {
    // Return cached token if still valid
    if (this.accessToken && Date.now() < this.tokenExpiry) {
      return this.accessToken;
    }

    try {
      const response = await axios.post(
        `${config.auth0.issuerBaseURL}/oauth/token`,
        {
          grant_type: 'client_credentials',
          client_id: config.management.clientId,
          client_secret: config.management.clientSecret,
          audience: config.auth0.audience,
        }
      );

      this.accessToken = response.data.access_token;
      // Set expiry to 5 minutes before actual expiry
      this.tokenExpiry = Date.now() + (response.data.expires_in - 300) * 1000;

      return this.accessToken;
    } catch (error) {
      console.error('Error getting Management API token:', error);
      throw error;
    }
  }

  /**
   * Get user by ID
   */
  async getUser(userId: string): Promise<any> {
    try {
      const token = await this.getAccessToken();
      const response = await axios.get(
        `${config.auth0.issuerBaseURL}/api/v2/users/${userId}`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );

      return response.data;
    } catch (error) {
      console.error('Error fetching user:', error);
      throw error;
    }
  }

  /**
   * Update user metadata
   */
  async updateUserMetadata(
    userId: string,
    metadata: Record<string, any>
  ): Promise<any> {
    try {
      const token = await this.getAccessToken();
      const response = await axios.patch(
        `${config.auth0.issuerBaseURL}/api/v2/users/${userId}`,
        {
          user_metadata: metadata,
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
          },
        }
      );

      return response.data;
    } catch (error) {
      console.error('Error updating user metadata:', error);
      throw error;
    }
  }

  /**
   * Assign roles to user
   */
  async assignRolesToUser(userId: string, roleIds: string[]): Promise<void> {
    try {
      const token = await this.getAccessToken();
      await axios.post(
        `${config.auth0.issuerBaseURL}/api/v2/users/${userId}/roles`,
        {
          roles: roleIds,
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
          },
        }
      );

      console.log(`Roles assigned to user ${userId}`);
    } catch (error) {
      console.error('Error assigning roles:', error);
      throw error;
    }
  }

  /**
   * Get user's roles
   */
  async getUserRoles(userId: string): Promise<any[]> {
    try {
      const token = await this.getAccessToken();
      const response = await axios.get(
        `${config.auth0.issuerBaseURL}/api/v2/users/${userId}/roles`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );

      return response.data;
    } catch (error) {
      console.error('Error fetching user roles:', error);
      throw error;
    }
  }

  /**
   * List all users (with pagination)
   */
  async listUsers(page: number = 0, perPage: number = 50): Promise<any> {
    try {
      const token = await this.getAccessToken();
      const response = await axios.get(
        `${config.auth0.issuerBaseURL}/api/v2/users`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
          params: {
            page,
            per_page: perPage,
            include_totals: true,
          },
        }
      );

      return response.data;
    } catch (error) {
      console.error('Error listing users:', error);
      throw error;
    }
  }

  /**
   * Delete user
   */
  async deleteUser(userId: string): Promise<void> {
    try {
      const token = await this.getAccessToken();
      await axios.delete(
        `${config.auth0.issuerBaseURL}/api/v2/users/${userId}`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      );

      console.log(`User ${userId} deleted`);
    } catch (error) {
      console.error('Error deleting user:', error);
      throw error;
    }
  }

  /**
   * Create a new user (password-based)
   */
  async createUser(userData: {
    email: string;
    password: string;
    name?: string;
    connection?: string;
  }): Promise<any> {
    try {
      const token = await this.getAccessToken();
      const response = await axios.post(
        `${config.auth0.issuerBaseURL}/api/v2/users`,
        {
          email: userData.email,
          password: userData.password,
          name: userData.name,
          connection: userData.connection || 'Username-Password-Authentication',
          email_verified: false,
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
          },
        }
      );

      return response.data;
    } catch (error) {
      console.error('Error creating user:', error);
      throw error;
    }
  }
}

3.3: Custom Middleware for Role-Based Access Control

Create src/middleware.ts:

typescript
// src/middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { Auth0Management } from './management';

const management = new Auth0Management();

/**
 * Check if user has specific role
 */
export function requireRole(role: string) {
  return async (req: any, res: Response, next: NextFunction) => {
    try {
      if (!req.oidc || !req.oidc.isAuthenticated()) {
        return res.status(401).json({ error: 'Not authenticated' });
      }

      const userId = req.oidc.user.sub;
      const roles = await management.getUserRoles(userId);

      const hasRole = roles.some((r: any) => r.name === role);

      if (!hasRole) {
        return res.status(403).json({
          error: 'Insufficient permissions',
          required: role,
        });
      }

      next();
    } catch (error) {
      console.error('Role check error:', error);
      res.status(500).json({ error: 'Authorization check failed' });
    }
  };
}

/**
 * Check if user has any of the specified roles
 */
export function requireAnyRole(roles: string[]) {
  return async (req: any, res: Response, next: NextFunction) => {
    try {
      if (!req.oidc || !req.oidc.isAuthenticated()) {
        return res.status(401).json({ error: 'Not authenticated' });
      }

      const userId = req.oidc.user.sub;
      const userRoles = await management.getUserRoles(userId);

      const hasAnyRole = userRoles.some((r: any) => roles.includes(r.name));

      if (!hasAnyRole) {
        return res.status(403).json({
          error: 'Insufficient permissions',
          required: roles,
        });
      }

      next();
    } catch (error) {
      console.error('Role check error:', error);
      res.status(500).json({ error: 'Authorization check failed' });
    }
  };
}

/**
 * Verify JWT access token for API routes
 */
export function verifyAccessToken(req: any, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No access token provided' });
  }

  const token = authHeader.substring(7); // Remove 'Bearer ' prefix

  try {
    // For production, you should verify the token signature
    // using Auth0's JWKS (JSON Web Key Set)
    const decoded = jwt.decode(token) as any;

    if (!decoded) {
      return res.status(401).json({ error: 'Invalid token' });
    }

    // Attach decoded token to request
    req.auth = decoded;
    next();
  } catch (error) {
    console.error('Token verification error:', error);
    res.status(401).json({ error: 'Invalid token' });
  }
}

/**
 * Rate limiting middleware (basic implementation)
 */
const requestCounts = new Map<string, { count: number; resetTime: number }>();

export function rateLimit(maxRequests: number = 100, windowMs: number = 60000) {
  return (req: any, res: Response, next: NextFunction) => {
    const identifier = req.oidc?.user?.sub || req.ip;
    const now = Date.now();

    const record = requestCounts.get(identifier);

    if (!record || now > record.resetTime) {
      requestCounts.set(identifier, {
        count: 1,
        resetTime: now + windowMs,
      });
      return next();
    }

    if (record.count >= maxRequests) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: Math.ceil((record.resetTime - now) / 1000),
      });
    }

    record.count++;
    next();
  };
}

3.4: Application Routes

Create src/routes.ts:

typescript
// src/routes.ts
import { Router, Request, Response } from 'express';
import { requiresAuth } from './auth';
import { requireRole, requireAnyRole, rateLimit } from './middleware';
import { Auth0Management } from './management';

const router = Router();
const management = new Auth0Management();

/**
 * Public homepage
 */
router.get('/', (req: any, res: Response) => {
  res.json({
    message: 'Auth0 SSO Demo',
    authenticated: req.oidc?.isAuthenticated() || false,
    user: req.oidc?.user || null,
  });
});

/**
 * Protected profile route
 */
router.get('/profile', requiresAuth(), async (req: any, res: Response) => {
  try {
    const userId = req.oidc.user.sub;

    // Fetch full user details from Auth0
    const userDetails = await management.getUser(userId);

    res.json({
      profile: {
        id: userDetails.user_id,
        email: userDetails.email,
        name: userDetails.name,
        picture: userDetails.picture,
        emailVerified: userDetails.email_verified,
        lastLogin: userDetails.last_login,
        loginsCount: userDetails.logins_count,
        metadata: userDetails.user_metadata,
        appMetadata: userDetails.app_metadata,
      },
    });
  } catch (error) {
    console.error('Profile error:', error);
    res.status(500).json({ error: 'Failed to fetch profile' });
  }
});

/**
 * Update user metadata
 */
router.post('/profile/metadata', requiresAuth(), async (req: any, res: Response) => {
  try {
    const userId = req.oidc.user.sub;
    const { metadata } = req.body;

    if (!metadata || typeof metadata !== 'object') {
      return res.status(400).json({ error: 'Invalid metadata' });
    }

    const updatedUser = await management.updateUserMetadata(userId, metadata);

    res.json({
      message: 'Metadata updated successfully',
      metadata: updatedUser.user_metadata,
    });
  } catch (error) {
    console.error('Metadata update error:', error);
    res.status(500).json({ error: 'Failed to update metadata' });
  }
});

/**
 * Admin route - requires 'admin' role
 */
router.get(
  '/admin/users',
  requiresAuth(),
  requireRole('admin'),
  async (req: Request, res: Response) => {
    try {
      const page = parseInt(req.query.page as string) || 0;
      const perPage = parseInt(req.query.per_page as string) || 50;

      const users = await management.listUsers(page, perPage);

      res.json({
        users: users.users,
        total: users.total,
        page,
        perPage,
      });
    } catch (error) {
      console.error('List users error:', error);
      res.status(500).json({ error: 'Failed to fetch users' });
    }
  }
);

/**
 * Admin route - assign roles to user
 */
router.post(
  '/admin/users/:userId/roles',
  requiresAuth(),
  requireRole('admin'),
  async (req: Request, res: Response) => {
    try {
      const { userId } = req.params;
      const { roleIds } = req.body;

      if (!Array.isArray(roleIds)) {
        return res.status(400).json({ error: 'roleIds must be an array' });
      }

      await management.assignRolesToUser(userId, roleIds);

      res.json({
        message: 'Roles assigned successfully',
        userId,
        roleIds,
      });
    } catch (error) {
      console.error('Assign roles error:', error);
      res.status(500).json({ error: 'Failed to assign roles' });
    }
  }
);

/**
 * API route with rate limiting
 */
router.get(
  '/api/data',
  requiresAuth(),
  rateLimit(10, 60000), // 10 requests per minute
  (req: Request, res: Response) => {
    res.json({
      data: 'Protected API data',
      timestamp: new Date().toISOString(),
    });
  }
);

/**
 * Multi-role route - requires 'admin' OR 'manager' role
 */
router.get(
  '/dashboard',
  requiresAuth(),
  requireAnyRole(['admin', 'manager']),
  (req: Request, res: Response) => {
    res.json({
      message: 'Dashboard data',
      accessLevel: 'admin or manager',
    });
  }
);

export default router;

3.5: Main Application

Create src/index.ts:

typescript
// src/index.ts
import express from 'express';
import { auth } from 'express-openid-connect';
import { config } from './config';
import { authConfig, attachUser } from './auth';
import routes from './routes';

const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Auth0 authentication middleware
app.use(auth(authConfig));

// Attach user to all requests
app.use(attachUser);

// CORS (configure appropriately for production)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', config.auth0.baseURL);
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

// Routes
app.use('/', routes);

// Error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error('Error:', err);
  res.status(err.status || 500).json({
    error: err.message || 'Internal server error',
  });
});

// Start server
app.listen(config.port, () => {
  console.log(`Server running on ${config.auth0.baseURL}`);
  console.log(`Login at: ${config.auth0.baseURL}/login`);
  console.log(`Profile at: ${config.auth0.baseURL}/profile`);
});

Update package.json:

json
{
  "scripts": {
    "dev": "nodemon --exec tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Step 4: Testing

4.1: Start the Server

Run the development server:

bash
npm run dev
```

Expected output:
```
Server running on http://localhost:3000
Login at: http://localhost:3000/login
Profile at: http://localhost:3000/profile

4.2: Test Authentication Flow

Step 1: Visit login page

bash
curl http://localhost:3000/

Response (not authenticated):

json
{
  "message": "Auth0 SSO Demo",
  "authenticated": false,
  "user": null
}

Step 2: Login via browser

Open browser and navigate to: http://localhost:3000/login

  1. You’ll be redirected to Auth0 Universal Login
  2. Sign up or sign in with email/password or social login
  3. Grant permissions
  4. You’ll be redirected back to your app

Step 3: Access protected profile

After login, visit: http://localhost:3000/profile

Expected response:

json
{
  "profile": {
    "id": "auth0|abc123",
    "email": "user@example.com",
    "name": "John Doe",
    "picture": "https://...",
    "emailVerified": true,
    "lastLogin": "2026-03-03T...",
    "loginsCount": 5,
    "metadata": {},
    "appMetadata": {}
  }
}

4.3: Test with cURL (using access token)

First, get your access token from the browser:

javascript
// In browser console after login
fetch('http://localhost:3000/profile', { credentials: 'include' })
  .then(r => r.json())
  .then(console.log)

For API testing, you need to configure API in Auth0 and request access tokens. Here’s how to test with session cookies:

bash
# Test profile (requires session cookie from browser)
curl -b cookies.txt http://localhost:3000/profile

# Test metadata update
curl -X POST http://localhost:3000/profile/metadata \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"metadata": {"company": "Acme Inc", "plan": "premium"}}'

4.4: Test Role-Based Access Control

Step 1: Create roles in Auth0 Dashboard

  1. Go to User ManagementRoles
  2. Create “admin” role
  3. Create “manager” role
  4. Assign roles to test users

Step 2: Test admin routes

bash
# Should return 403 if user doesn't have admin role
curl -b cookies.txt http://localhost:3000/admin/users

# Should return user list if user has admin role

4.5: Test Logout

bash
curl http://localhost:3000/logout

You’ll be redirected to Auth0 logout and then back to your app.

4.6: Create Test Script

Create src/test.ts:

typescript
// src/test.ts
import axios from 'axios';

const API_BASE = 'http://localhost:3000';

async function runTests() {
  console.log('Auth0 SSO Integration Tests\n');

  try {
    // Test 1: Check public endpoint
    console.log('Test 1: Access public homepage');
    const homeRes = await axios.get(API_BASE);
    console.log('Authenticated:', homeRes.data.authenticated);
    console.log('✓ Passed\n');

    // Note: Can't fully test auth flow without browser
    console.log('Test 2: Manual browser tests required');
    console.log('1. Visit http://localhost:3000/login');
    console.log('2. Complete login flow');
    console.log('3. Visit http://localhost:3000/profile');
    console.log('4. Test http://localhost:3000/admin/users (requires admin role)');
    console.log('5. Test http://localhost:3000/logout\n');

    console.log('For automated API testing, see Auth0 testing docs:');
    console.log('https://auth0.com/docs/get-started/auth0-overview/test-your-login-flow');

  } catch (error: any) {
    console.error('Test failed:', error.response?.data || error.message);
  }
}

runTests();

Common Errors & Troubleshooting

1. Error: “Callback URL mismatch” or redirect_uri error

Cause: The callback URL in your code doesn’t match what’s configured in Auth0 Application settings.

Fix: Ensure exact match including protocol, domain, and path:

typescript
// In Auth0 Dashboard → Applications → Settings
// Allowed Callback URLs must include:
http://localhost:3000/callback

// For production:
https://yourdomain.com/callback

// In .env, make sure base URL matches:
AUTH0_BASE_URL=http://localhost:3000  // No trailing slash!

If using custom callback route, configure it explicitly:

typescript
const authConfig: ConfigParams = {
  // ...
  routes: {
    callback: '/auth/callback', // Must match Auth0 settings
  },
};

// And in Auth0 Dashboard:
// http://localhost:3000/auth/callback

2. Error: “invalid_grant” or “Refresh token is invalid”

Cause: Session expired, refresh token revoked, or Auth0 connection disabled.

Fix: Configure proper session management and handle token refresh:

typescript
const authConfig: ConfigParams = {
  // ...
  session: {
    rollingDuration: 86400, // Extend session on activity (24 hours)
    absoluteDuration: 604800, // Force re-login after 7 days
  },
  authorizationParams: {
    scope: 'openid profile email offline_access', // offline_access for refresh tokens
  },
};

// Handle auth errors gracefully
app.use((err: any, req: any, res: any, next: any) => {
  if (err.name === 'UnauthorizedError') {
    // Redirect to login
    return res.redirect('/login');
  }
  next(err);
});

For Management API tokens, implement retry logic:

typescript
private async getAccessToken(): Promise<string> {
  const maxRetries = 3;
  for (let i = 0; i < maxRetries; i++) {
    try {
      // Token fetch logic...
      return this.accessToken;
    } catch (error: any) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
  throw new Error('Failed to get access token');
}

3. Error: Management API returns 403 or “Insufficient scope”

Cause: Management API credentials don’t have proper permissions, or you’re using the wrong client credentials.

Fix: Create a proper Machine-to-Machine (M2M) application:

  1. Go to ApplicationsCreate Application
  2. Choose “Machine to Machine Applications”
  3. Select “Auth0 Management API”
  4. Grant required scopes:
    • read:users
    • update:users
    • delete:users
    • create:users
    • read:roles
    • update:roles
    • read:user_idp_tokens

Update .env with M2M credentials:

bash
AUTH0_MANAGEMENT_CLIENT_ID=your_m2m_client_id
AUTH0_MANAGEMENT_CLIENT_SECRET=your_m2m_client_secret

Verify scope in token request:

typescript
const response = await axios.post(
  `${config.auth0.issuerBaseURL}/oauth/token`,
  {
    grant_type: 'client_credentials',
    client_id: config.management.clientId,
    client_secret: config.management.clientSecret,
    audience: `https://${config.auth0.domain}/api/v2/`, // Note the /api/v2/
  }
);

Security Checklist

  • Never commit .env files or Auth0 credentials to version control
  • Use HTTPS in production – HTTP exposes session tokens and auth codes
  • Implement CSRF protection using express-session with secure cookies
  • Set secure cookie flags in production: httpOnly, secure, sameSite: 'strict'
  • Validate redirect URLs to prevent open redirect vulnerabilities
  • Implement rate limiting on login endpoints to prevent brute force attacks
  • Enable MFA in Auth0 for all users, especially admins
  • Use short-lived access tokens (15-60 minutes) with refresh token rotation
  • Validate JWT signatures using Auth0’s JWKS endpoint for API routes
  • Implement proper RBAC – check roles server-side, never trust client claims
  • Log authentication events for security monitoring and compliance
  • Set up Auth0 Anomaly Detection to detect suspicious login patterns
  • Use Auth0 Rules/Actions to add custom security logic (IP whitelisting, device fingerprinting)
  • Regularly rotate client secrets and Management API credentials
  • Implement session fixation protection by regenerating session IDs after login
  • Set Content Security Policy (CSP) headers to prevent XSS attacks
  • Monitor Auth0 logs for failed login attempts and unauthorized access
  • Use environment-specific tenants (dev, staging, production) to isolate credentials
  • Implement logout on all devices feature for compromised accounts
  • Validate email domains for enterprise SSO to prevent unauthorized access

Leave a Comment

Your email address will not be published. Required fields are marked *