Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/native-session-lifetime-core-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Keep a session's `expireAt` and `abandonAt` at Unix epoch zero when the session has no maximum lifetime, instead of falling back to the current time.
6 changes: 3 additions & 3 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type {
} from '@clerk/shared/types';
import { isWebAuthnSupported as isWebAuthnSupportedOnWindow } from '@clerk/shared/webauthn';

import { unixEpochToDate } from '@/utils/date';
import { unixEpochToDate, unixEpochToDeadline } from '@/utils/date';
import { debugLogger } from '@/utils/debug';
import {
convertJSONToPublicKeyRequestOptions,
Expand Down Expand Up @@ -309,8 +309,8 @@ export class Session extends BaseResource implements SessionResource {

this.id = data.id;
this.status = data.status;
this.expireAt = unixEpochToDate(data.expire_at);
this.abandonAt = unixEpochToDate(data.abandon_at);
this.expireAt = unixEpochToDeadline(data.expire_at);
this.abandonAt = unixEpochToDeadline(data.abandon_at);
this.factorVerificationAge = data.factor_verification_age;
this.lastActiveAt = unixEpochToDate(data.last_active_at || undefined);
this.lastActiveOrganizationId = data.last_active_organization_id;
Expand Down
6 changes: 3 additions & 3 deletions packages/clerk-js/src/core/resources/SessionWithActivities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
SessionWithActivitiesResource,
} from '@clerk/shared/types';

import { unixEpochToDate } from '../../utils/date';
import { unixEpochToDate, unixEpochToDeadline } from '../../utils/date';
import { BaseResource } from './internal';

const mapSessionActivityJSONToSessionActivity = (data: SessionActivityJSON): SessionActivity => ({
Expand Down Expand Up @@ -65,8 +65,8 @@ export class SessionWithActivities extends BaseResource implements SessionWithAc

this.id = data.id;
this.status = data.status;
this.expireAt = unixEpochToDate(data.expire_at);
this.abandonAt = unixEpochToDate(data.abandon_at);
this.expireAt = unixEpochToDeadline(data.expire_at);
this.abandonAt = unixEpochToDeadline(data.abandon_at);
this.lastActiveAt = unixEpochToDate(data.last_active_at || undefined);
this.latestActivity = mapSessionActivityJSONToSessionActivity(data.latest_activity ?? ({} as SessionActivityJSON));
this.actor = data.actor;
Expand Down
25 changes: 25 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,31 @@ describe('Client Singleton', () => {
});
});

it('restores a session without a maximum lifetime from a client snapshot', () => {
const user = createUser({ first_name: 'John', last_name: 'Doe', id: 'user_1' });
const clientJSON: ClientJSON = {
object: 'client',
id: 'test_id',
status: 'active',
last_active_session_id: 'session_1',
sign_in: null,
sign_up: null,
sessions: [{ ...createSession({ id: 'session_1', status: 'active' }, user), expire_at: 0, abandon_at: 0 }],
created_at: Date.now() - 1000,
updated_at: Date.now(),
} as any;

// @ts-expect-error We cannot mess with the singleton when tests are running in parallel
const snapshot = new Client(clientJSON).__internal_toSnapshot();
// @ts-expect-error We cannot mess with the singleton when tests are running in parallel
const [restored] = new Client(snapshot).sessions;

expect(snapshot.sessions[0]).toMatchObject({ expire_at: 0, abandon_at: 0 });
expect(restored.expireAt.getTime()).toBe(0);
expect(restored.abandonAt.getTime()).toBe(0);
expect(restored.status).toBe('active');
});

it('__internal_toSnapshot()', () => {
const clientJSON = {
object: 'client',
Expand Down
44 changes: 44 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 @@ -8,6 +8,7 @@ import { eventBus } from '../../events';
import { createFapiClient } from '../../fapiClient';
import { SessionTokenCache } from '../../tokenCache';
import { BaseResource, Organization, Session } from '../internal';
import { SessionWithActivities } from '../SessionWithActivities';

const baseFapiClientOptions = {
frontendApi: 'clerk.example.com',
Expand All @@ -31,6 +32,49 @@ describe('Session', () => {
vi.useRealTimers();
});

it.each([
{ expire_at: 0, abandon_at: 0 },
{ expire_at: 2000000000000, abandon_at: 2000000000000 },
{ expire_at: 0, abandon_at: 2000000000000 },
{ expire_at: 2000000000000, abandon_at: 0 },
])('preserves session deadlines $expire_at and $abandon_at through snapshots', deadlines => {
const session = new Session({
id: 'session_lifetime',
status: 'active',
...deadlines,
public_user_data: { user_id: 'user_1' },
} as SessionJSON);
expect(session.expireAt.getTime()).toBe(deadlines.expire_at);
expect(session.abandonAt.getTime()).toBe(deadlines.abandon_at);

const snapshot = session.__internal_toSnapshot();
expect(snapshot).toMatchObject(deadlines);
const restored = new Session(snapshot);
expect(restored.expireAt.getTime()).toBe(deadlines.expire_at);
expect(restored.abandonAt.getTime()).toBe(deadlines.abandon_at);

const withActivities = new SessionWithActivities({ id: session.id, ...deadlines } as any, '/me/sessions');
expect(withActivities.expireAt.getTime()).toBe(deadlines.expire_at);
expect(withActivities.abandonAt.getTime()).toBe(deadlines.abandon_at);
});

it('keeps zero only for session deadlines', () => {
const session = new Session({
id: 'session_lifetime',
status: 'active',
expire_at: 0,
abandon_at: 0,
created_at: 0,
updated_at: 0,
public_user_data: { user_id: 'user_1' },
} as SessionJSON);

expect(session.expireAt.getTime()).toBe(0);
expect(session.abandonAt.getTime()).toBe(0);
expect(session.createdAt.getTime()).toBe(Date.now());
expect(session.updatedAt.getTime()).toBe(Date.now());
});

describe('getToken()', () => {
let dispatchSpy;

Expand Down
20 changes: 19 additions & 1 deletion packages/clerk-js/src/utils/__tests__/date.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import { unixEpochToDate } from '../date';
import { unixEpochToDate, unixEpochToDeadline } from '../date';

describe('date utilities', () => {
beforeAll(() => {
Expand All @@ -13,6 +13,10 @@ describe('date utilities', () => {
});

describe('unixEpochToDate', () => {
it('fallbacks to current date for zero', function () {
expect(unixEpochToDate(0).getTime()).toBe(Date.now());
});

it('converts a unix epoch to Date', function () {
const epoch = 1622575087;
const date = unixEpochToDate(epoch);
Expand All @@ -29,4 +33,18 @@ describe('date utilities', () => {
expect(date.getTime()).toBe(Date.now());
});
});

describe('unixEpochToDeadline', () => {
it('keeps zero as Unix epoch zero', () => {
expect(unixEpochToDeadline(0).getTime()).toBe(0);
});

it('converts a unix epoch to Date', () => {
expect(unixEpochToDeadline(1622575087).getTime()).toBe(1622575087);
});

it('fallbacks to current date for undefined values', () => {
expect(unixEpochToDeadline(undefined).getTime()).toBe(Date.now());
});
});
});
4 changes: 4 additions & 0 deletions packages/clerk-js/src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ export function unixEpochToDate(epochInSeconds?: number): Date {
const date = new Date(epochInSeconds || new Date());
return isValidDate(date) ? date : new Date();
}

export function unixEpochToDeadline(epoch?: number): Date {
return epoch === 0 ? new Date(0) : unixEpochToDate(epoch);
}
Loading