Skip to content

Commit

Permalink
feat(clerk-js): Ability to enforce password reset OAuth callback (#1757)
Browse files Browse the repository at this point in the history
  • Loading branch information
kostaspt committed Sep 22, 2023
1 parent 3ceb2a7 commit 0366e0b
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 3 deletions.
7 changes: 7 additions & 0 deletions .changeset/wet-icons-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/types': patch
'@clerk/localizations': patch
---

Adds the ability to force users to reset their password.
40 changes: 40 additions & 0 deletions packages/clerk-js/src/core/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,46 @@ describe('Clerk singleton', () => {
expect(mockNavigate).toHaveBeenCalledWith('/sign-in#/factor-one');
});
});

it('redirects user to reset-password, if the user is required to set a new password', async () => {
mockEnvironmentFetch.mockReturnValue(
Promise.resolve({
authConfig: {},
userSettings: mockUserSettings,
displayConfig: mockDisplayConfig,
isSingleSession: () => false,
isProduction: () => false,
isDevelopmentOrStaging: () => true,
}),
);

mockClientFetch.mockReturnValue(
Promise.resolve({
activeSessions: [],
signIn: new SignIn({
status: 'needs_new_password',
} as unknown as SignInJSON),
signUp: new SignUp(null),
}),
);

const mockSignInCreate = jest.fn().mockReturnValue(Promise.resolve({ status: 'needs_new_password' }));

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#/reset-password');
});
});
});

