---
title: Implement user impersonation
description: Enable user impersonation for testing and support by creating an admin-protected API endpoint.
sidebar:
  order: 20
---

## Overview

Impersonating a user allows you to login as them without using their credentials.
This is useful for testing purposes, or for customer support.

This guide shows you how to achieve this by only allowing a certain type of users, `admins`, to perform the impersonation.

## Before you start

:::danger[Impersonation is a privileged operation]
Require a valid staff session and enforce an explicit backend role or permission check before reading the target identifier, looking up the target, or creating a session. Hiding the UI, checking a role in the browser, restricting access to approved IP addresses, or knowing the endpoint URL is not authorization.
:::

For production use, apply these additional controls:

- Require a recent step-up authentication before each impersonation starts. For example, require the staff member to complete MFA again.
- Require a reason and write append-only audit events for every attempt and outcome. Include the actor's user ID, target user ID, reason, timestamp, outcome, and impersonation session handle. Do not rely only on a custom access token claim as the audit record. Fail closed if the start event cannot be recorded.
- Set a short maximum duration. Enforce the deadline on the backend, revoke the impersonation session when it expires, and provide an explicit way to terminate it early. Monitor and alert on unusual impersonation activity.
- Decide which targets and actions staff may access while impersonating. For example, prevent impersonation of other administrators and block credential, MFA, payment, and destructive account changes unless your policy explicitly allows them.


## Steps

### 1. Create the impersonation endpoint

Create a new API endpoint that accepts a stable user ID and creates a new impersonation session for that user. If you instead use an email address, phone number, or other account information, require the lookup to return exactly one user; never select the first of multiple matches.

