API Reference
Authentication & Authorization

Authentication & Authorization

The MyNATCA Platform implements OAuth 2.0 server-side authentication flow with Auth0, providing secure authentication for all ecosystem applications.

Authentication Architecture

OAuth 2.0 Server-Side Flow Implementation

Token Management

JWT vs JWE Token Handling

Critical Implementation Detail: The Platform receives two types of tokens from Auth0:

// Platform receives from Auth0
interface Auth0Tokens {
  accessToken: string;  // JWE encrypted token for Auth0 Management API
  idToken: string;      // JWT signed token for external API forwarding
}

Key Discovery: External APIs (MyNATCA) require JWT format tokens, not JWE:

// ❌ WRONG: Using accessToken (JWE format)
// accessToken: "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIi..." (JWE - encrypted)
 
// ✅ CORRECT: Using idToken (JWT format)
// idToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCI..."      (JWT - signed)
 
// Platform proxy middleware - FIXED implementation
on: {
  proxyReq: (proxyReq, req, res) => {
    // Priority: Use ID token (JWT) for external API forwarding
    if (req.session.user?.idToken) {
      proxyReq.setHeader('Authorization', `Bearer ${req.session.user.idToken}`);
      console.log('🔑 Forwarding Auth0 ID token to API (JWT format)');
    }
  }
}

Custom Claims Implementation

Auth0 Rules Configuration

NATCA-specific data is injected into JWT tokens via Auth0 Rules:

// Auth0 Rule: Add NATCA custom claims
function addNATCACustomClaims(user, context, callback) {
  const namespace = 'https://my.natca.org/';
 
  // Extract NATCA data from user metadata
  const natcaData = {
    natca_id: user.user_metadata?.natca_id,
    member_number: user.user_metadata?.member_number,
    facility_code: user.user_metadata?.facility_code,
    region_code: user.user_metadata?.region_code
  };
 
  // Add custom claims to tokens
  const customClaims = {
    [`${namespace}natca_id`]: natcaData.natca_id,
    [`${namespace}member_number`]: natcaData.member_number,
    [`${namespace}facility_code`]: natcaData.facility_code,
    [`${namespace}region_code`]: natcaData.region_code
  };
 
  // Apply to both tokens
  context.idToken = Object.assign(context.idToken, customClaims);
  context.accessToken = Object.assign(context.accessToken, customClaims);
 
  callback(null, user, context);
}

Custom Claims Extraction (Actual Implementation)

// Platform: routes/auth.js - OAuth callback implementation
router.get('/callback', async (req, res) => {
  try {
    const { code, state, error, error_description } = req.query;
 
    if (error) {
      console.error('Auth0 callback error:', error, error_description);
      return res.status(400).json({
        error: 'Authentication failed',
        details: error_description
      });
    }
 
    // Exchange code for tokens using direct HTTP request
    const tokenResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: process.env.AUTH0_CLIENT_ID,
        client_secret: process.env.AUTH0_CLIENT_SECRET,
        code: code,
        redirect_uri: process.env.AUTH0_REDIRECT_URI
      })
    });
 
    const tokenData = await tokenResponse.json();
    const { access_token, id_token } = tokenData;
 
    // Decode JWT ID token to extract custom claims
    const decodedToken = jwt.decode(id_token);
 
    // Extract NATCA custom claims (actual namespace used)
    const memberNumber = decodedToken['https://natcaInfo.net/member_no'];
    const natcaId = decodedToken['https://natcaInfo.net/natca_id'];
 
    // Store complete user session with Auth0 data and custom claims
    req.session.user = {
      sub: decodedToken.sub,
      email: decodedToken.email,
      name: decodedToken.name,
      nickname: decodedToken.nickname,
      picture: decodedToken.picture,
      memberNumber: memberNumber,
      natcaId: natcaId,
      accessToken: access_token,
      idToken: id_token,
      loginTime: new Date().toISOString(),
      // Preserve all Auth0 custom claims for compatibility
      ...Object.keys(decodedToken)
        .filter(key => key.includes('natca'))
        .reduce((acc, key) => {
          acc[key] = decodedToken[key];
          return acc;
        }, {})
    };
 
    // Parse return URL from state parameter
    let returnTo = '/';
    if (state) {
      try {
        const stateData = JSON.parse(Buffer.from(state, 'base64').toString());
        returnTo = stateData.returnTo || '/';
      } catch (e) {
        console.warn('Failed to parse state:', e);
      }
    }
 
    res.redirect(returnTo);
 
  } catch (error) {
    console.error('Auth callback error:', error);
    res.status(500).json({
      error: 'Authentication failed',
      details: error.message
    });
  }
});

Session Management

Redis Configuration (Actual Implementation)

// Platform: server.js - Session store configuration
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
 
