Bug report - "Not a valid Win32 FileTime") on Forms Authentication logins

Trond Nilsen 0 Reputation points
2026-07-13T23:36:22.37+00:00

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, valid msDS-UserPasswordExpiryTimeComputed, clean userAccountControl) and the built-in Administrator account.
  • 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

  1. 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's WIASupportedUserAgents list — 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.
  2. Enable the IdP-initiated sign-on page if not already enabled: Set-AdfsProperties -EnableIdPInitiatedSignonPage $true (or use any relying party's Forms Authentication login).
  3. Navigate to https://<adfs-host>/adfs/ls/idpinitiatedsignon.aspx in a fresh/private browser window.
  4. Enter a valid domain username and password for any account and submit.
  5. 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.

  1. ActiveDirectoryCpTrustStore.ValidateUser calls LsaLogonUserHelper.GetLsaLogonUser, which P/Invokes the Win32 LsaLogonUser API with LogonType = SECURITY_LOGON_TYPE.Network (value 3), 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)
  2. Because the logon type is Network, LsaLogonUser returns a profile buffer of type MSV1_0_LM20_LOGON_PROFILE (MessageType = MsV1_0LM20LogonProfile, value 3) — documented by Microsoft as "information about a network logon session." Its fields are MessageType, 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)
  3. GetPasswordExpiryDetails unconditionally reads offset +0x30 from the returned buffer, expecting the PasswordMustChange field of the different, MSV1_0_INTERACTIVE_PROFILE struct (MessageType = MsV1_0InteractiveProfile, value 2), where +0x30 is indeed the correct byte offset for PasswordMustChange (MessageType(4)+LogonCount(2)+BadPasswordCount(2)=8, then LogonTime/LogoffTime/KickOffTime/PasswordLastSet/PasswordCanChange at 8 bytes each = 0x08/0x10/0x18/0x20/0x28, landing PasswordMustChange at exactly 0x30). (https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ns-ntsecapi-msv1_0_interactive_profile)
  4. GetPasswordExpiryDetails never checks MessageType before reading this offset. Since the buffer it actually received is the smaller MSV1_0_LM20_LOGON_PROFILE (no expiry fields), offset +0x30 in that struct falls inside UserSessionKey — raw cryptographic session-key bytes, not a timestamp. These bytes are read as nextPasswordChange and passed to DateTime.FromFileTimeUtc(Int64).
  5. Session-key material is cryptographically derived and looks effectively random on every login. FromFileTimeUtc accepts 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.

Microsoft Security | Active Directory Federation Services
0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.