In order for this to work, admins need to first log in to the application as themselves. Once they create their session (like any regular user's session), they can call the API via a frontend UI that's only shown to them. You can detect the admin role on the frontend by seeing [this guide](/additional-verification/user-roles/protecting-routes#protect-frontend-routes).

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
<DependentContent passive group="node-frameworks">
<ContentOption title="Next.js" value="nextjs">
<NextjsRouterTypeSelect />
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Express" value="express">
```tsx
import express from "express";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import Session from "supertokens-node/recipe/session";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

let app = express();

app.post(
  "/impersonate",
  verifySession({
    overrideGlobalClaimValidators: async (globalValidators) => [
      ...globalValidators,
      UserRoles.UserRoleClaim.validators.includes("admin"),
    ],
  }),
  async (req, res) => {
    let email = "..."; // read from request body

    let user = await supertokens.listUsersByAccountInfo("public", {
      email,
    });

    if (user.length !== 1) {
      throw new Error("Identifier does not uniquely identify a user");
    }

    await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, {
      isImpersonation: true,
    });

    res.json({ message: "Impersonation successful!" });
  },
);
```
</ContentOption>
<ContentOption title="Hapi" value="hapi">
```tsx
import Hapi from "@hapi/hapi";
import { verifySession } from "supertokens-node/recipe/session/framework/hapi";
import Session from "supertokens-node/recipe/session";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

let server = Hapi.server({ port: 8000 });

server.route({
  path: "/impersonate",
  method: "post",
  options: {
    pre: [
      {
        method: verifySession({
          overrideGlobalClaimValidators: async (globalValidators) => [
            ...globalValidators,
            UserRoles.UserRoleClaim.validators.includes("admin"),
          ],
        }),
      },
    ],
  },
  handler: async (req, res) => {
    let email = "..."; // read from request body

    let user = await supertokens.listUsersByAccountInfo("public", {
      email,
    });

    if (user.length !== 1) {
      throw new Error("Identifier does not uniquely identify a user");
    }

    await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, {
      isImpersonation: true,
    });

    return res.response({ message: "Impersonation successful!" }).code(200);
  },
});
```
</ContentOption>
<ContentOption title="Fastify" value="fastify">
```tsx
import Fastify from "fastify";
import { verifySession } from "supertokens-node/recipe/session/framework/fastify";
import Session from "supertokens-node/recipe/session";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

let fastify = Fastify();

fastify.post(
  "/impersonate",
  {
    preHandler: verifySession({
      overrideGlobalClaimValidators: async (globalValidators) => [
        ...globalValidators,
        UserRoles.UserRoleClaim.validators.includes("admin"),
      ],
    }),
  },
  async (req, res) => {
    let email = "..."; // read from request body

    let user = await supertokens.listUsersByAccountInfo("public", {
      email,
    });

    if (user.length !== 1) {
      throw new Error("Identifier does not uniquely identify a user");
    }

    await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, {
      isImpersonation: true,
    });

    res.send({ message: "Impersonation successful!" });
  },
);
```
</ContentOption>
<ContentOption title="Aws Lambda" value="aws-lambda">
```tsx
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import { middleware } from "supertokens-node/framework/awsLambda";
import Session from "supertokens-node/recipe/session";
import { SessionEvent } from "supertokens-node/framework/awsLambda";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

async function impersonate(awsEvent: SessionEvent) {
  let email = "..."; // read from request body

  let user = await supertokens.listUsersByAccountInfo("public", {
    email,
  });

  if (user.length !== 1) {
    throw new Error("Identifier does not uniquely identify a user");
  }

  await Session.createNewSession(awsEvent, awsEvent, "public", user[0].loginMethods[0].recipeUserId, {
    isImpersonation: true,
  });

  return {
    body: JSON.stringify({ message: "Impersonation successful!" }),
    statusCode: 200,
  };
}

exports.handler = verifySession(impersonate, {
  overrideGlobalClaimValidators: async (globalValidators) => [
    ...globalValidators,
    UserRoles.UserRoleClaim.validators.includes("admin"),
  ],
});
```
</ContentOption>
<ContentOption title="Koa" value="koa">
```tsx
import KoaRouter from "koa-router";
import { verifySession } from "supertokens-node/recipe/session/framework/koa";
import Session from "supertokens-node/recipe/session";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

let router = new KoaRouter();

router.post(
  "/impersonate",
  verifySession({
    overrideGlobalClaimValidators: async (globalValidators) => [
      ...globalValidators,
      UserRoles.UserRoleClaim.validators.includes("admin"),
    ],
  }),
  async (ctx, next) => {
    let email = "..."; // read from request body

    let user = await supertokens.listUsersByAccountInfo("public", {
      email,
    });

    if (user.length !== 1) {
      throw new Error("Identifier does not uniquely identify a user");
    }

    await Session.createNewSession(ctx, ctx, "public", user[0].loginMethods[0].recipeUserId, {
      isImpersonation: true,
    });

    ctx.body = { message: "Impersonation successful!" };
  },
);
```
</ContentOption>
<ContentOption title="LoopBack" value="loopback">
```tsx
import { inject, intercept } from "@loopback/core";
import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";
import { verifySession } from "supertokens-node/recipe/session/framework/loopback";
import Session from "supertokens-node/recipe/session";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

class Login {
  constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {}
  @post("/impersonate")
  @intercept(
    verifySession({
      overrideGlobalClaimValidators: async (globalValidators) => [
        ...globalValidators,
        UserRoles.UserRoleClaim.validators.includes("admin"),
      ],
    }),
  )
  @response(200)
  async handler() {
    let email = "..."; // read from request body

    let user = await supertokens.listUsersByAccountInfo("public", {
      email,
    });

    if (user.length !== 1) {
      throw new Error("Identifier does not uniquely identify a user");
    }

    await Session.createNewSession(this.ctx, this.ctx, "public", user[0].loginMethods[0].recipeUserId, {
      isImpersonation: true,
    });
    return { message: "Impersonation successful!" };
  }
}
```
</ContentOption>
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="pages-router">

```tsx
import { superTokensNextWrapper } from "supertokens-node/nextjs";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { createNewSession } from "supertokens-node/recipe/session";
import { SessionRequest } from "supertokens-node/framework/express";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

export default async function impersonate(req: SessionRequest, res: any) {
  await superTokensNextWrapper(
    async (next) => {
      await verifySession({
        overrideGlobalClaimValidators: async (globalValidators) => [
          ...globalValidators,
          UserRoles.UserRoleClaim.validators.includes("admin"),
        ],
      })(req, res, next);
    },
    req,
    res,
  );

  let email = "..."; // read from request body

  let user = await supertokens.listUsersByAccountInfo("public", {
    email,
  });

  if (user.length !== 1) {
    throw new Error("Identifier does not uniquely identify a user");
  }

  await superTokensNextWrapper(
    async (next) => {
      await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, {
        isImpersonation: true,
      });
    },
    req,
    res,
  );
  res.json({
    message: "Impersonation successful!",
  });
}
```

</ConditionalContent>
</ContentOption>
<ContentOption title="Nestjs" value="nestjs">
```ts check=false reason="Requires surrounding framework application context"
import { Controller, Post, Res, Req, UseGuards } from "@nestjs/common";
import type { Response, Request } from "express";
import { AuthGuard } from "./auth/auth.guard";
import { createNewSession, SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session";
import supertokens from "supertokens-node";
import UserRoles from "supertokens-node/recipe/userroles";

@Controller()
export class ExampleController {
  // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide.
  @Post("impersonate")
  @UseGuards(
    new AuthGuard({
      overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [
        ...globalValidators,
        UserRoles.UserRoleClaim.validators.includes("admin"),
      ],
    }),
  )
  async postLogin(@Req() req: Request, @Res() res: Response): Promise<{ message: string }> {
    let email = "..."; // read from request body

    let user = await supertokens.listUsersByAccountInfo("public", {
      email,
    });

    if (user.length !== 1) {
      throw new Error("Identifier does not uniquely identify a user");
    }

    await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, {
      isImpersonation: true,
    });

    return { message: "Impersonation successful!" };
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Go" value="go">
```go
import (
	"net/http"

	"github.com/supertokens/supertokens-golang/recipe/emailpassword"
	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/session/claims"
	"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
	"github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		session.VerifySession(&sessmodels.VerifySessionOptions{
			OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
				globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil))
				return globalClaimValidators, nil
			},
		}, impersonate).ServeHTTP(rw, r)
	})
}