// Initialize Redis client for sessions
let redisClient;
if (process.env.REDIS_URL) {
  redisClient = createClient({
    url: process.env.REDIS_URL
  });
 
  redisClient.on('error', (err) => {
    console.error('Redis Client Error:', err);
  });
 
  redisClient.connect().catch(console.error);
}
 
// Session configuration
const sessionConfig = {
  secret: process.env.SESSION_SECRET || 'mynatca-dev-secret-change-in-prod',
  resave: false,
  saveUninitialized: false,
  name: 'mynatca.session',
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    domain: process.env.NODE_ENV === 'production' ? '.natca.org' : undefined,
    sameSite: process.env.NODE_ENV === 'production' ? 'lax' : 'lax'
  }
};
 
// Use Redis store if available
if (redisClient) {
  sessionConfig.store = new RedisStore({
    client: redisClient,
    prefix: 'mynatca:sess:'
  });
  console.log('✅ Using Redis for session storage');
} else {
  console.log('⚠️  Using memory store for sessions (not suitable for production)');
}
 
app.use(session(sessionConfig));

API Endpoints

Authentication Routes

The Platform provides the following authentication endpoints:

GET /api/auth/login

Initiates OAuth 2.0 flow by redirecting to Auth0.

Parameters:

  • returnTo (query, optional): URL to redirect after successful authentication

Implementation:

// routes/auth.js
router.get('/login', (req, res) => {
  const returnTo = req.query.returnTo || '/';
  const state = Buffer.from(JSON.stringify({ returnTo })).toString('base64');
  const redirectUri = process.env.AUTH0_REDIRECT_URI || `http://localhost:1300/api/auth/callback`;
 
  const authUrl = `https://${process.env.AUTH0_DOMAIN}/authorize?` +
    `response_type=code&` +
    `client_id=${process.env.AUTH0_CLIENT_ID}&` +
    `redirect_uri=${encodeURIComponent(redirectUri)}&` +
    `scope=${encodeURIComponent('openid profile email')}&` +
    `state=${encodeURIComponent(state)}`;
 
  res.redirect(authUrl);
});

GET /api/auth/callback

Handles OAuth 2.0 callback from Auth0.

Parameters:

  • code (query, required): Authorization code from Auth0
  • state (query, required): State parameter for CSRF protection

GET /api/auth/session

Returns current authentication status and user information.

Response:

{
  "authenticated": true,
  "user": {
    "sub": "auth0|...",
    "email": "user@natca.org",
    "name": "John Doe",
    "memberNumber": "12345",
    "natcaId": "67890",
    "loginTime": "2024-01-01T00:00:00.000Z"
  }
}

Implementation:

router.get('/session', (req, res) => {
  if (!req.session.user) {
    return res.status(401).json({
      authenticated: false,
      loginUrl: '/api/auth/login'
    });
  }
 
  // Return safe user info (no tokens)
  const { accessToken, idToken, ...safeUser } = req.session.user;
 
  res.json({
    authenticated: true,
    user: safeUser
  });
});

GET /api/auth/logout

Logs out user and destroys session.

Parameters:

  • returnTo (query, optional): URL to redirect after logout

POST /api/auth/service

Service-to-service authentication for internal applications.

Request Body:

{
  "serviceKey": "service-specific-key",
  "serviceName": "discord"
}

Session Management

Session Data Structure

// TypeScript: Session interface
interface PlatformSession {
  user: {
    sub: string;           // Auth0 user ID
    email: string;         // User email address
    name: string;          // Display name
    picture?: string;      // Avatar URL
 
    // Auth0 tokens
    accessToken: string;   // JWE token for Auth0 Management API
    idToken: string;       // JWT token for external API forwarding
 
    // NATCA-specific data from custom claims
    natcaData: {
      natcaId: string;
      memberNumber: string;
      facilityCode: string;
      regionCode: string;
    };
 
    // Session metadata
    loginTime: number;
    lastActivity: number;
  };
}

API Authentication Patterns

Frontend Authentication

Authentication Status Check

// Hub: Check authentication status
export const useAuth0 = () => {
  const user = ref(null);
  const isAuthenticated = ref(false);
  const loading = ref(true);
 
  const checkAuth = async () => {
    try {
      const response = await fetch('/api/auth/me', {
        credentials: 'include'
      });
 
      if (response.ok) {
        user.value = await response.json();
        isAuthenticated.value = true;
      } else {
        user.value = null;
        isAuthenticated.value = false;
      }
    } catch (error) {
      console.error('Auth check failed:', error);
      user.value = null;
      isAuthenticated.value = false;
    } finally {
      loading.value = false;
    }
  };
 
  const login = () => {
    window.location.href = '/api/auth/login';
  };
 
  const logout = async () => {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        credentials: 'include'
      });
 
      user.value = null;
      isAuthenticated.value = false;
 
      // Redirect to home or login
      window.location.href = '/';
    } catch (error) {
      console.error('Logout failed:', error);
    }
  };
 
  return {
    user: readonly(user),
    isAuthenticated: readonly(isAuthenticated),
    loading: readonly(loading),
    checkAuth,
    login,
    logout
  };
};

