or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

aot-native-support.mdauthentication-core.mdauthentication-events.mdauthentication-management.mdauthentication-tokens.mdauthorities.mdauthorization.mdcompromised-password.mdconcurrent-async.mddao-authentication.mdexpression-access-control.mdindex.mdjaas-authentication.mdjackson-serialization.mdmethod-security.mdobservation-metrics.mdone-time-tokens.mdprovisioning.mdsecurity-context.mdsession-management.mduser-details.md
tile.json

compromised-password.mddocs/

Compromised Password Checking

Spring Security Core provides support for checking passwords against databases of compromised passwords (e.g., Have I Been Pwned). This helps prevent users from using passwords that have been exposed in data breaches.

Key Information for Agents

Core Capabilities:

  • CompromisedPasswordChecker - Interface for checking if a password is compromised
  • ReactiveCompromisedPasswordChecker - Reactive version
  • CompromisedPasswordDecision - Result of password check
  • CompromisedPasswordException - Exception thrown when compromised password detected
  • Integration with password encoding and user provisioning

Key Interfaces and Classes:

  • CompromisedPasswordChecker - Interface: check(String rawPassword)
  • ReactiveCompromisedPasswordChecker - Interface: check(String rawPassword) returns Mono<CompromisedPasswordDecision>
  • CompromisedPasswordDecision - Class: isCompromised(), getCompromisedCount()
  • CompromisedPasswordException - Exception: thrown when compromised password is used

Default Behaviors:

  • CompromisedPasswordDecision defaults to not compromised
  • Checkers return decision (not exception) - exceptions thrown by higher-level components
  • Reactive checkers return Mono that may error

Threading Model:

  • Checkers are stateless (thread-safe)
  • CompromisedPasswordDecision is immutable (thread-safe)
  • Reactive checkers work with Mono

Lifecycle:

  • Checkers configured once and reused
  • Checks performed during password encoding/validation

Exceptions:

  • CompromisedPasswordException - Thrown when compromised password detected

Edge Cases:

  • Null password: Behavior depends on implementation (may throw or return not compromised)
  • Network failures: Reactive checkers may emit error
  • Empty password: Typically returns not compromised
  • Large compromised count: getCompromisedCount() may return large numbers

Core Interfaces

CompromisedPasswordChecker

Interface for checking if a password has been compromised (e.g., in a data breach).

package org.springframework.security.password;

interface CompromisedPasswordChecker {
    CompromisedPasswordDecision check(String rawPassword);
}

Key Methods:

CompromisedPasswordDecision check(String rawPassword)
  • Checks if the given raw password is compromised.
  • Returns a CompromisedPasswordDecision indicating the result.

Example:

CompromisedPasswordChecker checker = ...;
CompromisedPasswordDecision decision = checker.check("password123");

if (decision.isCompromised()) {
    long count = decision.getCompromisedCount();
    // Password is compromised, reject it
}

ReactiveCompromisedPasswordChecker

Reactive version of CompromisedPasswordChecker for use in reactive applications.

package org.springframework.security.password;

interface ReactiveCompromisedPasswordChecker {
    Mono<CompromisedPasswordDecision> check(String rawPassword);
}

Key Methods:

Mono<CompromisedPasswordDecision> check(String rawPassword)
  • Checks if the given raw password is compromised reactively.
  • Returns a Mono that emits the decision.

Example:

ReactiveCompromisedPasswordChecker checker = ...;
Mono<CompromisedPasswordDecision> decisionMono = checker.check("password123");

decisionMono
    .filter(CompromisedPasswordDecision::isCompromised)
    .flatMap(decision -> Mono.error(new CompromisedPasswordException(
        "Password has been compromised " + decision.getCompromisedCount() + " times"
    )))
    .block();

Decision Objects

CompromisedPasswordDecision

Represents the result of a compromised password check.

package org.springframework.security.password;

class CompromisedPasswordDecision {
    CompromisedPasswordDecision(boolean compromised);
    CompromisedPasswordDecision(boolean compromised, long compromisedCount);

    boolean isCompromised();
    long getCompromisedCount();
}

Constructors:

CompromisedPasswordDecision(boolean compromised)
  • Creates a decision with the specified compromised status.
CompromisedPasswordDecision(boolean compromised, long compromisedCount)
  • Creates a decision with compromised status and count of times the password was found in breaches.

Key Methods:

boolean isCompromised()
  • Returns true if the password is compromised, false otherwise.
long getCompromisedCount()
  • Returns the number of times this password was found in data breaches.
  • Returns 0 if the password is not compromised.

Constants:

static final CompromisedPasswordDecision NOT_COMPROMISED =
    new CompromisedPasswordDecision(false);

Example:

// Password not compromised
CompromisedPasswordDecision safe = new CompromisedPasswordDecision(false);
// safe.isCompromised() == false
// safe.getCompromisedCount() == 0

// Password compromised with count
CompromisedPasswordDecision compromised = new CompromisedPasswordDecision(
    true,
    1234567L
);
// compromised.isCompromised() == true
// compromised.getCompromisedCount() == 1234567

// Using constant
CompromisedPasswordDecision notCompromised =
    CompromisedPasswordDecision.NOT_COMPROMISED;

// Check decision
CompromisedPasswordDecision decision = checker.check("password123");
if (decision.isCompromised()) {
    long breachCount = decision.getCompromisedCount();
    throw new CompromisedPasswordException(
        "Password found in " + breachCount + " data breaches"
    );
}

Exception

CompromisedPasswordException

Exception thrown when a compromised password is detected.

package org.springframework.security.password;

class CompromisedPasswordException extends RuntimeException {
    CompromisedPasswordException(String message);
    CompromisedPasswordException(String message, Throwable cause);
}

Constructors:

CompromisedPasswordException(String message)
CompromisedPasswordException(String message, Throwable cause)

Example:

CompromisedPasswordDecision decision = checker.check("password123");

if (decision.isCompromised()) {
    throw new CompromisedPasswordException(
        "Password has been compromised in " +
        decision.getCompromisedCount() + " data breaches. " +
        "Please choose a different password."
    );
}

Integration Example

@Component
public class PasswordValidationService {

    private final CompromisedPasswordChecker passwordChecker;
    private final PasswordEncoder passwordEncoder;

    public PasswordValidationService(
            CompromisedPasswordChecker passwordChecker,
            PasswordEncoder passwordEncoder) {
        this.passwordChecker = passwordChecker;
        this.passwordEncoder = passwordEncoder;
    }

    public void validateAndEncodePassword(String rawPassword) {
        // Check if password is compromised
        CompromisedPasswordDecision decision = passwordChecker.check(rawPassword);

        if (decision.isCompromised()) {
            throw new CompromisedPasswordException(
                String.format(
                    "Password has been found in %d data breaches. " +
                    "Please choose a different password.",
                    decision.getCompromisedCount()
                )
            );
        }

        // Password is safe, encode it
        String encodedPassword = passwordEncoder.encode(rawPassword);
        // Store encoded password
    }
}

Reactive Integration:

@Service
public class ReactivePasswordService {

    private final ReactiveCompromisedPasswordChecker passwordChecker;
    private final PasswordEncoder passwordEncoder;

    public Mono<String> validateAndEncodePassword(String rawPassword) {
        return passwordChecker.check(rawPassword)
            .filter(CompromisedPasswordDecision::isCompromised)
            .flatMap(decision -> Mono.error(new CompromisedPasswordException(
                "Password compromised in " + decision.getCompromisedCount() + " breaches"
            )))
            .switchIfEmpty(Mono.fromCallable(() ->
                passwordEncoder.encode(rawPassword)
            ));
    }
}

Configuration Example

@Configuration
public class PasswordSecurityConfiguration {

    @Bean
    public CompromisedPasswordChecker compromisedPasswordChecker() {
        // Implementation would typically integrate with external service
        // like Have I Been Pwned API
        return new CompromisedPasswordChecker() {
            @Override
            public CompromisedPasswordDecision check(String rawPassword) {
                // Check against compromised password database
                // This is a placeholder - real implementation would call API
                if (isPasswordCompromised(rawPassword)) {
                    long count = getCompromisedCount(rawPassword);
                    return new CompromisedPasswordDecision(true, count);
                }
                return CompromisedPasswordDecision.NOT_COMPROMISED;
            }

            private boolean isPasswordCompromised(String password) {
                // Implementation checks against database
                return false;
            }

            private long getCompromisedCount(String password) {
                // Implementation returns count from database
                return 0L;
            }
        };
    }

    @Bean
    public ReactiveCompromisedPasswordChecker reactiveCompromisedPasswordChecker() {
        return new ReactiveCompromisedPasswordChecker() {
            @Override
            public Mono<CompromisedPasswordDecision> check(String rawPassword) {
                return Mono.fromCallable(() -> {
                    CompromisedPasswordDecision decision =
                        compromisedPasswordChecker().check(rawPassword);
                    return decision;
                });
            }
        };
    }
}

Error Handling

public class PasswordChangeService {

    private final CompromisedPasswordChecker passwordChecker;

    public void changePassword(String username, String newPassword) {
        try {
            CompromisedPasswordDecision decision = passwordChecker.check(newPassword);

            if (decision.isCompromised()) {
                throw new CompromisedPasswordException(
                    String.format(
                        "The password you entered has been found in %d data breaches. " +
                        "For your security, please choose a different password.",
                        decision.getCompromisedCount()
                    )
                );
            }

            // Proceed with password change
            updatePassword(username, newPassword);

        } catch (CompromisedPasswordException e) {
            // Log and rethrow
            logger.warn("Compromised password attempt for user: {}", username);
            throw e;
        }
    }
}

Reactive Error Handling:

public Mono<Void> changePassword(String username, String newPassword) {
    return passwordChecker.check(newPassword)
        .flatMap(decision -> {
            if (decision.isCompromised()) {
                return Mono.error(new CompromisedPasswordException(
                    "Password compromised in " + decision.getCompromisedCount() + " breaches"
                ));
            }
            return Mono.empty();
        })
        .then(updatePasswordReactive(username, newPassword))
        .doOnError(CompromisedPasswordException.class, e ->
            logger.warn("Compromised password attempt for user: {}", username)
        );
}

Package

  • org.springframework.security.password