User Impersonation
When customers report bugs you can’t reproduce, impersonation lets your support and engineering teams see exactly what users see.
PropelAuth BYO Impersonation provides secure, time-limited access to act as any user. It tracks who impersonated whom, lets you instantly revoke access, and distinguishes impersonation sessions from regular sessions so you can add your own logging or restrictions.
How Impersonation Works
Section titled “How Impersonation Works”Impersonation involves two parties: an employee (someone on your team) and a user (your customer).
- Employee initiates - A support agent or engineer starts an impersonation session from your admin dashboard
- System creates token - PropelAuth BYO generates a special impersonation token tied to both the employee and user
- Employee uses token - The token lets the employee access your app as the user
- Session is distinguishable - Your app knows this is an impersonation session, not a regular login
- Session expires - After a set time (default: 1 hour), the session automatically ends
The beauty is that your application code barely changes. You validate impersonation tokens almost exactly like regular session tokens, just with a different function call.
Basic Usage
Section titled “Basic Usage”Let’s walk through implementing impersonation in your application.
Creating an Impersonation Session
Section titled “Creating an Impersonation Session”To begin, you’ll need to set up a route that your employees can call to create an impersonation session:
app.post("/api/admin/impersonate", async (req, res) => { // Get the employee from your auth system (e.g., from their session) const employeeEmail = getEmployeeEmail(req); const { userId } = req.body;
// Create the impersonation session const result = await auth.impersonation.create({ employeeEmail: employeeEmail, targetUserId: userId, userAgent: req.headers["user-agent"], ipAddress: req.ip, });
if (!result.ok) { if (result.error.type === "UnauthorizedEmployee") { return res.status(403).json({ error: "Not authorized to impersonate" }); } console.error("Failed to create impersonation session:", result.error); return res.status(500).json({ error: "Failed to create impersonation session" }); }
// We recommend using the same cookie that your regular sessions use to avoid ambiguity res.cookie("sessionToken", result.data.impersonationSessionToken, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 60 * 60 * 1000, // 1 hour });
res.json({ success: true });});class ImpersonateRequest(BaseModel): user_id: str
@app.post("/api/admin/impersonate")async def impersonate(impersonate_request: ImpersonateRequest, request: Request, response: Response): # Get the employee from your auth system (e.g., from their session) employee_email = get_employee_email(request)
# Create the impersonation session result = await client.impersonation.create( employee_email=employee_email, target_user_id=impersonate_request.user_id, user_agent=request.headers.get("user-agent"), ip_address=request.client.host )
if not is_err(result): if result.error.type == "UnauthorizedEmployee": raise HTTPException(status_code=403, detail="Not authorized to impersonate") print("Failed to create impersonation session:", result.error) raise HTTPException(status_code=500, detail="Failed to create impersonation session")
# We recommend using the same cookie that your regular sessions use to avoid ambiguity response.set_cookie( key="sessionToken", value=result.data.impersonation_session_token, httponly=True, secure=True, samesite="lax", max_age=60 * 60 # 1 hour )
return {"success": True}@PostMapping("/api/admin/impersonate")public ResponseEntity<?> impersonate(@RequestBody ImpersonateRequest body, HttpServletRequest request, HttpServletResponse response) { // Get the employee from your auth system (e.g., from their session) String employeeEmail = getEmployeeEmail(request);
// Create the impersonation session try { CreateImpersonationSessionResponse result = client.impersonation.create( CreateImpersonationSessionCommand.builder() .employeeEmail(employeeEmail) .targetUserId(body.getUserId()) .userAgent(request.getHeader("User-Agent")) .ipAddress(request.getRemoteAddr()) .build() );
// We recommend using the same cookie that your regular sessions use to avoid ambiguity Cookie cookie = new Cookie("sessionToken", result.getImpersonationSessionToken()); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setSameSite("Lax"); cookie.setMaxAge(60 * 60); // 1 hour response.addCookie(cookie);
return ResponseEntity.ok(Map.of("success", true)); } catch (CreateImpersonationSessionException.UnauthorizedEmployee e) { return ResponseEntity.status(403).body(Map.of("error", "Not authorized to impersonate")); } catch (CreateImpersonationSessionException e) { System.err.println("Failed to create impersonation session: " + e.getMessage()); return ResponseEntity.status(500).body(Map.of("error", "Failed to create impersonation session")); }}[HttpPost("/api/admin/impersonate")]public async Task<IActionResult> Impersonate([FromBody] ImpersonateRequest body){ // Get the employee from your auth system (e.g., from their session) var employeeEmail = GetEmployeeEmail(Request);
// Create the impersonation session try { var result = await client.Impersonation.CreateAsync(new CreateImpersonationSessionCommand { EmployeeEmail = employeeEmail, TargetUserId = body.UserId, UserAgent = Request.Headers["User-Agent"].ToString(), IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() });
// We recommend using the same cookie that your regular sessions use to avoid ambiguity Response.Cookies.Append("sessionToken", result.ImpersonationSessionToken, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, MaxAge = TimeSpan.FromHours(1) });
return Ok(new { Success = true }); } catch (CreateImpersonationSessionException.UnauthorizedEmployee ex) { return StatusCode(403, new { Error = "Not authorized to impersonate" }); } catch (CreateImpersonationSessionException ex) { Console.Error.WriteLine($"Failed to create impersonation session: {ex.Message}"); return StatusCode(500, new { Error = "Failed to create impersonation session" }); }}The token returned starts with impersonate_, making it easy to identify. Using the same cookie as regular sessions prevents confusion and ensures you always know where to look for authentication.
Validating Sessions (Regular or Impersonation)
Section titled “Validating Sessions (Regular or Impersonation)”Use the same cookie for both regular and impersonation sessions, checking the prefix to determine which validation to use:
// GET /api/user/profileapp.get("/api/user/profile", async (req, res) => { const sessionToken = req.cookies.sessionToken; if (!sessionToken) { return res.status(401).json({ error: "Not authenticated" }); }
// Check if this is an impersonation session if (sessionToken.startsWith("impersonate_")) { return validateImpersonationSession(sessionToken); } else { return validateRegularSession(sessionToken); }});
async function validateImpersonationSession(sessionToken: string) { const validation = await auth.impersonation.validate({ impersonationToken: sessionToken, userAgent: req.headers["user-agent"] || "", ipAddress: req.ip || "", });
if (validation.ok) { const { targetUserId, employeeEmail } = validation.data;
// You might want to log this access console.log(`${employeeEmail} is viewing ${targetUserId}'s profile`);
// Return user data with impersonation indicator return res.json({ userData: await fetchUserData(targetUserId), isImpersonation: true, impersonatedBy: employeeEmail, }); } else { console.error("Invalid impersonation session:", validation.error); return res.status(401).json({ error: "Invalid session" }); }}
async function validateRegularSession(sessionToken: string) { // This is your existing session validation logic, we're including it for completeness const userData = validateOurSessions(sessionToken); if (userData) { return res.json({ userData: userData, isImpersonation: false, }); } else { return res.status(401).json({ error: "Invalid session" }); }}# GET /api/user/profile@app.get("/api/user/profile")async def get_user_profile(request: Request): session_token = request.cookies.get("sessionToken") if not session_token: raise HTTPException(status_code=401, detail="Not authenticated")
# Check if this is an impersonation session if session_token.startswith("impersonate_"): return await validate_impersonation_session(session_token, request) else: return await validate_regular_session(session_token)
async def validate_impersonation_session(session_token: str, request: Request): validation = await client.impersonation.validate( impersonation_token=session_token, user_agent=request.headers.get("user-agent"), ip_address=request.client.host )
if is_ok(validation): target_user_id = validation.data.target_user_id employee_email = validation.data.employee_email
# You might want to log this access print(f"{employee_email} is viewing {target_user_id}'s profile")
# Return user data with impersonation indicator return { "userData": await fetch_user_data(target_user_id), "isImpersonation": True, "impersonatedBy": employee_email } else: print("Invalid impersonation session:", validation.error) raise HTTPException(status_code=401, detail="Invalid session")
async def validate_regular_session(session_token: str): # This is your existing session validation logic, we're including it for completeness user_data = validate_our_sessions(session_token) if user_data: return { "userData": user_data, "isImpersonation": False } else: raise HTTPException(status_code=401, detail="Invalid session")// GET /api/user/profile@GetMapping("/api/user/profile")public ResponseEntity<?> getUserProfile(@CookieValue(name = "sessionToken", required = false) String sessionToken, HttpServletRequest request) { if (sessionToken == null) { return ResponseEntity.status(401).body(Map.of("error", "Not authenticated")); }
// Check if this is an impersonation session if (sessionToken.startsWith("impersonate_")) { return validateImpersonationSession(sessionToken, request); } else { return validateRegularSession(sessionToken); }}
private ResponseEntity<?> validateImpersonationSession(String sessionToken, HttpServletRequest request) { try { ValidateImpersonationSessionResponse validation = client.impersonation.validate( ValidateImpersonationSessionCommand.builder() .impersonationToken(sessionToken) .userAgent(request.getHeader("User-Agent")) .ipAddress(request.getRemoteAddr()) .build() );
String targetUserId = validation.getTargetUserId(); String employeeEmail = validation.getEmployeeEmail();
// You might want to log this access System.out.println(employeeEmail + " is viewing " + targetUserId + "'s profile");
// Return user data with impersonation indicator return ResponseEntity.ok(Map.of( "userData", fetchUserData(targetUserId), "isImpersonation", true, "impersonatedBy", employeeEmail )); } catch (ValidateImpersonationSessionException e) { System.err.println("Invalid impersonation session: " + e.getMessage()); return ResponseEntity.status(401).body(Map.of("error", "Invalid session")); }}
private ResponseEntity<?> validateRegularSession(String sessionToken) { // This is your existing session validation logic, we're including it for completeness Object userData = validateOurSessions(sessionToken); if (userData != null) { return ResponseEntity.ok(Map.of( "userData", userData, "isImpersonation", false )); } else { return ResponseEntity.status(401).body(Map.of("error", "Invalid session")); }}// GET /api/user/profile[HttpGet("/api/user/profile")]public async Task<IActionResult> GetUserProfile(){ var sessionToken = Request.Cookies["sessionToken"]; if (sessionToken == null) { return StatusCode(401, new { Error = "Not authenticated" }); }
// Check if this is an impersonation session if (sessionToken.StartsWith("impersonate_")) { return await ValidateImpersonationSession(sessionToken); } else { return await ValidateRegularSession(sessionToken); }}
private async Task<IActionResult> ValidateImpersonationSession(string sessionToken){ try { var validation = await client.Impersonation.ValidateAsync(new ValidateImpersonationSessionCommand { ImpersonationToken = sessionToken, UserAgent = Request.Headers["User-Agent"].ToString(), IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() });
var targetUserId = validation.TargetUserId; var employeeEmail = validation.EmployeeEmail;
// You might want to log this access Console.WriteLine($"{employeeEmail} is viewing {targetUserId}'s profile");
// Return user data with impersonation indicator return Ok(new { UserData = await FetchUserData(targetUserId), IsImpersonation = true, ImpersonatedBy = employeeEmail }); } catch (ValidateImpersonationSessionException ex) { Console.Error.WriteLine($"Invalid impersonation session: {ex.Message}"); return StatusCode(401, new { Error = "Invalid session" }); }}
private async Task<IActionResult> ValidateRegularSession(string sessionToken){ // This is your existing session validation logic, we're including it for completeness var userData = await ValidateOurSessions(sessionToken); if (userData != null) { return Ok(new { UserData = userData, IsImpersonation = false }); } else { return StatusCode(401, new { Error = "Invalid session" }); }}Using the same cookie prevents mistakes - you’ll never accidentally use the wrong user ID because you forgot to check for impersonation.
Ending Sessions
Section titled “Ending Sessions”Your logout endpoint should handle invalidating both regular and impersonation sessions:
// POST /api/logoutapp.post("/api/logout", async (req, res) => { const sessionToken = req.cookies.sessionToken; if (!sessionToken) { return res.status(200).json({ success: true }); }
if (sessionToken.startsWith("impersonate_")) { // Handle impersonation sessions await auth.impersonation.invalidateByToken({ impersonationSessionToken: sessionToken, }); } else { // This is your existing logout logic await logoutOurSession(sessionToken); }
// Clear the session cookie res.clearCookie("sessionToken"); res.json({ success: true });});# POST /api/logout@app.post("/api/logout")async def logout(request: Request, response: Response): session_token = request.cookies.get("sessionToken") if not session_token: return {"success": True}
if session_token.startswith("impersonate_"): # Handle impersonation sessions await auth.impersonation.invalidate_by_token( impersonation_session_token=session_token ) else: # This is your existing logout logic await logout_our_session(session_token)
# Clear the session cookie response.delete_cookie("sessionToken") return {"success": True}// POST /api/logout@PostMapping("/api/logout")public ResponseEntity<?> logout(@CookieValue(name = "sessionToken", required = false) String sessionToken, HttpServletResponse response) { if (sessionToken == null) { return ResponseEntity.ok(Map.of("success", true)); }
if (sessionToken.startsWith("impersonate_")) { // Handle impersonation sessions client.impersonation.invalidateByToken( InvalidateImpersonationSessionByTokenCommand.builder() .impersonationSessionToken(sessionToken) .build() ); } else { // This is your existing logout logic logoutOurSession(sessionToken); }
// Clear the session cookie Cookie cookie = new Cookie("sessionToken", ""); cookie.setMaxAge(0); response.addCookie(cookie); return ResponseEntity.ok(Map.of("success", true));}// POST /api/logout[HttpPost("/api/logout")]public async Task<IActionResult> Logout(){ var sessionToken = Request.Cookies["sessionToken"]; if (sessionToken == null) { return Ok(new { Success = true }); }
if (sessionToken.StartsWith("impersonate_")) { // Handle impersonation sessions await client.Impersonation.InvalidateByTokenAsync(new InvalidateImpersonationSessionByTokenCommand { ImpersonationSessionToken = sessionToken }); } else { // This is your existing logout logic await LogoutOurSession(sessionToken); }
// Clear the session cookie Response.Cookies.Delete("sessionToken"); return Ok(new { Success = true });}Configuring Who Can Impersonate
Section titled “Configuring Who Can Impersonate”Not everyone should be able to impersonate users. PropelAuth BYO gives you three ways to control access through your user_impersonation.jsonc configuration file:
{ "absolute_lifetime_secs": 3600, // 1 hour "who_can_impersonate": { // Choose one of these options: "allowed_employee_emails": ["support@company.com", "john@company.com"], // OR "allowed_employee_domains": ["company.com"], // OR "allow_all_because_i_will_gate_access_myself": true }}Choosing the Right Restriction
Section titled “Choosing the Right Restriction”Specific emails - Perfect for small teams or when only certain individuals should have access:
{ "who_can_impersonate": { "allowed_employee_emails": ["alice@company.com", "bob@company.com", "support@company.com"] }}Domain-based - Great when entire teams need access:
{ "who_can_impersonate": { "allowed_employee_domains": ["company.com", "support.company.com"] }}Self-managed - When you have your own permission system:
{ "who_can_impersonate": { "allow_all_because_i_will_gate_access_myself": true }}If you specify multiple options, the most restrictive wins. Specific emails override domain rules.
Security Considerations
Section titled “Security Considerations”Impersonation Audit Trail
Section titled “Impersonation Audit Trail”PropelAuth BYO maintains a complete audit trail of impersonation sessions:
- Who impersonated whom
- When sessions started and ended
- Failed impersonation attempts
- Manual session invalidations
// Fetching impersonation historyconst history = await auth.impersonation.fetchAllForUser({ userId: userId,});
if (history.ok) { history.data.sessions.forEach((session) => { console.log( `${session.employeeEmail} impersonated this user from ${session.createdAt} to ${session.expiresAt}`, ); });}# Fetching impersonation historyhistory = await client.impersonation.fetch_all_for_user(user_id=user_id)
if is_ok(history): for session in history.data.sessions: print( f"{session.employee_email} impersonated this user from {session.created_at} to {session.expires_at}" )// Fetching impersonation historytry { FetchAllImpersonationSessionsForUserResponse history = client.impersonation.fetchAllForUser( FetchAllImpersonationSessionsForUserCommand.builder() .userId(userId) .build() );
for (ImpersonationSessionInfo session : history.getSessions()) { System.out.println( session.getEmployeeEmail() + " impersonated this user from " + session.getCreatedAt() + " to " + session.getExpiresAt() ); }} catch (FetchAllImpersonationSessionsForUserException e) { System.err.println("Error fetching history: " + e.getMessage());}// Fetching impersonation historytry{ var history = await client.Impersonation.FetchAllForUserAsync(new FetchAllImpersonationSessionsForUserCommand { UserId = userId });
foreach (var session in history.Sessions) { Console.WriteLine( $"{session.EmployeeEmail} impersonated this user from " + $"{session.CreatedAt} to {session.ExpiresAt}" ); }}catch (FetchAllImpersonationSessionsForUserException ex){ Console.Error.WriteLine($"Error fetching history: {ex.Message}");}Session Limits
Section titled “Session Limits”Configure appropriate limits based on your needs:
{ "absolute_lifetime_secs": 3600, // 1 hour default "max_concurrent_per_employee": 3 // Limit concurrent sessions}Instant Revocation
Section titled “Instant Revocation”Immediately revoke access when needed using either the Invalidate All For Employee or Invalidate All for User functions:
// Revoke all sessions for a compromised employee accountawait auth.impersonation.invalidateAllForEmployee({ employeeEmail: "compromised@company.com",});
// Stop all impersonation of a specific userawait auth.impersonation.invalidateAllForUser({ userId: sensitiveUserId,});# Revoke all sessions for a compromised employee accountawait client.impersonation.invalidate_all_for_employee( employee_email="compromised@company.com")
# Stop all impersonation of a specific userawait client.impersonation.invalidate_all_for_user( user_id=sensitive_user_id)// Revoke all sessions for a compromised employee accountclient.impersonation.invalidateAllForEmployee( InvalidateAllImpersonationSessionsForEmployeeCommand.builder() .employeeEmail("compromised@company.com") .build());
// Stop all impersonation of a specific userclient.impersonation.invalidateAllForUser( InvalidateAllImpersonationSessionsForUserCommand.builder() .userId(sensitiveUserId) .build());// Revoke all sessions for a compromised employee accountawait client.Impersonation.InvalidateAllForEmployeeAsync(new InvalidateAllImpersonationSessionsForEmployeeCommand{ EmployeeEmail = "compromised@company.com"});
// Stop all impersonation of a specific userawait client.Impersonation.InvalidateAllForUserAsync(new InvalidateAllImpersonationSessionsForUserCommand{ UserId = sensitiveUserId});Restricting Actions During Impersonation
Section titled “Restricting Actions During Impersonation”Since you can detect when someone is impersonating, you can prevent sensitive actions:
// Any sessions validated with client.impersonation.validate() should add a flag like thisif (user.isImpersonation) { // Block sensitive operations like payment changes return res.status(403).json({ error: "Cannot modify payment methods during impersonation", });}# Any sessions validated with client.impersonation.validate() should add a flag like thisif user.is_impersonation: # Block sensitive operations like payment changes raise HTTPException( status_code=403, detail="Cannot modify payment methods during impersonation" )// Any sessions validated with client.impersonation.validate() should add a flag like thisif (user.isImpersonation()) { // Block sensitive operations like payment changes return ResponseEntity.status(403).body(Map.of( "error", "Cannot modify payment methods during impersonation" ));}// Any sessions validated with client.impersonation.validate() should add a flag like thisif (user.IsImpersonation){ // Block sensitive operations like payment changes return StatusCode(403, new { Error = "Cannot modify payment methods during impersonation" });}Working with Sessions
Section titled “Working with Sessions”Impersonation tokens work seamlessly with PropelAuth BYO’s session management. You can use the same patterns and features:
// Impersonation benefits from session featuresconst result = await client.impersonation.create({ employeeEmail: employeeEmail, targetUserId: userId, userAgent: req.headers["user-agent"], ipAddress: req.ip, metadata: { ticketId: "SUPPORT-1234", reason: "Customer requested help with billing", },});
// The impersonation token benefits from session features// like IP validation, user agent checking, etc.# Impersonation benefits from session featuresresult = await client.impersonation.create( employee_email=employee_email, target_user_id=user_id, user_agent=request.headers.get("user-agent"), ip_address=request.client.host, metadata={ "ticketId": "SUPPORT-1234", "reason": "Customer requested help with billing" })
# The impersonation token benefits from session features# like IP validation, user agent checking, etc.// Impersonation benefits from session featuresCreateImpersonationSessionResponse result = client.impersonation.create( CreateImpersonationSessionCommand.builder() .employeeEmail(employeeEmail) .targetUserId(userId) .userAgent(request.getHeader("User-Agent")) .ipAddress(request.getRemoteAddr()) .metadata(JsonValue.of(Map.of( "ticketId", "SUPPORT-1234", "reason", "Customer requested help with billing" ))) .build());
// The impersonation token benefits from session features// like IP validation, user agent checking, etc.// Impersonation benefits from session featuresvar result = await client.Impersonation.CreateAsync(new CreateImpersonationSessionCommand{ EmployeeEmail = employeeEmail, TargetUserId = userId, UserAgent = request.Headers["User-Agent"].ToString(), IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(), Metadata = JsonSerializer.SerializeToElement(new { ticketId = "SUPPORT-1234", reason = "Customer requested help with billing" })});
// The impersonation token benefits from session features// like IP validation, user agent checking, etc.This means you get session benefits like IP address tracking and user agent validation.
Dashboard Management
Section titled “Dashboard Management”The PropelAuth BYO dashboard gives you visibility into all impersonation activity:
Active Sessions - View current impersonation sessions with who is impersonating whom, start time, and quick invalidation.
Audit Trail - Complete history of who impersonated whom, when sessions started/ended, and any manual invalidations.
Emergency Controls - Invalidate all sessions for an employee, block specific employees, or stop all impersonation of a user.