Skip to main content

Multi-Tenant Authentication & Session Management

Overview

Axiom Genesis implements a subdomain-based multi-tenant authentication system where:

  • Subdomains map to tenant identifiers (e.g., tenant-a.axiom-genesis.com)
  • Nginx reverse proxy injects encrypted tenant ID via HTTP header
  • Unauthenticated users can load tenant-specific login UI without authentication
  • Post-login, tenant ID, user ID, and role are persisted in session and JWT token
  • All subsequent requests automatically include the tenant context for isolation

This ensures secure tenant isolation while maintaining a seamless single sign-on experience across subdomains.


Architecture

Multi-Tenant Request Flow

┌─────────────────────────────────────────────────────────────────┐
│ Subdomain Request │
│ User visits: https://tenant-code.axiom-genesis.com/login │
└────────────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 1. NGINX Reverse Proxy │
│ ├─ Extract subdomain: "tenant-code" │
│ ├─ Get tenant encryption key from environment │
│ ├─ Encrypt tenant ID: AES-256-GCM({type: "tenant", id: "1"}) │
│ └─ Inject header: x-tenant-id: {ciphertext, nonce, salt} │
└────────────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 2. Frontend (Next.js) - Login Page Load │
│ ├─ Request includes x-tenant-id header from nginx │
│ ├─ Validate header is present │
│ ├─ Extract/store encrypted tenant for subsequent requests │
│ └─ Fetch tenant config (logo, colors, settings) WITHOUT auth │
└────────────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 3. Backend - Tenant Data Fetch (Unauthenticated) │
│ ├─ Endpoint: GET /api/v1/auth/tenant-config │
│ ├─ Header: x-tenant-id (encrypted) │
│ ├─ NO JWT required │
│ ├─ Decrypt tenant ID in middleware │
│ ├─ Validate tenant exists in app_tenants table │
│ ├─ Return: logos, colors, app_settings, UI configuration │
│ └─ Store response in Redux for rendering │
└────────────────┬────────────────────────────────────────────────┘

(User enters credentials and submits login form)


┌─────────────────────────────────────────────────────────────────┐
│ 4. Login Submission (signin Endpoint) │
│ ├─ Endpoint: POST /api/v1/auth/signin │
│ ├─ Body: {email_id, password} │
│ ├─ Header: x-tenant-id (encrypted) │
│ ├─ Middleware: validateTenant (decrypt x-tenant-id) │
│ ├─ Controller: authController.signin() │
│ │ ├─ Call: AuthService.validateCredentials() │
│ │ │ └─ Check password against user.password_hash │
│ │ ├─ Call: AuthService.generateToken() │
│ │ │ └─ Create JWT with {user_id, tenant_id, role} │
│ │ └─ Return token, user_id, tenant_id, expires_in │
│ └─ Response: {token, user_id, tenant_id, expires_in} │
└────────────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 5. Frontend - Session Persistence │
│ ├─ Dispatch Redux: setAuth({ token, user_id, expires_in }) │
│ ├─ Dispatch Redux: setTenant({ tenant_id }) │
│ ├─ Store encrypted tenant in sessionStorage │
│ ├─ Redirect to dashboard │
│ └─ apiClient interceptor now includes: │
│ ├─ Authorization: Bearer <JWT_TOKEN> │
│ └─ x-tenant-id: <ENCRYPTED_TENANT_ID> │
└────────────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 6. Authenticated Requests - Automatic Context │
│ ├─ All subsequent API calls include: │
│ │ ├─ JWT token in Authorization header │
│ │ ├─ Encrypted tenant ID in x-tenant-id header │
│ ├─ Backend middleware verifies both headers │
│ ├─ Database queries scoped to tenant │
│ └─ Responses include only tenant-specific data │
└─────────────────────────────────────────────────────────────────┘

Component Responsibilities

1. NGINX Reverse Proxy Configuration

Purpose: Extract subdomain and inject encrypted tenant ID header

Configuration Pattern:

