Exercise: Password Strength

Exercise 14 Hard

Prerequisites for the exercise

  1. React Forms — Controlled Components

Objective

Create a PasswordInput component that can be used to determine a password's strength.

Description

We're all quite familiar with password input fields on the web, right? They are meant to hold...well..passwords, which are perhaps one of the most sensitive pieces of digital information.

But let's not get into so much digital detail and keep the discussion rudimentary.

Let's suppose we have a password field along with a button that reads 'Show':

Password field
Password field

When this button is clicked, the password's literal characters are displayed, with the button's own text replaced with 'Hide':

Password field converted to a text input field
Password field converted to a text input field

(By the way, having such simple passwords as 'Hello123?' is seriously problematic. Always choose passwords with a reasonably high entropy!)

When the button is clicked again, the previous configuration is restored.

Text input field converted back to a password field
Text input field converted back to a password field

In other words, the button is basically just show/hide toggle for the password.

Besides this, also suppose that there is a strength indicator bar beneath the password field to indicate the strength level of the password, based on a set criteria, along with a piece of text to explain that strength level.

Password field with strength indicator
Password field with strength indicator

This criteria is described as follows, assuming that the password isn't empty (i.e. not an empty string):

  • The password has at least one lowercase letter (a - z).
  • The password has at least one uppercase letter (A - Z).
  • The password has at least one digit (0 - 9).
  • The password has at least one symbol (a character none of the above).
  • The length of the password is greater than or equal to 13.
Keep in mind that these points are just for the sake of this exercise; they don't make up the strict requirements of a standard, secure password.

These five requirements each increase the password's strength level by 1. The level begins at 0, where the password is empty (i.e. it's an empty string).

For instance, if a given password has one lowercase letter and one digit, its level would be 2. Similarly, if a given password is 18 characters long and has 2 symbols in it with the rest of the characters being lowercase letters, its level would be 3.

Each level has an explanatory text associated with it, as follows:

  • '-' (Level 0)
  • 'Weak' (Level 1)
  • 'Average' (Level 2)
  • 'Good' (Level 3)
  • 'Very Good' (Level 4)
  • 'Ultimate' (Level 5)

Starting from level 1, each subsequent level serves to fill up as many strength bar segments (shown above) as the level number, with a color associated with that level.

Here's an example of a password with level 1 strength:

Password field with strength level 1
Password field with strength level 1

Similarly, here's an example of a password with a level 3 strength:

Password field with strength level 3
Password field with strength level 3

To give you a headstart on them, following are the colors (in CSS) you could use for each of the levels from 1 onwards, in the given order: red, orange, greenyellow, green, purple. For level 0, no segment is filled likewise no special color is needed.

As data is input into the password field, the entered password is checked against the aforementioned criteria, its strength determined — the level and its corresponding text — and this information displayed in situ in the respective location.

Now, let's get to the crux of the exercise.

In this exercise, you have to create a PasswordInput component that works as detailed below.

An optional, Boolean prop interactive specifies whether the password input should contain a strength indicator along with it or not. By default, interactive is false, hence there is no strength indicator.

Besides this, a value prop specifies the initial value to put inside the password field to begin with.

Note that the entire set of elements representing the password field, the show/hide button, strength bars, etc. MUST be wrapped inside a <div> block.

Shown below are two instances of using PasswordInput.

First, with the interactive attribute provided, and a given value to begin with:

function App() {
   return (
      <PasswordInput value="Hello123?" interactive />
   );
}

Live Example

Now, without interactive:

function App() {
   return (
      <PasswordInput value="Hello123?" />
   );
}

Live Example

Note that when interactive is falsey, it means that the interactive nature of the password field, in live-presenting a strength indicator, should be ceased completely. That is, the entered password MUST NOT be tested against the given criteria at all.

You're free to approach the logic of filling up the individual strength bar segments as you wish. You're also free to use CSS any way you like.

The main point of the exercise is to get the overall logic and strength indication right — exactly how the indication is presented is all up to your imagination.

View Solution

New file

Inside the directory you created for this course on React, create a new folder called Exercise-14-Password-Strength and put the .js solution files for this exercise within it.

Solution

First, let's get done with the general structure of PasswordInput before proceeding forward.

By this point in this course, we should be more than comfortable with setting up basic, controlled input fields.

Here's the code:

import { useState } from 'react';

function PasswordInput({
   value: initialValue = '',
   interactive = false
}) {
   const [value, setValue] = useState(initialValue);

   return (
      <div>
         <input type="password" value={value} onChange={e => setValue(e.target.value)} />
         <button>Show</button>
      </div>
   );
}

Perfect! Let's now dive into the real business.

There are essentially two parts to implementing the PasswordInput component:

  • The password show/hide toggler.
  • The strength indicator, to be shown only when the interactive prop is true.

We'll begin with the first part which is fairly easy to code.

