Skip to content

Clerk impersonation, the act claim, and what to lock while it is on

When an admin signs in as a user from the Clerk dashboard, the session token carries an act claim. Read it on the server, show a banner, and refuse account changes until it ends.

Clerk4 min readships at docs/solutions/clerk/clerk-impersonation-and-the-act-claim.md

Tags: clerk · impersonation · admin · security · nextjs

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. act is 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_forbidden error ("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.user calls 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/security shows the summary, not Clerk's forms.
  • Post a settings action with curl using that session's cookie: refused.
  • Open /settings/profile and /settings/security while 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.