# Map subdomain to tenant ID
map $host $tenant_code {
~^(?<subdomain>.+)\.axiom-genesis\.com$ $subdomain;
~^axiom-genesis\.com$ default;
default '';
}

# Tenant ID mapping (subdomain → numeric ID)
map $tenant_code $tenant_id {
'tenant-a' '1';
'tenant-b' '2';
'default' '1';
default '1';
}

server {
listen 443 ssl http2;
server_name ~^(.+)?\.axiom-genesis\.com$;

# Encrypt tenant ID and inject header
location / {
# Create JSON structure with tenant info
set $tenant_json '{"type":"tenant","id":"$tenant_id"}';

# Encrypt using environment variable (TENANT_ENCRYPTION_KEY)
# Note: In production, use a Lua module or external service for encryption
# This is a simplified example - actual encryption must happen server-side

# Inject encrypted tenant ID as header
proxy_set_header x-tenant-id $encrypted_tenant_id;

# Forward other essential headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# Proxy to frontend
proxy_pass http://frontend:3000;
}
}

Key Points:

  • Extracts subdomain from Host header
  • Maps subdomain to tenant ID from configuration
  • Encrypts tenant ID using AES-256-GCM before injection (uses TENANT_ENCRYPTION_KEY)
  • Passes encrypted value in x-tenant-id header
  • All requests from that subdomain automatically include this header

2. Frontend (Next.js) - Login Page

Purpose: Load tenant-specific UI without authentication

Header Retrieval & Storage

File: src/lib/encrypted-tenant-utils.ts

/**
* Retrieve encrypted tenant from:
* 1. Session storage (cached from previous header)
* 2. HTTP header (from nginx on initial page load - browser reads via document metadata)
* 3. Fallback to default tenant_id=1
*/
export async function getEncryptedTenantId(): Promise<string | null> {
// Check session storage first (cached from login)
let encrypted = getStoredEncryptedTenant();
if (encrypted) return encrypted;

// Check for nginx header (set as data attribute on document)
const headerValue = (document as any).__NGINX_X_TENANT_ID;
if (headerValue) {
storeEncryptedTenant(headerValue);
return headerValue;
}

// Fallback to default context
return null;
}

/**
* Store encrypted tenant in sessionStorage
* This persists across page reloads in same browser session
*/
export function storeEncryptedTenant(encryptedTenant: string): void {
if (typeof window !== "undefined") {
sessionStorage.setItem("encrypted-tenant-id", encryptedTenant);
}
}

API Client Request Interceptor

File: src/lib/apiClient.ts

import {
getStoredEncryptedTenant,
getEncryptedTenantId,
} from "./encrypted-tenant-utils";

/**
* Request Interceptor: Adds Authentication Token & Tenant Header
* Automatically includes x-tenant-id for all outgoing requests
*/
apiClient.interceptors.request.use(
async (config) => {
// 1. Add JWT token from Redux store (if authenticated)
let token: string | null = null;
if (isStoreInitialized()) {
const state = getStoreState();
token = state.auth.token;
}
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}

// 2. Add encrypted tenant ID header
// Priority:
// a) Stored encrypted tenant (from session storage)
// b) Fetch with smart fallback (header → default=1)
try {
let encryptedTenant = getStoredEncryptedTenant();

if (!encryptedTenant) {
encryptedTenant = await getEncryptedTenantId();
}

if (encryptedTenant) {
config.headers["x-tenant-id"] = encryptedTenant;
}
} catch (error) {
// Continue without tenant header - let server handle
console.warn("Failed to set x-tenant-id header:", error);
}

return config;
},
(error) => Promise.reject(error),
);

Login Page Flow

File: src/app/login/page.tsx (example)

import { useEffect, useState } from 'react';
import { useAppDispatch } from '@/hooks/useRedux';
import { setTenant } from '@/store/slices/tenantSlice';
import api from '@/services/api';

