Skip to content
5 changes: 5 additions & 0 deletions .changeset/strong-schools-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fixing redirect behavior when signing out from a multisession app with multple singed in accounts
36 changes: 36 additions & 0 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,42 @@ describe('Clerk singleton', () => {
expect(sut.navigate).toHaveBeenCalledWith('/after-sign-out');
});
});

it('properly restores auth state from remaining sessions after multisession sign-out', async () => {
const mockClient = {
signedInSessions: [mockSession1, mockSession2],
sessions: [mockSession1, mockSession2],
destroy: mockClientDestroy,
lastActiveSessionId: '1',
};

mockSession1.remove = jest.fn().mockImplementation(() => {
mockClient.signedInSessions = mockClient.signedInSessions.filter(s => s.id !== '1');
mockClient.sessions = mockClient.sessions.filter(s => s.id !== '1');
return Promise.resolve(mockSession1);
});

mockClientFetch.mockReturnValue(Promise.resolve(mockClient));

const sut = new Clerk(productionPublishableKey);
sut.navigate = jest.fn();
await sut.load();

expect(sut.session).toBe(mockSession1);
expect(sut.isSignedIn).toBe(true);

await sut.signOut({ sessionId: '1' });

await waitFor(() => {
expect(mockSession1.remove).toHaveBeenCalled();
expect(mockClientDestroy).not.toHaveBeenCalled();
expect(sut.navigate).toHaveBeenCalledWith('/');
});

expect(mockClient.lastActiveSessionId).toBe('2');
expect(sut.session).toBe(mockSession2);
expect(sut.isSignedIn).toBe(true);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Great addition; add guard-rail tests for non-current removal and active session selection

This verifies the main regression. Please add:

  • A test ensuring that removing a non-current session does not switch the active session.
  • A test ensuring that when the first remaining session is pending, we pick an active one as next.
  • Optionally assert that the next session’s getToken is invoked to sync cookies.

Example additions you can append near this block:

it('does not switch active session when signing out a non-current session', async () => {
  const mockClient = {
    signedInSessions: [mockSession1, mockSession2],
    sessions: [mockSession1, mockSession2],
    destroy: mockClientDestroy,
    lastActiveSessionId: '1',
  };
  mockClientFetch.mockReturnValue(Promise.resolve(mockClient));
  const sut = new Clerk(productionPublishableKey);
  await sut.load();

  await sut.signOut({ sessionId: '2' });

  await waitFor(() => {
    expect(mockSession2.remove).toHaveBeenCalled();
    expect(sut.session).toBe(mockSession1);
    expect(mockClient.lastActiveSessionId).toBe('1');
  });
});

it('prefers an active next session over pending when switching after current-session sign-out', async () => {
  const active = { ...mockSession2, id: '2', status: 'active', getToken: jest.fn() };
  const pending = { ...mockSession3, id: '3', status: 'pending', getToken: jest.fn() };
  const mockClient = {
    signedInSessions: [pending, active], // pending first to validate selection
    sessions: [mockSession1, pending, active],
    destroy: mockClientDestroy,
    lastActiveSessionId: '1',
  };
  mockSession1.remove = jest.fn().mockImplementation(() => {
    mockClient.signedInSessions = mockClient.signedInSessions.filter(s => s.id !== '1');
    mockClient.sessions = mockClient.sessions.filter(s => s.id !== '1');
    return Promise.resolve(mockSession1);
  });
  mockClientFetch.mockReturnValue(Promise.resolve(mockClient));

  const sut = new Clerk(productionPublishableKey);
  await sut.load();
  await sut.signOut({ sessionId: '1' });

  await waitFor(() => {
    expect(sut.session).toBe(active);
    expect(mockClient.lastActiveSessionId).toBe('2');
    // If you adopt token sync, assert:
    // expect(active.getToken).toHaveBeenCalled();
  });
});
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/__tests__/clerk.test.ts around lines 812 to 847,
the reviewer asks for two additional guard-rail tests: one ensuring removing a
non-current session does not switch the active session, and one ensuring that
when choosing the next session after removing the current session we prefer an
active session over a pending one (and optionally call its getToken to sync
cookies). Add a test that creates mockClient with signedInSessions
[mockSession1, mockSession2], sessions [mockSession1, mockSession2],
lastActiveSessionId '1', stub mockClientFetch to return it, load sut, call
signOut({ sessionId: '2' }), and assert mockSession2.remove was called,
sut.session remains mockSession1, and mockClient.lastActiveSessionId stays '1'.
Add a second test that constructs pending and active mocks (pending first in
signedInSessions), sets mockSession1.remove to remove session1 from client
arrays, stub mockClientFetch, load sut, signOut({ sessionId: '1' }), and assert
sut.session becomes the active session, mockClient.lastActiveSessionId updates
to the active id, and optionally assert active.getToken was called to sync
cookies.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add assertions for cookie/token sync and event emissions to prevent regressions

To ensure reload consistency, assert that the next session’s token is propagated or that a TokenUpdate was emitted. Also validate that UserSignOut is not emitted when removing a non-current session.

Examples:

  • Spy on eventBus.emit for events.TokenUpdate and verify it’s called with the next session token.
  • For non-current sign-out, assert eventBus.emit is not called with events.UserSignOut.
  • Optionally assert nextSession.getToken() is called if you choose that path.

I can draft these test additions if helpful.

🤖 Prompt for AI Agents
In packages/clerk-js/src/core/__tests__/clerk.test.ts around lines 813 to 847,
the test lacks assertions that the next session’s token is synced and that
correct events are emitted/omitted after removing a non-current session; add a
spy on eventBus.emit, assert that events.TokenUpdate is emitted with the next
session token (or assert nextSession.getToken() is called and its value is
propagated), and assert that events.UserSignOut is NOT emitted for the removed
non-current session; keep existing assertions about navigation and client
destruction and place the new event/token assertions after awaiting signOut so
they run against the final state.

});

describe('.navigate(to)', () => {
Expand Down
27 changes: 26 additions & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,33 @@ export class Clerk implements ClerkInterface {

await session?.remove();

if (this.client && this.client.signedInSessions.length > 0) {
const nextSession = this.client.signedInSessions[0];
if (nextSession) {
this.client.lastActiveSessionId = nextSession.id;
this.#setAccessors(nextSession);
this.#emit();
}
}

if (shouldSignOutCurrent) {
await executeSignOut();
const tracker = createBeforeUnloadTracker(this.#options.standardBrowser);

eventBus.emit(events.UserSignOut, null);

await tracker.track(async () => {
if (signOutCallback) {
await signOutCallback();
} else {
await this.navigate(redirectUrl);
}
});

if (tracker.isUnloading()) {
return;
}

await onAfterSetActive();
}
};

Expand Down