describe('.handleMagicLinkVerification()', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,11 @@ export default class Clerk implements ClerkInterface {
buildURL({ base: displayConfig.signInUrl, hashPath: '/factor-two' }, { stringify: true }),
);

const navigateToResetPassword = makeNavigate(
params.resetPasswordUrl ||
buildURL({ base: displayConfig.signInUrl, hashPath: '/reset-password' }, { stringify: true }),
);

const navigateAfterSignIn = makeNavigate(
params.afterSignInUrl || params.redirectUrl || displayConfig.afterSignInUrl,
);
Expand Down Expand Up @@ -932,6 +937,8 @@ export default class Clerk implements ClerkInterface {
return navigateToFactorOne();
case 'needs_second_factor':
return navigateToFactorTwo();
case 'needs_new_password':
return navigateToResetPassword();
default:
clerkOAuthCallbackDidNotCompleteSignInSignUp('sign in');
}
Expand All @@ -943,6 +950,12 @@ export default class Clerk implements ClerkInterface {
return navigateToFactorOne();
}

const userNeedsNewPassword = si.status === 'needs_new_password';

if (userNeedsNewPassword) {
return navigateToResetPassword();
}

const userNeedsToBeCreated = si.firstFactorVerificationStatus === 'transferable';

if (userNeedsToBeCreated) {
Expand Down
21 changes: 18 additions & 3 deletions packages/clerk-js/src/ui/components/SignIn/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

import { clerkInvalidFAPIResponse } from '../../../core/errors';
import { withRedirectToHomeSingleSessionGuard } from '../../common';
import { useCoreSignIn, useEnvironment } from '../../contexts';
Expand All @@ -20,6 +22,17 @@ export const _ResetPassword = () => {

const { t, locale } = useLocalizations();

const requiresNewPassword =
signIn.status === 'needs_new_password' &&
signIn.firstFactorVerification.strategy !== 'reset_password_email_code' &&
signIn.firstFactorVerification.strategy !== 'reset_password_phone_code';

React.useEffect(() => {
if (requiresNewPassword) {
card.setError(t(localizationKeys('signIn.resetPassword.requiredMessage')));
}
}, []);

const passwordField = useFormControl('password', '', {
type: 'password',
label: localizationKeys('formFieldLabel__newPassword'),
Expand Down Expand Up @@ -122,9 +135,11 @@ export const _ResetPassword = () => {
}}
/>
</Form.ControlRow>
<Form.ControlRow elementId={sessionsField.id}>
<Form.Control {...sessionsField.props} />
</Form.ControlRow>
{!requiresNewPassword && (
<Form.ControlRow elementId={sessionsField.id}>
<Form.Control {...sessionsField.props} />
</Form.ControlRow>
)}
<Form.SubmitButton
isDisabled={!canSubmit}
localizationKey={localizationKeys('signIn.resetPassword.formButtonPrimary')}
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function SignInRoutes(): JSX.Element {
continueSignUpUrl={signInContext.signUpContinueUrl}
firstFactorUrl={'../factor-one'}
secondFactorUrl={'../factor-two'}
resetPasswordUrl={'../reset-password'}
/>
</Route>
<Route path='choose'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,29 @@ describe('ResetPassword', () => {
await userEvent.click(screen.getByText(/back/i));
expect(fixtures.router.navigate).toHaveBeenCalledWith('../');
});

it('resets the password, when it is required for the user', async () => {
const { wrapper, fixtures } = await createFixtures();
fixtures.clerk.client.signIn.status = 'needs_new_password';
fixtures.clerk.client.signIn.firstFactorVerification.strategy = 'oauth_google';
fixtures.signIn.resetPassword.mockResolvedValue({
status: 'complete',
createdSessionId: '1234_session_id',
} as SignInResource);
const { userEvent } = render(<ResetPassword />, { wrapper });

expect(screen.queryByText(/account already exists/i)).toBeInTheDocument();
expect(screen.queryByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeInTheDocument();
await userEvent.type(screen.getByLabelText(/New password/i), 'testtest');
await userEvent.type(screen.getByLabelText(/Confirm password/i), 'testtest');
await userEvent.click(screen.getByRole('button', { name: /Reset Password/i }));
expect(fixtures.signIn.resetPassword).toHaveBeenCalledWith({
password: 'testtest',
signOutOfOtherSessions: true,
});
expect(fixtures.router.navigate).toHaveBeenCalledWith(
'../reset-password-success?createdSessionId=1234_session_id',
);
});
});
});
2 changes: 2 additions & 0 deletions packages/localizations/src/el-GR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ export const elGR: LocalizationResource = {
title: 'Επαναφορά κωδικού πρόσβασης',
formButtonPrimary: 'Επαναφορά κωδικού πρόσβασης',
successMessage: 'Ο κωδικός πρόσβασής σας έχει αλλάξει με επιτυχία. Σας συνδέουμε, παρακαλώ περιμένετε.',
requiredMessage:
'Υπάρχει ήδη λογαριασμός με μη επαληθευμένη διεύθυνση email. Παρακαλούμε επαναφέρετε τον κωδικό σας για λόγους ασφαλείας.',
},
resetPasswordMfa: {
detailsLabel: 'Πρέπει να επαληθεύσουμε την ταυτότητά σας πριν επαναφέρουμε τον κωδικό πρόσβασής σας.',
Expand Down
2 changes: 2 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export const enUS: LocalizationResource = {
title: 'Reset Password',
formButtonPrimary: 'Reset Password',
successMessage: 'Your password was successfully changed. Signing you in, please wait a moment.',
requiredMessage:
'An account already exists with an unverified email address. Please reset your password for security.',
},
resetPasswordMfa: {
detailsLabel: 'We need to verify your identity before resetting your password.',
Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ export type HandleOAuthCallbackParams = {
*/
secondFactorUrl?: string;

/**
* Full URL or path to navigate during sign in,
* if the user is required to reset their password.
*/
resetPasswordUrl?: string;

/**
* Full URL or path to navigate after an incomplete sign up.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ type _LocalizationResource = {
title: LocalizationValue;
formButtonPrimary: LocalizationValue;
successMessage: LocalizationValue;
requiredMessage: LocalizationValue;
};
resetPasswordMfa: {
detailsLabel: LocalizationValue;
Expand Down

0 comments on commit 0366e0b

Please sign in to comment.