-
Notifications
You must be signed in to change notification settings - Fork 53
fix: Correctly handle all isAuthenticationErrorData cases
#1573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,14 +12,22 @@ export type AuthenticationErrorCode = | |
| | 'mfa_verification' | ||
| | 'sso_required'; | ||
|
|
||
| export interface AuthenticationErrorData extends WorkOSErrorData { | ||
| code: AuthenticationErrorCode; | ||
| 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; | ||
| }); | ||
|
|
||
| const AUTHENTICATION_ERROR_CODES: ReadonlySet<string> = new Set<string>([ | ||
| 'email_verification_required', | ||
| 'organization_selection_required', | ||
|
|
@@ -29,24 +37,60 @@ const AUTHENTICATION_ERROR_CODES: ReadonlySet<string> = new Set<string>([ | |
| 'sso_required', | ||
| ]); | ||
|
|
||
| export function isAuthenticationErrorData( | ||
| function parseAuthenticationErrorCode( | ||
| value: unknown, | ||
| ): AuthenticationErrorCode | undefined { | ||
| if (typeof value !== 'string') { | ||
| return; | ||
| } | ||
|
|
||
| if (!AUTHENTICATION_ERROR_CODES.has(value)) { | ||
| return; | ||
| } | ||
|
|
||
| return value as AuthenticationErrorCode; | ||
| } | ||
|
|
||
| function getAuthenticationErrorCode( | ||
| data: AuthenticationErrorData, | ||
| ): AuthenticationErrorCode; | ||
| function getAuthenticationErrorCode( | ||
| data: WorkOSErrorData, | ||
| ): data is AuthenticationErrorData { | ||
| ): AuthenticationErrorCode | undefined; | ||
| function getAuthenticationErrorCode( | ||
| data: WorkOSErrorData, | ||
| ): AuthenticationErrorCode | undefined { | ||
| return ( | ||
| typeof data.code === 'string' && AUTHENTICATION_ERROR_CODES.has(data.code) | ||
| parseAuthenticationErrorCode(data.code) ?? | ||
| parseAuthenticationErrorCode(data.error) | ||
| ); | ||
| } | ||
|
|
||
| export function isAuthenticationErrorData( | ||
| data: WorkOSErrorData, | ||
| ): data is AuthenticationErrorData { | ||
| return getAuthenticationErrorCode(data) !== undefined; | ||
| } | ||
|
|
||
| export class AuthenticationException extends GenericServerException { | ||
| readonly name = 'AuthenticationException'; | ||
| override readonly code: AuthenticationErrorCode; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is definitely a better user experience. |
||
| readonly pendingAuthenticationToken: string | undefined; | ||
|
|
||
| constructor( | ||
| status: number, | ||
| readonly rawData: AuthenticationErrorData, | ||
| requestID: string, | ||
| ) { | ||
| super(status, rawData.message, rawData, requestID); | ||
| const code = getAuthenticationErrorCode(rawData); | ||
|
|
||
| super( | ||
| status, | ||
| rawData.message ?? (rawData.error_description as string | undefined), | ||
| rawData, | ||
| requestID, | ||
| ); | ||
| this.code = code; | ||
| this.pendingAuthenticationToken = rawData.pending_authentication_token; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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'); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the standard test API key placeholder on Line 374. Please replace the realistic-looking key with 🔧 Suggested change- const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
+ const workos = new WorkOS('n');Based on learnings: in workos-node TypeScript tests, 📝 Committable suggestion
Suggested change
🧰 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 |
||||||
| 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', () => { | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning
AuthenticationErrorCodeisn't actually correct (and might lead to downstream users implementing a lot more cases than actually exist).If
sso_requiredis the only case whereerroranderror_descriptionare present then maybe this can be a bit more specific?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it 👍