Federated identity management using Active Directory Federation Services
Bug report - "Not a valid Win32 FileTime") on Forms Authentication logins
AD FS defect report: ArgumentOutOfRangeException ("Not a valid Win32 FileTime") on Forms Authentication logins
AD FS's Forms Authentication login path can throw an unhandled System.ArgumentOutOfRangeException: Not a valid Win32 FileTime on a correct username and password, failing the login with a generic "An error occurred" page. The failure is intermittent — roughly 9 in 10 login attempts fail, about 1 in 10 succeed, with no discernible pattern (not tied to server uptime, reboots, account state, or any AD configuration). The root cause is a struct-type confusion in AD FS's own code: it requests a Network-type Windows logon for password validation, but then reads the result as if it were an Interactive-type logon profile — misinterpreting unrelated session-key bytes as a password-expiry timestamp.
Environment
- Windows Server 2022 (build 10.0.20348), patched to ~December 2025 (KB5068786 and prior).
- AD FS federation server role, installed on the domain controller itself (single-DC lab/test environment — Microsoft's reference topology recommends a separate member server, but this is documented as a performance recommendation only, not a supported/unsupported distinction, and is explicitly endorsed for environments under 1,000 users).
- Reproduces with both a fresh, fully-healthy test account (no
DONT_EXPIRE_PASSWD, validmsDS-UserPasswordExpiryTimeComputed, cleanuserAccountControl) and the built-inAdministratoraccount. - Reproduces via AD FS's IdP-initiated sign-on page (
/adfs/ls/idpinitiatedsignon.aspx) directly, with no relying-party application involved — ruling out any relying-party/client contribution.
Repro steps
- Ensure the client's browser is not eligible for Windows Integrated Authentication against this AD FS instance (e.g. its User-Agent isn't in
Get-AdfsProperties'sWIASupportedUserAgentslist — this is the default state for any modern Chromium-based browser, since the shipped default list only recognizes Internet Explorer/Trident and pre-2020 "Legacy" Edge). This routes the login to Forms Authentication. - Enable the IdP-initiated sign-on page if not already enabled:
Set-AdfsProperties -EnableIdPInitiatedSignonPage $true(or use any relying party's Forms Authentication login). - Navigate to
https://<adfs-host>/adfs/ls/idpinitiatedsignon.aspxin a fresh/private browser window. - Enter a valid domain username and password for any account and submit.
- Observe: the login fails on an unhandled
ArgumentOutOfRangeException, roughly 9 times out of 10 attempts (empirically, over ~44 sampled logins). Retry with the identical credentials; the outcome varies attempt to attempt with no discernible trigger.
Observed error
AD FS Admin log (Event ID 364, and Event ID 111 for the request), and the AD FS Tracing/Debug log (Event ID 168, "This is a bug, please report"):
System.ArgumentOutOfRangeException: Not a valid Win32 FileTime. Parameter name: fileTime
at System.DateTime.FromFileTimeUtc(Int64 fileTime)
at Microsoft.IdentityServer.Tokens.LsaLogonUserHelper.GetPasswordExpiryDetails(...)
at Microsoft.IdentityServer.Tokens.LsaLogonUserHelper.GetLsaLogonUser(...)
at Microsoft.IdentityServer.Service.LocalAccountStores.ActiveDirectory.ActiveDirectoryCpTrustStore.ValidateUser(IAuthenticationContext context)
Root cause
Confirmed via live native + managed debugging (WinDbg/cdb with SOS, procdump) directly on the AD FS process (Microsoft.IdentityServer.ServiceHost.exe) during both successful and failing login attempts.
-
ActiveDirectoryCpTrustStore.ValidateUsercallsLsaLogonUserHelper.GetLsaLogonUser, which P/Invokes the Win32LsaLogonUserAPI withLogonType = SECURITY_LOGON_TYPE.Network(value3), passed as a direct, literal argument in AD FS's own managed code. This is not an SSPI/Negotiate downgrade or an artifact of the caller — it's what AD FS's Forms Authentication validation path requests. (SECURITY_LOGON_TYPE,ntsecapi.h: https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-security_logon_type) - Because the logon type is
Network,LsaLogonUserreturns a profile buffer of typeMSV1_0_LM20_LOGON_PROFILE(MessageType = MsV1_0LM20LogonProfile, value3) — documented by Microsoft as "information about a network logon session." Its fields areMessageType, KickOffTime, LogoffTime, UserFlags, UserSessionKey[...], LogonDomainName, LanmanSessionKey[...], LogonServer, UserParameters. This struct has no password-expiry fields. (https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ns-ntsecapi-msv1_0_lm20_logon_profile) -
GetPasswordExpiryDetailsunconditionally reads offset +0x30 from the returned buffer, expecting thePasswordMustChangefield of the different,MSV1_0_INTERACTIVE_PROFILEstruct (MessageType = MsV1_0InteractiveProfile, value2), where +0x30 is indeed the correct byte offset forPasswordMustChange(MessageType(4)+LogonCount(2)+BadPasswordCount(2)=8, thenLogonTime/LogoffTime/KickOffTime/PasswordLastSet/PasswordCanChangeat 8 bytes each = 0x08/0x10/0x18/0x20/0x28, landingPasswordMustChangeat exactly 0x30). (https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ns-ntsecapi-msv1_0_interactive_profile) -
GetPasswordExpiryDetailsnever checksMessageTypebefore reading this offset. Since the buffer it actually received is the smallerMSV1_0_LM20_LOGON_PROFILE(no expiry fields), offset +0x30 in that struct falls insideUserSessionKey— raw cryptographic session-key bytes, not a timestamp. These bytes are read asnextPasswordChangeand passed toDateTime.FromFileTimeUtc(Int64). - Session-key material is cryptographically derived and looks effectively random on every login.
FromFileTimeUtcaccepts only[0, DateTime.MaxValue.ToFileTimeUtc()](≈14.4% of the 64-bit space) — so it throws whenever the session-key bytes happen to fall outside that range, which is empirically ~91% of attempts (38 of 44 sampled failing-state logins; the ~9% "successes" are simply attempts where the session-key bytes coincidentally decoded as an in-range date).
In short: GetPasswordExpiryDetails requests a Network logon (correct for password-only validation) but reads its result as if it always gets an Interactive logon profile back (incorrect) — a missing type check before a fixed-offset read, not a corrupted or miscomputed value.
Evidence this is confined to Forms Authentication
A downstream, later step in the same login flow — LsaLogonUserHelper.GetS4ULsaLogonUser (used for SSO session-token refresh, S4U) — was also captured live and always receives a genuine MSV1_0_INTERACTIVE_PROFILE with a valid password-expiry date, regardless of the ValidateUser call's outcome. Only the initial ValidateUser/GetLsaLogonUser call is affected.
Separately, we confirmed that Windows Integrated Authentication logins never exercise this code path at all and are unaffected: once a client's browser was made eligible for WIA (by adding a matching pattern to WIASupportedUserAgents), the identical login succeeded cleanly via a native Negotiate challenge, with no exception. The defect is specific to ActiveDirectoryCpTrustStore.ValidateUser's Forms Authentication password-validation call.
Why this may be rare in practice / undocumented
We could find no existing Microsoft documentation, KB, or community report describing this exact defect (searched: LsaLogonUserHelper/GetLsaLogonUser/ActiveDirectoryCpTrustStore combined with FromFileTimeUtc/ArgumentOutOfRangeException; MSV1_0_INTERACTIVE_PROFILE vs MSV1_0_LM20_LOGON_PROFILE mismatches generally; the relevant AD FS Admin-log Event IDs). The closest match found is an unrelated, unresolved 2015 Microsoft Q&A thread hitting the same call chain with a different symptom (a legitimate password-expiry false positive, not this type-confusion bug): https://learn.microsoft.com/en-us/answers/questions/4882338/adfs-2-0-false-notification-of-password-expiration
A plausible reason this is rarely seen: most real-world AD FS deployments' users authenticate via Windows Integrated Authentication (domain-joined clients with a compatible browser) rather than Forms Authentication with a directly-submitted username/password — so the buggy ValidateUser code path is comparatively rarely exercised outside of extranet access, non-domain-joined clients, or (as in our case) any client whose browser isn't on AD FS's WIASupportedUserAgents allowlist, which by default (at least on the box I was given to work on) only recognizes Internet Explorer/Trident and pre-Chromium "Legacy" Edge — meaning any modern Chromium-based browser (current Edge, Chrome, Firefox) is silently ineligible for WIA out of the box and falls back to this defective Forms Authentication path.
Suggested fix
In LsaLogonUserHelper.GetPasswordExpiryDetails, check the returned profile buffer's MessageType before reading PasswordMustChange/PasswordLastSet/PasswordCanChange — only attempt those reads when MessageType == MsV1_0InteractiveProfile. When the buffer is a different profile type (e.g. MsV1_0LM20LogonProfile, returned for a Network-type LsaLogonUser call), either skip expiry-detail computation entirely or request an Interactive-type logon if expiry data is actually required for this validation step.