Skip to content

fix: Correctly handle all isAuthenticationErrorData cases#1573

Merged
gjtorikian merged 3 commits intoworkos:mainfrom
davidcornu:authentication-error-precedence
Apr 24, 2026
Merged

fix: Correctly handle all isAuthenticationErrorData cases#1573
gjtorikian merged 3 commits intoworkos:mainfrom
davidcornu:authentication-error-precedence

Conversation

@davidcornu
Copy link
Copy Markdown
Contributor

@davidcornu davidcornu commented Apr 24, 2026

Description

#1561 added a new AuthenticationException type to handle the following cases:

export type AuthenticationErrorCode =
| 'email_verification_required'
| 'organization_selection_required'
| 'mfa_enrollment'
| 'mfa_challenge'
| 'mfa_verification'
| 'sso_required';

However, because the error handling logic first checks whether the payload has an error or error_description field

workos-node/src/workos.ts

Lines 486 to 487 in 96cf545

if (error || errorDescription) {
throw new OauthException(

sso_required is being treated as an OauthException instead of a AuthenticationException (see https://workos.com/docs/reference/authkit/authentication-errors#sso-required-error).

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.

Summary by CodeRabbit

  • Bug Fixes
    • Improved HTTP error classification to prioritize and reliably detect authentication failures (avoiding misclassification as OAuth or generic errors).
    • Authentication errors now surface consistent error codes and clearer messages sourced from multiple response fields.
  • Tests
    • Added tests to verify authentication error detection and that the thrown exception preserves status, raw response details, code, and message.

@davidcornu davidcornu requested review from a team as code owners April 24, 2026 17:23
@davidcornu davidcornu requested a review from mattolson April 24, 2026 17:23
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e6fb9782-626e-49b5-9e68-dfa6e68fa10d

📥 Commits

Reviewing files that changed from the base of the PR and between a573a19 and 18a7ec4.

📒 Files selected for processing (1)
  • src/common/exceptions/authentication.exception.ts

📝 Walkthrough

Walkthrough

Reorders HTTP error handling to detect authentication-error payloads first, refactors authentication error types/guards to accept error or code, adds parsing helpers, surfaces an explicit code on AuthenticationException, and adds a test asserting OAuth-style error payloads produce AuthenticationException.

Changes

Cohort / File(s) Summary
Error handling reordering
src/workos.ts
handleHttpError now checks isAuthenticationErrorData(data) first and throws AuthenticationException earlier; removed the later AuthenticationException check; other fallback branches unchanged.
Authentication error model & helpers
src/common/exceptions/authentication.exception.ts
Replaced single AuthenticationErrorData interface with a union allowing code or error ('sso_required') variants; added parseAuthenticationErrorCode / getAuthenticationErrorCode; updated isAuthenticationErrorData guard to use extracted code; AuthenticationException declares override readonly code and uses rawData.message ?? rawData.error_description for its message.
Tests
src/workos.spec.ts
Added test that mocks a 400 OAuth-style response (error / error_description) and asserts it rejects with AuthenticationException containing proper code, message, rawData, and status.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: reordering error handling to correctly process all authentication error cases through isAuthenticationErrorData checks.
Description check ✅ Passed The description explains the context and problem (incorrect error classification after PR 1561, sso_required treated as OauthException), but lacks detail on the solution and implementation approach.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR fixes a misclassification bug where sso_required errors — which carry both error/error_description fields and an auth error code — were being thrown as OauthException instead of AuthenticationException. The fix reorders the default branch in workos.ts to check isAuthenticationErrorData first, and extends the type model and error-code lookup to cover the sso_required payload shape.

Confidence Score: 5/5

Safe to merge — the fix is minimal, well-typed, and covered by a new regression test.

The root cause is clearly identified and the fix is a one-line reordering backed by a discriminated union and proper overloads. No P0 or P1 issues found; all three changed files are consistent with each other.

No files require special attention.

Important Files Changed

Filename Overview
src/workos.ts Moves isAuthenticationErrorData check before the `error
src/common/exceptions/authentication.exception.ts Refactors AuthenticationErrorData into a discriminated union, adds getAuthenticationErrorCode with proper overloads, and falls back to data.error_description when data.message is absent — all needed to support the sso_required shape.
src/workos.spec.ts Adds a test for the sso_required authentication error case, covering both instance type and field values of the thrown AuthenticationException.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[HTTP Response - default status] --> B{isAuthenticationErrorData?}
    B -->|"data.code or data.error in AUTH_CODES"| C[throw AuthenticationException\ncode = getAuthenticationErrorCode\nmessage = data.message ?? data.error_description]
    B -->|No| D{error or errorDescription?}
    D -->|Yes| E[throw OauthException]
    D -->|No| F{code and errors?}
    F -->|Yes| G[throw BadRequestException]
    F -->|No| H[throw GenericServerException]

    style C fill:#d4edda,stroke:#28a745
    style E fill:#fff3cd,stroke:#ffc107
Loading

Reviews (4): Last reviewed commit: "fix: Narrow `AuthenticationErrorData` to..." | Re-trigger Greptile

Comment thread src/workos.ts
@davidcornu davidcornu force-pushed the authentication-error-precedence branch from 2a69959 to 14a7c23 Compare April 24, 2026 17:28
@gjtorikian
Copy link
Copy Markdown
Contributor

gjtorikian commented Apr 24, 2026

Review Notes

Nice fix — the reordering in workos.ts is correct and the motivation is clear. Two things worth addressing before merging:

1. Type guard unsoundness on AuthenticationErrorData.code

isAuthenticationErrorData now returns true when the discriminant comes from data.error (not data.code), but the AuthenticationErrorData interface declares code: AuthenticationErrorCode as non-optional. After the type guard narrows, TypeScript consumers will believe rawData.code is always an AuthenticationErrorCode string — but sso_required responses definitively have no code field (confirmed against the API source and OpenAPI spec). The actual response shape is:

{
  "error": "sso_required",
  "error_description": "User must authenticate using one of the matching connections.",
  "email": "user@example.com",
  "connection_ids": ["conn_01FVYZ..."]
}

Any consumer doing something like:

if (e instanceof AuthenticationException) {
  switch (e.rawData.code) {  // TypeScript says AuthenticationErrorCode, runtime says undefined
    case 'sso_required': // never matches
  }
}

would silently get undefined with no compiler warning.

Suggestion: Make code optional in AuthenticationErrorData and add error? as a field so the types are honest about what's present.

2. Regression test (echoing greptile-apps bot)

+1 to the bot's suggestion — a test with a payload matching the actual API response shape would lock in this fix against future refactors:

{ error: 'sso_required', error_description: 'User must authenticate using one of the matching connections.', email: 'user@example.com', connection_ids: ['conn_123'] }

asserting AuthenticationException is thrown (not OauthException).

@davidcornu
Copy link
Copy Markdown
Contributor Author

Thanks for the review @gjtorikian. For context - I have no attachment to my current implementation, just figured I'd put together a quick fix as I've unfortunately become all-too-familiar with this part of the SDK over the last few months and the upgrade to v9 caused production errors for us (we got a OauthException where we were expecting a AuthenticationException). If you want to throw this away and implement your own fix don't hesitate to.


isAuthenticationErrorData now returns true when the discriminant comes from data.error (not data.code), but the AuthenticationErrorData interface declares code: AuthenticationErrorCode as non-optional. After the type guard narrows, TypeScript consumers will believe rawData.code is always an AuthenticationErrorCode string — but sso_required responses definitively have no code field (confirmed against the API source and OpenAPI spec).

Good point. Thanks for checking the source!

Suggestion: Make code optional in AuthenticationErrorData and add error? as a field so the types are honest about what's present.

This doesn't make for a great experience for downstream users as they now have to care about whether AuthenticationErrorData has a code or an error in order to correctly handle it. I would either

  • add a field on AuthenticationException that always returns a AuthenticationErrorCode (maybe code)
    • also add error?: string as you suggested
      • if we want to be more specific this could be "sso_required" instead of string
    • make the code field code?: string or Exclude<AuthenticationErrorCode, "sso_required"> to be more correct
  • keep sso_required as an OauthException (although that doesn't make much sense to me).

thoughts?

+1 to the bot's suggestion — a test with a payload matching the actual API response shape would lock in this fix against future refactors:
[...]
asserting AuthenticationException is thrown (not OauthException).

Sure thing. Surprised this wasn't flagged on the original PR.

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.
@gjtorikian
Copy link
Copy Markdown
Contributor

Yeah, I do agree that users shouldn't have to check both rawData.code and rawData.error. It mostly stinks because the API alternates between error/error_description and code/message.

As I took notes across the PR and the spec I realized it would probably just be easier for me to push the changes here, so. Consumers can just use e.code going forward.

I'm sort of speed running this (it's late on a Friday!), but the changes I made are, at least, technically accurate. Let me know if they solve your use case and I can push a release.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/workos.spec.ts`:
- Line 374: Replace the realistic API key literal passed into the WorkOS
constructor with the standard test placeholder 'n' to avoid secret-scanner
churn; locate the instantiation "new
WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU')" in the test (the WorkOS constructor
call) and change the argument to 'n' so the line reads new WorkOS('n').
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7d6faad5-cb72-4eee-bc04-8470e5a1a2f8

📥 Commits

Reviewing files that changed from the base of the PR and between 14a7c23 and a573a19.

📒 Files selected for processing (2)
  • src/common/exceptions/authentication.exception.ts
  • src/workos.spec.ts

Comment thread src/workos.spec.ts
headers: { 'X-Request-ID': 'a-request-id' },
});