export default function LoginPage() {
const dispatch = useAppDispatch();
const [tenantConfig, setTenantConfig] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// Step 1: Fetch tenant configuration (no auth required)
// The x-tenant-id header is automatically added by apiClient interceptor
const fetchTenantConfig = async () => {
try {
// GET /api/v1/auth/tenant-config
// This endpoint DOES NOT require JWT authentication
// It validates tenant ID from x-tenant-id header
const response = await api.auth.getTenantConfig();

setTenantConfig(response.data);

// Store tenant ID in Redux for other components
if (response.data.tenant_id) {
dispatch(setTenant({ tenant_id: response.data.tenant_id }));
}
} catch (error) {
console.error('Failed to load tenant configuration:', error);
} finally {
setLoading(false);
}
};

fetchTenantConfig();
}, [dispatch]);

const handleLogin = async (email: string, password: string) => {
try {
// POST /api/v1/auth/signin
// x-tenant-id header is automatically added by apiClient
const response = await api.auth.signin({
email_id: email,
password
});

// Step 2: Persist authentication in Redux
dispatch(setAuth({
token: response.data.token,
user_id: response.data.user_id,
expires_in: response.data.expires_in
}));

// Step 3: Persist tenant ID
dispatch(setTenant({
tenant_id: response.data.tenant_id
}));

// Step 4: Encrypt and store tenant for future requests
const encrypted = await encryptAndStoreTenant(response.data.tenant_id);

// Redirect to dashboard
router.push('/dashboard');
} catch (error) {
console.error('Login failed:', error);
}
};

return (
<div>
{/* Render tenant-specific UI from tenantConfig */}
<img src={tenantConfig?.logo_url} alt="Tenant Logo" />
<LoginForm onSubmit={handleLogin} />
</div>
);
}

3. Backend Middleware - Request Validation

Purpose: Decrypt tenant ID and validate tenant existence

File: middleware/validate-tenant.middleware.js

/**
* Tenant Header Validation Middleware
* - Validates x-tenant-id header format
* - Decrypts encrypted tenant ID
* - Attaches req.tenantId for downstream handlers
*/

const { ValidationError } = require("../utils/errors/AppError");
const { decryptTenantId } = require("../helpers/tenant-encryption.helper");

const validateTenant = (req, res, next) => {
try {
const encryptedTenantId = req.headers["x-tenant-id"];

// 1. Validate header exists
if (!encryptedTenantId) {
throw new ValidationError("Tenant ID is required in headers", {
"x-tenant-id": "required",
});
}

// 2. Validate header format (must be encryption JSON)
let isValidEncrypted = false;
try {
const parsed = JSON.parse(encryptedTenantId);
isValidEncrypted = parsed.ciphertext && parsed.nonce && parsed.salt;
} catch (e) {
// Not valid JSON with encryption fields
}

if (!isValidEncrypted) {
throw new ValidationError(
"Invalid tenant ID format - must be encrypted",
{
"x-tenant-id": "invalid_format_not_encrypted",
},
);
}

// 3. Decrypt tenant ID
try {
const masterKey = process.env.TENANT_ENCRYPTION_KEY;
if (!masterKey) {
throw new Error("Encryption key not configured on server");
}

const decryptedTenantId = decryptTenantId(encryptedTenantId, masterKey);

// Attach to request for use in controllers
req.tenantId = decryptedTenantId.trim();

next();
} catch (decryptError) {
throw new ValidationError("Failed to decrypt tenant ID", {
"x-tenant-id": "decryption_failed",
});
}
} catch (error) {
next(error);
}
};

module.exports = { validateTenant };

Key Points:

  • Validates encrypted format (must be JSON with ciphertext, nonce, salt)
  • Decrypts using TENANT_ENCRYPTION_KEY environment variable
  • Attaches req.tenantId for downstream controllers
  • Throws validation error if format is invalid or decryption fails

4. Backend Controller - Authentication

Purpose: Validate credentials and generate JWT token with tenant context

File: controllers/auth.controller.js

Signin Endpoint