Authenticated API Requests

// Hub: Authenticated HTTP client
export const createAuthenticatedClient = () => {
  const client = axios.create({
    baseURL: '/api',
    withCredentials: true, // Include session cookies
    timeout: 30000
  });
 
  // Request interceptor
  client.interceptors.request.use(
    (config) => {
      console.log(`🔥 API Request: ${config.method?.toUpperCase()} ${config.url}`);
      return config;
    },
    (error) => {
      console.error('Request error:', error);
      return Promise.reject(error);
    }
  );
 
  // Response interceptor
  client.interceptors.response.use(
    (response) => {
      return response;
    },
    (error) => {
      if (error.response?.status === 401) {
        // Redirect to login on authentication failure
        window.location.href = '/api/auth/login';
      }
      return Promise.reject(error);
    }
  );
 
  return client;
};
 
// Usage
const apiClient = createAuthenticatedClient();
const memberProfile = await apiClient.get('/mynatca/Member/12985');

Backend Authentication Middleware

Session Validation

// Platform: Authentication middleware
const requireAuth = (req, res, next) => {
  if (!req.session?.user) {
    return res.status(401).json({
      success: false,
      error: {
        code: 'UNAUTHORIZED',
        message: 'Authentication required'
      }
    });
  }
 
  // Check token expiration
  const tokenPayload = parseJWT(req.session.user.idToken);
  if (tokenPayload.exp && Date.now() >= tokenPayload.exp * 1000) {
    return res.status(401).json({
      success: false,
      error: {
        code: 'TOKEN_EXPIRED',
        message: 'Session has expired'
      }
    });
  }
 
  // Attach user data to request
  req.user = req.session.user;
  next();
};
 
// Helper function to parse JWT
const parseJWT = (token) => {
  try {
    const payload = token.split('.')[1];
    const decoded = Buffer.from(payload, 'base64').toString();
    return JSON.parse(decoded);
  } catch (error) {
    return {};
  }
};

API Route Protection

// Platform: Protected routes
app.get('/api/auth/me', requireAuth, (req, res) => {
  res.json({
    success: true,
    data: {
      sub: req.user.sub,
      email: req.user.email,
      name: req.user.name,
      picture: req.user.picture,
      natcaData: req.user.natcaData
    }
  });
});
 
app.get('/api/members/:id', requireAuth, async (req, res) => {
  try {
    // Access user's NATCA data
    const { natcaId } = req.user.natcaData;
    const requestedId = req.params.id;
 
    // Authorization: Users can only access their own data
    if (natcaId !== requestedId) {
      return res.status(403).json({
        success: false,
        error: {
          code: 'FORBIDDEN',
          message: 'Access denied to requested resource'
        }
      });
    }
 
    // Proceed with request
    const memberData = await fetchMemberData(requestedId);
    res.json({
      success: true,
      data: memberData
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: {
        code: 'INTERNAL_ERROR',
        message: 'Failed to retrieve member data'
      }
    });
  }
});

Bearer Token Forwarding

Proxy Middleware Implementation

Critical Technical Detail: http-proxy-middleware v3.0 changed callback syntax:

// Platform: middleware/proxy.js - WORKING v3.0 implementation
const { createProxyMiddleware } = require('http-proxy-middleware');
 
const createAuthenticatedProxy = (route) => {
  return createProxyMiddleware({
    target: route.target,
    changeOrigin: true,
    pathRewrite: route.pathRewrite,
 
    // ✅ CORRECT: v3.0 syntax - callbacks in 'on' object
    on: {
      proxyReq: (proxyReq, req, res) => {
        console.log('🔧 Proxy middleware executing for:', req.path);
 
        // Verify session exists
        if (!req.session?.user) {
          console.log('❌ No session found, cannot forward token');
          return;
        }
 
        // Forward JWT ID token (critical: NOT accessToken)
        if (req.session.user.idToken) {
          proxyReq.setHeader('Authorization', `Bearer ${req.session.user.idToken}`);
          console.log('🔑 Forwarding Auth0 ID token to API');
          console.log('🔍 Token preview:', req.session.user.idToken.substring(0, 50) + '...');
        } else {
          console.log('⚠️  ID token not found in session');
        }
      },
 
      proxyRes: (proxyRes, req, res) => {
        // Enable CORS for proxied responses
        const origin = req.headers.origin;
        if (origin) {
          proxyRes.headers['Access-Control-Allow-Origin'] = origin;
          proxyRes.headers['Access-Control-Allow-Credentials'] = 'true';
        }
 
        console.log('📡 Proxy response received:', proxyRes.statusCode);
      }
    }
  });
};
 
