Skip to content

Commit 4e08924

Browse files
authored
fix(shared): handle banned/deactivated 403 as unauthenticated
#8004 narrowed ClerkJS sign-out from any 4xx to isUnauthenticatedError, but that helper only matched 401 and 422. Terminal 403s for user_banned and user_deactivated stopped triggering sign-out as a result. Changes: - Add user_banned and user_deactivated to an unauthenticated403ErrorCodes set - isUnauthenticatedError returns true for 403 only when the response carries one of those codes - Generic 403 and 429 stay out of the sign-out path - Tests cover both directions Packages affected: - @clerk/shared: predicate + tests - @clerk/clerk-js: behavior change via the shared predicate
1 parent c529a10 commit 4e08924

3 files changed

Lines changed: 19 additions & 1 deletion

File tree

.changeset/tidy-signout-flow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
---
5+
6+
Treat terminal user-state 403 responses as unauthenticated in ClerkJS.

packages/shared/src/__tests__/error.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,15 @@ describe('isUnauthenticatedError', () => {
108108
expect(isUnauthenticatedError({ status: 422 })).toBe(true);
109109
});
110110

111+
it('returns true for terminal user state 403 errors', () => {
112+
expect(isUnauthenticatedError({ status: 403, errors: [{ code: 'user_banned' }] })).toBe(true);
113+
expect(isUnauthenticatedError({ status: 403, errors: [{ code: 'user_deactivated' }] })).toBe(true);
114+
});
115+
111116
it('returns false for other 4xx status codes', () => {
112117
expect(isUnauthenticatedError({ status: 400 })).toBe(false);
113118
expect(isUnauthenticatedError({ status: 403 })).toBe(false);
119+
expect(isUnauthenticatedError({ status: 403, errors: [{ code: 'not_allowed_access' }] })).toBe(false);
114120
expect(isUnauthenticatedError({ status: 404 })).toBe(false);
115121
expect(isUnauthenticatedError({ status: 429 })).toBe(false);
116122
});

packages/shared/src/errors/helpers.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,16 @@ export function is429Error(e: any): boolean {
4646
return e?.status === 429;
4747
}
4848

49+
const unauthenticated403ErrorCodes = new Set(['user_banned', 'user_deactivated']);
50+
4951
/**
5052
* Checks if the provided error indicates the user's session is no longer valid
5153
* and should trigger the unauthenticated flow (e.g. sign-out / redirect to sign-in).
5254
*
5355
* Only matches explicit authentication failure status codes:
5456
* - 401: session is invalid or expired
5557
* - 422: invalid session state (e.g. missing_expired_token)
58+
* - 403: terminal user state (e.g. user_banned, user_deactivated)
5659
*
5760
* 404 is intentionally excluded despite being returned for "session not found",
5861
* because it's also returned for unrelated resources (org not found, JWT template
@@ -64,7 +67,10 @@ export function is429Error(e: any): boolean {
6467
*/
6568
export function isUnauthenticatedError(e: any): boolean {
6669
const status = e?.status;
67-
return status === 401 || status === 422;
70+
const hasTerminalUserErrorCode =
71+
Array.isArray(e?.errors) && e.errors.some((error: any) => unauthenticated403ErrorCodes.has(error?.code));
72+
73+
return status === 401 || status === 422 || (status === 403 && hasTerminalUserErrorCode);
6874
}
6975

7076
/**

0 commit comments

Comments
 (0)