Skip to content

Commit

Permalink
feat(clerk-js,localizations,shared,types): Prompt user to reset pwned…
Browse files Browse the repository at this point in the history
… password at sign-in
  • Loading branch information
yourtallness committed Mar 22, 2024
1 parent e9bb47e commit 8a2a243
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 8 deletions.
21 changes: 15 additions & 6 deletions packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type AlternativeMethodsProps = {
onFactorSelected: (factor: SignInFactor) => void;
currentFactor: SignInFactor | undefined | null;
asForgotPassword?: boolean;
isPasswordPwned?: boolean;
};

export type AlternativeMethodListProps = AlternativeMethodsProps & { onHavingTroubleClick: React.MouseEventHandler };
Expand All @@ -28,23 +29,31 @@ export const AlternativeMethods = (props: AlternativeMethodsProps) => {
};

const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
const { onBackLinkClick, onHavingTroubleClick, onFactorSelected, asForgotPassword = false } = props;
const {
onBackLinkClick,
onHavingTroubleClick,
onFactorSelected,
asForgotPassword = false,
isPasswordPwned = false,
} = props;
const card = useCardState();
const resetPasswordFactor = useResetPasswordFactor();
const { firstPartyFactors, hasAnyStrategy } = useAlternativeStrategies({
filterOutFactor: props?.currentFactor,
});

const cardTitleKey = asForgotPassword
? isPasswordPwned
? 'signIn.passwordPwned.title'
: 'signIn.forgotPasswordAlternativeMethods.title'
: 'signIn.alternativeMethods.title';

return (
<Flow.Part part={asForgotPassword ? 'forgotPasswordMethods' : 'alternativeMethods'}>
<Card.Root>
<Card.Content>
<Header.Root showLogo>
<Header.Title
localizationKey={localizationKeys(
asForgotPassword ? 'signIn.forgotPasswordAlternativeMethods.title' : 'signIn.alternativeMethods.title',
)}
/>
<Header.Title localizationKey={localizationKeys(cardTitleKey)} />
{!asForgotPassword && (
<Header.Subtitle localizationKey={localizationKeys('signIn.alternativeMethods.subtitle')} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export function _SignInFactorOne(): JSX.Element {

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

const [isPasswordPwned, setIsPasswordPwned] = React.useState(false);

React.useEffect(() => {
// Handle the case where a user lands on alternative methods screen,
// clicks a social button but then navigates back to sign in.
Expand Down Expand Up @@ -98,6 +100,7 @@ export function _SignInFactorOne(): JSX.Element {
return (
<AlternativeMethods
asForgotPassword={showForgotPasswordStrategies}
isPasswordPwned={isPasswordPwned}
onBackLinkClick={canGoBack ? toggle : undefined}
onFactorSelected={f => {
selectFactor(f);
Expand Down Expand Up @@ -135,6 +138,10 @@ export function _SignInFactorOne(): JSX.Element {
}}
onForgotPasswordMethodClick={resetPasswordFactor ? toggleForgotPasswordStrategies : toggleAllStrategies}
onShowAlternativeMethodsClick={toggleAllStrategies}
onPasswordPwned={() => {
setIsPasswordPwned(true);
toggleForgotPasswordStrategies();
}}
/>
);
case 'email_code':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isUserLockedError } from '@clerk/shared/error';
import { isPasswordPwnedError, isUserLockedError } from '@clerk/shared/error';
import { useClerk } from '@clerk/shared/react';
import type { ResetPasswordCodeFactor } from '@clerk/types';
import React from 'react';
Expand All @@ -17,6 +17,7 @@ type SignInFactorOnePasswordProps = {
onForgotPasswordMethodClick: React.MouseEventHandler | undefined;
onShowAlternativeMethodsClick: React.MouseEventHandler | undefined;
onFactorPrepare: (f: ResetPasswordCodeFactor) => void;
onPasswordPwned?: () => void;
};

const usePasswordControl = (props: SignInFactorOnePasswordProps) => {
Expand All @@ -40,12 +41,13 @@ const usePasswordControl = (props: SignInFactorOnePasswordProps) => {
: onShowAlternativeMethodsClick
? onShowAlternativeMethodsClick
: () => null,
// onPasswordPwned,
},
};
};

export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) => {
const { onShowAlternativeMethodsClick } = props;
const { onShowAlternativeMethodsClick, onPasswordPwned } = props;
const card = useCardState();
const { setActive } = useClerk();
const signIn = useCoreSignIn();
Expand All @@ -58,6 +60,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
const clerk = useClerk();

const goBack = () => {
card.setError(undefined);
return navigate('../');
};

Expand All @@ -83,6 +86,14 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
return clerk.__internal_navigateWithError('..', err.errors[0]);
}

if (isPasswordPwnedError(err)) {
if (onPasswordPwned) {
card.setError({ ...err.errors[0], code: err.errors[0].code + '__sign_in' });
onPasswordPwned();
return;
}
}

handleError(err, [passwordControl], card.setError);
});
};
Expand Down
5 changes: 5 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ export const enUS: LocalizationResource = {
subtitle: 'Enter the password associated with your account',
title: 'Enter your password',
},
passwordPwned: {
title: 'Insecure password',
},
passkey: {
title: 'Use your passkey',
subtitle: "Using your passkey confirms it's you. Your device may ask for your fingerprint, face or screen lock.",
Expand Down Expand Up @@ -457,6 +460,8 @@ export const enUS: LocalizationResource = {
form_password_not_strong_enough: 'Your password is not strong enough.',
form_password_pwned:
'This password has been found as part of a breach and can not be used, please try another password instead.',
form_password_pwned__sign_in:
'This password has been found as part of a breach and can not be used, please reset your password.',
form_password_size_in_bytes_exceeded:
'Your password has exceeded the maximum number of bytes allowed, please shorten it or remove some special characters.',
form_password_validation_failed: 'Incorrect Password',
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export function isUserLockedError(err: any) {
return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'user_locked';
}

export function isPasswordPwnedError(err: any) {
return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_pwned';
}

export function parseErrors(data: ClerkAPIErrorJSON[] = []): ClerkAPIError[] {
return data.length > 0 ? data.map(parseError) : [];
}
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ type _LocalizationResource = {
subtitle: LocalizationValue;
actionLink: LocalizationValue;
};
passwordPwned: {
title: LocalizationValue;
};
passkey: {
title: LocalizationValue;
subtitle: LocalizationValue;
Expand Down Expand Up @@ -724,6 +727,7 @@ type UnstableErrors = WithParamName<{
captcha_unavailable: LocalizationValue;
captcha_invalid: LocalizationValue;
form_password_pwned: LocalizationValue;
form_password_pwned__sign_in: LocalizationValue;
form_username_invalid_length: LocalizationValue;
form_username_invalid_character: LocalizationValue;
form_param_format_invalid: LocalizationValue;
Expand Down

0 comments on commit 8a2a243

Please sign in to comment.