Featured image for "Exploring Spring Security's Compromised Password Checker"

Exploring Spring Security's Compromised Password Checker

May 30th, 2024
4 minute read
Spring Security Spring Spring boot

Recently, Spring Boot 3.3 was released with several new features. With this new release, Spring Security was also updated to 6.3. This version of Spring Security introduced a new feature called CompromisedPasswordChecker.

What is CompromisedPasswordChecker?

CompromisedPasswordChecker is a new feature in Spring Security that allows you to check if a password has been compromised in a data breach. Currently, there is only one implementation available, which is the HaveIBeenPwnedRestApiPasswordChecker. This implementation uses the Have I Been Pwned API to check if a password has been compromised.

More precisely, it uses the range API, where the first 5 characters of the SHA-1 hash of the password are sent to the API. The API then returns a list of SHA-1 hash suffixes that match the prefix, which are then checked against the full SHA-1 hash of the password.

Have I Been Pwned flow

How to use CompromisedPasswordChecker?

To use CompromisedPasswordChecker, you need to add the following dependency to your project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

After that, you need to configure a CompromisedPasswordChecker bean:

@Bean
public CompromisedPasswordChecker haveIBeenPwnedPasswordChecker() {
    return new HaveIBeenPwnedRestApiPasswordChecker();
}

If you authenticate now, your application will return a 401 Unauthorized status code if the password has been compromised.

# Returns 401 Unauthorized
curl \
  -X GET \
  --location "http://localhost:8080/api/user/current" \
  -H "Content-Type: application/json" \
  --basic --user user:password
# Returns 200 OK
curl \
  -X GET \
  --location "http://localhost:8080/api/user/current" \
  -H "Content-Type: application/json" \
  --basic --user user2:A-Password-That-Is-Not-Common-Or-Shared

Creating your own CompromisedPasswordChecker

While the default implementation works great, it has a few drawbacks, such as:

To address these concerns, you can create your own CompromisedPasswordChecker implementation. For example:

public class MyPasswordChecker implements CompromisedPasswordChecker {

    @Override
    public CompromisedPasswordDecision check(String password) {
        boolean isCompromised = false; // TODO: implement
        return new CompromisedPasswordDecision(isCompromised);
    }
}

Using SecLists

One potential implementation is to check whether the password is in any of the SecLists dumps. For example, you could compare it against the top 1 million common passwords.

To implement this, you first need to download the file to src/main/resources. Then you can implement the CompromisedPasswordChecker like this:

public class ResourcePasswordChecker implements CompromisedPasswordChecker {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final Resource resource;

    public ResourcePasswordChecker(Resource resource) {
        this.resource = resource;
    }

    @Override
    public CompromisedPasswordDecision check(String password) {
        try(Stream<String> lines = Files.lines(resource.getFile().toPath())) {
            boolean anyMatch = lines.anyMatch(line -> line.equals(password));
            return new CompromisedPasswordDecision(anyMatch);
        } catch (IOException ex) {
            logger.warn("Could not read password file", ex);
            return new CompromisedPasswordDecision(false);
        }
    }
}

And then configure the bean like this:

@Bean
public ResourcePasswordChecker resourcePasswordChecker() {
    return new ResourcePasswordChecker(new ClassPathResource("10-million-password-list-top-1000000.txt"));
}

Checking password entropy

Anyone familiar with xkcd’s correct horse battery staple comic knows that a long password is better than a short one.

xkcd comic

To check the entropy of a password, you can use the nbvcxz library to verify the strength of your password. This library is inspired by Dropbox’s zxcvbn. To use this library, you need to add the following dependency:

<dependency>
    <groupId>me.gosimple</groupId>
    <artifactId>nbvcxz</artifactId>
    <version>1.5.1</version>
</dependency>

After that, you can implement the CompromisedPasswordChecker like this:

public class NbvcxzPasswordChecker implements CompromisedPasswordChecker {
    private final Nbvcxz nbvcxz;

    public NbvcxzPasswordChecker(Nbvcxz nbvcxz) {
        this.nbvcxz = nbvcxz;
    }

    @Override
    public CompromisedPasswordDecision check(String password) {
        Result estimate = nbvcxz.estimate(password);
        return new CompromisedPasswordDecision(!estimate.isMinimumEntropyMet());
    }
}

This library provides an Nbvcxz class that can be used to estimate the strength of a password. To do this, you use the estimate method, which returns a Result object. This object contains information about the password, such as the number of guesses needed to crack it and the entropy. By checking if it meets the minimum entropy, you can determine if the password is strong enough.

All you need to do now is to configure the bean:

@Bean
public NbvcxzPasswordChecker nbvcxzPasswordChecker() {
    return new NbvcxzPasswordChecker(new Nbvcxz());
}

Conclusion

In this blog post, we explored Spring Security’s CompromisedPasswordChecker and how it can be used to check if a password has been compromised in a data breach. We also discussed how you can create your own CompromisedPasswordChecker implementation to check if a password is in a list of common passwords or to verify its entropy. This can be useful if you need some offline alternative to the default implementation, which uses the Have I Been Pwned API.

As usual, a full code example can be found on GitHub.