Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/purple-rules-prove.md
Original file line number Diff line number Diff line change
@@ -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
116 changes: 115 additions & 1 deletion packages/clerk-js/src/core/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down Expand Up @@ -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({
Expand Down
11 changes: 11 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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:
Expand Down