Password Policies

Modern Rules (NIST SP 800-63B)

  • Length beats complexity: require at least 8 characters (ShopEase uses 12), allow at least 64, allow spaces and emoji — passphrases are good.
  • Block known-bad passwords: common lists and breached passwords, plus ones containing the username or app name.
  • No forced composition rules ("one uppercase, one symbol") and no forced periodic changes; they lead to Password@1, Password@2...
  • Force a change only when there is evidence of compromise.

A Policy Checker

Example

import java.util.*;

public class Main {
    static final Set<String> BLOCKLIST = Set.of("password123", "qwerty123456", "123456789012", "iloveyou1234");

    static List<String> check(String password, String email) {
        List<String> problems = new ArrayList<>();
        String lower = password.toLowerCase();
        String userPart = email.substring(0, email.indexOf('@')).toLowerCase();
        if (password.length() < 12) problems.add("at least 12 characters");
        if (password.length() > 128) problems.add("at most 128 characters");
        if (BLOCKLIST.contains(lower)) problems.add("too common");
        if (lower.contains("shopease")) problems.add("must not contain the app name");
        if (userPart.length() >= 3 && lower.contains(userPart)) problems.add("must not contain your email name");
        if (password.matches(".*(.)\\1\\1.*")) problems.add("no character 3 times in a row");
        return problems;
    }

    public static void main(String[] args) {
        String email = "[email protected]";
        List<String> candidates = List.of(
            "Asha@123",
            "password123",
            "ShopEase@2026!",
            "asha.verma2026!",
            "Paaaassword-long",
            "monsoon chai at 5 pm",
            "Tr7#kLp9!qZx");
        for (String pw : candidates) {
            List<String> problems = check(pw, email);
            System.out.printf("%-22s %s%n", pw, problems.isEmpty() ? "OK" : "REJECTED: " + String.join(", ", problems));
        }
    }
}

Output

Asha@123               REJECTED: at least 12 characters
password123            REJECTED: at least 12 characters, too common
ShopEase@2026!         REJECTED: must not contain the app name
asha.verma2026!        REJECTED: must not contain your email name
Paaaassword-long       REJECTED: no character 3 times in a row
monsoon chai at 5 pm   OK
Tr7#kLp9!qZx           OK

Notice monsoon chai at 5 pm passes: a long, memorable passphrase is as strong as the random Tr7#kLp9!qZx (also OK, exactly 12 characters) and far easier to remember. password123 breaks two rules at once.

Breached Passwords and Upgrading Hashes

// Spring Security 6.3+: checks the password against Have I Been Pwned (k-anonymity: only
// the first 5 characters of the SHA-1 hash leave your server)
@Bean
CompromisedPasswordChecker compromisedPasswordChecker() {
    return new HaveIBeenPwnedRestApiPasswordChecker();
}

@PostMapping("/api/auth/signup")
public ResponseEntity<?> signup(@Valid @RequestBody SignupRequest req) {
    List<String> problems = passwordPolicy.check(req.password(), req.email());
    if (compromisedPasswordChecker.check(req.password()).isCompromised()) {
        problems.add("this password appeared in a data breach");
    }
    if (!problems.isEmpty()) {
        return ResponseEntity.badRequest().body(Map.of("password", problems));
    }
    userService.register(req.email(), passwordEncoder.encode(req.password()));
    return ResponseEntity.status(HttpStatus.CREATED).build();
}

Tip: Use PasswordEncoderFactories.createDelegatingPasswordEncoder(). Hashes are stored with a prefix like {bcrypt}$2a$10$..., so you can move to Argon2 later; implement UserDetailsPasswordService and Spring re-hashes each user's password automatically at their next login.