Skip to content

Commit

Permalink
Password strength (#2682)
Browse files Browse the repository at this point in the history
  • Loading branch information
fiskus authored Feb 21, 2022
1 parent 6fe2cd0 commit cfa4a4a
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 5 deletions.
96 changes: 96 additions & 0 deletions catalog/app/containers/Auth/PasswordStrength.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import cx from 'classnames'
import * as React from 'react'
import zxcvbn from 'zxcvbn'
import { fade } from '@material-ui/core/styles/colorManipulator'
import * as M from '@material-ui/core'

const useStyles = M.makeStyles((t) => ({
root: {
backgroundColor: t.palette.divider,
height: t.spacing(0.5),
position: 'relative',
transition: '.3s ease background-color',
'&:after': {
bottom: 0,
content: '""',
left: 0,
position: 'absolute',
top: 0,
transition: '.3s ease background-color, .3s ease width',
},
'&$tooGuessable': {
backgroundColor: fade(t.palette.error.dark, 0.3),
},
'&$tooGuessable:after': {
backgroundColor: t.palette.error.dark,
width: '10%',
},
'&$veryGuessable': {
backgroundColor: fade(t.palette.error.main, 0.3),
},
'&$veryGuessable:after': {
backgroundColor: t.palette.error.main,
width: '33%',
},
'&$somewhatGuessable': {
backgroundColor: fade(t.palette.warning.dark, 0.3),
},
'&$somewhatGuessable:after': {
backgroundColor: t.palette.warning.dark,
width: '55%',
},
'&$safelyUnguessable': {
backgroundColor: fade(t.palette.success.light, 0.3),
},
'&$safelyUnguessable:after': {
backgroundColor: t.palette.success.light,
width: '78%',
},
'&$veryUnguessable': {
backgroundColor: fade(t.palette.success.dark, 0.3),
},
'&$veryUnguessable:after': {
backgroundColor: t.palette.success.dark,
width: '100%',
},
},
tooGuessable: {},
veryGuessable: {},
somewhatGuessable: {},
safelyUnguessable: {},
veryUnguessable: {},
}))

type PasswordStrength = zxcvbn.ZXCVBNResult | null

export function useStrength(value: string): PasswordStrength {
return React.useMemo(() => {
if (!value) return null
return zxcvbn(value)
}, [value])
}

interface IndicatorProps {
strength: PasswordStrength
}

type ScoreState =
| 'tooGuessable'
| 'veryGuessable'
| 'somewhatGuessable'
| 'safelyUnguessable'
| 'veryUnguessable'

const ScoreMap: ScoreState[] = [
'tooGuessable',
'veryGuessable',
'somewhatGuessable',
'safelyUnguessable',
'veryUnguessable',
]

export function Indicator({ strength }: IndicatorProps) {
const classes = useStyles()
const stateClassName = strength ? classes[ScoreMap[strength.score]] : ''
return <div className={cx(classes.root, stateClassName)} />
}
50 changes: 47 additions & 3 deletions catalog/app/containers/Auth/SignUp.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import parseSearch from 'utils/parseSearch'
import useMutex from 'utils/useMutex'
import validate, * as validators from 'utils/validators'

import * as PasswordStrength from './PasswordStrength'
import * as Layout from './Layout'
import SSOAzure from './SSOAzure'
import SSOGoogle from './SSOGoogle'
Expand All @@ -28,6 +29,51 @@ const Container = Layout.mkLayout('Complete sign-up')

const MUTEX_ID = 'password'

const useWeakPasswordIconStyles = M.makeStyles((t) => ({
icon: {
color: t.palette.warning.dark,
},
}))

function WeakPasswordIcon() {
const classes = useWeakPasswordIconStyles()
return (
<M.Tooltip title="Password is too weak">
<M.Icon className={classes.icon} fontSize="small" color="inherit">
error_outline
</M.Icon>
</M.Tooltip>
)
}

function PasswordField({ input, ...rest }) {
const { value } = input
const strength = PasswordStrength.useStrength(value)
const isWeak = strength?.score <= 2
const helperText = strength?.feedback.suggestions.length
? `Hint: ${strength?.feedback.suggestions.join(' ')}`
: ''
return (
<>
<Layout.Field
InputProps={{
endAdornment: isWeak && (
<M.InputAdornment position="end">
<WeakPasswordIcon />
</M.InputAdornment>
),
}}
helperText={helperText}
type="password"
floatingLabelText="Password"
{...input}
{...rest}
/>
<PasswordStrength.Indicator strength={strength} />
</>
)
}

function PasswordSignUp({ mutex, next, onSuccess }) {
const sentry = Sentry.use()
const dispatch = redux.useDispatch()
Expand Down Expand Up @@ -143,12 +189,10 @@ function PasswordSignUp({ mutex, next, onSuccess }) {
}}
/>
<RF.Field
component={Layout.Field}
component={PasswordField}
name="password"
type="password"
validate={validators.required}
disabled={!!mutex.current || submitting}
floatingLabelText="Password"
errors={{
required: 'Enter a password',
invalid: 'Password must be at least 8 characters long',
Expand Down
26 changes: 25 additions & 1 deletion catalog/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion catalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@
"vega-lite": "^5.1.1",
"warning": "^4.0.3",
"wonka": "^4.0.15",
"xlsx": "^0.17.5"
"xlsx": "^0.17.5",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.21.6",
Expand Down Expand Up @@ -168,6 +169,7 @@
"@types/react-test-renderer": "^17.0.1",
"@types/remarkable": "^2.0.3",
"@types/semver": "^7.3.9",
"@types/zxcvbn": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"bundlewatch": "^0.3.2",
Expand Down

0 comments on commit cfa4a4a

Please sign in to comment.