/**
* POST /auth/signin
* @body {string} email_id - User email
* @body {string} password - User password
* @header {string} x-tenant-id - Encrypted tenant ID (required)
* @returns {object} {token, user_id, tenant_id, expires_in, expires_at}
*/
AuthController.signin = asyncHandler(async (req, res) => {
const { email_id, password, device_token } = req.body;
const tenantId = req.tenantId; // From validateTenant middleware

// 1. Validate input
if (!email_id || !password) {
throw new ValidationError("Email and password are required", {
email_id: email_id ? null : "required",
password: password ? null : "required",
});
}

// 2. Validate credentials against tenant-specific user record
// This ensures cross-tenant isolation
const user = await AuthService.validateCredentials(
email_id,
password,
tenantId,
);

// 3. Generate JWT token with tenant context
const tokenResult = await AuthService.generateToken(user);

// 4. Load tenant settings for client-side configuration
let tenantSettings = null;
try {
tenantSettings = await AuthService.getTenantSettings(tenantId);
} catch (settingsError) {
Logger.warn("signin_tenant_settings_failed", {
tenant_id: tenantId,
error: settingsError.message,
});
}

// 5. Log successful authentication
Logger.info("user_signin_successful", {
email_id,
tenant_id: tenantId,
user_id: user.id,
});

// 6. Return authentication response
return res.success(
{
message: "Signin successful",
data: {
token: tokenResult.token, // JWT with tenant_id embedded
user_id: user.id,
tenant_id: tenantId, // Also returned explicitly
expires_in: tokenResult.expiresIn, // Token TTL in seconds
expires_at: tokenResult.expiresAt, // Absolute expiration timestamp
type: tokenResult.type, // "Bearer"
tenantSettings: tenantSettings, // For client-side config
},
},
200,
);
});

Token Generation Service

File: services/auth.service.js

/**
* Generate JWT token for authenticated user
* Includes tenant_id in token payload for verification
*/
static async generateToken(user) {
const payload = {
user_id: user.id,
email: user.email_id,
tenant_id: user.tenant_id, // CRITICAL: Embed tenant ID in JWT
role_id: user.role_id,
iat: Math.floor(Date.now() / 1000),
};

// Token expires in 24 hours (or configured value)
const expiresIn = process.env.JWT_EXPIRES_IN || '24h';

const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: expiresIn,
algorithm: 'HS256'
});

return {
token: token,
expiresIn: ms(expiresIn) / 1000, // Convert to seconds
expiresAt: new Date(Date.now() + ms(expiresIn)).toISOString(),
type: 'Bearer'
};
}

5. Backend Route Configuration

Purpose: Apply middleware to routes for tenant-scoped authentication

File: routes/auth.routes.js

const { Router } = require("express");
const { validateTenant } = require("../middleware/validate-tenant.middleware");
const authController = require("../controllers/auth.controller");

const router = Router();

/**
* PUBLIC ROUTES (Tenant-ID required, but NO JWT authentication)
* These endpoints use validateTenant to decrypt x-tenant-id
* but do NOT require JWT token
*/

// Get tenant configuration (for login page rendering)
router.get(
"/tenant-config",
validateTenant, // ← Validates & decrypts x-tenant-id
authController.getTenantConfig, // ← No auth required
);

// Signin endpoint
router.post(
"/signin",
validateTenant, // ← Validates & decrypts x-tenant-id
authValidator.signin, // ← Validates email/password format
authController.signin, // ← Generates JWT token
);

/**
* PROTECTED ROUTES (Both x-tenant-id AND JWT required)
* These use both validateTenant and JWT middleware
*/

router.post(
"/logout",
validateTenant, // ← Validates x-tenant-id
authenticate, // ← Validates JWT token
authController.logout,
);

router.get(
"/profile",
validateTenant, // ← Validates x-tenant-id
authenticate, // ← Validates JWT token
authController.getProfile,
);

module.exports = router;

Encryption & Security

Tenant ID Encryption Strategy

