From 14a7c2395dfc0307e8450c821fa386ff9872edd3 Mon Sep 17 00:00:00 2001 From: David Cornu Date: Fri, 24 Apr 2026 13:16:07 -0400 Subject: [PATCH 1/3] fix: Correctly handle all `isAuthenticationErrorData` cases --- src/common/exceptions/authentication.exception.ts | 15 ++++++++++++--- src/workos.ts | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/common/exceptions/authentication.exception.ts b/src/common/exceptions/authentication.exception.ts index 8d89f3412..3088df28c 100644 --- a/src/common/exceptions/authentication.exception.ts +++ b/src/common/exceptions/authentication.exception.ts @@ -32,9 +32,18 @@ const AUTHENTICATION_ERROR_CODES: ReadonlySet = new Set([ export function isAuthenticationErrorData( data: WorkOSErrorData, ): data is AuthenticationErrorData { - return ( - typeof data.code === 'string' && AUTHENTICATION_ERROR_CODES.has(data.code) - ); + let discriminant: string | undefined; + + if (typeof data.code === 'string') { + discriminant = data.code; + } else if (typeof data.error === 'string') { + // Some errors like `sso_required` use an `error` field instead of `code` + discriminant = data.error; + } + + if (!discriminant) return false; + + return AUTHENTICATION_ERROR_CODES.has(discriminant); } export class AuthenticationException extends GenericServerException { diff --git a/src/workos.ts b/src/workos.ts index 9c4a143d6..d2ab92f17 100644 --- a/src/workos.ts +++ b/src/workos.ts @@ -483,7 +483,9 @@ export class WorkOS { ); } default: { - if (error || errorDescription) { + if (isAuthenticationErrorData(data)) { + throw new AuthenticationException(status, data, requestID); + } else if (error || errorDescription) { throw new OauthException( status, requestID, @@ -500,8 +502,6 @@ export class WorkOS { message, requestID, }); - } else if (isAuthenticationErrorData(data)) { - throw new AuthenticationException(status, data, requestID); } else { throw new GenericServerException( status, From a573a19992872459b526571c5be8553c861b7765 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 24 Apr 2026 17:58:58 -0400 Subject: [PATCH 2/3] fix: Make `AuthenticationErrorData` type-safe for both response shapes The API returns authentication errors in two shapes: 403s use `code` + `message`, while 400s (e.g. `sso_required`) use `error` + `error_description`. The previous type declared `code` as non-optional, so TypeScript consumers got `undefined` at runtime with no compiler warning after the type guard narrowed. Model the two shapes as a discriminated union and add a guaranteed `AuthenticationException.code` accessor that normalizes both into a single `AuthenticationErrorCode`. Adds a regression test with the actual `sso_required` payload shape. --- .../exceptions/authentication.exception.ts | 65 +++++++++++++++---- src/workos.spec.ts | 29 +++++++++ 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/common/exceptions/authentication.exception.ts b/src/common/exceptions/authentication.exception.ts index 3088df28c..d91b9eeb5 100644 --- a/src/common/exceptions/authentication.exception.ts +++ b/src/common/exceptions/authentication.exception.ts @@ -12,14 +12,24 @@ export type AuthenticationErrorCode = | 'mfa_verification' | 'sso_required'; -export interface AuthenticationErrorData extends WorkOSErrorData { - code: AuthenticationErrorCode; +interface BaseAuthenticationErrorData extends WorkOSErrorData { + error?: string; + error_description?: string; pending_authentication_token?: string; user?: UserResponse; organizations?: Array<{ id: string; name: string }>; connection_ids?: string[]; } +export type AuthenticationErrorData = + | (BaseAuthenticationErrorData & { + code: AuthenticationErrorCode; + }) + | (BaseAuthenticationErrorData & { + code?: AuthenticationErrorCode; + error: AuthenticationErrorCode; + }); + const AUTHENTICATION_ERROR_CODES: ReadonlySet = new Set([ 'email_verification_required', 'organization_selection_required', @@ -29,25 +39,44 @@ const AUTHENTICATION_ERROR_CODES: ReadonlySet = new Set([ 'sso_required', ]); -export function isAuthenticationErrorData( - data: WorkOSErrorData, -): data is AuthenticationErrorData { - let discriminant: string | undefined; +function parseAuthenticationErrorCode( + value: unknown, +): AuthenticationErrorCode | undefined { + if (typeof value !== 'string') { + return; + } - if (typeof data.code === 'string') { - discriminant = data.code; - } else if (typeof data.error === 'string') { - // Some errors like `sso_required` use an `error` field instead of `code` - discriminant = data.error; + if (!AUTHENTICATION_ERROR_CODES.has(value)) { + return; } - if (!discriminant) return false; + return value as AuthenticationErrorCode; +} + +function getAuthenticationErrorCode( + data: AuthenticationErrorData, +): AuthenticationErrorCode; +function getAuthenticationErrorCode( + data: WorkOSErrorData, +): AuthenticationErrorCode | undefined; +function getAuthenticationErrorCode( + data: WorkOSErrorData, +): AuthenticationErrorCode | undefined { + return ( + parseAuthenticationErrorCode(data.code) ?? + parseAuthenticationErrorCode(data.error) + ); +} - return AUTHENTICATION_ERROR_CODES.has(discriminant); +export function isAuthenticationErrorData( + data: WorkOSErrorData, +): data is AuthenticationErrorData { + return getAuthenticationErrorCode(data) !== undefined; } export class AuthenticationException extends GenericServerException { readonly name = 'AuthenticationException'; + override readonly code: AuthenticationErrorCode; readonly pendingAuthenticationToken: string | undefined; constructor( @@ -55,7 +84,15 @@ export class AuthenticationException extends GenericServerException { readonly rawData: AuthenticationErrorData, requestID: string, ) { - super(status, rawData.message, rawData, requestID); + const code = getAuthenticationErrorCode(rawData); + + super( + status, + rawData.message ?? rawData.error_description, + rawData, + requestID, + ); + this.code = code; this.pendingAuthenticationToken = rawData.pending_authentication_token; } } diff --git a/src/workos.spec.ts b/src/workos.spec.ts index 08b6a3516..83d07a97c 100644 --- a/src/workos.spec.ts +++ b/src/workos.spec.ts @@ -2,6 +2,7 @@ import fetch from 'jest-fetch-mock'; import { fetchOnce, fetchHeaders, fetchBody } from './common/utils/test-utils'; import { ApiKeyRequiredException, + AuthenticationException, GenericServerException, NotFoundException, OauthException, @@ -355,6 +356,34 @@ describe('WorkOS', () => { ), ); }); + + it('throws an AuthenticationException for known authentication errors', async () => { + const rawData = { + error: 'sso_required', + error_description: + 'User must authenticate using one of the matching connections.', + email: 'user@example.com', + connection_ids: ['conn_123'], + }; + + fetchOnce(rawData, { + status: 400, + headers: { 'X-Request-ID': 'a-request-id' }, + }); + + const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); + const request = workos.post('/path', {}); + + await expect(request).rejects.toBeInstanceOf(AuthenticationException); + await expect(request).rejects.toMatchObject({ + code: 'sso_required', + message: + 'User must authenticate using one of the matching connections.', + name: 'AuthenticationException', + rawData, + status: 400, + }); + }); }); describe('when the api responses with a 429', () => { From 18a7ec43f999f335f5db941d2360f608edaa33ea Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 24 Apr 2026 18:45:29 -0400 Subject: [PATCH 3/3] fix: Narrow `AuthenticationErrorData` to match actual API shapes Only `sso_required` uses the OAuth-style `error`/`error_description` response shape. Typing `error` as the full `AuthenticationErrorCode` union misleads downstream users into handling cases that don't exist. Addresses PR feedback from @davidcornu. --- src/common/exceptions/authentication.exception.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/common/exceptions/authentication.exception.ts b/src/common/exceptions/authentication.exception.ts index d91b9eeb5..93651bed4 100644 --- a/src/common/exceptions/authentication.exception.ts +++ b/src/common/exceptions/authentication.exception.ts @@ -13,8 +13,6 @@ export type AuthenticationErrorCode = | 'sso_required'; interface BaseAuthenticationErrorData extends WorkOSErrorData { - error?: string; - error_description?: string; pending_authentication_token?: string; user?: UserResponse; organizations?: Array<{ id: string; name: string }>; @@ -23,11 +21,11 @@ interface BaseAuthenticationErrorData extends WorkOSErrorData { export type AuthenticationErrorData = | (BaseAuthenticationErrorData & { - code: AuthenticationErrorCode; + code: Exclude; }) | (BaseAuthenticationErrorData & { - code?: AuthenticationErrorCode; - error: AuthenticationErrorCode; + error: 'sso_required'; + error_description: string; }); const AUTHENTICATION_ERROR_CODES: ReadonlySet = new Set([ @@ -88,7 +86,7 @@ export class AuthenticationException extends GenericServerException { super( status, - rawData.message ?? rawData.error_description, + rawData.message ?? (rawData.error_description as string | undefined), rawData, requestID, );