Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Implement user impersonation

Enable user impersonation for testing and support by creating an admin-protected API endpoint.

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

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.

Next.js router
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!" });
  },
);
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);
  },
});
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!" });
  },
);
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"),
  ],
});
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!" };
  },
);
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!" };
  }
}
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!" };
  }
}
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
}
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!"})
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!"})
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!"})
  • 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.

API reference

API schema and response details