Algorithm: AES-256-GCM (Authenticated Encryption)

Encryption Process (Nginx → Frontend):

// JSON structure to encrypt
const tenantData = {
type: "tenant",
id: "1", // Numeric tenant ID
timestamp: 1710235200 // Optional: prevent replay attacks
};

// Encryption parameters
const algorithm = 'aes-256-gcm';
const key = process.env.TENANT_ENCRYPTION_KEY; // 32 bytes (256 bits)
const iv = crypto.randomBytes(16); // 16 bytes
const salt = crypto.randomBytes(16); // 16 bytes (for key derivation)

// Encrypt
const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.write(JSON.stringify(tenantData));
const ciphertext = cipher.update(..., 'utf8', 'hex');
const authTag = cipher.getAuthTag();

// Format for transmission
const encrypted = JSON.stringify({
ciphertext: ciphertext,
nonce: iv.toString('hex'),
salt: salt.toString('hex'),
authTag: authTag.toString('hex'),
algorithm: 'aes-256-gcm'
});

// Send as x-tenant-id header
// x-tenant-id: {ciphertext, nonce, salt, authTag, algorithm}

Decryption Process (Backend):

// Receive encrypted header
const encryptedHeader = req.headers["x-tenant-id"];
const encrypted = JSON.parse(encryptedHeader);

// Decryption parameters
const key = process.env.TENANT_ENCRYPTION_KEY;
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
key,
Buffer.from(encrypted.nonce, "hex"),
);

decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));

// Decrypt
const decrypted = decipher.update(encrypted.ciphertext, "hex", "utf8");
decrypted += decipher.final("utf8");

// Parse & use
const tenantData = JSON.parse(decrypted);
req.tenantId = tenantData.id;

Benefits:

  • Confidentiality: AES-256 provides military-grade encryption
  • Integrity: GCM mode prevents tampering/modification
  • Authentication: Auth tag proves data wasn't altered
  • Freshness: Timestamp in payload can prevent replay attacks
  • Transparency: Header is base64-encoded JSON, not opaque binary

Redux State Management

Authentication State

File: src/store/slices/authSlice.ts

interface AuthState {
token: string | null; // JWT token from signin
user_id: string | null; // User ID
role_id: string | null; // User role
is_authenticated: boolean; // Boolean flag
expires_at: string | null; // Token expiration timestamp
loading: boolean; // During signin/logout
}

const authSlice = createSlice({
name: "auth",
initialState: {
token: null,
user_id: null,
role_id: null,
is_authenticated: false,
expires_at: null,
loading: false,
},
reducers: {
setAuth: (state, action) => {
state.token = action.payload.token;
state.user_id = action.payload.user_id;
state.role_id = action.payload.role_id;
state.is_authenticated = true;
state.expires_at = action.payload.expires_at;
// Auto-save to localStorage for persistence
localStorage.setItem("auth", JSON.stringify(state));
},
clearAuth: (state) => {
state.token = null;
state.user_id = null;
state.role_id = null;
state.is_authenticated = false;
state.expires_at = null;
localStorage.removeItem("auth");
},
},
});

Tenant State

File: src/store/slices/tenantSlice.ts

interface TenantState {
tenant_id: string | null; // Current tenant ID (from signin)
tenant_name: string | null; // Display name
logo_url: string | null; // Tenant-specific logo
colors: Record<string, string>; // Theming
settings: any; // Custom settings
}

const tenantSlice = createSlice({
name: "tenant",
initialState: {
tenant_id: null,
tenant_name: null,
logo_url: null,
colors: {},
settings: null,
},
reducers: {
setTenant: (state, action) => {
state.tenant_id = action.payload.tenant_id;
state.tenant_name = action.payload.tenant_name;
state.logo_url = action.payload.logo_url;
state.colors = action.payload.colors || {};
state.settings = action.payload.settings;
// Persist to sessionStorage
sessionStorage.setItem("tenant", JSON.stringify(state));
},
clearTenant: (state) => {
state.tenant_id = null;
state.tenant_name = null;
state.logo_url = null;
state.colors = {};
state.settings = null;
sessionStorage.removeItem("tenant");
},
},
});

