Skip to content
Open
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
8 changes: 8 additions & 0 deletions .changeset/drop-session-minter-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@clerk/clerk-js": minor
"@clerk/shared": minor
---

Always use the Session Minter request shape for `/tokens` calls. The previous gate, sourced from `auth_config.session_minter` on the environment payload, is removed so all instances send the prior session token in the request body and `forceOrigin=true` when `skipCache` is set. The FAPI proxy strips both fields when no minter is reachable, so behavior is unchanged for instances not yet enrolled. The legacy `expired_token` retry path on 422 `missing_expired_token` is no longer needed and has been deleted.

`AuthConfigResource.sessionMinter` and `AuthConfigJSON.session_minter` are removed.
3 changes: 0 additions & 3 deletions packages/clerk-js/src/core/resources/AuthConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export class AuthConfig extends BaseResource implements AuthConfigResource {
reverification: boolean = false;
singleSessionMode: boolean = false;
preferredChannels: Record<string, PhoneCodeChannel> | null = null;
sessionMinter: boolean = false;

public constructor(data: Partial<AuthConfigJSON> | null = null) {
super();
Expand All @@ -24,7 +23,6 @@ export class AuthConfig extends BaseResource implements AuthConfigResource {
this.reverification = this.withDefault(data.reverification, this.reverification);
this.singleSessionMode = this.withDefault(data.single_session_mode, this.singleSessionMode);
this.preferredChannels = this.withDefault(data.preferred_channels, this.preferredChannels);
this.sessionMinter = this.withDefault(data.session_minter, this.sessionMinter);
return this;
}

Expand All @@ -35,7 +33,6 @@ export class AuthConfig extends BaseResource implements AuthConfigResource {
object: 'auth_config',
reverification: this.reverification,
single_session_mode: this.singleSessionMode,
session_minter: this.sessionMinter,
};
}
}
28 changes: 4 additions & 24 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { createCheckAuthorization } from '@clerk/shared/authorization';
import { isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser';
import {
ClerkOfflineError,
ClerkRuntimeError,
ClerkWebAuthnError,
is4xxError,
is429Error,
MissingExpiredTokenError,
} from '@clerk/shared/error';
import { ClerkOfflineError, ClerkRuntimeError, ClerkWebAuthnError, is4xxError, is429Error } from '@clerk/shared/error';
import {
convertJSONToPublicKeyRequestOptions,
serializePublicKeyCredentialAssertion,
Expand Down Expand Up @@ -483,28 +476,15 @@ export class Session extends BaseResource implements SessionResource {
): Promise<TokenResource> {
const path = template ? `${this.path()}/tokens/${template}` : `${this.path()}/tokens`;
// TODO: update template endpoint to accept organizationId
const sessionMinterEnabled = Session.clerk?.__internal_environment?.authConfig?.sessionMinter;
const params: Record<string, string | null> = template
? {}
: {
organizationId: organizationId ?? null,
...(sessionMinterEnabled && this.lastActiveToken ? { token: this.lastActiveToken.getRawString() } : {}),
...(sessionMinterEnabled && skipCache ? { forceOrigin: 'true' } : {}),
...(this.lastActiveToken ? { token: this.lastActiveToken.getRawString() } : {}),
...(skipCache ? { forceOrigin: 'true' } : {}),
};

if (sessionMinterEnabled) {
// Session Minter sends the token in the body, no expired_token retry needed
return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined);
}

// TODO: Remove this expired_token retry flow when the sessionMinter flag is removed
const lastActiveToken = this.lastActiveToken?.getRawString();
return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined).catch(e => {
if (MissingExpiredTokenError.is(e) && lastActiveToken) {
return Token.create(path, { ...params }, { expired_token: lastActiveToken });
}
throw e;
});
return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined);
}

#dispatchTokenEvents(token: TokenResource, shouldDispatch: boolean): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('AuthConfig', () => {
id: '',
reverification: true,
single_session_mode: true,
session_minter: false,
});
});
});
190 changes: 3 additions & 187 deletions packages/clerk-js/src/core/resources/__tests__/Session.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClerkAPIResponseError, ClerkOfflineError } from '@clerk/shared/error';
import { ClerkOfflineError } from '@clerk/shared/error';
import type { InstanceType, OrganizationJSON, SessionJSON } from '@clerk/shared/types';
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest';

