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 Auth0state(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.accessToken3. 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 monitorDebug 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.