Java Password Validation Regex: Lookaheads Explained

Build a password validator in Java with regex lookaheads, see how each lookahead works and how to report which rule failed, and compare composition rules with the length-and-blocklist approach of NIST SP 800-63B-4.

Java regex

A Java password validation regex checks each password rule with a lookahead, a zero-width test written (?=…), and checks the length once at the end. The common pattern ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).{8,64}$ requires a lowercase letter, an uppercase letter, a digit and a special character, in any order, in a password of 8 to 64 characters. Each lookahead is independent, so rules can be added or removed without rewriting the rest.

This tutorial explains how each lookahead works, how negative lookaheads reject things such as whitespace or the user name, and how to build a configurable validator that tells the user which rule failed. It also fixes a bug in the regex that the earlier version of this page used.

Current NIST guidance advises against composition rules such as “one uppercase letter and one symbol”. Section 7 summarizes what NIST SP 800-63B-4 asks for instead, and shows a length-and-blocklist check that follows it. The lookahead technique is still worth knowing, because many applications must enforce a company policy.

The regex, the configurable policy and the NIST-style length rule look like this in Java:

Pattern STRONG = Pattern.compile(
    "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^a-zA-Z0-9]).{8,64}$");

STRONG.matcher("Apple@123").matches();      // true
STRONG.matcher("apple@123").matches();      // false, no uppercase
STRONG.matcher("Apple1234").matches();      // false, no special character
STRONG.matcher("Ap@1").matches();           // false, too short

PasswordPolicy policy = PasswordPolicy.builder()
    .length(8, 64).requireLowercase().requireUppercase()
    .requireDigit().requireSpecial().forbidWord("lokesh").build();
policy.violations("apple123");
// [needs an uppercase letter, needs a character that is not a letter or digit]

Pattern.compile("^.{15,64}$").matcher("green apple tree").matches();   // true, NIST-style length rule

1. The Regex, Part by Part

The pattern has an anchor at each end, four lookaheads in the middle and one part that consumes the characters:

PartMeaning
^Start of the input; every lookahead below is tested from here
(?=.*[a-z])Somewhere ahead there is a lowercase letter
(?=.*[A-Z])Somewhere ahead there is an uppercase letter
(?=.*\d)Somewhere ahead there is a digit
(?=.*[^a-zA-Z0-9])Somewhere ahead there is a character that is not a letter or digit
.{8,64}The password itself: 8 to 64 characters of any kind except a line break
$End of the input

