From 4652d65874ea6bc4c1b6afa9209d5715c0871f2e Mon Sep 17 00:00:00 2001 From: Fouad Date: Mon, 13 Feb 2023 10:27:04 +0000 Subject: [PATCH] Update password strength checking (#2994) * use zxcvbn password strength checker * prevent user from proceeding with a weak password + add tips * create storybook for password strength component * update storybook * update password-strength --- nym-wallet/package.json | 4 +- .../auth/components/password-strength.tsx | 112 +++++++++--------- .../components/passwordStrength.stories.tsx | 59 +++++++++ .../src/pages/auth/pages/connect-password.tsx | 7 +- .../src/pages/auth/pages/create-password.tsx | 6 +- nym-wallet/src/pages/bonding/Bonding.tsx | 3 +- .../node-settings/apy-playground/index.tsx | 3 +- .../general-settings/ParametersSettings.tsx | 2 +- nym-wallet/src/requests/queries.ts | 6 +- yarn.lock | 10 ++ 10 files changed, 137 insertions(+), 75 deletions(-) create mode 100644 nym-wallet/src/pages/auth/components/passwordStrength.stories.tsx diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 9dc201f639..65ebc83226 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -52,7 +52,8 @@ "string-to-color": "^2.2.2", "use-clipboard-copy": "^0.2.0", "uuid": "^8.3.2", - "yup": "^0.32.9" + "yup": "^0.32.9", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@babel/core": "^7.15.0", @@ -76,6 +77,7 @@ "@types/react-dom": "^18.0.10", "@types/semver": "^7.3.8", "@types/uuid": "^8.3.4", + "@types/zxcvbn": "^4.4.1", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "babel-loader": "^8.3.0", diff --git a/nym-wallet/src/pages/auth/components/password-strength.tsx b/nym-wallet/src/pages/auth/components/password-strength.tsx index 7e867f73fd..87ad9a3aa9 100644 --- a/nym-wallet/src/pages/auth/components/password-strength.tsx +++ b/nym-wallet/src/pages/auth/components/password-strength.tsx @@ -1,52 +1,61 @@ /* eslint-disable no-nested-ternary */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; +import zxcvbn, { ZXCVBNScore } from 'zxcvbn'; import { LockOutlined } from '@mui/icons-material'; import { LinearProgress, Stack, Typography, Box } from '@mui/material'; -type TStrength = 'weak' | 'medium' | 'strong' | 'init'; - -const strong = /^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/; -const medium = /^(((?=.*[a-z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[0-9])))(?=.{6,})/; - const colorMap = { - init: 'inherit' as 'inherit', - weak: 'error' as 'error', - medium: 'warning' as 'warning', - strong: 'success' as 'success', + 4: 'success' as 'success', + 3: 'success' as 'success', + 2: 'warning' as 'warning', + 1: 'error' as 'error', + 0: 'error' as 'error', }; -const getText = (strength: TStrength) => { - switch (strength) { - case 'strong': +const getText = (score: ZXCVBNScore) => { + switch (score) { + case 4: + return 'Very strong password'; + case 3: return 'Strong password'; - case 'medium': - return 'Medium strength password'; - case 'weak': + case 2: + return 'Average password'; + case 1: return 'Weak password'; + case 0: + return 'Very weak password'; default: - return 'Password strength'; + return ''; } }; -const getTextColor = (strength: TStrength) => { - switch (strength) { - case 'strong': +const getColor = (score: ZXCVBNScore) => { + switch (score) { + case 4: return 'success.main'; - case 'medium': + case 3: + return 'success.main'; + case 2: return 'warning.main'; - case 'weak': + case 1: + return 'error.main'; + case 0: return 'error.main'; default: return 'grey.500'; } }; -const getPasswordStrength = (strength: TStrength) => { - switch (strength) { - case 'strong': +const getPasswordStrength = (score: ZXCVBNScore) => { + switch (score) { + case 4: return 100; - case 'medium': + case 3: + return 75; + case 2: return 50; + case 1: + return 25; default: return 0; } @@ -54,47 +63,32 @@ const getPasswordStrength = (strength: TStrength) => { export const PasswordStrength = ({ password, - onChange, + withWarnings, + handleIsSafePassword, }: { password: string; - onChange: (isStrong: boolean) => void; + withWarnings?: boolean; + handleIsSafePassword: (isSafe: boolean) => void; }) => { - const [strength, setStrength] = useState('init'); + const result = zxcvbn(password); - useEffect(() => { - if (password.length === 0) { - setStrength('init'); - return; - } + handleIsSafePassword(result.score > 1); - if (password.match(strong)) { - setStrength('strong'); - return; - } - - if (password.match(medium)) { - setStrength('medium'); - return; - } - setStrength('weak'); - }, [password]); - - useEffect(() => { - if (strength === 'strong') { - onChange(true); - } else { - onChange(false); - } - }, [strength]); + if (!password.length) return null; return ( - - - - - {getText(strength)} - + + + + + + {getText(result.score)} + + + {withWarnings && result.feedback.warning && ( + {result.feedback.warning} + )} ); diff --git a/nym-wallet/src/pages/auth/components/passwordStrength.stories.tsx b/nym-wallet/src/pages/auth/components/passwordStrength.stories.tsx new file mode 100644 index 0000000000..35d6270c68 --- /dev/null +++ b/nym-wallet/src/pages/auth/components/passwordStrength.stories.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Box, Stack, TextField } from '@mui/material'; +import { PasswordStrength } from './password-strength'; + +export default { + title: 'Wallet / Password Strength', + component: PasswordStrength, +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + const [password, setPassword] = React.useState(args.password); + return ( + + setPassword(e.target.value)} sx={{ mb: 0.5 }} /> + {!!password.length && } + + ); +}; + +export const VeryStrong = Template.bind({}); +VeryStrong.args = { password: 'fedgklnrf34£', withWarnings: true, handleIsSafePassword: () => undefined }; + +export const Strong = Template.bind({}); +Strong.args = { password: '"56%abc123?@', withWarnings: true, handleIsSafePassword: () => undefined }; + +export const Average = Template.bind({}); +Average.args = { password: '"abc123?', withWarnings: true, handleIsSafePassword: () => undefined }; + +export const Weak = Template.bind({}); +Weak.args = { password: 'abc123?', withWarnings: true, handleIsSafePassword: () => undefined }; + +export const VeryWeak = Template.bind({}); +VeryWeak.args = { + password: 'abc123', + withWarnings: true, + handleIsSafePassword: () => undefined, +}; + +export const WithName = Template.bind({}); +WithName.args = { + password: 'fred', + withWarnings: true, + handleIsSafePassword: () => undefined, +}; + +export const WithSequence = Template.bind({}); +WithSequence.args = { + password: '121212', + withWarnings: true, + handleIsSafePassword: () => undefined, +}; + +export const Default = Template.bind({}); +Default.args = { + password: 'abc123', + withWarnings: true, + handleIsSafePassword: () => undefined, +}; diff --git a/nym-wallet/src/pages/auth/pages/connect-password.tsx b/nym-wallet/src/pages/auth/pages/connect-password.tsx index 133d0a27cb..476f0a9aca 100644 --- a/nym-wallet/src/pages/auth/pages/connect-password.tsx +++ b/nym-wallet/src/pages/auth/pages/connect-password.tsx @@ -9,9 +9,8 @@ import { Subtitle, Title, PasswordStrength } from '../components'; export const ConnectPassword = () => { const [confirmedPassword, setConfirmedPassword] = useState(''); - const [isStrongPassword, setIsStrongPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); - + const [isSafePassword, setIsSafePassword] = useState(false); const { mnemonic, password, setPassword, resetState } = useContext(AuthContext); const navigate = useNavigate(); @@ -49,7 +48,7 @@ export const ConnectPassword = () => { label="Password" autoFocus /> - setIsStrongPassword(isStrong)} /> + {