Support teams need to reproduce issues that users report. Sometimes that means looking at a user's data, sometimes it means stepping through a flow that is broken for that specific account. "User impersonation" is the capability that lets a support engineer access the application from the user's perspective without knowing their password. Done poorly, impersonation creates a backdoor with no audit trail that any support employee can abuse to access any account at any time. Done well, it is a controlled, fully audited capability that is essential for effective support.
Why a separate token is non-negotiable
The first design decision: impersonation sessions must use a distinct token type, never a regular user session token. The impersonation token carries both the impersonator's identity and the target user's identity. This distinction must be visible in every audit log entry and must be checked before any action that should not be available during impersonation.
interface ImpersonationToken {
iss: string;
iat: number;
exp: number;
jti: string;
// The support engineer who initiated impersonation
actor_id: string;
actor_email: string;
// The user being impersonated
sub: string; // target user ID
org: string; // target org ID
// Marks this as an impersonation session — never issue a normal token with this
impersonation: true;
// Ticket reference for audit purposes
ticket_id?: string;
}
// Issue impersonation token
async function issueImpersonationToken(
actorId: string,
targetUserId: string,
ticketId?: string
): Promise<string> {
// Authorization check: actor must have impersonation permission
const actor = await db.users.findById(actorId);
if (!actor.permissions.includes('support:impersonate')) {
throw new Error('impersonation_not_authorized');
}
const target = await db.users.findById(targetUserId);
const payload: ImpersonationToken = {
iss: 'https://auth.example.com',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour max
jti: crypto.randomUUID(),
actor_id: actorId,
actor_email: actor.email,
sub: targetUserId,
org: target.orgId,
impersonation: true,
ticket_id: ticketId,
};
// Log the impersonation start event
await auditLog.record({
event: 'impersonation.started',
actorId,
targetUserId,
ticketId,
ip: getCurrentRequestIP(),
timestamp: new Date(),
});
return signJWT(payload, IMPERSONATION_SIGNING_KEY);
}
Audit logging every action
A single audit entry at impersonation start is not enough. Every meaningful action taken during an impersonation session should be attributed to the impersonating actor, not to the target user. This is critical for compliance and for investigating incidents: if data is modified during an impersonation session, the audit log must show that the modification was made by a support engineer, not by the user themselves.
// Middleware: extract actor context from impersonation token
function resolveRequestContext(req: Request): RequestContext {
const token = extractBearerToken(req);
const payload = verifyJWT(token);
if (payload.impersonation) {
return {
userId: payload.sub, // The user being impersonated (for data access)
orgId: payload.org,
isImpersonated: true,
actorId: payload.actor_id, // The real actor for audit purposes
actorEmail: payload.actor_email,
};
}
return {
userId: payload.sub,
orgId: payload.org,
isImpersonated: false,
actorId: payload.sub,
actorEmail: null,
};
}
// Audit log helper: always use actor, not userId
async function recordAuditEvent(ctx: RequestContext, event: string, metadata: object) {
await db.auditEvents.create({
event,
userId: ctx.userId, // Whose data was affected
actorId: ctx.actorId, // Who actually did it
isImpersonated: ctx.isImpersonated,
actorEmail: ctx.actorEmail,
metadata,
timestamp: new Date(),
ip: getCurrentRequestIP(),
});
}
Impersonation scope limitations
Even with full read access, certain actions should be blocked during impersonation. A support engineer should never be able to change a user's password, update billing information, link or unlink authentication methods, or modify MFA settings — even if the regular user can do these things. These capabilities represent durable security changes to the account that must only happen when the account owner authenticates directly.
// Block sensitive operations during impersonation
function blockDuringImpersonation(req: Request, res: Response, next: NextFunction) {
const ctx = req.requestContext;
if (ctx.isImpersonated) {
return res.status(403).json({
error: 'action_not_available_during_impersonation',
message: 'This action requires the account owner to be directly authenticated.',
});
}
next();
}
// Apply to sensitive routes
router.post('/account/change-password', blockDuringImpersonation, changePasswordHandler);
router.put('/account/mfa', blockDuringImpersonation, updateMFAHandler);
router.post('/account/link/:provider', blockDuringImpersonation, linkOAuthHandler);
router.delete('/billing/payment-method', blockDuringImpersonation, removePaymentHandler);
The exit mechanism
The impersonation session must have a clear, obvious, accessible way to end. The support engineer's UI should display a persistent banner showing whose account they are viewing, how long the session has left, and a prominent exit button. The exit button invalidates the impersonation token and returns the engineer to their own admin session.
// End impersonation session
app.post('/admin/impersonation/end', requireAdminAuth, async (req, res) => {
const { impersonationTokenJti } = req.body;
// Revoke the impersonation token
await redis.setex(`revoked_token:${impersonationTokenJti}`, 7200, '1');
await auditLog.record({
event: 'impersonation.ended',
actorId: req.admin.id,
jti: impersonationTokenJti,
timestamp: new Date(),
});
res.json({ success: true });
});
RBAC guard
Impersonation is a sensitive capability that should be a named, grantable permission, not an implicit property of being a support employee. Grant the support:impersonate permission explicitly to specific users or roles, log every grant in the admin audit log, and review the list quarterly. Use time-limited grants where possible — a support engineer handling an escalation may need impersonation access for 30 days, not permanently.
Consider requiring approval for impersonation: the support engineer requests access to a specific user's account in a ticketing system, a supervisor approves the request, and the approval creates an impersonation grant that is valid for a specific time window. This two-person rule means no single support employee can unilaterally access any account.