Creating stronger passwords with AuthKit

A technical deep dive on how AuthKit ensures strong user passwords.

Every developer who has ever implemented a login flow is embroiled in an arms race with a most cunning adversary. Not hackers trying to bypass your security systems, but your own users trying to circumvent your password requirements. 

In November 2023, WorkOS released AuthKit, a login box packed with features to make your and your customer’s lives easier. One of those features is automatic password validation. This means that if a user inevitably tries to sign up with a weak password, AuthKit will prevent the action and give clear reasons why the password is bad. In this post we’ll deep dive into the tech that powers this simple yet very effective bit of user interface that will save your users from themselves.

The world needs stronger passwords

Let’s first go over some basics, like why we are consistently tortured with increasingly elaborate password requirements.

You’ve probably encountered some overly draconian password requirements in the wild, examples like “Password must contain uppercase, lowercase and special characters” or “Password must not use the same character more than once”. While these might seem like they were engineered to annoy you, the main reason behind them is to prevent you from picking a password that is too easy for a computer to guess.

Simply put, computers are very good at making many guesses at passwords in very short amounts of time. These “brute force” attacks are made more effective by starting with guessing passwords that contain actual words in a variety of languages (“dictionary attacks”) or contain “l33t” speak (replacing letters with numbers that look similar). 

This is stymied somewhat in production environments, where attempts at a password should be rate limited to only allow a set amount of attempts per minute. Attackers however can and do gain access to encrypted password databases, which they can then subject to offline attacks where the number of password attempts can be in the range of billions of attempts per second.

The problem with passwords is that they need to be complex enough to not be easily guessable by a computer, but also be simple enough that a human can actually remember them. As writer Matt Levine put it:

The trick with information security is that you have to make your security measures strong enough to defeat criminals, but not so strong that they annoy your employees into disabling them.

Developers have to walk a tight line of enforcing strict security rules, whilst also making sure users do not adopt bad habits, like using the same password everywhere or coming up with easily guessable passwords.

Password transparency with AuthKit

AuthKit makes this process easier by being fully transparent about a password’s strength and communicating this in a user friendly way. 

Under the hood AuthKit uses the excellent zxcvbn-ts library to determine the strength of a password and give feedback on why it’s insecure.

In a nutshell, zxcvbn-ts provides the following calculations to estimate a password’s strength: 

  1. Entropy Calculation: Instead of just counting character types, zxcvbn-ts estimates the number of guesses an attacker would need to crack the password. This is done by calculating the entropy of the password, which is a measure of randomness and unpredictability.
  1. Pattern Recognition: The library uses pattern matching to identify common password elements, such as dates, repeated characters, sequences (like "abcd"), keyboard patterns (like "qwerty"), and common words or phrases. This helps in understanding how predictable a password might be.
  1. Dictionary Checks: The library includes a range of dictionaries (like common passwords, names, English words, etc.) against which it checks the password. If a password is too similar to a word in these dictionaries, it's considered weaker, as dictionary attacks are a common cracking method.
  1. Attack-Time Estimation: The library also estimates the time it would take for an attacker to crack the password under different scenarios, such as an offline fast attack or an online attack with rate limiting. This gives a more tangible sense of the password's strength.

Additionally, the library includes a list of known compromised passwords provided by the haveibeenpwned database service. This allows us to tell you directly if your password was seen earlier on a list of compromised passwords, immediately making it insecure no matter how complicated it is.

A score value between one and four is given to the password, where a one is so weak it could be cracked in seconds and a four indicates it would take centuries to guess the password.

