User Impersonation
Some bugs only exist for one person. A record that won’t open, a menu item that isn’t there, a total that looks wrong — all of it depends on the web roles, table permissions and Dataverse security roles attached to that contact. User impersonation lets an internal administrator sign in as that contact and see precisely what they see, then step back out.
When it helps
Impersonation answers questions that a description of the problem can’t.
- Reproducing a support issue. Rather than reasoning about which permission might be missing, you look at the page the way the person reporting it does.
- Verifying a permission change. After editing a web role or a table permission, confirm the effect on a real contact instead of inferring it.
- Checking what a role actually exposes. Useful before go-live, when the gap between intended and actual access is easiest to get wrong.
Using it
The affordance lives in the profile menu, and appears only for users allowed to use it.
- Sign in as an internal user holding the administrator role, then open the profile menu and choose View as another user.
- Type at least three characters to search by name or email address, or paste a user ID for an exact match.
- Pick the person and confirm. The portal reloads as them — same web roles, same table permissions, same rows.
- A banner stays pinned to the top of every page for as long as it lasts. Choose Stop to return to your own session.
Who can impersonate
Two conditions are checked on the server for every call, independently of what the interface offered:
- The signed-in user holds the
SystemAdminrole. Your project decides which Dataverse security role grants it — the generated templates map the built-in System Administrator role plus a configurable one throughPortalIdentityOptions.SystemAdminRoleName. - The signed-in user is a genuine internal Dataverse user. This is checked separately from the role, so a portal contact can never impersonate even if your project grants it the role by mistake.
The target is always a portal contact, resolved through the contact table alone. Supplying another internal user’s ID simply doesn’t resolve — there is no path from here to a second staff account.
Impersonation only ever reduces access
An internal user becomes a contact, never the reverse and never another internal user. That direction is what keeps the feature safe to ship: the impersonated session can only ever do less than the administrator could, so there is no privilege to be gained by using it.
Restricting who can be impersonated
By default every active contact is a candidate. Implement IImpersonationTargetFilter to narrow that — to exclude contacts linked to staff accounts, records outside the administrator’s business unit, or anything else your organization treats as off limits.
public sealed class StaffContactsAreOffLimits : IImpersonationTargetFilter
{
public async Task<bool> CanImpersonateAsync(
ClaimsPrincipal impersonator, Guid targetContactId, CancellationToken ct)
{
// Return false to hide the contact from search AND refuse impersonation.
return await this.IsOrdinaryPortalContactAsync(targetContactId, ct);
}
}
// Program.cs
builder.Services.AddScoped<IImpersonationTargetFilter, StaffContactsAreOffLimits>();
The filter governs search as well as impersonation. A contact you reject is invisible in the picker rather than merely unselectable — otherwise search would quietly become a way to enumerate people an administrator can never actually become.
The audit trail
Every start, stop and refused attempt is recorded. Refusals are recorded deliberately: a trail of successes alone cannot answer whether anyone tried.
Dataverse cannot record who was behind the session
The portal connects to Dataverse as a single application user and expresses the signed-in session by impersonating the target. The administrator is never transmitted, so in the Dataverse audit log “an administrator acting as a contact” and “that contact signing in normally” are indistinguishable — and anything written during impersonation is attributed to the contact in
createdbyandmodifiedby. The portal-side trail is the only place this information exists, which is why it isn’t optional.
Out of the box every event is written to the application log. Implement IImpersonationAuditSink to persist it wherever your organization keeps its audit records — sinks are added alongside the built-in one rather than replacing it.
public sealed class ImpersonationAuditTable : IImpersonationAuditSink
{
public Task OnStartedAsync(ImpersonationAuditEvent e, CancellationToken ct) => this.WriteAsync("started", e, ct);
public Task OnStoppedAsync(ImpersonationAuditEvent e, CancellationToken ct) => this.WriteAsync("stopped", e, ct);
// Denied attempts matter as much as successful ones — a trail of
// successes alone can't answer "did anyone try?".
public Task OnDeniedAsync(ImpersonationAuditEvent e, string reason, CancellationToken ct) => this.WriteAsync(reason, e, ct);
}
// Program.cs — added, not replaced: the framework's own logging sink stays.
builder.Services.AddTransient<IImpersonationAuditSink, ImpersonationAuditTable>();
From React
The same experience ships for React: a profile-menu entry, the picker, and the banner. The underlying actions are on useAuth() if you’d rather build your own interface around them.
const auth = useAuth();
// Search, then start. Both are refused server-side unless the caller
// is an internal user holding the SystemAdmin role.
const { results } = await auth.searchImpersonationTargets('smith');
await auth.impersonate(results[0].contactId);
// While impersonating, `auth.user` describes the CONTACT.
auth.user.isImpersonating; // true
auth.user.impersonatorName; // the administrator behind the session
await auth.stopImpersonation();
Worth knowing
- If the impersonated contact changes their password mid-session, the session ends and you’re returned to the signed-out state rather than to your own account. Signing in again restores it.
- Impersonation can’t be nested. Stop the current session before starting another.
- It doesn’t outlive the browser session — closing the browser ends it.
