A single sign-on application I work on used to calculate Active Directory password expiry by taking pwdLastSet, adding the domain's maxPwdAge, and using the result. That worked until the organization introduced fine-grained password policies, which allow different accounts to follow different expiry rules within the same domain.
Some accounts began receiving warnings for expiry dates that did not apply to them. The calculation was correct for the default policy, but not necessarily for the account being checked.
The fix is to stop calculating the date. Active Directory exposes msDS-UserPasswordExpiryTimeComputed, a constructed attribute that the domain controller calculates when it reads the account. It applies the password policy that actually governs that account, including fine-grained policies.
Application code still needs to request the attribute explicitly. A missing constructed attribute does not necessarily raise an exception, so the search can appear to work while quietly returning no expiry value.
$auth = [System.DirectoryServices.AuthenticationTypes]::Secure -bor
[System.DirectoryServices.AuthenticationTypes]::SecureSocketsLayer
$searchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://dc.example.com:636/DC=example,DC=com")
$searchRoot.AuthenticationType = $auth
$searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=jdoe))"
[void]$searcher.PropertiesToLoad.Add("msDS-UserPasswordExpiryTimeComputed")
$result = $searcher.FindOne()
$raw = [Int64]$result.Properties["msds-userpasswordexpirytimecomputed"][0]
if ($raw -eq [Int64]::MaxValue) { "Password never expires" }
elseif ($raw -eq 0) { "Must change at next logon" }
else { [DateTime]::FromFileTime($raw) }
There are three values worth handling before converting the result to a date. Int64.MaxValue means the password never expires. A value of zero means the user must change the password at the next logon. Any other value is a Windows file time and can be converted with DateTime.FromFileTime.
I compared both calculations against the live directory instead of trusting two test accounts. The comparison covered 4,230 accounts across three domains. The old and new results agreed for 4,041 accounts. Of the 121 accounts governed by fine-grained policies, 45 had been assigned a date that was wrong by 305 days. Another 123 accounts were off by exactly one hour across daylight saving transitions, because the old code used local calendar arithmetic while the domain controller calculated the value from UTC file-time data.
One detail matters before reusing the snippet above in application code. Reading the attribute through a DirectorySearcher returns it as a System.Int64, which is why the cast above works. Reading the same attribute through a DirectoryEntry property returns it as an IADsLargeInteger COM object instead, with separate high and low parts, so that path has to convert the value before comparing it. The reader below goes through a ConvertLargeInteger helper for exactly that reason.
Private Function ReadComputedExpiry(entry As DirectoryEntry) As DateTime?
Const AttributeName As String = "msDS-UserPasswordExpiryTimeComputed"
If Not entry.Properties.Contains(AttributeName) Then
Return Nothing
End If
Dim raw As Long = ConvertLargeInteger(entry.Properties(AttributeName).Value)
If raw = Int64.MaxValue Then
Return DateTime.MaxValue ' password never expires
End If
If raw = 0 Then
Return DateTime.MinValue ' must change at next logon
End If
Return DateTime.FromFileTime(raw)
End Function
The order matters. Check the special values before converting them. Trying to turn the never-expires value into a date throws an exception.
If the constructed attribute is absent, the application should treat that as a directory-read problem rather than as "never expires." A fallback may still be necessary on an authentication path, but it should be visible in logs or metrics so a missing attribute does not look like a valid result.
Dim computed As DateTime? = ReadComputedExpiry(entry)
If computed.HasValue Then
Return computed.Value
End If
If Not lastSet.Equals(DateTime.MinValue) Then
Return lastSet.AddDays(GetDefaultMaxAgeDays())
End If
Return DateTime.MaxValue
The fallback preserves the old behavior when Active Directory cannot provide the computed value. It is no longer the normal path, though, and the application no longer has to guess which password policy applies to each account.
If your code adds days to pwdLastSet, replace that calculation with a read of msDS-UserPasswordExpiryTimeComputed. Ask the domain controller for the answer it already knows.