Expand Down Expand Up @@ -1624,11 +1624,7 @@ describe('Session', () => {
beforeEach(() => {
dispatchSpy = vi.spyOn(eventBus, 'emit');
fetchSpy = vi.spyOn(BaseResource, '_fetch' as any);
BaseResource.clerk = clerkMock({
__internal_environment: {
authConfig: { sessionMinter: true },
},
}) as any;
BaseResource.clerk = clerkMock() as any;
});

afterEach(() => {
Expand Down Expand Up @@ -1748,11 +1744,7 @@ describe('Session', () => {
beforeEach(() => {
dispatchSpy = vi.spyOn(eventBus, 'emit');
fetchSpy = vi.spyOn(BaseResource, '_fetch' as any);
BaseResource.clerk = clerkMock({
__internal_environment: {
authConfig: { sessionMinter: true },
},
}) as any;
BaseResource.clerk = clerkMock() as any;
});

afterEach(() => {
Expand Down Expand Up @@ -1812,182 +1804,6 @@ describe('Session', () => {
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('forceOrigin');
});

it('does not include forceOrigin when sessionMinter is false even with skipCache true', async () => {
BaseResource.clerk = clerkMock({
__internal_environment: {
authConfig: { sessionMinter: false },
},
}) as any;

const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
last_active_token: { object: 'token', jwt: mockJwt },
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON);

SessionTokenCache.clear();

fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt });

await session.getToken({ skipCache: true });

expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy.mock.calls[0][0].body).not.toHaveProperty('forceOrigin');
});
});

describe('origin outage mode fallback', () => {
let dispatchSpy: ReturnType<typeof vi.spyOn>;
let fetchSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
SessionTokenCache.clear();
dispatchSpy = vi.spyOn(eventBus, 'emit');
fetchSpy = vi.spyOn(BaseResource, '_fetch' as any);
BaseResource.clerk = clerkMock() as any;
});

afterEach(() => {
dispatchSpy?.mockRestore();
fetchSpy?.mockRestore();
BaseResource.clerk = null as any;
});

it('should retry with expired token when API returns 422 with missing_expired_token error', async () => {
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
last_active_token: { object: 'token', jwt: mockJwt },
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON);

SessionTokenCache.clear();

const errorResponse = new ClerkAPIResponseError('Missing expired token', {
data: [
{ code: 'missing_expired_token', message: 'Missing expired token', long_message: 'Missing expired token' },
],
status: 422,
});
fetchSpy.mockRejectedValueOnce(errorResponse);

fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt });

await session.getToken();

expect(fetchSpy).toHaveBeenCalledTimes(2);

expect(fetchSpy.mock.calls[0][0]).toMatchObject({
path: '/client/sessions/session_1/tokens',
method: 'POST',
body: { organizationId: null },
});

expect(fetchSpy.mock.calls[1][0]).toMatchObject({
path: '/client/sessions/session_1/tokens',
method: 'POST',
body: { organizationId: null },
search: { expired_token: mockJwt },
});
});

it('should not retry with expired token when lastActiveToken is not available', async () => {
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
last_active_token: null,
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as unknown as SessionJSON);

SessionTokenCache.clear();

const errorResponse = new ClerkAPIResponseError('Missing expired token', {
data: [
{ code: 'missing_expired_token', message: 'Missing expired token', long_message: 'Missing expired token' },
],
status: 422,
});
fetchSpy.mockRejectedValue(errorResponse);

await expect(session.getToken()).rejects.toMatchObject({
status: 422,
errors: [{ code: 'missing_expired_token' }],
});

expect(fetchSpy).toHaveBeenCalledTimes(1);
});

it('should not retry with expired token for non-422 errors', async () => {
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
last_active_token: { object: 'token', jwt: mockJwt },
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as SessionJSON);

SessionTokenCache.clear();

const errorResponse = new ClerkAPIResponseError('Bad request', {
data: [{ code: 'bad_request', message: 'Bad request', long_message: 'Bad request' }],
status: 400,
});
fetchSpy.mockRejectedValueOnce(errorResponse);

await expect(session.getToken()).rejects.toThrow(ClerkAPIResponseError);

expect(fetchSpy).toHaveBeenCalledTimes(1);
});

it('should not retry with expired token when error code is different', async () => {
const session = new Session({
status: 'active',
id: 'session_1',
object: 'session',
user: createUser({}),
last_active_organization_id: null,
last_active_token: { object: 'token', jwt: mockJwt },
actor: null,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
} as unknown as SessionJSON);

SessionTokenCache.clear();

const errorResponse = new ClerkAPIResponseError('Validation failed', {
data: [{ code: 'validation_error', message: 'Validation failed', long_message: 'Validation failed' }],
status: 422,
});
fetchSpy.mockRejectedValue(errorResponse);

await expect(session.getToken()).rejects.toMatchObject({
status: 422,
errors: [{ code: 'validation_error' }],
});

expect(fetchSpy).toHaveBeenCalledTimes(1);
});
});

describe('agent', () => {
Expand Down
1 change: 0 additions & 1 deletion packages/shared/src/types/authConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ export interface AuthConfigResource extends ClerkResource {
* Preferred channels for phone code providers.
*/
preferredChannels: Record<string, PhoneCodeChannel> | null;
sessionMinter: boolean;
__internal_toSnapshot: () => AuthConfigJSONSnapshot;
}
1 change: 0 additions & 1 deletion packages/shared/src/types/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,6 @@ export interface AuthConfigJSON extends ClerkResourceJSON {
claimed_at: number | null;
reverification: boolean;
preferred_channels?: Record<string, PhoneCodeChannel>;
session_minter?: boolean;
}

export interface VerificationJSON extends ClerkResourceJSON {
Expand Down
Loading