Skip to content

Commit 0366e0b

Browse files
authored
feat(clerk-js): Ability to enforce password reset OAuth callback (#1757)
1 parent 3ceb2a7 commit 0366e0b

File tree

10 files changed

+114
-3
lines changed

10 files changed

+114
-3
lines changed

.changeset/wet-icons-rest.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/types': patch
4+
'@clerk/localizations': patch
5+
---
6+
7+
Adds the ability to force users to reset their password.

packages/clerk-js/src/core/clerk.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,46 @@ describe('Clerk singleton', () => {
13131313
expect(mockNavigate).toHaveBeenCalledWith('/sign-in#/factor-one');
13141314
});
13151315
});
1316+
1317+
it('redirects user to reset-password, if the user is required to set a new password', async () => {
1318+
mockEnvironmentFetch.mockReturnValue(
1319+
Promise.resolve({
1320+
authConfig: {},
1321+
userSettings: mockUserSettings,
1322+
displayConfig: mockDisplayConfig,
1323+
isSingleSession: () => false,
1324+
isProduction: () => false,
1325+
isDevelopmentOrStaging: () => true,
1326+
}),
1327+
);
1328+
1329+
mockClientFetch.mockReturnValue(
1330+
Promise.resolve({
1331+
activeSessions: [],
1332+
signIn: new SignIn({
1333+
status: 'needs_new_password',
1334+
} as unknown as SignInJSON),
1335+
signUp: new SignUp(null),
1336+
}),
1337+
);
1338+
1339+
const mockSignInCreate = jest.fn().mockReturnValue(Promise.resolve({ status: 'needs_new_password' }));
1340+
1341+
const sut = new Clerk(frontendApi);
1342+
await sut.load({
1343+
navigate: mockNavigate,
1344+
});
1345+
if (!sut.client) {
1346+
fail('we should always have a client');
1347+
}
1348+
sut.client.signIn.create = mockSignInCreate;
1349+
1350+
await sut.handleRedirectCallback();
1351+
1352+
await waitFor(() => {
1353+
expect(mockNavigate).toHaveBeenCalledWith('/sign-in#/reset-password');
1354+
});
1355+
});
13161356
});
13171357

13181358
describe('.handleMagicLinkVerification()', () => {

packages/clerk-js/src/core/clerk.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,11 @@ export default class Clerk implements ClerkInterface {
887887
buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }),
888888
);
889889

890+
const navigateToResetPassword = makeNavigate(
891+
params.resetPasswordUrl ||
892+
buildURL({ base: displayConfig.signInUrl, hashPath: '/reset-password' }, { stringify: true }),
893+
);
894+
890895
const navigateAfterSignIn = makeNavigate(
891896
params.afterSignInUrl || params.redirectUrl || displayConfig.afterSignInUrl,
892897
);
@@ -932,6 +937,8 @@ export default class Clerk implements ClerkInterface {
932937
return navigateToFactorOne();
933938
case 'needs_second_factor':
934939
return navigateToFactorTwo();
940+
case 'needs_new_password':
941+
return navigateToResetPassword();
935942
default:
936943
clerkOAuthCallbackDidNotCompleteSignInSignUp('sign in');
937944
}
@@ -943,6 +950,12 @@ export default class Clerk implements ClerkInterface {
943950
return navigateToFactorOne();
944951
}
945952

953+
const userNeedsNewPassword = si.status === 'needs_new_password';
954+
955+
if (userNeedsNewPassword) {
956+
return navigateToResetPassword();
957+
}
958+
946959
const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable';
947960

