diff --git a/.changeset/tidy-signout-flow.md b/.changeset/tidy-signout-flow.md new file mode 100644 index 00000000000..ad9a0c0dfb6 --- /dev/null +++ b/.changeset/tidy-signout-flow.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Treat terminal user-state 403 responses as unauthenticated in ClerkJS. diff --git a/packages/shared/src/__tests__/error.spec.ts b/packages/shared/src/__tests__/error.spec.ts index b4d21e8e45d..47981be4a4e 100644 --- a/packages/shared/src/__tests__/error.spec.ts +++ b/packages/shared/src/__tests__/error.spec.ts @@ -108,9 +108,15 @@ describe('isUnauthenticatedError', () => { expect(isUnauthenticatedError({ status: 422 })).toBe(true); }); + it('returns true for terminal user state 403 errors', () => { + expect(isUnauthenticatedError({ status: 403, errors: [{ code: 'user_banned' }] })).toBe(true); + expect(isUnauthenticatedError({ status: 403, errors: [{ code: 'user_deactivated' }] })).toBe(true); + }); + it('returns false for other 4xx status codes', () => { expect(isUnauthenticatedError({ status: 400 })).toBe(false); expect(isUnauthenticatedError({ status: 403 })).toBe(false); + expect(isUnauthenticatedError({ status: 403, errors: [{ code: 'not_allowed_access' }] })).toBe(false); expect(isUnauthenticatedError({ status: 404 })).toBe(false); expect(isUnauthenticatedError({ status: 429 })).toBe(false); }); diff --git a/packages/shared/src/errors/helpers.ts b/packages/shared/src/errors/helpers.ts index 93d22ef627b..1aeb4c698d7 100644 --- a/packages/shared/src/errors/helpers.ts +++ b/packages/shared/src/errors/helpers.ts @@ -46,6 +46,8 @@ export function is429Error(e: any): boolean { return e?.status === 429; } +const unauthenticated403ErrorCodes = new Set(['user_banned', 'user_deactivated']); + /** * Checks if the provided error indicates the user's session is no longer valid * and should trigger the unauthenticated flow (e.g. sign-out / redirect to sign-in). @@ -53,6 +55,7 @@ export function is429Error(e: any): boolean { * Only matches explicit authentication failure status codes: * - 401: session is invalid or expired * - 422: invalid session state (e.g. missing_expired_token) + * - 403: terminal user state (e.g. user_banned, user_deactivated) * * 404 is intentionally excluded despite being returned for "session not found", * because it's also returned for unrelated resources (org not found, JWT template @@ -64,7 +67,10 @@ export function is429Error(e: any): boolean { */ export function isUnauthenticatedError(e: any): boolean { const status = e?.status; - return status === 401 || status === 422; + const hasTerminalUserErrorCode = + Array.isArray(e?.errors) && e.errors.some((error: any) => unauthenticated403ErrorCodes.has(error?.code)); + + return status === 401 || status === 422 || (status === 403 && hasTerminalUserErrorCode); } /**