func impersonate(w http.ResponseWriter, r *http.Request) {
	email := "..." // read from request body

	// we are using emailpassword recipe here, but you can use the recipe you use
	// as well..
	user, err := emailpassword.GetUserByEmail("public", email)
	if err != nil {
		// Send 500 to client
		return
	}

	if user == nil {
		// Send 400 to client cause user does not exist
		return
	}

	_, err = session.CreateNewSession(r, w, "public", user.ID, map[string]interface{}{
		"isImpersonation": true,
	}, nil)
	if err != nil {
		err = supertokens.ErrorHandler(err, r, w)
		if err != nil {
			// Send 500 to client
		}
		return
	}

	// Send 200 success to client
}
```
</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-frameworks" label="Python framework">
<ContentOption title="FastAPI" value="fastapi">
```python check=false reason="Requires surrounding framework application context"
from fastapi import Depends, Request
from fastapi.responses import JSONResponse

from supertokens_python.asyncio import list_users_by_account_info
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.asyncio import create_new_session
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.userroles import UserRoleClaim
from supertokens_python.types.base import AccountInfoInput


@app.post("/impersonate")
async def impersonate(
    request: Request,
    session: SessionContainer = Depends(
        verify_session(
            # We add the UserRoleClaim's includes validator
            override_global_claim_validators=lambda global_validators,
            session,
            user_context: global_validators
            + [UserRoleClaim.validators.includes("admin")]
        )
    ),
):
    email = "..."  # get from request body

    # we use the email password recipe here, but you can use the recipe you use
    user = await list_users_by_account_info("public", AccountInfoInput(email=email))

    if len(user) != 1:
        # return a 400 error because the identifier is missing or ambiguous
        return

    await create_new_session(
        request,
        "public",
        user[0].login_methods[0].recipe_user_id,
        {"isImpersonation": True},
    )

    return JSONResponse({"message": "Impersonation complete!"})

```
</ContentOption>
<ContentOption title="Flask" value="flask">
```python check=false reason="Requires surrounding framework application context"
from flask import jsonify
from flask.wrappers import Request

from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.session.syncio import create_new_session
from supertokens_python.recipe.userroles import UserRoleClaim
from supertokens_python.syncio import list_users_by_account_info
from supertokens_python.types.base import AccountInfoInput


@app.route("/impersonate", methods=["POST"])
@verify_session(
    # We add the UserRoleClaim's includes validator
    override_global_claim_validators=lambda global_validators,
    session,
    user_context: global_validators + [UserRoleClaim.validators.includes("admin")]
)
def login(request: Request):
    email = "..."  # get from request body

    # we use the email password recipe here, but you can use the recipe you use
    user = list_users_by_account_info("public", AccountInfoInput(email=email))

    if len(user) != 1:
        # return a 400 error because the identifier is missing or ambiguous
        return

    create_new_session(
        request,
        "public",
        user[0].login_methods[0].recipe_user_id,
        {"isImpersonation": True},
    )

    return jsonify({"message": "Impersonation complete!"})

```
</ContentOption>
<ContentOption title="Django" value="django">
```python
from django.http import HttpRequest, JsonResponse