The special-character class [^a-zA-Z0-9] is a negated class: it matches any character that is not listed. This is simpler than listing symbols such as [@#$%], which rejects a password whose only symbol is “!” or “-“. Note that a space also counts as “not a letter or digit”, so “Apple @123” passes.

2. How a Lookahead Works

A lookahead tests whether its pattern can match at the current position, then returns to that position. It never consumes characters, which is why it is called zero-width. A search for (?=.*\d) alone succeeds in “abc1” but matches an empty string at index 0:

Matcher m = Pattern.compile("(?=.*\\d)").matcher("abc1");
m.find();                                   // true
m.group();                                  // ""
m.start();                                  // 0

"abc1".matches("(?=.*\\d)");                // false, nothing consumed
"abc1".matches("(?=.*\\d).*");              // true
"abc1".matches("(?=\\d).*");                // false, no digit at position 0

The last two lines show why each lookahead starts with .*. Without it, (?=\d) asks “is the first character a digit?”. With it, (?=.*\d) asks “is there a digit anywhere after zero or more other characters?”.

Because every lookahead starts at the same position, right after ^, the lookaheads combine like a logical AND. All of them must succeed before .{8,64} reads a single character. Their order does not change the result.

3. Adding the Rules One at a Time

Building the regex step by step shows what each lookahead removes. Each row adds one lookahead; each column is one sample password (T = matches, F = does not):

RegexApple@123apple@123APPLE@123Apple@abcApple1234Ap@1
^.{8,64}$TTTTTF
+ (?=.*[a-z])TTFTTF
+ (?=.*[A-Z])TFFTTF
+ (?=.*\d)TFFFTF
+ (?=.*[^a-zA-Z0-9])TFFFFF

Only “Apple@123” passes every rule. “Ap@1” fails from the first row, because the length check applies whatever the lookaheads say.

4. Negative Lookaheads for Things to Reject

A negative lookahead, written (?!…), succeeds when its pattern can NOT match ahead. It expresses rules like “must not contain”:

  • (?!.*\s) rejects any whitespace.
  • (?!.*(.)\1\1) rejects three identical characters in a row; (.) captures one character and \1 refers back to it.
  • (?!.*(?i:\Qlokesh\E)) rejects the user name in any letter case.
"Apple @123".matches("^(?!.*\\s).+$");            // false
"Appple@123".matches("^(?!.*(.)\\1\\1).+$");      // false, "ppp"
"Apple@123".matches("^(?!.*(.)\\1\\1).+$");       // true

Pattern noUser = Pattern.compile("^(?!.*(?i:" + Pattern.quote("lokesh") + ")).+$");
noUser.pattern();                                 // ^(?!.*(?i:\Qlokesh\E)).+$
noUser.matcher("Lokesh@2026").matches();          // false
noUser.matcher("Apple@2026").matches();           // true

When user input becomes part of a regex, we always wrap it with Pattern.quote(). It puts the text between \Q and \E, so characters such as “.” or “*” in a user name are treated as plain text. The (?i:…) group turns on case-insensitive matching for that part only.

5. A Bug in the Old Regex

The previous version of this article used ((?=.*[a-z])(?=.*d)(?=.*[@#$%])(?=.*[A-Z]).{6,16}). The digit rule is missing its backslash: (?=.*d) looks for the letter “d”, not for a digit.

OLD.matcher("Password@").matches();     // true, no digit at all
OLD.matcher("Apple@123").matches();     // false, has digits but no "d"

In a Java string literal, the regex \d must be written as “\\d”. A single backslash in the source, as in “\d”, does not compile, so the mistake usually happens when a regex is copied from a web page that dropped one backslash. A test with a password that has digits but no letter “d” catches this bug at once. The old pattern also limited passwords to 16 characters, which is far below what current guidance recommends.

6. A Configurable Validator With Error Messages

A single matches() call only says “invalid”. Users need to know which rule they missed. The example project has a PasswordPolicy class that solves both needs with the same rules:

  • Each rule is stored as a lookahead string plus a message.
  • build() joins all lookaheads and the length part into one regex and compiles it once.
  • violations() tests each rule on its own and returns the messages of the failed ones.

Each builder method adds one rule. The forbidWord(word) method, which rejects a word such as the user name, stores this message and lookahead:

rules.add(new Rule("must not contain \"" + word + "\"",
    "(?!.*(?i:" + Pattern.quote(word) + "))"));

The combined regex and the messages for some inputs:

PasswordPolicy policy = PasswordPolicy.builder()
    .length(8, 64).requireLowercase().requireUppercase()
    .requireDigit().requireSpecial().forbidWord("lokesh").build();

policy.regex();
// ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9])(?!.*(?i:\Qlokesh\E)).{8,64}$

policy.isValid("Apple@123");        // true
policy.violations("Ap@1");          // [must be 8 to 64 characters long]
policy.violations("Lokesh@123");    // [must not contain "lokesh"]

The Pattern is created once in the constructor and stored in a final field. A Pattern is immutable and thread-safe, so one policy object can serve all requests. The length check in violations() uses codePointCount() instead of length(): an emoji such as U+1F600 has a length() of 2 but is one code point, and the regex dot also counts it as one character.

7. What NIST SP 800-63B-4 Recommends

NIST SP 800-63B-4, published in July 2025, is the US government guideline for authentication, and many companies follow it. Its password rules (section 3.1.1.2) differ from the classic “upper, lower, digit, symbol” policy:

  • Passwords used as the only authentication factor must be at least 15 characters long; passwords used only together with a second factor (MFA) must be at least 8.
  • Verifiers should allow a maximum length of at least 64 characters, and should accept all printing ASCII characters, the space and Unicode characters.
  • Verifiers shall not impose other composition rules, such as requiring a mix of character types.
  • Every new password shall be compared against a blocklist of common, expected or breached passwords, including the service name and the user name.
  • Verifiers shall not require periodic password changes, but shall force a change when there is evidence of compromise.

Its appendix on password strength explains why. Studies of breached password databases show that composition rules add less strength than expected and make passwords harder to remember. Users also respond in predictable ways: someone who would pick “password” picks “Password1!” when a digit, an uppercase letter and a symbol are required.

A NIST-style check needs a regex only for length, and the rest is plain Java:

Pattern.compile("^.{15,64}$").matcher("green apple tree").matches();   // true

nistViolations("green apple tree", "lokesh");       // []
nistViolations("apple tree", "lokesh");             // [must be 15 to 64 characters long]
nistViolations("PASSWORD123", "lokesh");            // [must be 15 to 64 characters long, is a commonly used password]
nistViolations("lokesh likes apples", "lokesh");    // [must not contain the user name]

The blocklist in the example has four entries to keep it readable. A real one contains many thousands of breached passwords. Whatever the rules, the password itself is stored only as a slow, salted hash; the article on secure password hashing covers PBKDF2 and bcrypt.

8. A Note on Performance

Each (?=.*X) first runs .* to the end of the input and then backtracks until it finds X. For a password of at most 64 characters, that cost is too small to measure. Two habits keep it that way:

  • Check the maximum length before running the regex, or keep {8,64} in the pattern, so a 1 MB request body is never scanned by four lookaheads.
  • Optionally, write each lookahead with a negated class, such as (?=[^a-z]*[a-z]) or (?=\D*\d), which moves forward to the first match without backtracking.
Pattern STRONG_NEGATED = Pattern.compile(
    "^(?=[^a-z]*[a-z])(?=[^A-Z]*[A-Z])(?=\\D*\\d)(?=[a-zA-Z0-9]*[^a-zA-Z0-9]).{8,64}$");

The tests in the example project check that STRONG and STRONG_NEGATED give the same result for every sample password.

9. Password Validator Code and Tests

The project on GitHub contains PasswordPolicy, the regexes from this article and the NIST-style check. The main class prints every result shown above, including the step-by-step table from section 3, and the JUnit 6 tests assert them. The project targets Java 25 and builds with Maven.

mvn -q compile exec:java
mvn test

10. Conclusion

Lookaheads let one regex check several independent rules: each (?=.*X) requires X somewhere in the password, each (?!.*X) forbids it, and a final .{min,max} checks the length. Testing each rule separately turns the same lookaheads into clear error messages. Before adding composition rules, though, it is worth reading NIST SP 800-63B-4: a 15-character minimum, a 64-character or higher maximum and a blocklist protect accounts better than “one symbol and one digit”.

11. References

The NIST guideline is the source for section 7; the JavaDocs cover lookaheads, inline flags and quoting.

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi, you have an error in you regex at line 42, there should be patternBuilder.append("(?=.*\d)");

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.