Abstract
I’m fond of these little, stop-light colored visualizers which tell you (albeit with a grain of salt) how strong your password is. Plus, for an application whose target audience is a predominantly non-technical user-base, I feel it’s almost a duty to help spread the good-word of better password creation.
We’ll be creating a component which wraps Redwood’s native PasswordField
. Our component will make use of zxcvbn-ts to estimate our user’s password’s guessability; we’ll even get a chance to see how Redwood shines, by taking advantage of built-in configuration to lazy-load parts of zxcvbn - keeping our application performance-friendly. Finally, we’ll take the password’s strength and visually inform the user of its status; we’ll hook into form validation, to ensure our user - or one who is visually impaired - doesn’t submit their password without meeting our strength requirement.
I’ll be writing this guide with the assumption you’re also using TypeScript. It should translate pretty one-to-one to JavaScript. Happy to help if not.
tl;dr
Nice to see you again, dear friend - a variation of the finished source can be found on my GitHub. Happy hacking
Dependencies
As the Abstract section stated, we’ll need the zxcvbn-ts library to facilitate estimating our password’s guessability.
From the root of your Redwood project:
yarn add -W web @zxcvbn-ts/core @zxcvbn-ts/language-common @zxcvbn-ts/language-en
Lazy loading zxcvbn’s dictionaries
To actually estimate our password’s guessability, we’ll need to load a dictionary for zxcvbn to use. These dictionaries are quite large (~5mb total). To not include these dictionaries if our user doesn’t actually need them, we’ll create a function which lazy loads them on-the-fly.
// web/src/lib/zxcvbn.ts
import { ZxcvbnOptions } from '@zxcvbn-ts/core'
export const loadZxcvbn = async () => {
const common = await import('@zxcvbn-ts/language-common')
const en = await import('@zxcvbn-ts/language-en')
const options = {
dictionary: {
...common.default.dictionary,
...en.default.dictionary,
},
graphs: common.default.adjacencyGraphs,
translations: en.default.translations,
}
ZxcvbnOptions.setOptions(options)
}
After building and implementing your component, navigate to it with the network inspector open. You should see
common
anden
dictionaries loaded as separate scripts.
Making the PasswordStrengthField
Component
Start by generating a new component:
yarn rw g component PasswordStrengthField
Let’s make sure zxcvbn is loaded when React finishes mounting our component. We should also define PasswordField
and its prop-passthrough while we’re getting the basics out of the way.
import { useEffect } from 'react'
import type { ComponentPropsWithoutRef as ComponentProps } from 'react'
import { PasswordField } from '@redwoodjs/forms'
import { loadZxcvbn } from 'src/lib/zxcvbn'
interface PasswordStrengthFieldProps
extends ComponentProps<typeof PasswordField> {}
const PasswordStrengthField = (props: PasswordStrengthFieldProps) => {
useEffect(() => {
loadZxcvbn()
}, [])
return (
<>
<PasswordField {...props} />
</>
)
}
We’ll need to track our field’s value as the user updates it. Fortunately, react-hook-form provides just the hook: useWatch
, we can import it from @redwoodjs/forms
.
import { PasswordField, useWatch } from '@redwoodjs/forms'
const PasswordStrengthField = ({
name,
...props
}: PasswordStrengthFieldProps) => {
const password = useWatch({ name })
return (
<>
<PasswordField name={name} {...props} />
</>
)
}
Now let’s setup a piece of state that we’ll use to keep track of the current password’s “strength”. We’ll also create an effect which will update this state with the strength of the password, done whenever the password’s value changes.
import { useEffect, useState } from 'react'
import type { ZxcvbnResult } from '@zxcvbn-ts/core'
const PasswordStrengthField = ({
name,
...props
}: PasswordStrengthFieldProps) => {
// useWatch(...)
const [strength, setStrength] = useState<ZxcvbnResult >()
// useEffect to load zxcvbn
useEffect(() => {
const getStrength = async () => {
const res = await zxcvbn(password)
setStrength(res)
}
if (typeof password === 'string') {
getStrength()
}
}, [password, setStrength])
// return ...
}
Let’s really finish off this component by integrating react-hook-form’s validation with our now-accessible strength.
For validation, we’ll be focusing on
strength.score
. You can read about its meaning on the documentation forzxcvbn-ts
- found underresult.score
.
import type { RegisterOptions } from '@redwoodjs/forms'
interface ValidationOptions extends RegisterOptions {
strength: {
message: string
value: number
}
}
interface PasswordStrengthFieldProps
extends ComponentProps<typeof PasswordField> {
validation: ValidationOptions
}
const PasswordStrengthField = ({
name,
validation: { strength: validateStrength, ...validation },
...props
}: PasswordStrengthFieldProps) => {
// ...
return (
<>
<PasswordField
name={name}
validation={{
validate: () =>
strength?.score >= validateStrength.value ||
validateStrength.message,
...validation,
}}
{...props}
/>
</>
)
Finally, let’s make our stop-light. Let’s also add a bit of text to convey the password’s strength. We’ll also hide the stop-light from screen-readers.
/* PasswordStrengthField.css */
.passwordStrength-meter {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-items: evenly;
}
.passwordStrength-meter > * {
height: 0.3rem;
width: 100%;
border-radius: 1rem;
background-color: rgba(0, 0, 0, 0.15);
}
.passwordStrength-meter > * ~ * {
margin-left: 1rem;
}
.passwordStrength-score-1 > *.active {
background-color: red;
}
.passwordStrength-score-2 > *.active {
background-color: orange;
}
.passwordStrength-score-3 > *.active,
.passwordStrength-score-4 > *.active {
background-color: green;
}
import './PasswordStrengthField.css'
const getStrengthText = (strength = 0) => {
switch(strength) {
case 0:
return 'None'
case 1:
return 'Low'
case 2:
return 'Fair'
case 3:
return 'Good'
case 4:
return 'Great'
default:
return 'Unknown'
}
}
const PasswordStrengthField = (...) => {
// ..
return (
<>
<PasswordField ... />
<div
aria-hidden="true"
className={`passwordStrength-meter passwordStrength-score-${strength?.score || 0}`}
>
<span className={strength?.score > 0 ? 'active' : ''} />
<span className={strength?.score > 1 ? 'active' : ''} />
<span className={strength?.score > 2 ? 'active' : ''} />
<span className={strength?.score > 3 ? 'active' : ''} />
</div>
<p>Password Strength: {getStrengthText(strength?.score)}</p>
</>
)
}
Wrapping Up
Hopefully this guide wasn’t too hard to follow (let me know if it was). Like my example in the tl;dr section shows, there is a lot of room for expansion and customization in this workflow (do share any customizations you come up with!).
zxcvbn provides a lot more information about the password’s estimation than just its score. It even includes the reason why a password was rejected and suggestions on how to improve its score. Personally, I use this to build a tooltip that helps to explain the meaning of “Password Strength” to my users - and give them feedback on their entered password (see gif at the top).