The National Institute of Standards and Technology (NIST) released a set of recommendations for handling passwords in software applications (SP 800-63-4 Digital Identity Guidelines).
The guidelines recommend that applications encourage users to create memorable passwords as long as they want (at least 8 characters when used with multi-factor authentication), using any characters. Applications should also check passwords against a list of passwords known to be commonly used, expected, or compromised and prevent users from using such passwords.
This blog post looks at a few examples that show how to implement this recommendation in a web application.
zxcvbn-ts ¶
zxcvbn-ts is a maintained TypeScript implementation of the zxcvbn password strength estimator.
It combines dictionaries, keyboard patterns, repeated sequences, l33t substitutions, and other heuristics to estimate how difficult a password is to guess.
Add the library in npm-managed projects with
npm install @zxcvbn-ts/core @zxcvbn-ts/language-common @zxcvbn-ts/language-en
and then configure it with the common and English language packages:
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
zxcvbnOptions.setOptions({
translations: zxcvbnEnPackage.translations,
graphs: zxcvbnCommonPackage.adjacencyGraphs,
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary,
},
});
You can then check a password with
const result = zxcvbn(password);
The function expects the password in plain text. It also supports an optional second parameter, an array of user-specific values that should make the password score worse.
const result = zxcvbn(password, ['foo', 'bar']);
The result object contains several properties about the guessability of the password. For instance, result.guesses returns the estimated number of guesses needed to crack the password, and the other properties estimate crack times for different attack scenarios.
This example focuses on the result.score property, which contains a number between 0 and 4:
- 0: too guessable: risky password
- 1: very guessable: protection from throttled online attacks
- 2: somewhat guessable: protection from unthrottled online attacks
- 3: safely unguessable: moderate protection from offline slow-hash scenario
- 4: very unguessable: strong protection from offline slow-hash scenario
The sample application uses this score to drive a <meter> element and display feedback as the user types.
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
import { pwnedPassword } from 'hibp';
zxcvbnOptions.setOptions({
translations: zxcvbnEnPackage.translations,
graphs: zxcvbnCommonPackage.adjacencyGraphs,
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary,
},
});
const strength = {
0: 'Too guessable',
1: 'Very guessable',
2: 'Somewhat guessable',
3: 'Safely unguessable',
4: 'Very unguessable'
};
const password = document.getElementById('password');
const meter = document.getElementById('password-strength-meter');
const text = document.getElementById('password-strength-text');
const showPasswordFlag = document.getElementById('showPasswordFlag');
const passwordHibp = document.getElementById('password_hibp');
const output = document.getElementById('password_hibp_output');
const passwordSelfHostedHibp = document.getElementById('password_shhibp');
const outputSelfHostedHibp = document.getElementById('password_shhibp_output');
password.addEventListener('input', () => {
const value = password.value;
if (value === '') {
meter.value = 0;
text.textContent = '';
return;
}
const result = zxcvbn(value);
const feedback = [result.feedback.warning, ...result.feedback.suggestions]
.filter(Boolean)
.join(' ');
meter.value = result.score;
text.textContent = feedback === ''
? `Strength: ${strength[result.score]}.`
: `Strength: ${strength[result.score]}. ${feedback}`;
});
If you need the same style of password-strength estimation in Java, have a look at nbvcxz and zxcvbn4j.
hibp ¶
hibp is a JavaScript client library for the Have I been pwned? service.
Add the library in an npm-managed project with
npm install hibp
The library supports the current Have I Been Pwned API v3.
In this example, I only need the pwnedPassword method.
import { pwnedPassword } from 'hibp';
This method calls the Pwned Passwords API and returns how many times a password has appeared in a breach.
It expects the password in plain text as an argument. It runs asynchronously and returns a Promise.
The current API guidance recommends avoiding incremental searches while the user is typing. In the sample application, the check runs when the password field loses focus.
passwordHibp.addEventListener('blur', () => {
void checkHibp();
});
passwordSelfHostedHibp.addEventListener('blur', () => {
void checkSelfHostedHibp();
});
async function checkHibp() {
if (passwordHibp.value === '') {
output.textContent = '';
return;
}
output.textContent = 'Checking...';
try {
const count = await pwnedPassword(passwordHibp.value);
output.textContent = count > 0
? `This password has appeared ${count} times in the Pwned Passwords dataset. Choose a different one.`
: 'This password was not found in the Pwned Passwords dataset.';
} catch (error) {
output.textContent = error instanceof Error ? error.message : String(error);
}
}
This method does not send the password in plain text to the Have I been pwned? server. Instead, it first calculates the SHA-1 hash of the plain text password locally and then sends the first 5 characters of the hash to the service. Have I been pwned? returns a list of all the hashes that start with these 5 characters. The pwnedPassword function then checks the list to see if it contains our password.
It returns either 0 if the password was not found in the Have I been pwned? database or a number greater than 0. This number represents how often the password has been exposed in breaches.
Self-hosted Have I been pwned? database ¶
The hosted Pwned Passwords API is usually the simplest solution. If your application must keep the whole check inside your own infrastructure, you can host the password database yourself.
In a previous blog post, I described the process of how to download and import the Have I been pwned? password database into an embedded Xodus database.
After following the steps from that blog post, you have a local Xodus database. The Spring Boot sample below opens that database from the passwords.xodus.path property and exposes a RestController that returns the breach count for the submitted password.
@RestController
@CrossOrigin
public class SelfHostedHibp {
private final Environment env;
public SelfHostedHibp(@Value("${passwords.xodus.path}") String databasePath) {
this.env = Environments.newInstance(Path.of(databasePath).toAbsolutePath().toString());
}
@PreDestroy
public void destroy() {
if (this.env != null) {
this.env.close();
}
}
private Integer haveIBeenPwned(String password) {
return this.env.computeInReadonlyTransaction(txn -> {
Store store = this.env.openStore("passwords", StoreConfig.WITHOUT_DUPLICATES, txn);
byte[] passwordBytes = sha1(password);
ByteIterable key = new ArrayByteIterable(passwordBytes);
ByteIterable bi = store.get(txn, key);
if (bi != null) {
return IntegerBinding.compressedEntryToInt(bi);
}
return null;
});
}
private static byte[] sha1(String password) {
try {
return MessageDigest.getInstance("SHA-1").digest(password.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-1 algorithm is not available", e);
}
}
@PostMapping("/selfHostedHibpCheck")
public int selfHostedHibpCheck(@RequestBody String password) {
Integer count = haveIBeenPwned(password);
if (count != null) {
return count;
}
return 0;
}
Call the /selfHostedHibpCheck endpoint with the Fetch API in JavaScript. The service returns how many times a password has appeared in a breach. If the number is 0, the password is not present in the local database.
async function checkSelfHostedHibp() {
if (passwordSelfHostedHibp.value === '') {
outputSelfHostedHibp.textContent = '';
return;
}
outputSelfHostedHibp.textContent = 'Checking...';
try {
const response = await fetch('/selfHostedHibpCheck', {
body: passwordSelfHostedHibp.value,
headers: {
'Content-Type': 'text/plain;charset=UTF-8'
},
method: 'POST'
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const count = await response.json();
outputSelfHostedHibp.textContent = count === 0
? 'This password was not found in the local Pwned Passwords dataset.'
: `This password has appeared ${count} times in the local Pwned Passwords dataset.\nChoose a different one.`;
} catch (error) {
outputSelfHostedHibp.textContent = error instanceof Error ? error.message : String(error);
}
}
You can find the complete examples on GitHub.