Clerk lets an administrator sign in as any user from the dashboard (Users, the row's menu, Impersonate user) or through the Backend API's actor tokens. The session that results is a real session for that user. Your app cannot tell it apart unless it looks for one claim.
The claim
An impersonated session's token carries act:
{ "sub": "user_2abc", "act": { "sub": "user_2admin" } }
sub is the user being viewed. act.sub is the person doing the viewing. On
the server, auth() returns it as actor:
import { auth } from "@clerk/nextjs/server";
const { userId, actor } = await auth();
const impersonatedBy = actor ? actor.sub : null;
It comes from the verified session token, so a browser cannot add or remove it.
Not every actor is a person
Clerk also issues actor tokens for AI agents acting for a user. Those carry an actor with a type that marks them as an agent. "An admin is looking at my account" and "my assistant is doing a task I asked for" need different treatment. If you only mean the first, filter the second out:
function impersonatorOf(actor: { sub: string; type?: string } | null | undefined) {
if (!actor || actor.type === "agent") return null;
return actor.sub;
}
Put it on your session user
Whatever your app calls the current user, give it one nullable field and let everything read that:
interface SessionUser {
id: string;
email: string | null;
// ...
impersonatedBy: string | null;
}
Then three things key off it.
A banner on every signed-in page. "You are viewing as jane@example.com. Stop." Without it, an admin forgets and does something as the user.
Account changes are off. Password, email, connected accounts, MFA,
deleting the account, payment methods. An admin viewing as someone must not
change how that person signs in. If your settings page embeds Clerk's
<UserProfile />, render a read-only summary instead while the claim is set,
because <UserProfile /> itself does not know about your policy. Check it in
your own server actions too, not only in the UI:
const user = await requireUser();
if (user.impersonatedBy) {
return { error: "Account changes are off while you are viewing as this user." };
}
The audit log names both. Anything written during the session records
impersonatedBy next to the user id. "The user cancelled their plan" and "an
admin cancelled it while viewing as them" are different support tickets.
What Clerk enforces, and what it does not
Worth knowing before you write "account changes are off" anywhere.
Clerk enforces:
- The claim.
actis part of the signed session token. The browser cannot add it, change it or strip it. - The token. An actor token works once, expires (you choose how soon), and can be revoked before it is used.
- The length. The session ends at the actor token's
sessionMaxDurationInSeconds(30 minutes by default) or after 10 minutes with no activity. - The record. Impersonation sessions are logged, and the Backend API's session list shows them with their actor.
Clerk does not promise:
- A read-only session. Its docs do not list which account changes an actor
session is refused. The Frontend API has an
impersonated_session_forbiddenerror ("This action isn't available while impersonating a user."), but no documented list of the actions behind it. - That its components know your policy. Nothing in Clerk's docs says
<UserProfile />or<UserButton />hide their forms for an actor session. - That the admin cannot go around your UI. The session in the admin's
browser is a real Clerk session for the user, and
window.Clerk.usercalls Clerk's Frontend API directly. Your server never sees those calls, so no check of yours runs on them.
So the honest setup is the one above: read-only summaries in place of Clerk's profile components, a refusal in every server action of yours that changes the account, and a confirm dialog that says the lock is the app's, not Clerk's. It stops mistakes. It does not stop an admin who opens the console, and the short session length is what bounds that.
Ending it
Signing out ends the impersonated session; Clerk does not restore the admin's
own session. The admin signs in again as themselves. A Stop button is a
SignOutButton with a clear label.
Cost
Impersonation is metered. At the time of writing Clerk's pricing page lists 5 impersonations a month on every plan, and unlimited with its Administration add-on. Check https://clerk.com/pricing before you promise support staff they can use it all day.
Test it
- Impersonate a test user from the dashboard. The banner shows, and
/settings/securityshows the summary, not Clerk's forms. - Post a settings action with curl using that session's cookie: refused.
- Open
/settings/profileand/settings/securitywhile impersonating: no Clerk form on either page. - Sign in as the same user in another browser, without impersonating: no banner, everything editable. The claim belongs to the session, not the user.