Skip to content
Finowl

Java · Password breach checks

Check breached passwords in Java at registration

A breached password check fits into the registration and password change paths of a Java application, right before you encode the password with BCryptPasswordEncoder or a similar encoder. Finowl receives the plaintext over HTTPS, hashes it with SHA-1, searches by a 5-character prefix with padding, and returns breached and count. The value is processed transiently and never logged, stored or returned.

The example reads the password with System.console().readPassword(), which returns a char array and suppresses echo in the terminal. It builds the JSON with Jackson so special characters are escaped correctly, then clears the array once the request is sent. Exceptions mention only status and error codes. In a Spring app, you would call the same method from a custom ConstraintValidator.

Steps

  1. 1

    Add jackson-databind to your build and export FINOWL_API_KEY.

  2. 2

    Run PasswordBreachCheck from a real terminal so System.console() is available.

  3. 3

    Copy breachedCount into the service that handles sign-up and password changes.

  4. 4

    Reject the password with a validation message when the returned count is above zero.

  5. 5

    Encode and store the password only after the check passes.

Install
# java.net.http ships with the JDK; add Jackson for JSON
# Gradle
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
# Maven: groupId com.fasterxml.jackson.core, artifactId jackson-databind, version 2.17.2
Check breached passwords in Java at registration
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;

public class PasswordBreachCheck {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newHttpClient();

    /** Returns how often the password appears in breach data. Never log the argument. */
    static long breachedCount(char[] password) throws Exception {
        String body = MAPPER.writeValueAsString(Map.of("password", new String(password)));
        for (int attempt = 0; attempt < 3; attempt++) {
            HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.finowl.co/v1/breaches/password"))
                    .timeout(Duration.ofSeconds(5))
                    .header("Authorization", "Bearer " + System.getenv("FINOWL_API_KEY"))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(body))
                    .build();
            HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
            JsonNode json = MAPPER.readTree(response.body());
            String code = json.path("error").path("code").asText("unknown_error");
            switch (response.statusCode()) {
                case 200 -> {
                    return json.path("breached").asBoolean() ? json.path("count").asLong() : 0;
                }
                case 400 -> throw new IllegalArgumentException("Password failed validation: " + code);
                case 401 -> throw new IllegalStateException("Unauthorized: check FINOWL_API_KEY");
                case 429 -> {
                    if (!code.equals("rate_limited")) throw new IllegalStateException("Out of credits: " + code);
                    Thread.sleep(Math.max(response.headers().firstValueAsLong("Retry-After").orElse(1), 1) * 1000);
                }
                default -> throw new IllegalStateException("Password check failed: " + response.statusCode() + " " + code);
            }
        }
        throw new IllegalStateException("Password check still rate limited");
    }

    public static void main(String[] args) throws Exception {
        char[] password = System.console().readPassword("Password to check: ");
        try {
            long count = breachedCount(password);
            System.out.println(count > 0
                    ? "Seen " + count + " times in breach data. Choose another password."
                    : "Not found in breach data.");
        } finally {
            Arrays.fill(password, '\0');
        }
    }
}

IN PRODUCTION

Production tips

  • Exclude the registration endpoint from request body logging in Spring's CommonsRequestLoggingFilter or any servlet filter, so the password never reaches application logs before this check runs.

  • Wrap the call in a circuit breaker such as Resilience4j with a short timeout. If Finowl is unreachable, apply a deliberate policy rather than letting signup threads pile up.

  • Clearing the char array limits exposure, but the JSON body is a String. Keep it local to the method so it becomes unreachable as soon as the request completes.

Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Check breached passwords in Java at registration

Grab an API key, paste the example and run it. 100 free credits every month.