// ❌ BROKEN: Old v2.x syntax (callbacks as direct properties)
// const brokenProxy = createProxyMiddleware({
//   onProxyReq: (proxyReq, req, res) => {
//     // This callback never executes in v3.0
//   }
// });

Dynamic Route Configuration

// Platform: Dynamic proxy route setup
const setupProxyRoutes = async () => {
  try {
    // Fetch routes from database
    const { data: routes } = await supabase
      .from('proxy_routes')
      .select('*');
 
    routes.forEach(route => {
      const proxy = createAuthenticatedProxy(route);
      app.use(route.path, proxy);
      console.log(`✅ Proxy route configured: ${route.path} -> ${route.target}`);
    });
  } catch (error) {
    console.error('Failed to setup proxy routes:', error);
  }
};

Security Considerations

Token Security

// Platform: JWT token validation
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-client');
 
const client = jwksClient({
  jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
  cache: true,
  cacheMaxAge: 600000, // 10 minutes
  rateLimit: true,
  jwksRequestsPerMinute: 10
});
 
const verifyToken = async (token) => {
  try {
    const decoded = jwt.decode(token, { complete: true });
    const key = await client.getSigningKey(decoded.header.kid);
    const signingKey = key.getPublicKey();
 
    return jwt.verify(token, signingKey, {
      audience: process.env.AUTH0_AUDIENCE,
      issuer: `https://${process.env.AUTH0_DOMAIN}/`,
      algorithms: ['RS256']
    });
  } catch (error) {
    throw new Error(`Token verification failed: ${error.message}`);
  }
};

Session Security

// Platform: Session security headers
app.use((req, res, next) => {
  // Security headers
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
 
  // CSP for authenticated pages
  if (req.session?.user) {
    res.setHeader('Content-Security-Policy',
      "default-src 'self'; " +
      "script-src 'self' 'unsafe-inline'; " +
      "style-src 'self' 'unsafe-inline'; " +
      "img-src 'self' data: https:; " +
      "connect-src 'self';"
    );
  }
 
  next();
});

Rate Limiting

// Platform: Authentication rate limiting
const rateLimit = require('express-rate-limit');
 
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // Limit to 5 login attempts per IP
  message: {
    success: false,
    error: {
      code: 'RATE_LIMITED',
      message: 'Too many login attempts'
    }
  },
  standardHeaders: true,
  legacyHeaders: false
});
 
app.use('/api/auth/login', authLimiter);

Troubleshooting Authentication Issues

Common Problems and Solutions

1. onProxyReq Callback Not Executing

Problem: Bearer tokens not being forwarded to external APIs

Solution: Update to http-proxy-middleware v3.0 syntax:

// ✅ CORRECT v3.0 syntax
createProxyMiddleware({
  on: {
    proxyReq: (proxyReq, req, res) => { /* Executes properly */ }
  }
});
 
// ❌ BROKEN v2.x syntax
createProxyMiddleware({
  onProxyReq: (proxyReq, req, res) => { /* Never executes */ }
});

2. Token Format Mismatch

Problem: External APIs rejecting authentication tokens

Solution: Use idToken (JWT) instead of accessToken (JWE):

// ✅ CORRECT: JWT format accepted by external APIs
req.session.user.idToken
 
// ❌ WRONG: JWE format rejected by external APIs
req.session.user.accessToken

3. Session Persistence Issues

Problem: Users logged out unexpectedly

Solution: Verify Redis connection and session configuration:

# Check Redis connectivity
redis-cli ping
 
# Monitor Redis session storage
redis-cli monitor

Debug Logging

// Platform: Comprehensive auth debugging
const debugAuth = (req, res, next) => {
  console.log('🔍 Auth Debug Info:');
  console.log('  Session exists:', !!req.session);
  console.log('  User authenticated:', !!req.session?.user);
  console.log('  Has access token:', !!req.session?.user?.accessToken);
  console.log('  Has ID token:', !!req.session?.user?.idToken);
 
  if (req.session?.user?.idToken) {
    const tokenPayload = parseJWT(req.session.user.idToken);
    console.log('  Token expires:', new Date(tokenPayload.exp * 1000));
    console.log('  NATCA ID:', tokenPayload['https://my.natca.org/natca_id']);
  }
 
  next();
};
 
app.use('/api', debugAuth);

This authentication system provides secure, scalable authentication for the MyNATCA ecosystem while maintaining compatibility with external APIs and supporting advanced features like custom claims and Bearer token forwarding.