Password security is a concern that is all-to-often either completely overlooked or woefully implemented. Thankfully, there's a really great library I discovered that takes all of the guess work out of scoring passwords. It's called zxcvbn
. It's so named because users often use sequential keys on the keyboard to make their passwords more memorable.
There's just one problem with zxcvbn
, the bundle size is nearly 800kB! That's a deal breaker for any front-end code.
As you may have already guessed from the title, I have a great solution to this problem: CloudFlare Workers! We're going to create a password scoring microservice! 🤗 CloudFlare Workers are blazing fast edge functions and once you set this up, you'll be able to use it for any future projects as well.
We're going to start by creating a worker using CloudFlare's wrangler
cli tool. Setting it up and publishing it will take less than 5 minutes. Lastly, we'll create a create-next-app
project (you could do create-react-app
as well) and create a really slick front-end password feedback component based on the zxcvbn
result we get from our service.
Setting Up Our CloudFlare Worker
Getting started with CloudFlare is a no-brainer. I'm going to assume you already have a free CloudFlare account.
npm create cloudflare@latest zxcvbn-worker
Choose whether you want to use Typescript (I recommend so), git for version control (yes) and then opt in to go ahead and deploy your app. You'll need to go through the authentication process, then select your team account and it will be live in less than 30 seconds.
We only need to install one dependency:
npm i zxcvbn
npm i -D @types/zxcvbn
Next, open up the project in your favorite editor and edit index.ts
:
// index.ts
import { checkPasswordStrength } from './lib/pw';
interface I_CheckPasswordReqestBody {
password: string;
}
// Function to generate CORS headers
function getCommonHeaders() {
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'no-store',
'Content-Type': 'application/json',
};
}
export default {
async fetch(request, env, ctx): Promise<Response> {
// Handle CORS preflight request
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: getCommonHeaders(),
});
}
// Allow only POST requests
if (request.method !== 'POST') {
return new Response(null, { status: 405, headers: getCommonHeaders() });
}
try {
// Parse the request body
const body = (await request.json()) as I_CheckPasswordReqestBody;
const { password } = body;
// Check if the password field is missing or empty
if (!password) {
return new Response(JSON.stringify({ error: 'Password is required' }), {
status: 400,
headers: {
...getCommonHeaders(),
},
});
}
// Check the password strength
const result = checkPasswordStrength(password);
return new Response(JSON.stringify(result), {
headers: {
...getCommonHeaders(),
},
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid request' }), {
status: 400,
headers: {
...getCommonHeaders(),
},
});
}
},
} satisfies ExportedHandler<Env>;
Lastly, we'll need to create our helper function:
// lib/pw.ts
import zxcvbn from 'zxcvbn';
export type T_CheckPasswordStrengthResult = ReturnType<typeof zxcvbn>;
export function checkPasswordStrength(password: string): T_CheckPasswordStrengthResult {
return zxcvbn(password);
}
That be it! Simply run npx wrangler deploy
and test with Thunder Client or Postman.
Integrating with Our Front-End
Go ahead and spin up either a React or Next.js app and open the project up in your favorite editor.
I have some additional dependencies for the demo app (link at bottom), but the only one you really need is @types/zxcvbn
, if you're using Typescript, of course (which you should). If you don't understand some of the Tailwind classes, that is because I'm using DaisyUI, a super-duper light plugin for Tailwind. DaisyUI offers themes and semantic color schemes (i.e. primary, secondary, error, etc.)
First, we'll create our password feedback component that we'll be able to use in any form that has a password:
// src/components/PasswordFeedback.tsx
import React from 'react';
import { FaRegTimesCircle, FaRegCheckCircle } from 'react-icons/fa';
import type { ZXCVBNResult } from 'zxcvbn';
interface Props {
zxcvbn: ZXCVBNResult | null;
minScore: number;
className?: string;
isLoading: boolean;
}
// Define color stops for each score level
const colors = ['#FF0000', '#FF4400', '#FFA200', '#FFFF00', '#37FF00'];
const words = ['Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
export default function PasswordFeedback({ zxcvbn, className = '', minScore, isLoading }: Props) {
// Calculate the score index (0-4)
const score = zxcvbn ? Math.min(zxcvbn.score, 4) : 0;
// Create gradient stops based on score
const gradientStops = colors
.map((color, index) => `${color} ${(index / 4) * 100}%`)
.slice(0, score + 1)
.join(', ');
// Width of the filled portion of the bar
const widthPercentage = (score / 4) * 100;
const feedbackDisplay = zxcvbn ? zxcvbn.crack_times_display.offline_slow_hashing_1e4_per_second : '_______';
return (
<div className={`flex flex-col ${className}`}>
<div className="text-sm">
<span className="font-bold">Password Strength:</span> {words[score]}
</div>
<div className="flex items-center gap-2 w-full mt-1">
<div className="w-full h-4 bg-gray-200 rounded overflow-hidden relative">
<div
className="h-full"
style={{
width: `${widthPercentage}%`,
background: `linear-gradient(to right, ${gradientStops})`,
}}
></div>
</div>
<div className="text-xl">
{score < minScore ? (
<FaRegTimesCircle className="text-red-500" />
) : (
<FaRegCheckCircle className="text-green-500" />
)}
</div>
</div>
<div className="text-xs text-gray-500 mt-1">
{isLoading ? (
<>
<div className="flex items-center gap-2">
<div className="loading loading-xs" />
<span>Checking password strength...</span>
</div>
</>
) : (
<>
Your password would take
<span className="font-bold">{feedbackDisplay}</span>
to crack.
</>
)}
</div>
{zxcvbn && zxcvbn.feedback.suggestions.length > 0 ? (
<div className="text-xs text-gray-800 mt-4">
<span className="font-bold">Suggestions:</span>
<ul className="list-disc list-inside mt-2">
{zxcvbn.feedback.suggestions.map((suggestion, index) => (
<li key={index}>{suggestion}</li>
))}
</ul>
</div>
) : (
<div className="h-12" />
)}
</div>
);
}
This component accepts the zxcvbn
response object and displays a nifty little password meter based on the score (0-4). It also displays any suggestions and the verbose "your password will take XX years to crack". All of this comes from zxcvbn
!
Next, here is our helper function:
import type { ZXCVBNResult } from 'zxcvbn';
export default async function checkPasswordStrength(password: string): Promise<ZXCVBNResult> {
const url = 'https://url-of-your-cloudflare-worker.com';
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
return await response.json();
}
Lastly, we'll put it all together in our form view:
'use client';
// Components
import React, { useState, useCallback, useEffect } from 'react';
import PasswordFeedback from '@/components/PasswordFeedback';
// Utilities
import checkPasswordStrength from '@/lib/passwordStrength';
import { debounce } from 'lodash-es';
import { usePoptart } from 'react-poptart';
// Types
import type { ZXCVBNResult } from 'zxcvbn';
const minScore = 3;
export default function HomeView() {
const poptart = usePoptart();
// State
const [password, setPassword] = useState('');
const [zxcvbn, setZxcvbn] = useState<ZXCVBNResult | null>(null);
const [score, setScore] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// This function checks the password strength using the CloudFlare endpoint
const handleCheckPasswordStrength = async (password: string) => {
setIsLoading(true);
try {
const result = await checkPasswordStrength(password);
setZxcvbn(result);
setScore(result.score);
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
// This is a debounced version of the password strength checker
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedCheckPasswordStrength = useCallback(debounce(handleCheckPasswordStrength, 500), []);
// Effects
useEffect(() => {
if (password) {
debouncedCheckPasswordStrength(password);
} else {
setZxcvbn(null);
setScore(0);
}
}, [password, debouncedCheckPasswordStrength]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
setError(null);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (score < minScore) {
const message = 'Password is too weak';
setError(message);
poptart.push({ message, type: 'error' });
} else {
setError(null);
poptart.push({ message: 'Password is strong!', type: 'success' });
}
};
// Render
return (
<form className="w-full max-w-lg m-auto flex flex-col gap-6 p-6 border-2 rounded-xl" onSubmit={handleSubmit}>
<h1 className="text-2xl font-bold text-center">Password Strength Checker</h1>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Password</span>
</div>
<input
type="password"
placeholder="Enter password"
className="input input-bordered w-full"
value={password}
onChange={handleChange}
/>
<div className="label">
<span className="label-text-alt text-error">{error}</span>
</div>
</label>
<PasswordFeedback zxcvbn={zxcvbn} minScore={minScore} isLoading={isLoading} />
<button className="btn btn-primary" type="submit">
Submit
</button>
</form>
);
}
Here I'm using useCallback
along with lodash.debounce
to debounce the calls to the worker so we're not going crazy. It leaves a little bit of a delay after you finish typing, but I think it's quite acceptable UX-wise.
The component is relatively good on cumulative layout shift (CLF). The suggestions (if any) might extend the height a small amount, but other than that it's pretty solid!
Resources
Thank You!
Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the Designly Blog. Also, please leave your comments! I love to hear thoughts from my readers.
If you want to support me, please follow me on Spotify!
Current Projects
- Snoozle.io- An AI app that generates bedtime stories for kids ❤️
- react-poptart - A React Notification / Alerts Library (under 20kB)
- Spectravert - A cross-platform video converter (ffmpeg GUI)
- Smartname.app - An AI name generator for a variety of purposes
Looking for a web developer? I'm available for hire! To inquire, please fill out a contact form.
Top comments (0)