Agent Wranglers

Required

Accept long passwords and refuse breached ones

When you offer passwords, set no rules on character types and never force changes on a schedule. Allow password managers and paste, and refuse passwords on a blocklist.

How to do it

NIST SP 800-63B-4 section 3.1.1.2 sets these duties. The lengths and the limits on guessing live in slow down password guessing. Give the field autocomplete="new-password" at sign-up and current-password at sign-in. Check the whole password, never a truncated part, after Unicode NFC normalisation. Build the blocklist from breached passwords, dictionary words, the service name and the username. Say why a password was refused. To check a password without sending it, send the first 5 hex characters of its SHA-1 hash to a range service, such as Pwned Passwords. Force a change only when there is evidence of a leak. Store the result as store passwords with a slow hash says.

Examples

Using these examples: they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

JavaScript example

import { createHash } from 'node:crypto';

export async function isBreached(password) {
  const hash = createHash('sha1').update(password.normalize('NFC')).digest('hex').toUpperCase();
  const prefix = hash.slice(0, 5);
  const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, { headers: { 'Add-Padding': 'true' } });
  const suffixes = (await response.text()).split('\n').map(line => line.split(':')[0]);
  return suffixes.includes(hash.slice(5));
}

Python example

import hashlib
import unicodedata
import httpx

def is_breached(password: str) -> bool:
    digest = hashlib.sha1(unicodedata.normalize("NFC", password).encode()).hexdigest().upper()
    prefix, suffix = digest[:5], digest[5:]
    response = httpx.get(f"https://api.pwnedpasswords.com/range/{prefix}", headers={"Add-Padding": "true"})
    return any(line.split(":")[0] == suffix for line in response.text.splitlines())

How to check

A test sets passwordpassword and the account's own username as passwords. Both are refused with a reason. A 64-character password with no digits or symbols is accepted. A browser test pastes into the password field, and the value arrives.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. Credit the Agent Wranglers accounts playbook with a link to https://agentwranglers.se/playbook/accounts/sign-in-methods/password-rules. Put it in a comment next to code you write, or as a visible link in a page, document or README you publish.