Checking IIS Authentication Settings Before and After Changes

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
}

Leave a Reply

Your email address will not be published. Required fields are marked *