Platform
Session Management

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 login

Solution: 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:

  1. Reverse proxy adds headers:

    X-Forwarded-Proto: https
    X-Forwarded-For: 1.2.3.4
    X-Forwarded-Host: platform.natca.org
  2. Express recognizes HTTPS:

    req.protocol === 'https'  // true (from X-Forwarded-Proto)
    req.secure === true        // true
    req.hostname === 'platform.natca.org'  // from X-Forwarded-Host
  3. Secure cookies work:

    • Browser receives: Set-Cookie: session=...; Secure; HttpOnly
    • Cookie sent only over HTTPS connections
    • Session persists correctly

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=1300

Auth0 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_secret

Session 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 proxy configured correctly for reverse proxy
  • SESSION_SECRET set to strong random value
  • REDIS_URL points to production Redis instance
  • Cookie secure flag 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=Lax

Common 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:/data

Troubleshooting

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.