Skip to content

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.

Impersonation involves two parties: an employee (someone on your team) and a user (your customer).

  1. Employee initiates - A support agent or engineer starts an impersonation session from your admin dashboard
  2. System creates token - PropelAuth BYO generates a special impersonation token tied to both the employee and user
  3. Employee uses token - The token lets the employee access your app as the user
  4. Session is distinguishable - Your app knows this is an impersonation session, not a regular login
  5. 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.

Let’s walk through implementing impersonation in your application.

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/profile
app.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.

Your logout endpoint should handle invalidating both regular and impersonation sessions:

// POST /api/logout
app.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 });
}

Not everyone should be able to impersonate users. PropelAuth BYO gives you three ways to control access through your user_impersonation.jsonc configuration file:

/configs/user_impersonation.jsonc
{
"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
}
}

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.

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 history
const 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 history
history = 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 history
try {
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 history
try
{
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}");
}

Configure appropriate limits based on your needs:

{
"absolute_lifetime_secs": 3600, // 1 hour default
"max_concurrent_per_employee": 3 // Limit concurrent sessions
}

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 account
await auth.impersonation.invalidateAllForEmployee({
employeeEmail: "compromised@company.com",
});
// Stop all impersonation of a specific user
await auth.impersonation.invalidateAllForUser({
userId: sensitiveUserId,
});
# Revoke all sessions for a compromised employee account
await client.impersonation.invalidate_all_for_employee(
employee_email="compromised@company.com"
)
# Stop all impersonation of a specific user
await client.impersonation.invalidate_all_for_user(
user_id=sensitive_user_id
)
// Revoke all sessions for a compromised employee account
client.impersonation.invalidateAllForEmployee(
InvalidateAllImpersonationSessionsForEmployeeCommand.builder()
.employeeEmail("compromised@company.com")
.build()
);
// Stop all impersonation of a specific user
client.impersonation.invalidateAllForUser(
InvalidateAllImpersonationSessionsForUserCommand.builder()
.userId(sensitiveUserId)
.build()
);
// Revoke all sessions for a compromised employee account
await client.Impersonation.InvalidateAllForEmployeeAsync(new InvalidateAllImpersonationSessionsForEmployeeCommand
{
EmployeeEmail = "compromised@company.com"
});
// Stop all impersonation of a specific user
await client.Impersonation.InvalidateAllForUserAsync(new InvalidateAllImpersonationSessionsForUserCommand
{
UserId = sensitiveUserId
});

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 this
if (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 this
if 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 this
if (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 this
if (user.IsImpersonation)
{
// Block sensitive operations like payment changes
return StatusCode(403, new
{
Error = "Cannot modify payment methods during impersonation"
});
}

Impersonation tokens work seamlessly with PropelAuth BYO’s session management. You can use the same patterns and features:

// Impersonation benefits from session features
const 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 features
result = 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 features
CreateImpersonationSessionResponse 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 features
var 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.

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.