3 * @brief Authentication and tenant impersonation API endpoints
4 * @description Provides authentication endpoints for tenant impersonation in multi-tenant
5 * MSP environment. Allows root/MSP users to impersonate other tenants for support and
6 * administration purposes, with full audit logging. Includes endpoints for starting and
7 * stopping impersonation sessions.
10 * - Tenant impersonation (MSP → Customer tenant)
11 * - Exit impersonation (return to root tenant)
12 * - Full audit trail of impersonation sessions
13 * - IP address and user agent tracking
14 * - JWT token generation with impersonation context
17 * - Only root/MSP/admin roles can impersonate
18 * - All impersonation events are logged
19 * - JWT tokens contain impersonation flag
20 * - 2-hour token expiration
21 * @author Independent Business Group
25 * @requires jsonwebtoken
26 * @requires ../services/db
27 * @requires ../middleware/auth
28 * @requires ../middleware/tenant
31const express = require('express');
32const router = express.Router();
33const jwt = require('jsonwebtoken');
34const pool = require('../services/db');
35const authenticateToken = require('../middleware/auth');
36const { setTenantContext } = require('../middleware/tenant');
39 * @api {post} /auth/exit-impersonation Exit tenant impersonation
40 * @apiName ExitImpersonation
41 * @apiGroup Authentication
42 * @apiDescription Exits current tenant impersonation session and returns user to
43 * their root tenant context. Issues new JWT token without impersonation flag. Records
44 * the impersonation stop event in audit log with IP address and user agent for
47 * **Use Case:** MSP admin who was impersonating a customer tenant needs to return
48 * to their root tenant to manage other customers or access MSP-level features.
49 * @apiHeader {string} Authorization Bearer JWT token with impersonation flag
50 * @apiSuccess {string} token New JWT token for root tenant
51 * @apiSuccess {string} message Success message
52 * @apiSuccess {object} user User object with root tenant context
53 * @apiError {string} error Error message
54 * @apiError {number} 403 User is not MSP/root/admin role
55 * @apiError {number} 500 Token generation or audit logging failed
56 * @apiExample {curl} Example Request:
58 * -H "Authorization: Bearer eyJhbGc..." \
59 * "https://api.everydaytech.au/auth/exit-impersonation"
60 * @apiExample {json} Success Response:
63 * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
64 * "message": "Exited impersonation mode",
66 * "user_id": "uuid-here",
67 * "tenant_id": "00000000-0000-0000-0000-000000000001",
69 * "impersonated": false
73 * @see {@link module:routes/auth.impersonate} for starting impersonation
75router.post('/exit-impersonation', authenticateToken, setTenantContext, async (req, res) => {
77 if (!req.user || !(req.user.role === 'msp' || req.user.role === 'root' || req.user.role === 'admin')) {
78 return res.status(403).json({ error: 'Only MSP/root/admin can exit impersonation' });
80 // Issue new JWT for root context (no impersonation)
81 const rootTenantId = '00000000-0000-0000-0000-000000000001';
83 user_id: req.user.impersonator_id || req.user.user_id,
84 tenant_id: rootTenantId,
85 tenantId: rootTenantId,
87 name: req.user.impersonator_name || req.user.name,
88 email: req.user.email,
89 is_msp: req.user.role === 'msp' || req.user.role === 'root',
92 // Remove impersonation-specific fields
93 delete payload.impersonator_id;
94 delete payload.impersonator_name;
95 const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '2h' });
98 const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
99 let impersonatorId = req.user.user_id;
100 if (!uuidRegex.test(String(impersonatorId))) {
101 impersonatorId = req.user.tenantId && uuidRegex.test(String(req.user.tenantId))
103 : '00000000-0000-0000-0000-000000000000';
106 `INSERT INTO impersonation_audit (
107 impersonator_id, impersonator_name, impersonated_tenant_id, impersonated_tenant_name, action, ip_address, user_agent
108 ) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
116 req.headers['user-agent'] || ''
120 console.error('[auth/exit-impersonation] Audit log insert error:', auditErr);
123 // Set token as HTTP-only cookie
124 // Use SameSite=None for production cross-origin (App Platform separate domains)
125 res.cookie('auth_token', token, {
127 secure: true, // Always require HTTPS in production
128 sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
129 maxAge: 2 * 60 * 60 * 1000, // 2 hours
130 domain: process.env.NODE_ENV === 'production' ? '.ondigitalocean.app' : undefined
135 console.error('[auth/exit-impersonation] Error:', err);
136 res.status(500).json({ error: 'Failed to exit impersonation', details: err.message });
141 * @api {post} /auth/impersonate Start tenant impersonation
142 * @apiName ImpersonateTenant
143 * @apiGroup Authentication
144 * @apiDescription Starts tenant impersonation session, allowing MSP/admin users to
145 * access another tenant's context as if they were that tenant. Issues new JWT token
146 * with impersonated tenant context. Records impersonation start event in audit log
147 * with full details for compliance and security tracking.
149 * **Use Case:** MSP support staff needs to troubleshoot issues in a customer's
150 * account by viewing their dashboard, tickets, and data as they would see it.
152 * **Security Considerations:**
153 * - Only root, MSP, and admin roles can impersonate
154 * - All impersonation sessions are audit logged
155 * - Impersonated flag is set in JWT for transparency
156 * - IP address and user agent tracked
157 * - 2-hour session expiration
158 * @apiHeader {string} Authorization Bearer JWT token (root/MSP/admin)
159 * @apiHeader {string} Content-Type application/json
160 * @apiParam {string} tenant_id UUID of tenant to impersonate
161 * @apiSuccess {string} token New JWT token with impersonated tenant context
162 * @apiSuccess {string} message Success message with tenant name
163 * @apiSuccess {object} user User object with impersonated tenant context
164 * @apiSuccess {string} user.tenant_id Impersonated tenant ID
165 * @apiSuccess {boolean} user.impersonated Always true
166 * @apiSuccess {string} user.role User's original role
167 * @apiError {string} error Error message
168 * @apiError {number} 400 tenant_id is required
169 * @apiError {number} 403 User is not MSP/root/admin role
170 * @apiError {number} 404 Tenant not found
171 * @apiError {number} 500 Token generation or audit logging failed
172 * @apiExample {curl} Example Request:
174 * -H "Authorization: Bearer eyJhbGc..." \
175 * -H "Content-Type: application/json" \
176 * -d '{"tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}' \
177 * "https://api.everydaytech.au/auth/impersonate"
178 * @apiExample {json} Success Response:
181 * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
182 * "message": "Successfully impersonating Acme Corporation",
184 * "user_id": "original-user-id",
185 * "tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
187 * "impersonated": true,
188 * "original_tenant_id": "00000000-0000-0000-0000-000000000001"
192 * @apiExample {json} Error Response (Not Authorized):
193 * HTTP/1.1 403 Forbidden
195 * "error": "Only MSP/root/admin can impersonate tenants"
199 * @see {@link module:routes/auth.exitImpersonation} for stopping impersonation
200 * @see {@link module:middleware/tenant.setTenantContext} for tenant filtering
202router.post('/impersonate', authenticateToken, setTenantContext, async (req, res) => {
204 const { tenant_id } = req.body;
205 if (!tenant_id) return res.status(400).json({ error: 'Missing tenant_id' });
208 console.log('[auth/impersonate] Request from user:', {
209 user_id: req.user?.user_id,
210 role: req.user?.role,
211 name: req.user?.name,
212 tenant_id: req.user?.tenant_id
215 if (!req.user || !(req.user.role === 'msp' || req.user.role === 'root' || req.user.role === 'admin')) {
216 console.log('[auth/impersonate] 403 - User role not authorized:', req.user?.role);
217 return res.status(403).json({ error: 'Only MSP, root, or admin can impersonate tenants', user_role: req.user?.role });
220 // Lookup impersonated tenant
221 const tenantRes = await pool.query('SELECT name FROM tenants WHERE tenant_id = $1', [tenant_id]);
222 if (tenantRes.rows.length === 0) {
223 return res.status(404).json({ error: 'Tenant not found' });
225 const impersonatedTenantName = tenantRes.rows[0].name;
227 // Issue new JWT for impersonated tenant
228 // IMPORTANT: Only include specific fields to avoid carrying over is_msp=true
230 user_id: req.user.user_id,
235 email: req.user.email,
236 is_msp: false, // Act as regular tenant user, not MSP
238 impersonator_id: req.user.user_id,
239 impersonator_name: req.user.name,
240 impersonated_tenant_name: impersonatedTenantName
244 token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '2h' });
246 console.error('[auth/impersonate] JWT sign error:', jwtErr, 'Payload:', payload);
247 return res.status(500).json({ error: 'JWT sign error', details: jwtErr.message });
252 // Use a valid UUID for impersonator_id
253 const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
254 let impersonatorId = req.user.user_id;
255 if (!uuidRegex.test(String(impersonatorId))) {
256 impersonatorId = req.user.tenantId && uuidRegex.test(String(req.user.tenantId))
258 : '00000000-0000-0000-0000-000000000000'; // fallback null UUID
261 `INSERT INTO impersonation_audit (
262 impersonator_id, impersonator_name, impersonated_tenant_id, impersonated_tenant_name, action, ip_address, user_agent
263 ) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
268 impersonatedTenantName,
271 req.headers['user-agent'] || ''
275 console.error('[auth/impersonate] Audit log insert error:', auditErr, {
276 impersonator_id: req.user.user_id,
277 impersonator_name: req.user.name,
278 impersonated_tenant_id: tenant_id,
279 impersonated_tenant_name: impersonatedTenantName,
281 user_agent: req.headers['user-agent'] || ''
283 return res.status(500).json({ error: 'Audit log insert error', details: auditErr.message });
286 // Set token as HTTP-only cookie
287 // Use SameSite=None for production cross-origin (App Platform separate domains)
288 res.cookie('auth_token', token, {
290 secure: true, // Always require HTTPS in production
291 sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
292 maxAge: 2 * 60 * 60 * 1000, // 2 hours
293 domain: process.env.NODE_ENV === 'production' ? '.ondigitalocean.app' : undefined
298 console.error('[auth/impersonate] Error:', err, {
304 res.status(500).json({ error: 'Failed to impersonate tenant', details: err.message });
308module.exports = router;