Platform Session Management
The MyNATCA Platform uses Redis-backed Express sessions with Auth0 for secure, persistent authentication across the ecosystem.
Overview
Session management is critical for maintaining user authentication state across the Platform and all integrated applications (Hub, Discord verification, etc.). The Platform implements:
- Redis Session Store: Distributed session storage for scalability
- Auth0 Integration: OAuth 2.0 authentication with JWT tokens
- Secure Cookies: HttpOnly, Secure, SameSite cookie configuration
- Trust Proxy Configuration: Required for production deployment behind reverse proxies
Trust Proxy Configuration
Critical Production Requirement
When deploying the Platform behind a reverse proxy (Digital Ocean App Platform, nginx, etc.), you must set trust proxy to ensure secure cookies work correctly.
Problem Without Trust Proxy
// WITHOUT trust proxy configuration
app.use(session({
cookie: {
secure: true // Requires HTTPS
}
}));
// Result: Sessions fail because Express sees HTTP from reverse proxy
// Symptoms: Sessions not persisting, users logged out immediately after loginSolution: Enable Trust Proxy
// server.js - REQUIRED for production
const express = require('express');
const app = express();
// Trust first proxy (Digital Ocean App Platform, nginx, etc.)
app.set('trust proxy', 1);
// Now Express recognizes X-Forwarded-Proto header
app.use(session({
cookie: {
secure: process.env.NODE_ENV === 'production' // Works correctly
}
}));How Trust Proxy Works
When trust proxy is enabled:
-
Reverse proxy adds headers:
X-Forwarded-Proto: https X-Forwarded-For: 1.2.3.4 X-Forwarded-Host: platform.natca.org -
Express recognizes HTTPS:
req.protocol === 'https' // true (from X-Forwarded-Proto) req.secure === true // true req.hostname === 'platform.natca.org' // from X-Forwarded-Host -
Secure cookies work:
- Browser receives:
Set-Cookie: session=...; Secure; HttpOnly - Cookie sent only over HTTPS connections
- Session persists correctly
- Browser receives:
Trust Proxy Values
// Trust first proxy only (recommended for Digital Ocean)
app.set('trust proxy', 1);
// Trust specific IP or subnet
app.set('trust proxy', '127.0.0.1');
app.set('trust proxy', 'loopback, 10.0.0.0/8');
// Trust all proxies (not recommended for production)
app.set('trust proxy', true);
// Custom trust function
app.set('trust proxy', (ip) => {
return ip === '127.0.0.1' || ip === '::1';
});Session Configuration
Complete Session Setup
// server.js - Complete session configuration
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const app = express();
// CRITICAL: Trust proxy for production
app.set('trust proxy', 1);
// Redis client setup
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
socket: {
connectTimeout: 10000,
commandTimeout: 5000
}
});
redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});
redisClient.connect().catch(console.error);
// Session middleware
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
name: 'platform.session',
resave: false,
saveUninitialized: false,
rolling: true, // Refresh session on every request
cookie: {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
httpOnly: true, // Prevent JavaScript access
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'lax', // CSRF protection
domain: process.env.COOKIE_DOMAIN // Optional: '.natca.org' for subdomains
}
}));Environment Variables
# Session Configuration
SESSION_SECRET=your_long_random_secret_key_min_32_chars
REDIS_URL=redis://localhost:6379
COOKIE_DOMAIN=.natca.org # Optional: for cross-subdomain sessions
# Auth0 Configuration
AUTH0_DOMAIN=natca-prod.us.auth0.com
AUTH0_CLIENT_ID=your_client_id
AUTH0_CLIENT_SECRET=your_client_secret
AUTH0_CALLBACK_URL=https://platform.natca.org/api/auth/callback
# Server Configuration
NODE_ENV=production
PORT=1300Auth0 Integration
OAuth 2.0 Flow
// routes/auth.js
const { auth } = require('express-openid-connect');
const config = {
authRequired: false,
auth0Logout: true,
secret: process.env.SESSION_SECRET,
baseURL: process.env.BASE_URL || 'https://platform.natca.org',
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
routes: {
login: '/api/auth/login',
logout: '/api/auth/logout',
callback: '/api/auth/callback'
}
};
// Auth0 middleware
app.use(auth(config));
// Store user in session after callback
app.use((req, res, next) => {
if (req.oidc.isAuthenticated()) {
req.session.user = {
sub: req.oidc.user.sub,
name: req.oidc.user.name,
email: req.oidc.user.email,
idToken: req.oidc.idToken,
accessToken: req.oidc.accessToken
};
}
next();
});Session Data Structure
// req.session contents after successful authentication
{
user: {
sub: 'auth0|123456789',
name: 'John Doe',
email: 'john.doe@natca.org',
idToken: 'eyJhbGciOiJSUzI1Ni...', // JWT token for API forwarding
accessToken: 'encrypted_access_token', // JWE for Auth0 Management API
memberNumber: '12345',
natcaId: '67890',
facilityCode: 'ZTL',
regionCode: 'ASO'
},
cookie: {
originalMaxAge: 604800000,
expires: '2025-10-08T12:00:00.000Z',
secure: true,
httpOnly: true,
path: '/'
}
}Session Persistence Issues
Common Problems and Solutions
Issue 1: Sessions Not Persisting After Login
Symptoms:
- User successfully authenticates with Auth0
- Immediately redirected and logged out
- Session cookie not being set in browser
Root Cause:
Platform behind reverse proxy without trust proxy configuration.
Solution:
// Add to server.js BEFORE session middleware
app.set('trust proxy', 1);Issue 2: Sessions Expiring Too Quickly
Symptoms:
- Users logged out after short period
- Session maxAge not being respected
Root Cause:
Redis connection issues or missing rolling: true option.
Solution:
app.use(session({
store: new RedisStore({ client: redisClient }),
rolling: true, // Refresh session TTL on every request
resave: false, // Don't save unchanged sessions
saveUninitialized: false // Don't create empty sessions
}));Issue 3: Cross-Subdomain Session Issues
Symptoms:
- Session works on
platform.natca.org - Session doesn't work on
hub.natca.org
Solution:
app.use(session({
cookie: {
domain: '.natca.org', // Note the leading dot
path: '/'
}
}));Note: Both applications must use the same Redis instance and SESSION_SECRET.
Issue 4: Redis Connection Failures
Symptoms:
- Sessions fail intermittently
- "Redis connection timeout" errors
Solution:
const redisClient = createClient({
url: process.env.REDIS_URL,
socket: {
connectTimeout: 10000,
commandTimeout: 5000,
reconnectStrategy: (retries) => {
if (retries > 10) return new Error('Max retries reached');
return Math.min(retries * 100, 3000);
}
}
});
// Add error handling
redisClient.on('error', (err) => {
logger.error('Redis client error', err);
});
redisClient.on('connect', () => {
logger.info('Redis client connected');
});
redisClient.on('ready', () => {
logger.info('Redis client ready');
});Session Security
Best Practices
Secure Cookie Configuration
const cookieConfig = {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
httpOnly: true, // Prevent XSS attacks
secure: true, // HTTPS only (production)
sameSite: 'lax', // CSRF protection
signed: true // Cookie signing (optional)
};Session Secret Management
# Generate strong session secret
openssl rand -base64 32
# Store in environment variable (NOT in code)
SESSION_SECRET=your_generated_secret_here
# Use different secrets for staging and production
SESSION_SECRET_STAGING=staging_secret
SESSION_SECRET_PRODUCTION=production_secretSession Invalidation
// Logout endpoint with proper cleanup
app.get('/api/auth/logout', (req, res) => {
// Destroy session
req.session.destroy((err) => {
if (err) {
logger.error('Session destruction failed', err);
}
// Clear cookie
res.clearCookie('platform.session');
// Redirect to Auth0 logout
res.redirect(`https://${process.env.AUTH0_DOMAIN}/v2/logout?returnTo=${encodeURIComponent(process.env.BASE_URL)}`);
});
});Redis Session Store
Production Configuration
// High-availability Redis configuration
const redisClient = createClient({
url: process.env.REDIS_URL,
socket: {
connectTimeout: 10000,
commandTimeout: 5000,
keepAlive: 30000,
noDelay: true,
reconnectStrategy: (retries) => {
if (retries > 20) {
logger.error('Redis max retries exceeded');
return new Error('Redis connection failed');
}
// Exponential backoff
return Math.min(retries * 100, 5000);
}
},
// Connection pooling
maxRetriesPerRequest: 3,
enableReadyCheck: true,
enableOfflineQueue: true
});Session Store Options
app.use(session({
store: new RedisStore({
client: redisClient,
prefix: 'platform:sess:', // Namespace sessions
ttl: 7 * 24 * 60 * 60, // 7 days in seconds
disableTouch: false, // Update TTL on access
disableTTL: false // Enable TTL support
})
}));Monitoring Redis Sessions
// Health check endpoint
app.get('/api/health/session', async (req, res) => {
try {
// Test Redis connection
await redisClient.ping();
// Get session count
const keys = await redisClient.keys('platform:sess:*');
res.json({
status: 'healthy',
redis: 'connected',
activeSessions: keys.length,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Session health check failed', error);
res.status(503).json({
status: 'unhealthy',
redis: 'disconnected',
error: error.message
});
}
});Deployment Checklist
Pre-Deployment Verification
-
trust proxyconfigured correctly for reverse proxy -
SESSION_SECRETset to strong random value -
REDIS_URLpoints to production Redis instance - Cookie
secureflag enabled for production - Auth0 callback URLs configured correctly
- Redis connection pooling configured
- Session TTL appropriate for use case
Post-Deployment Testing
# Test session creation
curl -c cookies.txt https://platform.natca.org/api/auth/login
# Test session persistence
curl -b cookies.txt https://platform.natca.org/api/auth/session
# Expected response:
{
"authenticated": true,
"user": { ... }
}
# Test secure cookie
curl -I https://platform.natca.org/api/auth/login
# Look for: Set-Cookie: platform.session=...; Secure; HttpOnly; SameSite=LaxCommon Deployment Issues
Digital Ocean App Platform
// Required configuration for Digital Ocean
app.set('trust proxy', 1); // Trust DO reverse proxy
// Use DO Redis addon connection string
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';Nginx Reverse Proxy
# nginx.conf - Required headers
location / {
proxy_pass http://platform:1300;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Host $host;
}Docker Deployment
# docker-compose.yml
services:
platform:
environment:
- TRUST_PROXY=1
- REDIS_URL=redis://redis:6379
- SESSION_SECRET=${SESSION_SECRET}
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis_data:/dataTroubleshooting
Debug Session Issues
// Add session debugging middleware
app.use((req, res, next) => {
logger.debug('Session debug', {
sessionID: req.sessionID,
authenticated: req.oidc?.isAuthenticated(),
protocol: req.protocol,
secure: req.secure,
hostname: req.hostname,
headers: {
'x-forwarded-proto': req.get('x-forwarded-proto'),
'x-forwarded-host': req.get('x-forwarded-host')
}
});
next();
});Session Diagnostics Script
// scripts/test-session.js
const redis = require('redis');
const client = redis.createClient({
url: process.env.REDIS_URL
});
client.connect();
// List all sessions
client.keys('platform:sess:*').then(keys => {
console.log(`Active sessions: ${keys.length}`);
keys.forEach(async (key) => {
const session = await client.get(key);
const ttl = await client.ttl(key);
console.log(`Session: ${key}, TTL: ${ttl}s`);
});
});Proper session management with trust proxy configuration ensures seamless authentication across the MyNATCA ecosystem while maintaining security and user experience.