Ever need to check the complexity of a password that will be used in AD? Ever need to import a list of users or reset their passwords in AD from a predefined list that has been given to you? I have updated my code for my AD Password Complexity check. Check it out.
First off before we can talk about complex passwords, we need to all understand what the criteria of a complex password for an Active Directory account is. It’s quite well defined here – https://technet.microsoft.com/en-us/library/cc786468(v=ws.10).aspx .
As that link exists on the Internet, everyone has their own interpretation and implementation. In my implementation I’ve skipped over looking at the password history. I’m only checking to see that the password is the minimum length and that it follows the complexity rules (if set in AD). Using the link above, here is a summary of the complexity rules:
-
Passwords must not contain the user’s entire samAccountName (Account Name) value or entire displayName (Full Name) value. Both checks are not case sensitive:
-
The samAccountName is checked in its entirety only to determine whether it is part of the password. If the samAccountName is less than three characters long, this check is skipped.
- The displayName is parsed for delimiters: commas, periods, dashes or hyphens, underscores, spaces, pound signs, and tabs. If any of these delimiters are found, the displayName is split and all parsed sections (tokens) are confirmed not to be included in the password. Tokens that are less than three characters in length are ignored, and substrings of the tokens are not checked. For example, the name “Erin M. Hagens” is split into three tokens: “Erin,” “M,” and “Hagens.” Because the second token is only one character long, it is ignored. Therefore, this user could not have a password that included either “erin” or “hagens” as a substring anywhere in the password.
-
-
Passwords must contain characters from three of the following five categories:
- Uppercase characters of European languages (A through Z, with diacritic marks, Greek and Cyrillic characters)
- Lowercase characters of European languages (a through z, sharp-s, with diacritic marks, Greek and Cyrillic characters)
- Base 10 digits (0 through 9)
- Nonalphanumeric characters: ~!@#$%^&*_-+=`|\(){}[]:;”‘<>,.?/
- Any Unicode character that is categorized as an alphabetic character but is not uppercase or lowercase. This includes Unicode characters from Asian languages.
Now that we are on the same page of what AD Complexity means, let’s look at some code I wrote.
A Look at the Code
First off, we need to get the password complexity of the AD. So let’s import the ActiveDirectory module and get the password Default Domain Policy setting.
Code Preparation
Import-Module ActiveDirectory
To make this code re-usable, I’ll create a function called Test-PasswordForDomain. As you’ve seen from the complexity rules above, I’m going to pass in some variables, but only one is manadory, the password! The other 3 parameters are optional, but will help increase the chances of your password passing the AD complexity rules. If you look above, you’ll see that part of the complexity check is to ensure that the password does not contain the SamAccountName or any part of the display name in the password. If you don’t have those, we can check the other complexity rules, but again, it can’t fully ensure that Active Directory will accept your password.
Create a Function
Function Test-PasswordForDomain { Param ( [Parameter(Mandatory=$true)][string]$Password, [Parameter(Mandatory=$false)][string]$AccountSamAccountName = "", [Parameter(Mandatory=$false)][string]$AccountDisplayName, [Microsoft.ActiveDirectory.Management.ADEntity]$PasswordPolicy = (Get-ADDefaultDomainPasswordPolicy -ErrorAction SilentlyContinue) ) <# Rest of the code in the blog goes here #> return $false } return $false }
Below are the code snippets that we’ll copy into the function. Then we’ll have the correct tests and make the base function comply as well as we can do the domain policy.
Check 1 – Minimum Password Length
This check is simple. Are we meeting the minimum password length?
If ($Password.Length -lt $PasswordPolicy.MinPasswordLength) { return $false }
Check 2 – Is the SAM Account Name part of the Password
Again, simple check, that is, if we passed it into the function. If we didn’t, we just skip the check.
if (($AccountSamAccountName) -and ($Password -match "$AccountSamAccountName")) { return $false }
Check 3 – Is the Display Name part of the Password
Now this is still a simple check, but you have to read the text above that Microsoft provides. It says if ANY PART of the display name that is split by the characters below, the password should fail the complexity rules.
if ($AccountDisplayName) { $tokens = $AccountDisplayName.Split(",.-,_ #`t") foreach ($token in $tokens) { if (($token) -and ($Password -match "$token")) { return $false } } }
Check 4 – Complexity Rules for the Password
The code makes sense, but we need to make sure that we satisfy as many combinations of the complexity rules defined above.
- First cmatch “[A-Z\p{Lu}\s]” – Match any UPPERCASE characters A-Z and spaces, but the funky \p{Lu} also means any UNICODE (accented characters) that are also upper case
- Second cmatch “[a-z\p{Ll}\s]” – Match any lowercase characters a-z and spaces (Yes I know I check twice for spaces), but the funky \p{Ll} also means any UNICODE (accented characters) that are also lower case
- [\d] – We found a numerical digit [0-9]
- [^\w] – \w means [A-Za-z _]. So with a ^ in the front it’s basically saying, any other character. Example: !@#$%^&*()[];
Pretty simple right?
-
Looks like we fulfilled these Microsoft requirements
- Uppercase characters of European languages (A through Z, with diacritic marks, Greek and Cyrillic characters)
- Lowercase characters of European languages (a through z, sharp-s, with diacritic marks, Greek and Cyrillic characters)
- Base 10 digits (0 through 9)
- Nonalphanumeric characters: ~!@#$%^&*_-+=`|\(){}[]:;”‘<>,.?/
- Any Unicode character that is categorized as an alphabetic character but is not uppercase or lowercase. This includes Unicode characters from Asian languages. Honestly, I did not test this, it may work with the \p{Ll or Lu} test.
if ($PasswordPolicy.ComplexityEnabled -eq $true) { If ( ($Password -cmatch "[A-Z\p{Lu}\s]") ` -and ($Password -cmatch "[a-z\p{Ll}\s]") ` -and ($Password -match "[\d]") ` -and ($Password -match "[^\w]") ) { return $true } } else { return $false }
Testing the Passwords
Now we need an example to work with, so let’s set a few different passwords
Now that the function is built, here are a few of the test scenarios that I used to run through the function.
Test-PasswordForDomain "a1B@" # Fail - Valid Character Set, but too short Test-PasswordForDomain "a1B@password" # Pass - Valid Character Set Test-PasswordForDomain "ß!1B@PASSWORD" # Pass - Valid Character Set even with unicode characters (Used a lowercase ß) Test-PasswordForDomain "ẞ!1B@PASSWORD" # Fail - No lowercase (Used an uppercase ẞ) Test-PasswordForDomain "!äÄöäöAl5lajrnäöäö1" -AccountDisplayName "Allan. Rafuse, Jr" # Fail - Contains JR (case insensitive) which is part of the split apart Display Name Test-PasswordForDomain "!äÄöäöAl5lanäöäö1" -AccountDisplayName "Allan. Rafuse, Jr" # Pass - JR (case insensitive) removed from the password
Enjoy!
Technically you don’t need the IF in testing the whether all regexec match or not, it will work same if you just put
return = $(($Password -cmatch “[A-Z\p{Lu}\s]”) `
-and ($Password -cmatch “[a-z\p{Ll}\s]”) `
-and ($Password -match “[\d]”) `
-and ($Password -match “[^\w]”) )
on the other hand, your code test for all 4 rules, so password that match only 3 categories will fail the test while it is acceptable to AD. the easy way got to overcome this will be to + instead of “and” and check if the sum is more than 3 ($true = 1, $false = 0), so the code may look like this:
If (
($Password -cmatch “[A-Z\p{Lu}\s]”) `
+ ($Password -cmatch “[a-z\p{Ll}\s]”) `
+ ($Password -match “[\d]”) `
+ ($Password -match “[^\w]”) -ge 3
) {
return $true
}
} else {
return $false
}
@Lukas – You are 100% correct about not requiring the if statements. I usually add the few lines of extra code as debugging RegEx statements (and others) can sometimes be tedious. Creating a full body for the If/Else statement allows me to then in production code or while debugging, write out to the console or error files. Thanks for your comment.
Hi Allan,
It seems your code is accepting the password in clear text while testing it for the pre-defined password complexity rules.
However, in reality AD stores user’s password as one-way hash. Is there a way to determine if a given AD user’s password (which is stored as one-way hash) matching a pre-defined password complexity rule.
Please advise.
Thanks,
Rahul
Microsoft only accept specific ‘special characters’. [^\w] will match any, including spaces, EUR or British Pound.
This is what I’d suggest, building on the 3-out-of-4 comment from Lukas – note in addition to escaping the Regex you need to replace the ‘]’ because otherwise the ‘]’ will close the regex in the if statement.
$permittedSpecialChars = [Regex]::Escape(‘~!@#$%^&*_+=`|(){}[]:;””,.?/’) -replace ‘]’,’\]’
If (
($Password -cmatch ‘[A-Z\p{Lu}]’) `
+ ($Password -cmatch ‘[a-z\p{Ll}]’) `
+ ($Password -match ‘\d’) `
+ ($Password -match “[$permittedSpecialChars]”) -ge 3 )
{
return $true
}
else
{
return $false
}
& extra tests 🙂
Test-PasswordComplexity ‘Bad password’ -AccountDisplayName ‘azure’ # Fail – spaces don’t count as special chars
Test-PasswordComplexity ‘© or Copyleft’ # Fail – copyright sign not treated as an acceptable special char
Test-PasswordComplexity ‘We are in the £’ # Fail – pound sign not special enough either!
Test-PasswordComplexity “I ‘quote’ thee” # Pass – quotes are good
Test-PasswordComplexity “I put a ] on the wall” # Pass – ] is good
I like this but how do we get it to check password history. I have many people that struggle with creating password but when they fail they struggle to know why. This script will clarify that.
Thanks
Any final version that does actually work?
This parameter is giving an error.
Param : The term ‘Param’ is not recognized as the name of a cmdlet, function, script file, or operable program