948961
if (userNeedsToBeCreated) {

packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React from 'react';
2+
13
import { clerkInvalidFAPIResponse } from '../../../core/errors';
24
import { withRedirectToHomeSingleSessionGuard } from '../../common';
35
import { useCoreSignIn, useEnvironment } from '../../contexts';
@@ -20,6 +22,17 @@ export const _ResetPassword = () => {
2022

2123
const { t, locale } = useLocalizations();
2224

25+
const requiresNewPassword =
26+
signIn.status === 'needs_new_password' &&
27+
signIn.firstFactorVerification.strategy !== 'reset_password_email_code' &&
28+
signIn.firstFactorVerification.strategy !== 'reset_password_phone_code';
29+
30+
React.useEffect(() => {
31+
if (requiresNewPassword) {
32+
card.setError(t(localizationKeys('signIn.resetPassword.requiredMessage')));
33+
}
34+
}, []);
35+
2336
const passwordField = useFormControl('password', '', {
2437
type: 'password',
2538
label: localizationKeys('formFieldLabel__newPassword'),
@@ -122,9 +135,11 @@ export const _ResetPassword = () => {
122135
}}
123136
/>
124137
</Form.ControlRow>
125-
<Form.ControlRow elementId={sessionsField.id}>
126-
<Form.Control {...sessionsField.props} />
127-
</Form.ControlRow>
138+
{!requiresNewPassword && (
139+
<Form.ControlRow elementId={sessionsField.id}>
140+
<Form.Control {...sessionsField.props} />
141+
</Form.ControlRow>
142+
)}
128143
<Form.SubmitButton
129144
isDisabled={!canSubmit}
130145
localizationKey={localizationKeys('signIn.resetPassword.formButtonPrimary')}

packages/clerk-js/src/ui/components/SignIn/SignIn.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function SignInRoutes(): JSX.Element {
4747
continueSignUpUrl={signInContext.signUpContinueUrl}
4848
firstFactorUrl={'../factor-one'}
4949
secondFactorUrl={'../factor-two'}
50+
resetPasswordUrl={'../reset-password'}
5051
/>
5152
</Route>
5253
<Route path='choose'>

packages/clerk-js/src/ui/components/SignIn/__tests__/ResetPassword.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,29 @@ describe('ResetPassword', () => {
138138
await userEvent.click(screen.getByText(/back/i));
139139
expect(fixtures.router.navigate).toHaveBeenCalledWith('../');
140140
});
141+
142+
it('resets the password, when it is required for the user', async () => {
143+
const { wrapper, fixtures } = await createFixtures();
144+
fixtures.clerk.client.signIn.status = 'needs_new_password';
145+
fixtures.clerk.client.signIn.firstFactorVerification.strategy = 'oauth_google';
146+
fixtures.signIn.resetPassword.mockResolvedValue({
147+
status: 'complete',
148+
createdSessionId: '1234_session_id',
149+
} as SignInResource);
150+
const { userEvent } = render(<ResetPassword />, { wrapper });
151+
152+
expect(screen.queryByText(/account already exists/i)).toBeInTheDocument();
153+
expect(screen.queryByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeInTheDocument();
154+
await userEvent.type(screen.getByLabelText(/New password/i), 'testtest');
155+
await userEvent.type(screen.getByLabelText(/Confirm password/i), 'testtest');
156+
await userEvent.click(screen.getByRole('button', { name: /Reset Password/i }));
157+
expect(fixtures.signIn.resetPassword).toHaveBeenCalledWith({
158+
password: 'testtest',
159+
signOutOfOtherSessions: true,
160+
});
161+
expect(fixtures.router.navigate).toHaveBeenCalledWith(
162+
'../reset-password-success?createdSessionId=1234_session_id',
163+
);
164+
});
141165
});
142166
});

packages/localizations/src/el-GR.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ export const elGR: LocalizationResource = {
157157
title: 'Επαναφορά κωδικού πρόσβασης',
158158
formButtonPrimary: 'Επαναφορά κωδικού πρόσβασης',
159159
successMessage: 'Ο κωδικός πρόσβασής σας έχει αλλάξει με επιτυχία. Σας συνδέουμε, παρακαλώ περιμένετε.',
160+
requiredMessage:
161+
'Υπάρχει ήδη λογαριασμός με μη επαληθευμένη διεύθυνση email. Παρακαλούμε επαναφέρετε τον κωδικό σας για λόγους ασφαλείας.',
160162
},
161163
resetPasswordMfa: {
162164
detailsLabel: 'Πρέπει να επαληθεύσουμε την ταυτότητά σας πριν επαναφέρουμε τον κωδικό πρόσβασής σας.',

packages/localizations/src/en-US.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ export const enUS: LocalizationResource = {
166166
title: 'Reset Password',
167167
formButtonPrimary: 'Reset Password',
168168
successMessage: 'Your password was successfully changed. Signing you in, please wait a moment.',
169+
requiredMessage:
170+
'An account already exists with an unverified email address. Please reset your password for security.',
169171
},
170172
resetPasswordMfa: {
171173
detailsLabel: 'We need to verify your identity before resetting your password.',

packages/types/src/clerk.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,12 @@ export type HandleOAuthCallbackParams = {
467467
*/
468468
secondFactorUrl?: string;
469469

470+
/**
471+
* Full URL or path to navigate during sign in,
472+
* if the user is required to reset their password.
473+
*/
474+
resetPasswordUrl?: string;
475+
470476
/**
471477
* Full URL or path to navigate after an incomplete sign up.
472478
*/

packages/types/src/localization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ type _LocalizationResource = {
183183
title: LocalizationValue;
184184
formButtonPrimary: LocalizationValue;
185185
successMessage: LocalizationValue;
186+
requiredMessage: LocalizationValue;
186187
};
187188
resetPasswordMfa: {
188189
detailsLabel: LocalizationValue;

0 commit comments

Comments
 (0)