Error Handling & Edge Cases

Missing x-tenant-id Header

Scenario: User bypasses nginx (e.g., direct API call without subdomain)

Frontend Response (apiClient):

// Header not available
// Fall back to getEncryptedTenantId() which returns null
// Request made WITHOUT x-tenant-id header

Backend Response (validateTenant middleware):

// Error: "Tenant ID is required in headers"
// Status: 400 Bad Request
// User redirected to login page with error message

Invalid/Corrupted Encryption

Scenario: Header modified in transit or encrypted with wrong key

Frontend: Handled gracefully, continues without header

Backend Response:

// Error: "Failed to decrypt tenant ID"
// Status: 400 Bad Request
// Log warning with full error details for debugging
// Prevent database queries until resolved

Token Expiration During Session

Scenario: User's JWT token expires after 24 hours

Frontend Response (apiClient interceptor):

// Detect 401 Unauthorized response
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Clear auth state
dispatch(clearAuth());
// Clear user session
clearEncryptedTenant();
// Redirect to login
router.push("/login");
}
return Promise.reject(error);
},
);

Cross-Tenant Security

Scenario: User from Tenant A tries to access Tenant B's resources

Protection Layers:

  1. Header Validation: validateTenant middleware verifies encryption
  2. JWT Validation: Token includes tenant_id which is verified
  3. Query Scoping: All database queries include WHERE tenant_id = req.tenantId
  4. Authorization: Role-based access control applies within tenant scope

Configuration

Environment Variables

Frontend (.env.local or .env.development):

# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_API_TIMEOUT=30000

# Tenant Encryption
NEXT_PUBLIC_TENANT_ENCRYPTION_KEY=your-32-byte-base64-encoded-key

Backend (.env):

# Server Configuration
NODE_ENV=development
PORT=3001
LOG_LEVEL=info

# JWT Configuration
JWT_SECRET=your-super-secret-jwt-signing-key
JWT_EXPIRES_IN=24h

# Tenant Encryption (MUST match frontend key)
TENANT_ENCRYPTION_KEY=your-32-byte-base64-encoded-key

# Database (Configuration DB - stores app_tenants, users, etc.)
DB_HOST=localhost
DB_PORT=1433
DB_USER=sa
DB_PASSWORD=MyPassword123!
DB_DATABASE=RADICS_CFG

# Redis (for session storage & caching)
REDIS_URL=redis://localhost:6379

API Endpoints

Public Endpoints (Tenant ID required, no JWT)

EndpointMethodHeaderBodyReturns
/auth/tenant-configGETx-tenant-id-{logo_url, colors, settings, tenant_name}
/auth/signinPOSTx-tenant-id{email_id, password}{token, user_id, tenant_id, expires_in}
/auth/signup-otpPOSTx-tenant-id{email_id}{message, email_id}
/auth/create-accountPOSTx-tenant-id{password, session}{token, user_id, tenant_id}

Protected Endpoints (Both headers required)

EndpointMethodHeadersBodyReturns
/auth/logoutPOSTx-tenant-id, Authorization-{message}
/auth/profileGETx-tenant-id, Authorization-{user_id, email_id, role_id, ...}
/workflowsGETx-tenant-id, Authorization-[{id, name, status, ...}]
/recordsGETx-tenant-id, Authorization-[{record data}]

Testing Multi-Tenant Auth

Prerequisites

  • Both frontend and backend running
  • MSSQL database with app_tenants and users seeded
  • Encryption keys configured in environment

Test Flow

# 1. Start with subdomain (nginx will inject x-tenant-id)
# Browser: https://tenant-a.axiom-genesis.local/login

# 2. Inspect network request to /api/v1/auth/tenant-config
# Should see:
# Request Header: x-tenant-id: {encrypted json}
# Response: {logo_url, colors, tenant_name}