In practise, AuthKit only has 6 types of password validation errors, as demonstrated here:

    export type PasswordValidationError =
    | {
        tag: 'PasswordContainsEmail';
    | {
        tag: 'PasswordTooShort';
        minimumLength: number;
    | {
        tag: 'PasswordTooLong';
        maximumLength: number;
    | {
        tag: 'PasswordPwned';
        occurrences: number;
    | {
        tag: 'PasswordTooWeak';
        score: number;
        warning: string;
        suggestions: string[];
    | {
        tag: 'PasswordMissingCharacterType';
        characterType: PasswordMissingCharacterType;
        symbols?: string;

All of the above are configured via the WorkOS dashboard, where you can opt for the default strong password policy or craft your own:

One error worth calling out is `PasswordPwned`, which is thrown when the suggested password has been previously seen in a data breach. We go one step further and actually query the “haveibeenpwned” API to see exactly how many times that particular password has been seen before.

    export const checkPassword = async (
      password: string,
    ): Promise => {
      const hash = crypto.createHash('sha1').update(password, 'utf8').digest('hex').toUpperCase();

      const hashPrefix = hash.slice(0, 5);
      const hashSuffix = hash.slice(5, 40);

      const response = await axios.get(
        urljoin('', hashPrefix)

      const entries =
        .map((line) => {
          const [suffix, occurrences] = line.split(':');

          if (!suffix || !occurrences) {
            return undefined;

          return { suffix, occurrences: parseInt(occurrences, 10) };

      const matchingEntry = entries.find((entry) => entry.suffix === hashSuffix);
      if (!matchingEntry) {
        return { tag: 'NotFound' };

      return { tag: 'Pwned', occurrences: matchingEntry.occurrences };

The above function takes the password, hashes it and based on the prefix of the hash queries the haveibeenpwned API to find how many times the password has been found in recorded breaches. We only pass the hash prefix to ensure that the full password isn’t leaked to any external services.

This extra bit of information goes a long way to educate users on the dangers of insecure passwords. It’s one thing to know your password was found in a breach, it’s another to find out that it’s potentially been found hundreds or thousands of times, meaning that perhaps your super secure password is neither as secure nor original as you thought it was. 

The importance of timing and messaging

The tech behind password validation is interesting, but what makes it all come together is how to relay that information to your user. 

If your suggested password runs afoul of the service’s password requirements, the worst thing a site can do is submit the form to the server and make the user wait for the response (potentially clearing out all other form data) to find out that their password isn’t valid. 

With AuthKit and zxcvbn-ts, the validation happens entirely client side. Users need to click the “Continue” button to start the validation, as validating while still typing is annoying (“I know it’s not secure, I haven’t finished yet!”). On page load the password configuration set from the WorkOS dashboard is retrieved and zxcvbn-ts is ready to validate passwords without requiring a server roundtrip. 

If a password is insecure, AuthKit will use easy to understand language to explain why. For example, here’s the message displayed when a password contains the email address the user is trying to sign up with:

      message: 'The provided password contains the associated user email address. Passwords must not contain easily guessable user information.',
      code: 'password_contains_email',

Extra information is good, but too much information can be problematic. The results of zxcvbn-ts give us an estimation on how quickly a particular password can be cracked in a variety of scenarios. Using jargon like “password can be guessed in less than a second in an offline attack with fast hash and multiple cores” is unlikely to mean much to the layperson and could just confuse and annoy them further. 

Instead, simple messaging such as “The provided password is not strong enough. Consider adding more words that are less common and capitalizing more than the first letter.” gets the point across with useful suggestions.

Protect your users from password reuse

Using a password validation system is a great way to get your users to come up with secure passwords, but it still leaves them vulnerable to the most common way of cracking passwords: password reuse. 

Anyone who doesn’t use a password manager likely has one (hopefully) strong password that they either use exactly or vary slightly between services. This means that no matter how strong the password requirements and security are on your end, if an entirely different service has their username and password database stolen then your users are potentially directly affected if they use the same password everywhere. This attack vector is known as “credential stuffing” and is usually the first thing attackers try when a new trove of credentials is leaked or stolen.

You can’t force your users to use password managers to mitigate this, but you can protect them by adding in an additional layer of security: multi-factor authentication. This is the practice of requiring your users to use an authenticator app to generate a secure code in addition to the password. Since the attackers are unlikely to have any way of generating said code on their own, MFA will effectively mitigate the dangers of password reuse.

Wrapping up

We at WorkOS are committed to continue making AuthKit the world’s best login box, which is why we sweat the details and strive to make it as usable as it is feature rich. 

Have questions about this post or want to know more about AuthKit? Reach out to us on 𝕏 or schedule some time with our solutions engineering team. 

In this article

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.