When maintaining Exchange Server or other IIS-based applications, configuration changes — such as cumulative updates, patches, or manual adjustments — can sometimes alter authentication settings without notice.
These changes may lead to unexpected login prompts, authentication failures, or inconsistent client access.
To help detect and document such changes, I use a simple but powerful PowerShell script that captures IIS authentication settings before and after any updates or modifications.
How the Script Works
The script queries IIS and exports the current authentication configuration to a CSV file.
After performing your updates (for example, applying an Exchange CU or modifying a virtual directory), you run the script again to generate a second CSV snapshot.
Each CSV contains detailed authentication settings for every relevant IIS site and virtual directory — including Windows, Basic, OAuth, and other authentication providers.
You can then use PowerShell’s built-in Compare-Object command to identify any differences between the two CSV files:
Compare-Object (Import-Csv .\Auth-Before.csv) (Import-Csv .\Auth-After.csv) -Property Site,Path,AuthType,Enabled
This comparison highlights exactly which authentication settings have changed, allowing administrators to verify that everything still aligns with best practices and organizational standards.
Why This Is Useful
Authentication misconfigurations can be subtle — and difficult to diagnose once users begin reporting access issues.
By documenting authentication settings before and after maintenance, you gain:
- Full visibility into IIS authentication changes.
- Quick detection of unexpected configuration drift.
- Simplified troubleshooting after updates or patching.
- A reliable audit trail for compliance and change management.
Practical Example
Before installing a new Exchange cumulative update, you run the script and save the output as Auth-Before.csv.
After the update completes, you run it again to create Auth-After.csv.
By comparing the two files, you instantly see whether any authentication methods were enabled, disabled, or modified — all without manually checking dozens of virtual directories.
n summary:
This PowerShell script provides a simple, reliable way to monitor and compare IIS authentication settings before and after system changes — helping administrators maintain consistent, secure configurations and avoid service disruptions.
<#
.SYNOPSIS
Extended local snapshot of IIS Authentication and Extended Protection (EP) settings per site/app/vdir on an Exchange server.
.DESCRIPTION
Scans all IIS sites, applications, and virtual directories, and collects:
• Anonymous / Basic / Windows / Digest authentication status (enabled/disabled)
• Windows authentication providers (NTLM / Negotiate order)
• Basic authentication realm (if configured)
• SSL flags (Require SSL / client certificate mode)
• Extended Protection settings (tokenChecking, flags, SPNs)
• (Optional) Exchange virtual directories (OWA, ECP, EWS, Autodiscover, MAPI, ActiveSync, OAB, Outlook Anywhere)
Also performs a brief risk assessment regarding reverse proxy (e.g. NetScaler) compatibility with Extended Protection settings.
Output is saved in C:\CNXA\IIS-Auth-Snapshots\<timestamp> as CSV, JSON, and SUMMARY.txt.
.PARAMETER OutputRoot
Root output folder. Default: C:\CNXA\IIS-Auth-Snapshots
.PARAMETER IncludeExchangeVdirs
Include Exchange virtual directories (OWA, ECP, EWS, Autodiscover, MAPI, ActiveSync, OAB, Outlook Anywhere) if corresponding cmdlets are available.
#>
[CmdletBinding()]
param(
[string]$OutputRoot = 'C:\CNXA\IIS-Auth-Snapshots',
[switch]$IncludeExchangeVdirs
)
begin {
$ErrorActionPreference = 'Stop'
Import-Module WebAdministration -ErrorAction Stop
$timestamp = (Get-Date).ToString('yyyy-MM-ddTHH-mm-ss')
$runRoot = Join-Path $OutputRoot $timestamp
New-Item -ItemType Directory -Path $runRoot -Force | Out-Null
Write-Host ("Snapshot folder: {0}" -f $runRoot) -ForegroundColor Cyan
function Get-AuthInfo([string]$Location, [string]$PathType) {
$row = [ordered]@{
Location = $Location
PathType = $PathType # Site / App / VDir
anonymousAuthentication = $null
basicAuthentication = $null
windowsAuthentication = $null
digestAuthentication = $null
windowsProviders = $null
basicRealm = $null
sslFlags = $null
ep_tokenChecking = $null # None/Allow/Require (IIS viser ofte: Off/Accept/Require)
ep_flags = $null # None/Proxy/ProxyCohosting/NoServiceNameCheck
ep_spns = $null # Semi-kolon-separeret liste
Risk_NetScaler_EP = $null # Heuristik: EP + sandsynlig proxy = risiko
Error = $null
}
try {
foreach($name in 'anonymousAuthentication','basicAuthentication','windowsAuthentication','digestAuthentication'){
try {
$state = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter "system.webServer/security/authentication/$name" -Name enabled -ErrorAction Stop).Value
$row[$name] = [bool]$state
} catch { $row[$name] = $null }
}
# Providers for Windows auth
try {
$providers = (Get-WebConfiguration -PSPath IIS:\ -Location $Location -Filter 'system.webServer/security/authentication/windowsAuthentication/providers').Collection |
ForEach-Object { $_.Value }
$row['windowsProviders'] = ($providers -join ',')
} catch { $row['windowsProviders'] = $null }
# Basic realm
try {
$realm = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter 'system.webServer/security/authentication/basicAuthentication' -Name 'realm' -ErrorAction Stop).Value
$row['basicRealm'] = $realm
} catch { $row['basicRealm'] = $null }
# SSL flags
try {
$flags = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter 'system.webServer/security/access' -Name 'sslFlags' -ErrorAction Stop).Value
$row['sslFlags'] = $flags
} catch { $row['sslFlags'] = $null }
# Extended Protection (under windowsAuthentication/extendedProtection)
try {
$epBase = 'system.webServer/security/authentication/windowsAuthentication/extendedProtection'
$token = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter $epBase -Name 'tokenChecking' -ErrorAction Stop).Value
$flags2 = (Get-WebConfigurationProperty -PSPath IIS:\ -Location $Location -Filter $epBase -Name 'flags' -ErrorAction Stop).Value
$spnCol = (Get-WebConfiguration -PSPath IIS:\ -Location $Location -Filter ($epBase + '/spn')).Collection
$spns = @(); if($spnCol){ $spns = $spnCol | ForEach-Object { $_.Value } }
$row['ep_tokenChecking'] = $token
$row['ep_flags'] = $flags2
$row['ep_spns'] = ($spns -join ';')
} catch {
# Hvis noderne ikke findes, lader vi felter være null
}
# Heuristik for risiko bag reverse proxy: Hvis WindowsAuth=On og EP tokenChecking er 'Require' eller flags indeholder 'Proxy', så marker risiko
if($row.windowsAuthentication -and ($row.ep_tokenChecking -match 'Require' -or ($row.ep_flags -and $row.ep_flags -match 'Proxy'))){
$row.Risk_NetScaler_EP = $true
} else {
$row.Risk_NetScaler_EP = $false
}
}
catch { $row['Error'] = $_.Exception.Message }
return [pscustomobject]$row
}
}
process {
$rows = @()
# Sites
$sites = Get-Website | Sort-Object Name
foreach($s in $sites){
$rows += Get-AuthInfo -Location $s.Name -PathType 'Site'
# Apps
$apps = Get-WebApplication -Site $s.Name -ErrorAction SilentlyContinue
foreach($a in $apps){
$loc = "$($s.Name)$($a.Path)"
$rows += Get-AuthInfo -Location $loc -PathType 'App'
# VDirs under app
$vdirs = Get-WebVirtualDirectory -Site $s.Name -Application $a.Path -ErrorAction SilentlyContinue
foreach($v in $vdirs){
$loc2 = "$($s.Name)$($v.Path)"
$rows += Get-AuthInfo -Location $loc2 -PathType 'VDir'
}
}
# Root VDirs (hvis ingen apps)
$rootVdirs = Get-WebVirtualDirectory -Site $s.Name -Application '/' -ErrorAction SilentlyContinue
foreach($rv in $rootVdirs){
$loc3 = "$($s.Name)$($rv.Path)"
$rows += Get-AuthInfo -Location $loc3 -PathType 'VDir'
}
}
# Gem IIS-auth + EP
$csvPath = Join-Path $runRoot 'iis-auth.csv'
$jsonPath = Join-Path $runRoot 'iis-auth.json'
$rows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvPath
$rows | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8
Write-Host ("IIS auth skrevet: {0}" -f $csvPath) -ForegroundColor Green
# Lidt opsummering og fokus på EP + velkendte Exchange-kataloger
$summary = New-Object System.Text.StringBuilder
[void]$summary.AppendLine("IIS Auth / Extended Protection Snapshot - " + $timestamp)
[void]$summary.AppendLine("")
$hot = $rows | Where-Object { $_.Risk_NetScaler_EP -eq $true }
if($hot.Count -gt 0){
[void]$summary.AppendLine("!! Potentielle problemer bag reverse proxy (EP Require/Proxy + WindowsAuth):")
$hot | Select-Object Location,PathType,windowsAuthentication,ep_tokenChecking,ep_flags,sslFlags | Format-Table | Out-String | ForEach-Object { [void]$summary.Append($_) }
} else {
[void]$summary.AppendLine("Ingen åbenlyse EP/Proxy-risici fundet baseret på heuristik.")
}
$summaryPath = Join-Path $runRoot 'SUMMARY.txt'
$summary.ToString() | Set-Content -Path $summaryPath -Encoding UTF8
if($IncludeExchangeVdirs){
if(Get-Command Get-OwaVirtualDirectory -ErrorAction SilentlyContinue){
$ex = [ordered]@{}
$ex.OWA = Get-OwaVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,FormsAuthentication,BasicAuthentication,WindowsAuthentication,OAuthAuthentication,RequireSSL
$ex.ECP = Get-EcpVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,FormsAuthentication,BasicAuthentication,WindowsAuthentication,OAuthAuthentication,RequireSSL
$ex.EWS = Get-WebServicesVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,BasicAuthentication,WindowsAuthentication,OAuthAuthentication,RequireSSL
$ex.Auto = Get-AutodiscoverVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,WindowsAuthentication,OAuthAuthentication,RequireSSL
$ex.MAPI = Get-MapiVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,IISAuthenticationMethods,RequireSSL
$ex.AS = Get-ActiveSyncVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,BasicAuthentication,WindowsAuthentication,ClientCertAuth,ExternalAuthenticationMethods,RequireSSL
$ex.OAB = Get-OabVirtualDirectory | Select-Object Server,Name,InternalUrl,ExternalUrl,RequireSSL
$ex.OA = Get-OutlookAnywhere | Select-Object Server,Name,ExternalHostname,InternalHostname,SSLOffloading,ExternalClientAuthenticationMethod,InternalClientAuthenticationMethod,IISAuthenticationMethods
# Flatten for CSV
$exRows = @()
foreach($k in $ex.Keys){ if($null -ne $ex[$k]){ $exRows += $ex[$k] | Select-Object * } }
if($exRows.Count -gt 0){
$exCsv = Join-Path $runRoot 'exchange-vdirs.csv'
$exJson = Join-Path $runRoot 'exchange-vdirs.json'
$exRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $exCsv
$ex | ConvertTo-Json -Depth 6 | Set-Content -Path $exJson -Encoding UTF8
Write-Host ("Exchange VDirs skrevet: {0}" -f $exCsv) -ForegroundColor Green
} else {
Set-Content -Path (Join-Path $runRoot 'exchange-vdirs.txt') -Value 'No Exchange VDirs returned.'
}
} else {
Set-Content -Path (Join-Path $runRoot 'exchange-vdirs.txt') -Value 'Exchange cmdlets not found; skipping.'
}
}
}
end {
# no-op
}