# 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.

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`:

```json
{ "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`:

```ts
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:

```ts
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:

```ts
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:

```ts
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.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
