Skip to content

Commit b472643

Browse files
authored
feat(clerk-js,localizations,shared,types): Prompt user to reset pwned password at sign-in (clerk#3075)
1 parent cd2bf9d commit b472643

10 files changed

Lines changed: 254 additions & 23 deletions

File tree

.changeset/sour-kings-fry.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/localizations': minor
3+
'@clerk/clerk-js': minor
4+
'@clerk/shared': minor
5+
'@clerk/types': minor
6+
---
7+
8+
Support for prompting a user to reset their password if it is found to be compromised during sign-in.

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import { SignInSocialButtons } from './SignInSocialButtons';
1212
import { useResetPasswordFactor } from './useResetPasswordFactor';
1313
import { withHavingTrouble } from './withHavingTrouble';
1414

15+
type AlternativeMethodsMode = 'forgot' | 'pwned' | 'default';
16+
1517
export type AlternativeMethodsProps = {
1618
onBackLinkClick: React.MouseEventHandler | undefined;
1719
onFactorSelected: (factor: SignInFactor) => void;
1820
currentFactor: SignInFactor | undefined | null;
1921
asForgotPassword?: boolean;
22+
mode?: AlternativeMethodsMode;
2023
};
2124

2225
export type AlternativeMethodListProps = AlternativeMethodsProps & { onHavingTroubleClick: React.MouseEventHandler };
@@ -28,45 +31,48 @@ export const AlternativeMethods = (props: AlternativeMethodsProps) => {
2831
};
2932

3033
const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
31-
const { onBackLinkClick, onHavingTroubleClick, onFactorSelected, asForgotPassword = false } = props;
34+
const { onBackLinkClick, onHavingTroubleClick, onFactorSelected, mode = 'default' } = props;
3235
const card = useCardState();
3336
const resetPasswordFactor = useResetPasswordFactor();
3437
const { firstPartyFactors, hasAnyStrategy } = useAlternativeStrategies({
3538
filterOutFactor: props?.currentFactor,
3639
});
3740

41+
const flowPart = determineFlowPart(mode);
42+
const cardTitleKey = determineTitle(mode);
43+
const isReset = determineIsReset(mode);
44+
3845
return (
39-
<Flow.Part part={asForgotPassword ? 'forgotPasswordMethods' : 'alternativeMethods'}>
46+
<Flow.Part part={flowPart}>
4047
<Card>
4148
<CardAlert>{card.error}</CardAlert>
4249
<Header.Root>
4350
{onBackLinkClick && <Header.BackLink onClick={onBackLinkClick} />}
44-
<Header.Title
45-
localizationKey={localizationKeys(
46-
asForgotPassword ? 'signIn.forgotPasswordAlternativeMethods.title' : 'signIn.alternativeMethods.title',
47-
)}
48-
/>
51+
<Header.Title localizationKey={cardTitleKey} />
4952
</Header.Root>
5053
{/*TODO: extract main in its own component */}
5154
<Flex
5255
direction='col'
5356
elementDescriptor={descriptors.main}
5457
gap={6}
5558
>
56-
{asForgotPassword && resetPasswordFactor && (
59+
{isReset && resetPasswordFactor && (
5760
<ArrowBlockButton
5861
leftIcon={getButtonIcon(resetPasswordFactor)}
5962
textLocalizationKey={getButtonLabel(resetPasswordFactor)}
6063
elementDescriptor={descriptors.alternativeMethodsBlockButton}
6164
textElementDescriptor={descriptors.alternativeMethodsBlockButtonText}
6265
arrowElementDescriptor={descriptors.alternativeMethodsBlockButtonArrow}
6366
isDisabled={card.isLoading}
64-
onClick={() => onFactorSelected(resetPasswordFactor)}
67+
onClick={() => {
68+
card.setError(undefined);
69+
onFactorSelected(resetPasswordFactor);
70+
}}
6571
/>
6672
)}
6773
{hasAnyStrategy && (
6874
<>
69-
{asForgotPassword && (
75+
{isReset && (
7076
<Text
7177
localizationKey={localizationKeys(
7278
'signIn.forgotPasswordAlternativeMethods.label__alternativeMethods',
@@ -91,7 +97,10 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
9197
arrowElementDescriptor={descriptors.alternativeMethodsBlockButtonArrow}
9298
key={i}
9399
isDisabled={card.isLoading}
94-
onClick={() => onFactorSelected(factor)}
100+
onClick={() => {
101+
card.setError(undefined);
102+
onFactorSelected(factor);
103+
}}
95104
/>
96105
))}
97106
</Flex>
@@ -149,3 +158,35 @@ export function getButtonIcon(factor: SignInFactor) {
149158

150159
return icons[factor.strategy as keyof typeof icons];
151160
}
161+
162+
function determineFlowPart(mode: AlternativeMethodsMode) {
163+
switch (mode) {
164+
case 'forgot':
165+
return 'forgotPasswordMethods';
166+
case 'pwned':
167+
return 'passwordPwnedMethods';
168+
default:
169+
return 'alternativeMethods';
170+
}
171+
}
172+
173+
function determineTitle(mode: AlternativeMethodsMode): LocalizationKey {
174+
switch (mode) {
175+
case 'forgot':
176+
return localizationKeys('signIn.forgotPasswordAlternativeMethods.title');
177+
case 'pwned':
178+
return localizationKeys('signIn.passwordPwned.title');
179+
default:
180+
return localizationKeys('signIn.alternativeMethods.title');
181+
}
182+
}
183+
184+
function determineIsReset(mode: AlternativeMethodsMode): boolean {
185+
switch (mode) {
186+
case 'forgot':
187+
case 'pwned':
188+
return true;
189+
default:
190+
return false;
191+
}
192+
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33

44
import { withRedirectToHomeSingleSessionGuard } from '../../common';
55
import { useCoreSignIn, useEnvironment } from '../../contexts';
6-
import { ErrorCard, LoadingCard, withCardStateProvider } from '../../elements';
6+
import { ErrorCard, LoadingCard, useCardState, withCardStateProvider } from '../../elements';
77
import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies';
88
import { localizationKeys } from '../../localization';
99
import { useRouter } from '../../router';
@@ -35,6 +35,7 @@ export function _SignInFactorOne(): JSX.Element {
3535
const { preferredSignInStrategy } = useEnvironment().displayConfig;
3636
const availableFactors = signIn.supportedFirstFactors;
3737
const router = useRouter();
38+
const card = useCardState();
3839

3940
const lastPreparedFactorKeyRef = React.useRef('');
4041
const [{ currentFactor }, setFactor] = React.useState<{
@@ -57,6 +58,8 @@ export function _SignInFactorOne(): JSX.Element {
5758

5859
const [showForgotPasswordStrategies, setShowForgotPasswordStrategies] = React.useState(false);
5960

61+
const [isPasswordPwned, setIsPasswordPwned] = React.useState(false);
62+
6063
React.useEffect(() => {
6164
// Handle the case where a user lands on alternative methods screen,
6265
// clicks a social button but then navigates back to sign in.
@@ -93,11 +96,18 @@ export function _SignInFactorOne(): JSX.Element {
9396
const canGoBack = factorHasLocalStrategy(currentFactor);
9497

9598
const toggle = showAllStrategies ? toggleAllStrategies : toggleForgotPasswordStrategies;
99+
const backHandler = () => {
100+
card.setError(undefined);
101+
setIsPasswordPwned(false);
102+
toggle?.();
103+
};
104+
105+
const mode = showForgotPasswordStrategies ? (isPasswordPwned ? 'pwned' : 'forgot') : 'default';
96106

97107
return (
98108
<AlternativeMethods
99-
asForgotPassword={showForgotPasswordStrategies}
100-
onBackLinkClick={canGoBack ? toggle : undefined}
109+
mode={mode}
110+
onBackLinkClick={canGoBack ? backHandler : undefined}
101111
onFactorSelected={f => {
102112
selectFactor(f);
103113
toggle?.();
@@ -126,6 +136,10 @@ export function _SignInFactorOne(): JSX.Element {
126136
}}
127137
onForgotPasswordMethodClick={resetPasswordFactor ? toggleForgotPasswordStrategies : toggleAllStrategies}
128138
onShowAlternativeMethodsClick={toggleAllStrategies}
139+
onPasswordPwned={() => {
140+
setIsPasswordPwned(true);
141+
toggleForgotPasswordStrategies();
142+
}}
129143
/>
130144
);
131145
case 'email_code':

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isUserLockedError } from '@clerk/shared/error';
1+
import { isPasswordPwnedError, isUserLockedError } from '@clerk/shared/error';
22
import type { ResetPasswordCodeFactor } from '@clerk/types';
33
import React from 'react';
44

@@ -16,6 +16,7 @@ type SignInFactorOnePasswordProps = {
1616
onForgotPasswordMethodClick: React.MouseEventHandler | undefined;
1717
onShowAlternativeMethodsClick: React.MouseEventHandler | undefined;
1818
onFactorPrepare: (f: ResetPasswordCodeFactor) => void;
19+
onPasswordPwned?: () => void;
1920
};
2021

2122
const usePasswordControl = (props: SignInFactorOnePasswordProps) => {
@@ -44,7 +45,7 @@ const usePasswordControl = (props: SignInFactorOnePasswordProps) => {
4445
};
4546

4647
export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) => {
47-
const { onShowAlternativeMethodsClick } = props;
48+
const { onShowAlternativeMethodsClick, onPasswordPwned } = props;
4849
const card = useCardState();
4950
const { setActive } = useCoreClerk();
5051
const signIn = useCoreSignIn();
@@ -80,6 +81,12 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
8081
return clerk.__internal_navigateWithError('..', err.errors[0]);
8182
}
8283

84+
if (isPasswordPwnedError(err) && onPasswordPwned) {
85+
card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' });
86+
onPasswordPwned();
87+
return;
88+
}
89+
8390
handleError(err, [passwordControl], card.setError);
8491
});
8592
};

0 commit comments

Comments
 (0)