from supertokens_python.asyncio import list_users_by_account_info
from supertokens_python.recipe.session.asyncio import create_new_session
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from supertokens_python.recipe.userroles import UserRoleClaim
from supertokens_python.types.base import AccountInfoInput


@verify_session(
    # We add the UserRoleClaim's includes validator
    override_global_claim_validators=lambda global_validators,
    session,
    user_context: global_validators + [UserRoleClaim.validators.includes("admin")]
)
async def impersonate(request: HttpRequest):
    email = "..."  # get from request body

    # we use the email password recipe here, but you can use the recipe you use
    user = await list_users_by_account_info("public", AccountInfoInput(email=email))

    if len(user) != 1:
        # return a 400 error because the identifier is missing or ambiguous
        return

    await create_new_session(
        request,
        "public",
        user[0].login_methods[0].recipe_user_id,
        {"isImpersonation": True},
    )

    return JsonResponse({"message": "User logged in!"})

```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<CodeGroup passive group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="app-router">

```tsx check=false reason="Requires surrounding framework application context and application audit logging"
import { NextResponse, NextRequest } from "next/server";
import SuperTokens from "supertokens-node";
import { withPreParsedRequestResponse } from "supertokens-node/nextjs";
import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom";
import Session, { createNewSession } from "supertokens-node/recipe/session";
import UserRoles from "supertokens-node/recipe/userroles";

import { backendConfig } from "@/app/config/backend";
import {
  assertRecentImpersonationStepUp,
  createImpersonationAttemptId,
  getAuthorizedImpersonationTenant,
  getImpersonationExpiry,
  recordImpersonationAuditEvent,
  registerImpersonationExpiry,
} from "@/app/auth/impersonation-security";

SuperTokens.init(backendConfig());

function copyCollectedCredentials(source: CollectingResponse, destination: CollectingResponse) {
  source.headers.forEach((value, key) => destination.setHeader(key, value, false));
  for (const cookie of source.cookies) {
    destination.setCookie(
      cookie.key,
      cookie.value,
      cookie.domain,
      cookie.secure,
      cookie.httpOnly,
      cookie.expires,
      cookie.path,
      cookie.sameSite,
    );
  }
}