# 3. Submit login form
# Should see:
# Request: POST /api/v1/auth/signin
# Body: {email_id: "user@tenant-a.com", password: "..."}
# Header: x-tenant-id: {encrypted}
# Response: {token, user_id, tenant_id}

# 4. Check Redux state (DevTools)
# auth.token = "eyJhbGc..."
# auth.user_id = "uuid"
# tenant.tenant_id = "1"

# 5. Verify subsequent requests include auth
# GET /api/v1/workflows
# Headers:
# x-tenant-id: {encrypted}
# Authorization: Bearer eyJhbGc...

Curl Testing

# Get tenant config (no auth needed)
curl -X GET http://localhost:3001/api/v1/auth/tenant-config \
-H "x-tenant-id: {encrypted-tenant-id}"

# Signin
curl -X POST http://localhost:3001/api/v1/auth/signin \
-H "x-tenant-id: {encrypted-tenant-id}" \
-H "Content-Type: application/json" \
-d '{"email_id":"user@example.com","password":"password123"}'

# Logout (requires JWT)
curl -X POST http://localhost:3001/api/v1/auth/logout \
-H "x-tenant-id: {encrypted-tenant-id}" \
-H "Authorization: Bearer {jwt-token}"

Common Issues & Debugging

Issue: "Tenant ID is required in headers"

Causes:

  • Nginx not injecting header
  • Frontend not sending header on requests
  • Header lost during redirect

Debug Steps:

# 1. Check browser network tab
# - Request to /api/v1/auth/signin should show x-tenant-id header

# 2. Verify nginx config is active
# nginx -t && nginx -s reload

# 3. Check browser console
# console.log(getStoredEncryptedTenant()) # Should return encrypted value

# 4. Check apiClient interceptor
# - Add console.log in request interceptor to see what headers are being sent

Issue: "Failed to decrypt tenant ID"

Causes:

  • TENANT_ENCRYPTION_KEY mismatch between frontend and backend
  • Header corrupted during transmission
  • Wrong encryption format

Debug Steps:

# 1. Verify encryption keys match
# Frontend: NEXT_PUBLIC_TENANT_ENCRYPTION_KEY
# Backend: TENANT_ENCRYPTION_KEY
# echo $TENANT_ENCRYPTION_KEY # Should be same 32-byte base64 string

# 2. Check header format in network tab
# x-tenant-id should be valid JSON: {"ciphertext": "...", "nonce": "...", "salt": "...", "authTag": "..."}

# 3. Enable debug logging in middleware
# Add: console.log('Encrypted header:', encryptedTenantId);

Issue: User logged in to Tenant A, seen data from Tenant B

Causes:

  • Missing WHERE tenant_id = ? in database query
  • JWT token not validated
  • x-tenant-id header ignored

Debug Steps:

# 1. Check database query
# SELECT * FROM workflows WHERE tenant_id = @tenant_id

# 2. Print req.tenantId in controller
# console.log('Tenant ID from header:', req.tenantId);

# 3. Verify JWT payload includes tenant_id
# jwt.decode(token) # Should show {"tenant_id": "1", ...}

# 4. Check database connection routing
# Ensure queries use correct tenant database


Summary

Axiom Genesis Multi-Tenant Authentication:

  1. Subdomain Routing: Nginx extracts tenant code from subdomain (e.g., tenant-a.domain.com)
  2. Header Injection: NGINX encrypts tenant ID and injects as x-tenant-id header
  3. Unauthenticated Tenant Load: Frontend fetches tenant config without JWT
  4. User Signin: Validates credentials, generates JWT with tenant_id embedded
  5. Session Persistence: Redux stores token, sessions store encrypted tenant ID
  6. Automatic Context: All subsequent requests include both headers for multi-tenant isolation
  7. Security: Encryption at REST, HTTPS in transit, JWT validation, query scoping

This design ensures secure, scalable multi-tenancy while maintaining a seamless user experience across subdomains.