[Guide] Password strength visualizer

strength visualizer video

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 :slight_smile:

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 and en 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 for zxcvbn-ts - found under result.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).

4 Likes

Nice one!

Do you think there would be use for

yarn rw g component PasswordStrengthField

to be a

yarn rw g setup PasswordStrengthField

that generates and stands up a component?

Or better as a How To doc like this?

1 Like

Thank you for the kind words :relaxed:

I definitely think the setup script would be useful, for those interested - I’d already thought of pulling this component out into its own library. Happy to rewrite this as a How-To for the docs - have done it before for another post and it really helped me helping everyone :grin:

Imho, I think the setup command lines itself up more with Redwood’s track record. There isn’t a boat load of educational takeaway from following this guide (the lazy-loading, but that’s a bit trivial, no?) - whereas there is takeaway from having the tedious/“boring” bit done for you so you can focus on making it look like your own component/adding custom functionality.

Then, personally, I’d rather the setup command than a library to provide the most down-stream customization as possible - while not having to go too crazy complex with the implementation of the library.

My biggest word against incorporating it in the framework is that it’s just another thing that’ll need to get ripped out if/when the story for plugins takes off - but who knows, again, if/when that happens :woman_shrugging:

Great tutorial Ryan! Very nice of you to share with the community :slight_smile:

On the topic passwords, I can recommend to have a look through this https://stealthbits.com/blog/nist-password-guidelines/

One nice thing to add here (ripped from that article) would be to add a check against the Have I Been Pwned? database to block re-use any leaked passwords

As for wrapping this up in some command, I think an npm package might be the best option if you can make it self-contained enough.

1 Like

@realStandal So smooth. Well done! :clap:

image

1 Like

I love the idea of integrating Have I Been Pwned, thank you! I’ll need to do a bit of experimenting on the library versus setup.

Only word to describe that is awesome!

1 Like