export function POST(request: NextRequest) {
  return withPreParsedRequestResponse(
    request,
    async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => {
      const actorSession = await Session.getSession(baseRequest, baseResponse, {
        sessionRequired: true,
        overrideGlobalClaimValidators: async (globalValidators) => [
          ...globalValidators,
          UserRoles.UserRoleClaim.validators.includes("admin"),
        ],
      });

      const actorUserId = actorSession.getUserId();
      const attemptId = createImpersonationAttemptId();
      await recordImpersonationAuditEvent({
        attemptId,
        actorUserId,
        outcome: "ATTEMPT_STARTED",
      });

      try {
        await assertRecentImpersonationStepUp(actorSession);
      } catch {
        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          outcome: "STEP_UP_FAILED",
        });
        return NextResponse.json({ message: "Impersonation request denied" }, { status: 403 });
      }

      // This application-owned helper derives allowed tenants from trusted
      // server-side staff entitlements. It must not trust a request-body tenant ID.
      let tenantId: string;
      try {
        tenantId = await getAuthorizedImpersonationTenant({
          actorUserId,
          actorSessionTenantId: actorSession.getTenantId(),
        });
      } catch {
        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          outcome: "TENANT_AUTHORIZATION_FAILED",
        });
        return NextResponse.json({ message: "Impersonation request denied" }, { status: 403 });
      }

      // Authorization and step-up have succeeded. Only now read the target and reason.
      let targetRecipeUserId = "..."; // validate and read a stable recipe user ID from the request body
      let reason = "..."; // require a non-empty support or incident reason
      let targetUser: Awaited<ReturnType<typeof SuperTokens.getUser>>;
      try {
        targetUser = await SuperTokens.getUser(targetRecipeUserId);
      } catch {
        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          tenantId,
          reason,
          outcome: "TARGET_LOOKUP_FAILED",
        });
        return NextResponse.json({ message: "Impersonation request denied" }, { status: 400 });
      }
      const targetLoginMethod = targetUser?.loginMethods.find(
        (loginMethod) => loginMethod.recipeUserId.getAsString() === targetRecipeUserId,
      );

      if (!targetUser || !targetLoginMethod?.tenantIds.includes(tenantId)) {
        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          targetRecipeUserId,
          tenantId,
          reason,
          outcome: "TARGET_DENIED",
        });
        return NextResponse.json({ message: "Impersonation request denied" }, { status: 400 });
      }

      let expiresAt: number;
      try {
        expiresAt = await getImpersonationExpiry({ actorUserId, tenantId });
      } catch {
        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          targetUserId: targetUser.id,
          targetRecipeUserId,
          tenantId,
          reason,
          outcome: "EXPIRY_DERIVATION_FAILED",
        });
        return NextResponse.json({ message: "Impersonation request denied" }, { status: 500 });
      }
      await recordImpersonationAuditEvent({
        attemptId,
        actorUserId,
        targetUserId: targetUser.id,
        targetRecipeUserId,
        tenantId,
        reason,
        outcome: "ATTEMPT_APPROVED",
        expiresAt,
      });

      const stagedResponse = new CollectingResponse();
      let impersonationSession: Awaited<ReturnType<typeof createNewSession>> | undefined;
      let stage: "SESSION_CREATION" | "EXPIRY_REGISTRATION" | "SUCCESS_AUDIT" = "SESSION_CREATION";

      try {
        impersonationSession = await createNewSession(baseRequest, stagedResponse, tenantId, targetLoginMethod.recipeUserId, {
          isImpersonation: true,
          impersonatedBy: actorUserId,
          impersonationExpiresAt: expiresAt,
        });

        stage = "EXPIRY_REGISTRATION";
        await registerImpersonationExpiry({
          sessionHandle: impersonationSession.getHandle(),
          expiresAt,
        });

        stage = "SUCCESS_AUDIT";
        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          targetUserId: targetUser.id,
          targetRecipeUserId,
          tenantId,
          reason,
          outcome: "SESSION_CREATED",
          impersonationSessionHandle: impersonationSession.getHandle(),
          expiresAt,
        });
      } catch {
        let revoked: boolean | undefined;
        if (impersonationSession) {
          try {
            revoked = await Session.revokeSession(impersonationSession.getHandle());
          } catch {
            revoked = false;
          }
        }

        let failureOutcome = "SESSION_CREATION_FAILED";
        if (stage === "EXPIRY_REGISTRATION") {
          failureOutcome = "EXPIRY_REGISTRATION_FAILED";
        } else if (stage === "SUCCESS_AUDIT") {
          failureOutcome = "SUCCESS_AUDIT_FAILED";
        }

        await recordImpersonationAuditEvent({
          attemptId,
          actorUserId,
          targetUserId: targetUser.id,
          targetRecipeUserId,
          tenantId,
          reason,
          outcome: failureOutcome,
          impersonationSessionHandle: impersonationSession?.getHandle(),
          revoked,
          revocationFailed: revoked === false,
          expiresAt,
        });
        return NextResponse.json({ message: "Impersonation request failed" }, { status: 500 });
      }

      // Staged credentials remain unreachable until every post-creation control succeeds.
      copyCollectedCredentials(stagedResponse, baseResponse);
      return NextResponse.json({ message: "Impersonation successful" });
    },
  );
}
```

</ConditionalContent>
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

:::info[Multi Tenancy]
Most examples use the default `"public"` tenant. In a multi-tenant application, derive the target tenant from trusted server-side data and verify that the actor may impersonate users in that tenant.

Never authorize a tenant only because its ID was supplied in the request body. If the client selects a tenant, treat that value as client-controlled input and check it against the actor's server-side tenant assignments before target lookup or session creation.
:::

- The API should be called from your frontend application so that the frontend SDK's network interceptors run.
- In the APIs above, required-session verification and backend admin-role validation run before the request reads or looks up the target. A missing or invalid session is rejected, and a session without the required role is rejected with `403`.
- Prefer a stable target user or recipe-user ID. If you look up by account information instead, reject zero or multiple matches rather than selecting the first result.
- A new session is then created using the target user's user ID. The `isImpersonation` flag is added to the access token payload so that the frontend can show that the staff member is impersonating a user. Backend APIs can also use this claim to restrict actions while impersonating. Treat claims such as `isImpersonation` and `impersonatedBy` as enforcement context, not as a durable audit log.
- The new session tokens attach to the response and overwrite the active admin credentials in that browser. This does not revoke the original admin session in SuperTokens. Cookies apply if the request contains the `st-auth-mode: "cookie"` header; otherwise, the mode is header-based authentication. The frontend interceptors set this header automatically.
- Signing out revokes the current impersonation session. Also provide explicit early termination and enforce your maximum duration on the backend; do not depend on the user remembering to sign out.
