Customizing the Patterns
The default patterns cover the most common password structures, but every application has words of its own worth blocking, and some have structures of their own. This guide covers the building blocks that patterns are made of, the predefined word lists and decorations, and how to extend or replace the default patterns.
Two layers
A check is driven by an IPasswordMatchersProvider, which returns the top-level patterns to try for a password. Default builds its patterns from the word lists and decorations in CommonMatchers and the contextual subjects of the current check. Both layers are made of PasswordMatcher objects created with the factory methods described first, so everything below composes the same way.
Building Blocks
The static factory methods on PasswordMatcher create every kind of matcher. Matchers are immutable and safe to build once and share, including from static fields.
Text
Text matches a word or phrase. Matching ignores case and, by default, accepts common character substitutions (4 or @ for a, 0 for o, 5 or $ for s and so on), consecutive repeats of the word, and a single separator character after each occurrence:
var matcher = PasswordMatcher.Text("hunter");
// Matches "hunter", "HUNTER", "hun73r", "hunterhunter", "hunter.hunter", "hunter-"
Pass a PasswordMatchType to control how the match is reported, and turn off substitutions, repeats or the trailing separator when they do not make sense for the word. Substitutions should be turned off for values that are themselves digits or symbols, since 69 should not also match g9, and for words of one or two letters, since a lone 1 or @ is far more likely to be a decoration than the word i or a:
var matcher = PasswordMatcher.Text("69", checkSubstitutions: false);
Sequence
Sequence matches its matchers one after another, in order. This is how phrases and sentence-like patterns are built:
var matcher = PasswordMatcher.Sequence([
PasswordMatcher.Text("let"),
PasswordMatcher.Text("me"),
PasswordMatcher.Text("in"),
]);
// Matches "letmein", "let me in", "l3tm31n"
Any
Any matches whichever of its matchers matches, trying them in order. It is how a word list becomes a single matcher:
var seasons = PasswordMatcher.Any([
PasswordMatcher.Text("spring"),
PasswordMatcher.Text("summer"),
PasswordMatcher.Text("autumn"),
PasswordMatcher.Text("fall"),
PasswordMatcher.Text("winter"),
]);
Optional
Optional matches its matcher or nothing at all. Use it inside a sequence to make a part of a pattern optional. The absent case is tried first, so a sequence of Text("pass"), Optional(Text("word")) reports password as the two words pass and word only when the shorter match does not lead to a complete match of the whole password:
var matcher = PasswordMatcher.Sequence([
PasswordMatcher.Optional(PasswordMatcher.Text("the")),
seasons,
PasswordMatcher.Optional(CommonMatchers.YearMatcher),
]);
// Matches "summer", "thesummer", "summer2024", "the.winter.1999"
Repeat
Repeat matches its matcher a number of times in a row, within a range. Each occurrence is matched independently, so repeating an alternative lets a different option match each time, which is how "up to four words from this list, in any order" is expressed:
var matcher = PasswordMatcher.Repeat(seasons, 1, 4);
// Matches "summer", "summerwinter", "fall.spring.summer"; not "summerfallwinterspringsummer" (five occurrences)
Fewer occurrences are tried before more, so the explanation with the fewest parts is preferred.
Permutations
Permutations matches its matchers in any order, using each at most once. By default any non-empty subset of them matches; pass mustMatchAllMatchers: true to require every one:
var matcher = PasswordMatcher.Permutations([
PasswordMatcher.Text("red", matchRepeats: false),
PasswordMatcher.Text("green", matchRepeats: false),
PasswordMatcher.Text("blue", matchRepeats: false),
]);
// Matches "red", "bluered", "greenbluered"; not "redred" or "redbluered"
Each matcher must be a distinct instance, since the permutation tracks which ones have been used by identity. Use it when "each at most once" is the actual rule, as the default contextual subject provider does for the words of a name; use Repeat over an Any when parts may recur.
CharSequence
CharSequence matches runs of at least three characters that are adjacent on a keyboard or keypad or consecutive in the alphabet, in either direction and, for keyboard sequences, with or without the shift key held. Combine the CharSequenceTypes flags to choose which:
| Type | Examples |
|---|---|
| KeyboardRow | qwerty, asdfgh, 12345, ytrewq, !@#$ |
| KeyboardDiagonal | 1qaz, 2wsx, zaq1, 4esz |
| KeyboardZigzag | 1q2w3e4r, q1w2e3r4 |
| NumericKeypad | 789, 741, 7410, 2580, 159, 753 |
| Alphabet | abc, abcdef, zyx |
var matcher = PasswordMatcher.CharSequence(CharSequenceTypes.KeyboardRow | CharSequenceTypes.KeyboardDiagonal);
The longest sequence is tried first, so qwertyuiop is reported as one sequence rather than several short ones. All selects every type.
FixedDigits
FixedDigits matches an exact number of digits, optionally restricted to a range of values. YearMatcher is a four-digit matcher restricted to 1900 through ten years from now. A two-digit matcher for day or month numbers would be:
var dayOrMonth = PasswordMatcher.FixedDigits(2, minValue: 1, maxValue: 31, PasswordMatchType.Common);
RepeatedChars
RepeatedChars matches a run of the same character, optionally limited to a range of lengths and to characters that pass a filter:
var symbols = PasswordMatcher.RepeatedChars(1, 8, c => c is '!' or '$' or '?' or '#' or '*');
// Matches "!", "!!!", "####"; not "!?" (different characters) or "!!!!!!!!!" (too long)
RepeatedGroups
RepeatedGroups matches a short group of characters repeated two or more times, which neither a run nor a sequence can describe. Groups consisting of a single repeated character are left to RepeatedChars:
var groups = PasswordMatcher.RepeatedGroups(2, 3, 2, null);
// Matches "121212", "abcabc", "1!1!"; not "1213" (no repeat) or "1111" (a run, not a group)
AnyChars
AnyChars matches a short run of arbitrary characters, optionally limited to characters that pass a filter. It is the one value matcher that tries its shortest match first, so that it consumes as few arbitrary characters as possible and leaves the rest to structured matchers:
var digits = PasswordMatcher.AnyChars(1, 4, char.IsDigit);
// Matches "4", "47", "4792"; not "47921" (too long) or "47a"
Use it sparingly. A few arbitrary characters are a decoration an attacker's rules generate; more than a few are entropy, and matching them would reject strong passwords.
Composition rules
A few constraints keep patterns well-formed, and the factory methods throw ArgumentException when they are violated:
- Any, Repeat and Permutations cannot directly contain optional matchers. Wrap the optional part in a sequence instead.
- Sequence needs at least one matcher that is not optional.
- Permutations needs distinct matcher instances and supports up to MaxMatchers of them.
Order matters in two ways. The matchers returned by a provider are tried in order and the first complete match is reported, so put the patterns whose explanation is most useful first. Within a pattern, a value matcher tries its longest match first (except AnyChars, which tries its shortest), so a keyboard sequence or a run of repeated characters is reported whole rather than split.
The Common Word Lists and Decorations
CommonMatchers exposes the word lists that the default patterns are built from, grouped by the role a word plays in a sentence, along with the decoration matchers. Each is a matcher you can use in your own patterns:
| Matcher | Role | Examples |
|---|---|---|
| GeneralSubjectMatcher | The nouns and pronouns that passwords are made of | password, admin, i, you, dragon, ninja, letmein |
| AdjectiveMatcher | Adjectives | best, cool, bad, sexy |
| VerbMatcher | Verbs | love, hate, want, login |
| SubjectDeterminerMatcher | Words before a subject | the, my, your, this |
| SubjectConjunctionMatcher | Words joining two subjects | and, or, to, in, into |
| SubjectToSubjectCopularVerbMatcher | "is", "am" and "are" between subjects | i am admin |
| SubjectToAdjectiveCopularVerbMatcher | "is", "am" and "are" before an adjective, with optional "so" and "the", "a" or "an" | is so the best |
| AdjectiveToSubjectConjunctionMatcher | Words joining an adjective to a subject | cool as, cool like |
| VerbSuffixMatcher | "to" after a verb | want to, login to |
| AllWordsMatcher | Any word from any of the lists above | |
| YearMatcher | Four-digit years from 1900 to ten years from now | 1975, 2024 |
| AllCharSequencesMatcher | Every type of character sequence | qwerty, 1qaz, 1q2w3e, 7410, abc |
| DigitRunMatcher | Two or more of the same digit | 11, 000000 |
| SymbolRunMatcher | Two or more of the same symbol | !!, #### |
| DigitOrSymbolGroupMatcher | A repeated group of two or three digits or symbols | 121212, 1!1! |
| StructuredDecorationMatcher | Any one of the five decorations above | |
| DecorationRunMatcher | One to six structured decorations in any order | 2024!!, 1234qwerty, 123123 |
| ShortDecorationMatcher | One to four arbitrary digits or symbols other than separators | 47, #1, 4792 |
| JoinerMatcher | A single digit or symbol joining words | the ! in ninja!admin |
| DecorationMatcher | What commonly precedes or follows the words: a decoration run with at most one short decoration | 1, #1, 2024!!, @123, !!!2024 |
See The Default Patterns for the complete lists and the patterns they are combined in.
Extending the Default Provider
Derive from PasswordMatchersProvider and override GetMatchers to add patterns before or after the defaults. The base implementation returns the default patterns for the given subjects, so adding to them is a matter of yielding your own alongside:
public class MyMatchersProvider : PasswordMatchersProvider
{
private static readonly PasswordMatcher ProductNames = PasswordMatcher.Any([
PasswordMatcher.Text("widgetpro"),
PasswordMatcher.Text("widgetlite"),
PasswordMatcher.Sequence([PasswordMatcher.Text("widget"), PasswordMatcher.Text("pro")]),
PasswordMatcher.Sequence([PasswordMatcher.Text("widget"), PasswordMatcher.Text("lite")]),
]);
// Pattern: [decoration] product [product] [decoration]
private static readonly PasswordMatcher ProductPattern = PasswordMatcher.Sequence([
PasswordMatcher.Optional(CommonMatchers.DecorationMatcher),
PasswordMatcher.Repeat(ProductNames, 1, 2),
PasswordMatcher.Optional(CommonMatchers.DecorationMatcher),
]);
public MyMatchersProvider() : base(ContextualSubjectMatcherProvider.Default) { }
public override IEnumerable<PasswordMatcher> GetMatchers(IEnumerable<ContextualSubject>? subjects)
{
yield return ProductPattern;
foreach (var matcher in base.GetMatchers(subjects))
yield return matcher;
}
}
Then build an evaluator around it, and use that evaluator everywhere instead of Default:
var evaluator = new PasswordMatchEvaluator(new MyMatchersProvider());
Adding words to the default patterns
Words that should take part in the sentence-like default patterns, rather than only in a pattern of their own, are best added as contextual subjects, since the default provider treats subjects and common words identically. Register them once as the provider's application subjects, with General as the type for plain words:
var provider = new PasswordMatchersProvider(ContextualSubjectMatcherProvider.Default, [
new("Widget Pro"),
new("Widget Lite"),
new("Acme Widgets", ContextualSubjectType.Company),
]);
var evaluator = new PasswordMatchEvaluator(provider);
This makes iamthewidgetproadmin match the same way iamthebusinessadmin does for a company subject containing "business", for every user, without changing any pattern. Deriving from the provider, as above, is only needed when the structure of the patterns should change.
Replacing the Provider
Implement IPasswordMatchersProvider directly when the default patterns are not what you want at all, for example for a non-English user base. The single GetMatchers method returns the patterns to try for a password, in order, given the contextual subjects for the check. You can still use the CommonMatchers word lists and decorations and Default to turn subjects into matchers:
public class SimpleMatchersProvider : IPasswordMatchersProvider
{
public IEnumerable<PasswordMatcher> GetMatchers(IEnumerable<ContextualSubject>? subjects)
{
var subjectMatchers = subjects?
.Select(ContextualSubjectMatcherProvider.Default.GetMatcher)
.OfType<PasswordMatcher>()
.ToList() ?? [];
var word = PasswordMatcher.Any([..subjectMatchers, CommonMatchers.GeneralSubjectMatcher]);
// Pattern: [decoration] word [word] [decoration]
yield return PasswordMatcher.Sequence([
PasswordMatcher.Optional(CommonMatchers.DecorationMatcher),
PasswordMatcher.Repeat(word, 1, 2),
PasswordMatcher.Optional(CommonMatchers.DecorationMatcher),
]);
}
}
Note
Matchers that depend on the contextual subjects have to be built per check, as above, because the subjects differ for every user. Matchers that do not depend on them should be built once and stored in static fields, as the default provider does with the common word lists and decorations.
Next Steps
Continue with these related articles:
- Writing Custom Matchers - When the building blocks are not enough, write a matcher of your own.
- The Default Patterns - The complete word lists, the decorations and the default patterns.
- How Matching Works - Why ordering works the way it does.