const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the standard test API key placeholder on Line 374.

Please replace the realistic-looking key with 'n' to match suite convention and avoid secret-scanner churn in tests.

🔧 Suggested change
-        const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
+        const workos = new WorkOS('n');

Based on learnings: in workos-node TypeScript tests, new WorkOS('n') is the established placeholder-key convention and deviations should be flagged.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
const workos = new WorkOS('n');
🧰 Tools
🪛 Betterleaks (1.1.2)

[high] 374-374: Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data.

(stripe-access-token)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/workos.spec.ts` at line 374, Replace the realistic API key literal passed
into the WorkOS constructor with the standard test placeholder 'n' to avoid
secret-scanner churn; locate the instantiation "new
WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU')" in the test (the WorkOS constructor
call) and change the argument to 'n' so the line reads new WorkOS('n').

@davidcornu
Copy link
Copy Markdown
Contributor Author

I'm sort of speed running this (it's late on a Friday!), but the changes I made are, at least, technically accurate. Let me know if they solve your use case and I can push a release.

No rush. Solved my issue by reverting some of our v9 changes and adding a code comment that points to this PR 😬.

Comment on lines +15 to +32
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;
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning AuthenticationErrorCode isn't actually correct (and might lead to downstream users implementing a lot more cases than actually exist).

If sso_required is the only case where error and error_description are present then maybe this can be a bit more specific?

interface BaseAuthenticationErrorData extends WorkOSErrorData {
  pending_authentication_token?: string;
  user?: UserResponse;
  organizations?: Array<{ id: string; name: string }>;
  connection_ids?: string[];
}

export type AuthenticationErrorData =
  | (BaseAuthenticationErrorData & {
      code: Exclude<AuthenticationErrorCode, "sso_required">;
    })
  | (BaseAuthenticationErrorData & {
      error: "sso_required";
      error_description: string;
    });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it 👍


export class AuthenticationException extends GenericServerException {
readonly name = 'AuthenticationException';
override readonly code: AuthenticationErrorCode;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely a better user experience.

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.
@gjtorikian gjtorikian merged commit 6ccda92 into workos:main Apr 24, 2026
8 checks passed
gjtorikian added a commit that referenced this pull request Apr 24, 2026
Co-authored-by: Garen J. Torikian <gjtorikian@users.noreply.github.com>
@davidcornu davidcornu deleted the authentication-error-precedence branch April 25, 2026 16:29
@davidcornu
Copy link
Copy Markdown
Contributor Author

Thanks @gjtorikian ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants