Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/short-taxes-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
---

Don't display impersonation for agents
3 changes: 3 additions & 0 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ClerkWebAuthnError, is4xxError, MissingExpiredTokenError } from '@clerk
import { retry } from '@clerk/shared/retry';
import type {
ActClaim,
AgentActClaim,
CheckAuthorization,
EmailCodeConfig,
EnterpriseSSOConfig,
Expand Down Expand Up @@ -50,6 +51,7 @@ export class Session extends BaseResource implements SessionResource {
lastActiveToken!: TokenResource | null;
lastActiveOrganizationId!: string | null;
actor!: ActClaim | null;
agent!: AgentActClaim | null;
user!: UserResource | null;
publicUserData!: PublicUserData;
factorVerificationAge: [number, number] | null = null;
Expand Down Expand Up @@ -311,6 +313,7 @@ export class Session extends BaseResource implements SessionResource {
this.lastActiveAt = unixEpochToDate(data.last_active_at || undefined);
this.lastActiveOrganizationId = data.last_active_organization_id;
this.actor = data.actor || null;
this.agent = data.actor?.type === 'agent' ? (data.actor as AgentActClaim) : null;
this.createdAt = unixEpochToDate(data.created_at);
this.updatedAt = unixEpochToDate(data.updated_at);
this.user = new User(data.user);
Expand Down
53 changes: 53 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1233,4 +1233,57 @@ describe('Session', () => {
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
});

describe('agent', () => {
it('sets agent to null when actor is null', () => {
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON);

expect(session.actor).toBeNull();
expect(session.agent).toBeNull();
});

it('sets agent to null when actor has no type (impersonation)', () => {
const actor = { sub: 'user_2' };
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
actor,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON);

expect(session.actor).toEqual(actor);
expect(session.agent).toBeNull();
});

it('sets agent to the actor when actor has type "agent"', () => {
const actor = { sub: 'user_2', type: 'agent' as const };
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
actor,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON);

expect(session.actor).toEqual(actor);
expect(session.agent).toEqual(actor);
expect(session.agent?.type).toBe('agent');
});
});
});
3 changes: 2 additions & 1 deletion packages/clerk-js/src/test/fixture-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => {
saml_accounts?: Array<Partial<SamlAccountJSON>>;
organization_memberships?: Array<string | OrgParams>;
tasks?: SessionJSON['tasks'];
actor?: SessionJSON['actor'];
};

const createPublicUserData = (params: WithUserParams) => {
Expand Down Expand Up @@ -83,7 +84,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => {
id: baseClient.sessions.length.toString(),
object: 'session',
last_active_organization_id: activeOrganization,
actor: null,
actor: params.actor ?? null,
user: createUser(params),
public_user_data: createPublicUserData(params),
created_at: new Date().getTime(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';
import { render } from '@/test/utils';

import { ImpersonationFab } from '../';

const { createFixtures } = bindCreateFixtures('UserButton');

describe('ImpersonationFab', () => {
it('does not render when user has no actor', async () => {
const { wrapper } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
});
render(<ImpersonationFab />, { wrapper });
expect(document.getElementById('cl-impersonationEye')).toBeNull();
});

it('renders when user has actor without type (impersonation)', async () => {
const { wrapper } = await createFixtures(f => {
f.withUser({
email_addresses: ['test@clerk.com'],
actor: { sub: 'user_impersonated' },
});
});
render(<ImpersonationFab />, { wrapper });
expect(document.getElementById('cl-impersonationEye')).toBeInTheDocument();
});

it('does not render when user has actor with type "agent"', async () => {
const { wrapper } = await createFixtures(f => {
f.withUser({
email_addresses: ['test@clerk.com'],
actor: { sub: 'user_agent', type: 'agent' },
});
});
render(<ImpersonationFab />, { wrapper });
expect(document.getElementById('cl-impersonationEye')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ const ImpersonationFabInternal = () => {
const { parsedInternalTheme } = useAppearance();
const containerRef = useRef<HTMLDivElement>(null);
const actor = session?.actor;
const isImpersonating = !!actor;
const agent = session?.agent;
const isImpersonating = !!actor && !agent;

//essentials for calcs
const eyeWidth = parsedInternalTheme.sizes.$16;
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/src/types/jwtv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,28 @@ export type VersionedJwtPayload =

export type JwtPayload = JWTPayloadBase & CustomJwtSessionClaims & VersionedJwtPayload;

/**
* The type of the actor claim.
*/
export type ActClaimType = 'agent';

/**
* JWT Actor - [RFC8693](https://www.rfc-editor.org/rfc/rfc8693.html#name-act-actor-claim).
* @inline
*/
export interface ActClaim {
sub: string;
type?: ActClaimType;
[x: string]: unknown;
}

/**
* ActClaim narrowed to actor type `'agent'`. Use for session.agent.
*
* @inline
*/
export type AgentActClaim = ActClaim & { type: 'agent' };

/**
* The current state of the session which can only be `active` or `pending`.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
PhoneCodeSecondFactorConfig,
TOTPAttempt,
} from './factors';
import type { ActClaim } from './jwtv2';
import type { ActClaim, AgentActClaim } from './jwtv2';
import type {
OrganizationCustomPermissionKey,
OrganizationCustomRoleKey,
Expand Down Expand Up @@ -226,6 +226,7 @@ export interface SessionResource extends ClerkResource {
lastActiveOrganizationId: string | null;
lastActiveAt: Date;
actor: ActClaim | null;
agent: AgentActClaim | null;
tasks: Array<SessionTask> | null;
currentTask?: SessionTask;
/**
Expand Down
Loading