Active Directory Authentication and access

Account inventory and stale accounts

Read logon attributes correctly, find unusual account flags, and create review lists without deleting accounts.

Collect a useful account baseline

Run from an AD PowerShell session with access to the target domain. Keep an explicit DC in the collection when comparing results:

Get-ADUser -Filter * -Server dc1.contoso.com `
    -Properties Enabled, PasswordLastSet, LastLogonDate, whenCreated,
        userAccountControl, ServicePrincipalName |
    Select-Object SamAccountName, Enabled, PasswordLastSet, LastLogonDate,
        whenCreated, userAccountControl, ServicePrincipalName |
    Export-Csv .\account-review.csv -NoTypeInformation

This writes a local report containing directory information, so store and retain it accordingly. Use a defined -SearchBase for a large environment or a review of one business unit. There’s usually no need to retrieve every property.

AD Administrative Center is also useful here: expand its search criteria and open Windows PowerShell History to see the commands behind a query. It is a convenient way to turn an ad hoc question into a repeatable collection.

Last logon has several meanings

Attribute or property What it tells you Important limit
lastLogon The last logon time recorded by the queried DC Not replicated; query all relevant DCs for the maximum
lastLogonTimestamp A replicated indicator intended for inactivity searches Updated on an interval, so it can lag actual use
LastLogonDate The AD PowerShell module’s readable conversion of lastLogonTimestamp It has the same lag; it is not a separate authoritative log
logonCount A logon count recorded by a particular DC Not a forest-wide lifetime usage count

The normal lastLogonTimestamp update interval is roughly 9-14 days under its default configuration. Check msDS-LogonTimeSyncInterval if a custom interval matters to the review. Microsoft’s schema definition describes the replicated timestamp’s update rule.

A DC checks that update threshold during qualifying activity. There isn’t a background task stamping every account every fortnight. If the latest activity doesn’t warrant a write, the old date stays; if it does, replication adds its own delay. The AD engineering explanation of the timestamp explains why it works this way for inactivity reporting.

For one user, query each DC and retain the per-DC values:

$logons = foreach ($dc in Get-ADDomainController -Filter *) {
    $user = Get-ADUser -Identity alice -Server $dc.HostName `
        -Properties lastLogon -ErrorAction Stop
    [pscustomobject]@{
        DC = $dc.HostName
        LastLogonUtc = if ($user.lastLogon -gt 0) {
            [DateTime]::FromFileTimeUtc([int64]$user.lastLogon)
        } else { $null }
    }
}
$logons | Sort-Object LastLogonUtc -Descending

If a DC is unreachable, the collection is incomplete. Even a complete collection cannot recover a value from a retired DC or replace the authentication logs needed for an incident timeline.

Recent activity is not proof of a person signing in

Services can request S4U authentication on a user’s behalf, and S4U2Self can update lastLogonTimestamp. A recent replicated value can therefore reflect an application action involving that identity rather than a new interactive sign-in by its owner. Conversely, a workstation unlocked offline does not establish new domain activity.

Incorrect future time on a DC can also stamp a future value, which can interfere with subsequent inactivity searches after the clock is fixed. Identify and correct the time source first; do not directly overwrite system-maintained timestamps as housekeeping. Microsoft’s future-timestamp investigation covers both the S4U caveat and the supported repair conditions.

Read the domain’s explicit interval alongside the collection:

$domain = Get-ADDomain -Server dc1.contoso.com
Get-ADObject -Server dc1.contoso.com -Identity $domain.DistinguishedName `
    -Properties msDS-LogonTimeSyncInterval |
    Select-Object DistinguishedName, msDS-LogonTimeSyncInterval

If the value is absent, the documented default applies; it doesn’t mean a zero-day threshold. Save that result with the DC name and report so the next person knows how to read the dates.

Build a stale-account review list

$cutoff = (Get-Date).AddDays(-90)
Get-ADUser -Filter 'Enabled -eq $true' `
    -Properties LastLogonDate, PasswordLastSet, whenCreated |
    Where-Object {
        $_.LastLogonDate -and $_.LastLogonDate -lt $cutoff
    } |
    Select-Object SamAccountName, LastLogonDate, PasswordLastSet, whenCreated

Ninety days is an example review threshold. Choose it against the organisation’s actual usage and retention requirements. Review accounts with no timestamp separately: they include never-used accounts, new accounts, and cases where the chosen evidence does not capture the account’s use.

An old logon date is a starting point. Check the owner, scheduled tasks, services, leave and application dependencies. Agree on a recovery plan before disabling, moving or deleting the account. Keeping a disabled account for an agreed retention period can make a mistake easier to reverse.

Use a separate list for enabled accounts with no replicated logon time, and compare their creation date before treating them as old:

Get-ADUser -Filter 'Enabled -eq $true' -Server dc1.contoso.com `
    -Properties LastLogonDate, whenCreated, PasswordLastSet, ServicePrincipalName |
    Where-Object { -not $_.LastLogonDate } |
    Select-Object SamAccountName, whenCreated, PasswordLastSet, ServicePrincipalName

An account created yesterday and one created ten years ago can both appear here. Check a service principal or retained migration account with its application owner. Appearing in this list doesn’t make an account safe to remove.

UserAccountControl is a bit field

Comparing userAccountControl to one complete numeric value misses accounts with additional flags. Use the AD properties or LDAP’s bitwise matching rule:

# Reversible password storage allowed: bit 0x80 (128)
Get-ADUser -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=128)'

# Kerberos preauthentication not required: bit 0x400000 (4194304)
Get-ADUser -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=4194304)'

# Password not required: inspect the flag, not an assumed blank password
Get-ADUser -Filter 'PasswordNotRequired -eq $true' `
    -Properties PasswordNotRequired, Enabled |
    Select-Object SamAccountName, Enabled, PasswordNotRequired

Other useful outliers include password-never-expires, DES-only, and delegation flags. Record what needs the setting and who owns the exception. A flag permitting a condition does not establish that the condition is in use.

Do not infer current lockout or password expiry just by decoding the persisted userAccountControl value. Use LockedOut, PasswordExpired, or the computed msDS-User-Account-Control-Computed attribute. Microsoft’s flag reference explains that distinction.

Computed state belongs to a particular DC and policy view

Get-ADUser -Identity alice -Server dc1.contoso.com `
    -Properties userAccountControl, msDS-User-Account-Control-Computed,
        msDS-UserPasswordExpiryTimeComputed, pwdLastSet, accountExpires |
    Select-Object SamAccountName, userAccountControl,
        msDS-User-Account-Control-Computed, msDS-UserPasswordExpiryTimeComputed,
        pwdLastSet, accountExpires

Password expiry, account expiry, a must-change condition, and a disabled account are separate reasons for a failed workflow. Interpret special zero and maximum integer values before converting them into dates. The computed password-expiry calculation accounts for exemptions and effective maximum password age; it is not just pwdLastSet plus the default domain policy. See the computed expiry specification.

← All Active Directory topics