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
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
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:
npx tsc --init
Update tsconfig.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:
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
- Sign up at auth0.com
- Create a new tenant (e.g.,
yourcompany-dev.auth0.com) - Go to Applications → Create Application
- Choose “Regular Web Application”
- Select “Express” as technology
- Configure settings:
- Allowed Callback URLs:
http://localhost:3000/callback - Allowed Logout URLs:
http://localhost:3000 - Allowed Web Origins:
http://localhost:3000
- Allowed Callback URLs:
- Note your Domain, Client ID, and Client Secret
2.2: Configure Environment Variables
Create .env file:
# .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:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Add to .gitignore:
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
echo "dist/" >> .gitignore
Create src/config.ts:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
{
"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:
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
curl http://localhost:3000/
Response (not authenticated):
{
"message": "Auth0 SSO Demo",
"authenticated": false,
"user": null
}
Step 2: Login via browser
Open browser and navigate to: http://localhost:3000/login
- You’ll be redirected to Auth0 Universal Login
- Sign up or sign in with email/password or social login
- Grant permissions
- You’ll be redirected back to your app
Step 3: Access protected profile
After login, visit: http://localhost:3000/profile
Expected response:
{
"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:
// 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:
# 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
- Go to User Management → Roles
- Create “admin” role
- Create “manager” role
- Assign roles to test users
Step 2: Test admin routes
# 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
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:
// 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:
// 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:
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:
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:
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:
- Go to Applications → Create Application
- Choose “Machine to Machine Applications”
- Select “Auth0 Management API”
- Grant required scopes:
read:usersupdate:usersdelete:userscreate:usersread:rolesupdate:rolesread:user_idp_tokens
Update .env with M2M credentials:
AUTH0_MANAGEMENT_CLIENT_ID=your_m2m_client_id
AUTH0_MANAGEMENT_CLIENT_SECRET=your_m2m_client_secret
Verify scope in token request:
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-sessionwith 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

Huzaifa Asif is a dedicated software and integration specialist at TheSportsAngel, focused on making complex API and system integrations simple and actionable. With over 3+ years of hands-on experience in backend development, CRM/ERP connectivity, and third-party platform integrations, he transforms technical architecture into clear, step-by-step coding guides that both developers and non-technical users can follow.