Start off by laying out an <input> field whose type is determined conditionally, depending on the state value shown. This state value is initially false (the password's characters are initially hidden), and is udpated — or better to say, toggled — by the given button.

Following is the code we get in the end:

import { useState } from 'react';

function PasswordInput({
   value: initialValue = '',
   interactive = false
}) {
   const [value, setValue] = useState(initialValue);
const [shown, setShown] = useState(false); return ( <div> <input type={shown ? 'text' : 'password'} value={value} onChange={e => setValue(e.target.value)} /> <button>{shown ? 'Hide' : 'Show'}</button> </div> ); }

Now over to implementing the second part, which is slightly more involved to implement.

But before we code, let's spare a few minutes in thinking about how to approach creating the strength indicator.

The five strength level segments could easily be created by mapping over a dummy 5-element array, to prevent repetition and make the segment-filling logic a little bit easier (we'll see how).

The entire <div> element containing these segments could be called .pwd-strength, with an additional modifier class added to it to denote a particular level; let's call this .pwd-strength--{level}, where {level} is the strength level.

In that respect, each strength level segment therein could be called .pwd-strength_bar (being a <span> element). When a segment is filled, it would have a child (<span>) element in it called .pwd-strength_bar_filled.

In the following CSS code, we implement the styles of all these classes:

.pwd-strength_bar,
.pwd-strength_bar_filled {
   background-color: lightgrey;
   width: 20px;
   height: 4px;
}

.pwd-strength_bar {
   margin-right: 3px;
   display: inline-block;
}

.pwd-strength_bar_filled {
   margin-right: 0;
   display: block;
}

.pwd-strength--1 .pwd-strength_bar_filled {
   background-color: red;
}
.pwd-strength--2 .pwd-strength_bar_filled {
   background-color: orange;
}
.pwd-strength--3 .pwd-strength_bar_filled {
   background-color: greenyellow;
}
.pwd-strength--4 .pwd-strength_bar_filled {
   background-color: green;
}
.pwd-strength--5 .pwd-strength_bar_filled {
   background-color: purple;
}

This completes our thought process for the design and structure of the strength indicator; we still have to crack its logic.

That is, how do we determine the strength level and its corresponding text?

Well, let's think, think and think... After all, programming is mainly thinking and less so coding.

The solution that we come up with is to employ a custom hook to compute all the necessary information of a given password value, i.e. its strength level (as a number) and the corresponding text (as a string), and then return that information.

Let's call this hook useStrength().

Its first argument would be the interactive prop (to determine whether we even need to process the given password value for its strength or not) while its second argument would be value.

To keep the discussion short, let's first see the definition of useStrength() and then reason about the code:

function useStrength(interactive, value) {
   if (!interactive) {
      return [];
   }
   const strengthText = ['-', 'Weak', 'Average', 'Good', 'Very good', 'Ultimate'];
   const patterns = [/[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z0-9]/, /.{15,}/];

   let level = 0;
   for (var pattern of patterns) {
      if (pattern.test(value)) {
         level++;
      }
   }
   return [level, strengthText[level]];
}

The main points to note here are as follows:

  • When interactive is false, no further processing is done; the function immediately returns an empty array. An array is returned because the return value of useStrength() is destructured in PasswordInput — if we don't return an array above, the destructuring operation would fail.
  • strengthText provides an array of strings representing the text that corresponds to each strength level (as mentioned in the description above).
  • patterns provides an array of regular expressions to use to test for each of the given criteria (as mentioned in the description above).
  • level specifies the strength level, starting at 0.
  • A for...of loop iterates over each pattern in patterns, and tests the given password, value, against it. If the pattern is found, level is incremented.

With this custom hook made, now we just ought to go back to our PasswordInput component and code it to completion, for we now have all the information we need to complete the component.

Consider the following code:

import { useState } from 'react';

function useStrength(interactive, value) { /* ... */ }

function PasswordInput({
   value: initialValue = '',
   interactive = false
}) {
   const [value, setValue] = useState(initialValue);
   const [shown, setShown] = useState(false);
const [level, text] = useStrength(interactive, value); return ( <div> <input type={shown ? 'text' : 'password'} value={value} onChange={e => setValue(e.target.value)} /> <button>{shown ? 'Hide' : 'Show'}</button> {interactive && ( <div className={'pwd-strength pwd-strength--' + level}> {new Array(5).fill(0).map((_, i) => ( <span className="pwd-strength_bar"> {i <= (level - 1) && <span className="pwd-strength_bar_filled"></span>} </span> ))} {text} </div> )} </div> ); }

Let's quickly see what's happening here:

  • The entire password strength indicator section is conditionally rendered, when interactive is true.
  • new Array(5).fill(0) creates a dummy array of five elements. This is done purely to allow us to use map() to produce a collection of five <span> elements, corresponding to the five strength level segments.
  • i <= (level - 1) decides whether the current segment should be filled. The filling is performed by adding a .pwd-strength_bar_filled element (which is also a <span>) inside the current segment.
  • Right after this sequence of 5 segments, we render text (recall that it holds the text corresponding to the current strength level of the password).

And this is pretty much everything we need in order to completely and successfully implement PasswordInput.

Live Example

No doubt that this exercise was quite an involved one but it was surely worth the effort. We got to learn a diverse set of concepts, including the crucially important concept of custom hooks in React.

Custom hooks is a really powerful pattern — in fact, a desirable pattern — to use in React apps. Many React libraries heavily use custom hooks to simplify complex logic and to provide developers with a wide range of functionality to hook their components into.