diff --git a/.changeset/purple-rules-prove.md b/.changeset/purple-rules-prove.md new file mode 100644 index 00000000000..fff773ee1bf --- /dev/null +++ b/.changeset/purple-rules-prove.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': minor +--- + +Handle user_locked error encountered in an oauth flow by redirecting to /sign-up or /sign-in diff --git a/packages/clerk-js/src/core/clerk.test.ts b/packages/clerk-js/src/core/clerk.test.ts index 9156262d572..7db257a5a0e 100644 --- a/packages/clerk-js/src/core/clerk.test.ts +++ b/packages/clerk-js/src/core/clerk.test.ts @@ -930,7 +930,7 @@ describe('Clerk singleton', () => { }); }); - it('redirects the user to the afterSignInUrl if one was provider', async () => { + it('redirects the user to the afterSignInUrl if one was provided', async () => { mockEnvironmentFetch.mockReturnValue( Promise.resolve({ authConfig: {}, @@ -1322,6 +1322,120 @@ describe('Clerk singleton', () => { }); }); + it('redirects to sign-up if an oauth flow fails due to the user being locked', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [], + signIn: new SignIn(null), + signUp: new SignUp({ + status: 'missing_requirements', + missing_fields: [], + unverified_fields: ['email_address'], + verifications: { + external_account: { + status: 'unverified', + error: { + error: { + code: 'user_locked', + long_message: 'Your account is locked. Please contact yolo@swag.com for more information.', + message: 'Account locked', + }, + }, + }, + }, + } as unknown as SignUpJSON), + }), + ); + + const mockSignUpCreate = jest.fn().mockReturnValue( + Promise.resolve( + new SignUp({ + status: 'missing_requirements', + missing_fields: ['phone_number'], + } as any as SignUpJSON), + ), + ); + + const sut = new Clerk(frontendApi); + await sut.load({ + navigate: mockNavigate, + }); + if (!sut.client) { + fail('we should always have a client'); + } + sut.client.signUp.create = mockSignUpCreate; + + await sut.handleRedirectCallback(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/sign-up'); + }); + }); + + it('redirects to sign-in if an oauth flows fails due to the user being locked', async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + authConfig: {}, + userSettings: mockUserSettings, + displayConfig: mockDisplayConfig, + isSingleSession: () => false, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + onWindowLocationHost: () => false, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + activeSessions: [], + signIn: new SignIn({ + status: 'needs_first_factor', + first_factor_verification: { + status: 'unverified', + strategy: 'oauth_google', + external_verification_redirect_url: null, + error: { + code: 'user_locked', + long_message: 'Your account is locked. Please contact yolo@swag.com for more information.', + message: 'Account locked', + }, + expire_at: 1631777672389, + }, + second_factor_verification: null, + } as any as SignInJSON), + signUp: new SignUp(null), + }), + ); + + const mockSignInCreate = jest.fn().mockReturnValue(Promise.resolve({ status: 'needs_first_factor' })); + + const sut = new Clerk(frontendApi); + await sut.load({ + navigate: mockNavigate, + }); + if (!sut.client) { + fail('we should always have a client'); + } + sut.client.signIn.create = mockSignInCreate; + + await sut.handleRedirectCallback(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/sign-in'); + }); + }); + it('redirects user to reset-password, if the user is required to set a new password', async () => { mockEnvironmentFetch.mockReturnValue( Promise.resolve({ diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1f7e1d81cc3..bb41f710a0b 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1036,6 +1036,17 @@ export default class Clerk implements ClerkInterface { } } + const userLockedFromSignUp = su.externalAccountErrorCode == 'user_locked'; + const userLockedFromSignIn = si.firstFactorVerificationErrorCode == 'user_locked'; + + if (userLockedFromSignUp) { + return navigateToSignUp(); + } + + if (userLockedFromSignIn) { + return navigateToSignIn(); + } + const userHasUnverifiedEmail = si.status === 'needs_first_factor'; if (userHasUnverifiedEmail) { diff --git a/packages/clerk-js/src/core/constants.ts b/packages/clerk-js/src/core/constants.ts index 66416ee8f50..733fa9646a6 100644 --- a/packages/clerk-js/src/core/constants.ts +++ b/packages/clerk-js/src/core/constants.ts @@ -16,6 +16,7 @@ export const ERROR_CODES = { OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: 'oauth_email_domain_reserved_by_saml', NOT_ALLOWED_ACCESS: 'not_allowed_access', SAML_USER_ATTRIBUTE_MISSING: 'saml_user_attribute_missing', + USER_LOCKED: 'user_locked', }; export const SIGN_IN_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username']; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 03bb13750e3..ff34cec6a0d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -179,6 +179,7 @@ export function _SignInStart(): JSX.Element { case ERROR_CODES.NOT_ALLOWED_ACCESS: case ERROR_CODES.SAML_USER_ATTRIBUTE_MISSING: case ERROR_CODES.OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: + case ERROR_CODES.USER_LOCKED: card.setError(error.longMessage); break; default: diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 08c7ed4d7b0..0901378cfa2 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -18,8 +18,7 @@ import { useCardState } from '../../elements/contexts'; import { useLoadingStatus } from '../../hooks'; import { useRouter } from '../../router'; import type { FormControlState } from '../../utils'; -import { createPasswordError } from '../../utils'; -import { buildRequest, handleError, useFormControl } from '../../utils'; +import { buildRequest, createPasswordError, handleError, useFormControl } from '../../utils'; import { SignUpForm } from './SignUpForm'; import type { ActiveIdentifier } from './signUpFormHelpers'; import { determineActiveFields, emailOrPhone, getInitialActiveIdentifier, showFormFields } from './signUpFormHelpers'; @@ -151,6 +150,7 @@ function _SignUpStart(): JSX.Element { case ERROR_CODES.NOT_ALLOWED_ACCESS: case ERROR_CODES.SAML_USER_ATTRIBUTE_MISSING: case ERROR_CODES.OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: + case ERROR_CODES.USER_LOCKED: card.setError(error.longMessage); break; default: