docs
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.
Core Capabilities:
CompromisedPasswordChecker - Interface for checking if a password is compromisedReactiveCompromisedPasswordChecker - Reactive versionCompromisedPasswordDecision - Result of password checkCompromisedPasswordException - Exception thrown when compromised password detectedKey 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 usedDefault Behaviors:
CompromisedPasswordDecision defaults to not compromisedMono that may errorThreading Model:
CompromisedPasswordDecision is immutable (thread-safe)MonoLifecycle:
Exceptions:
CompromisedPasswordException - Thrown when compromised password detectedEdge Cases:
getCompromisedCount() may return large numbersInterface 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)CompromisedPasswordDecision indicating the result.Example:
CompromisedPasswordChecker checker = ...;
CompromisedPasswordDecision decision = checker.check("password123");
if (decision.isCompromised()) {
long count = decision.getCompromisedCount();
// Password is compromised, reject it
}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)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();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)CompromisedPasswordDecision(boolean compromised, long compromisedCount)Key Methods:
boolean isCompromised()true if the password is compromised, false otherwise.long getCompromisedCount()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 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."
);
}@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
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;
});
}
};
}
}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)
);
}org.springframework.security.password