Exchange SE mit Modern Auth absichern

Exchange SE Modern Auth
Modern Auth mit Exchange Server Subscription Edition

Bereits der Exchange Server 2019 bekam mit CU13 die Option auf Modern Auth (OAuth 2.0). Anfangs eingeschränkt auf Outlook on the Web (OWA) / das Exchange Control Panel (ECP). Später mit CU14/15(?) kam Modern Auth für MAPI over HTTP (MAPI/HTTP), die Exchange Web Services (EWS), Exchange Active Sync (EAS) und das Offline Adressbook (OAB). IMAP, POP und Outlook Anywhere (RPC/HTTP)* gehen leer aus.

ProtokollADFS Modern Auth wird unterstützt
MAPI over HTTP (MAPI/HTTP)
Outlook Anywhere (RPC/HTTP)❌ (*Deprecated in Exchange Server SE ab CU1?)
Outlook on the Web (OWA)✅ (claims-based authentication)
Exchange Admin Center (ECP)✅ (claims-based authentication)
Exchange Active Sync (EAS)
Exchange Web Services (EWS)
Offline Address Book (OAB)
Internet Message Access Protocol (IMAP)
Post Office Protocol (POP)
Was benötigen wir oder auch „Die Formalitäten“
  • Einen Domain Controller (BFT-DC01)
    • Domain-Admin User (Administrator)
  • Einen Exchange Server (BFT-EXSE)
    • Exchange-Admin User (Exch.Admin)
  • Einen Active Directory Federation Services (ADFS) Server (BFT-ADFS01)
    • Active Directory User mit lokalen Adminrechten auf dem ADFS Server (ADFS.Admin)
    • Active Directory Gruppe (ADFS Admins)
  • Windows 11 Client oder Windows Server 2025 Terminalserver mit Outlook
    • Anforderungen an das Betriebssystem:
      • Windows 11 22H2 mit KB5023706
      • Windows Server 2025
        • Kein Support / keine Funktion in Windows Server 2019 und Windows Server 2022!
      • macOS Sequoia / iOS ab 17.6.1
    • Anforderungen an Outlook
      • M365 Apps im Semi Annual Channel ab Version 2402
      • M365 Apps Monthly Enterprise Channel ab Version 2304
      • M365 Apps im Current Channel ab Version 2304
      • Outlook 2024 (Volume License / Retail) ab Version 2408
      • Outlook 2021 (Retail) ab Version 2304; (Outlook 2021 (Volume License) hat keinen Support!)
      • Outlook for Mac (Microsoft 365) ab Version 2504; (Outlook 2024 for Mac hat keinen Support!)
      • Apple Mail ab macOS Sequoia / iOS 17.6.1
Eins Vorweg

Es ist eigentlich selbstverständlich, aber.. 😉 Bitte, bitte erstellt euch eine Testumgebung ähnlich eurer produktiven Umgebung(en) und geht die Scripte Schritt für Schritt durch. Macht dann gleich die (vermutlich) benötigten Anpassungen für eure Umgebung(en)! Die Scripte werden zwar (vermutlich) alles korrekt einrichten, ob und wie sinnvoll das aber in eure Umgebung(en) passt, müsste sich zeigen. 🤔

Meine Testumgebung(en) für diesen Beitrag

Der oder die Domain Controller, Exchange Server sowie Clients / Terminalserver inkl. passender Outlook Version setze ich einmal als vorhanden, aktuell gepatched und einsatzbereit voraus. Der ADFS Server ist idealerweise bereits Mitglied der Domäne und ebenfalls tagesaktuell gepatched. Sollte es noch keinen „ADFS.Admin“ oder die Gruppe „ADFS Admins“ geben, wird das Script diese anlegen (und unschön in den Users Container stecken). Hier kann in den ersten Zeilen (10 bis 15) in den Scripten die Benennung aller beteiligter User / Gruppen / Server angepasst werden. Am sinnvollsten dürfte es sein, die Benutzer / Gruppen / Server vorab an passender Stelle im Active Directory anzulegen.

Vorbereitung der Active Directory Domäne

Zur Vorbereitung der Active Directory Domäne kann das folgende Script „Prepare-ADFS-Domain.ps1“ genutzt werden. In den ersten Zeilen (12 bis 17) sollten Anpassungen erfolgen, die entsprechend auf eure Umgebung zu treffen. Im Anschluss geht das Script her und erstellt ein selbst signiertes Zertifikat für LDAPs, was für diesen Artikel eigentlich nicht notwendig ist. 😉 Das Zertifikat wird anschließend in ein neues Gruppenrichtlinienobjekt importiert, damit es auf allen Membern als vertrauenswürdig anerkannt wird. Anschließend wird ggfs. eine DNS Host Zone für den ADFS Service Name und der DNS A-Record erstellt. Sofern noch keine Group Managed Service Accounts im Spiel sind, wird ein neuer KDS Root Key und anschließend der gMSA für den ADFS Server erstellt. Sollte es den ADFS Admin Account sowie die Gruppe noch nicht geben, werden diese erstellt. Anschließend werden die für ADFS benötigten Organisations Einheiten – wie in Creating an AD FS farm without Domain Administrator privileges | Microsoft Learn beschrieben – angelegt und berechtigt. Die fertige „adminConfig“ findet sich dann in „C:\_install\ADFSAdminConfig.txt“ und wird im nächsten Schritt auf dem neuen ADFS Server benötigt.

# Prepare-ADFS-Domain.ps1
#
#

$objADDomain = Get-ADDomain -Current LocalComputer
$DNSServer = $objADDomain.PDCEmulator
$CertGPO = "C_Distribute Root certificates"
$ADFSGPO = "C_ADFS configuration and SSOn"
$ADFSUGPO = "U_ADFS configuration and SSOn"

# Start: Edit as needed
$ADFSServerAcc = "BFT-ADFS01"
$ADFSAdminAcc = "ADFS.Admin"
$ADFSAdminGroup = "ADFS Admins"
$ADFSSvcAcc = "gMSA-ADFS"
$ADFSFQDN = "adfs.binfordtools.net"
$ADFSIPAddress = "192.168.51.15"
# End: Edit as needed

if(-not (Test-Path -Path "C:\_install" -PathType Container)){
    New-Item -Path "C:\" `
        -Name "_install" `
        -Type Directory |
            Out-Null
}

Write-Host "Creating LDAPs / LDAP TLS certificate"
$Cert = New-SelfSignedCertificate -Subject "CN=$(-join($env:COMPUTERNAME, ".", $env:USERDNSDOMAIN)), O=$env:USERDNSDOMAIN, C=DE" `
    -DnsName $(-join($env:COMPUTERNAME, ".", $env:USERDNSDOMAIN)), $env:USERDNSDOMAIN, $env:COMPUTERNAME `
    -CertStoreLocation "cert:\LocalMachine\My" `
    -NotAfter (Get-Date).AddYears(10) `
    -KeyAlgorithm RSA `
    -KeyLength 2048 `
    -KeyExportPolicy Exportable

Write-Host "Exporting certificate to C:\_install\ADLDAÜSCert.cer"
Export-Certificate -Cert $Cert `
    -FilePath "C:\_install\ADLDAPSCert.cer" |
        Out-Null

Write-Host "Importing certificate from C:\_install\ADLDAÜSCert.cer to trusted roots"
Import-Certificate -FilePath "C:\_install\ADLDAPSCert.cer" `
    -CertStoreLocation Cert:\LocalMachine\Root |
        Out-Null

Write-Host "Checking for GPO `"$CertGPO`""
$objCertGPO = Get-GPO -All |
    Where-Object { $_.DisplayName -eq $CertGPO }
if($null -eq $objCertGPO){
    Write-Host "Creating GPO `"$CertGPO`""
    $objCertGPO = New-GPO -Name $CertGPO
    $objCertGPO.GpoStatus = "UserSettingsDisabled"
    Write-Host "Linking GPO `"$CertGPO`" to `"$($objADDomain.DistinguishedName)`""
    New-GPLink -Guid $objCertGPO.Id `
        -Target $objADDomain.DistinguishedName `
        -LinkEnabled Yes |
            Out-Null
}

$certRegPath = "HKLM:\SOFTWARE\Microsoft\SystemCertificates\Root\Certificates\{0}" `
    -f $Cert.Thumbprint
$certBlob = Get-ItemProperty -Path $certRegPath `
    -Name "Blob" |
        Select-Object Blob `
            -Expand Blob

Write-Host "Adding LDAPs certificate to GPO `"$CertGPO`""
$certPolRegPath = "HKLM\SOFTWARE\Policies\Microsoft\SystemCertificates\Root\Certificates\{0}" `
    -f $Cert.Thumbprint
Set-GPRegistryValue -Name $CertGPO `
    -Key $certPolRegPath `
    -ValueName "Blob" `
    -Type Binary `
    -Value $certBlob |
        Out-Null

Write-Host "Checking for GPO `"$ADFSGPO`""
$objADFSGPO = Get-GPO -All |
    Where-Object { $_.DisplayName -eq $ADFSGPO }
if($null -eq $objADFSGPO){
    Write-Host "Creating GPO `"$ADFSGPO`""
    $objADFSGPO = New-GPO -Name $ADFSGPO
    $objADFSGPO.GpoStatus = "UserSettingsDisabled"
    Write-Host "Linking GPO `"$ADFSGPO`" to `"$($objADDomain.DistinguishedName)`""
    New-GPLink -Guid $objADFSGPO.Id `
        -Target $objADDomain.DistinguishedName `
        -LinkEnabled Yes |
            Out-Null
}

Write-Host "Setting registry keys in GPO `"$ADFSGPO`" for Modern Auth"
Set-GPPrefRegistryValue -Guid $objADFSGPO.Id `
    -Context Computer `
    -Key "HKLM\SOFTWARE\Policies\Microsoft\AAD\AuthTrustedDomains\https://$ADFSFQDN/" `
    -ValueName "(Default)" `
    -Value "" `
    -Type String `
    -Action Update |
        Out-Null

Set-GPPrefRegistryValue -Guid $objADFSGPO.Id `
    -Context Computer `
    -Key "HKLM\SOFTWARE\Policies\Microsoft\AAD\AuthTrustedDomains\https://$ADFSFQDN" `
    -ValueName "(Default)" `
    -Value "" `
    -Type String `
    -Action Update |
        Out-Null

Write-Host "Checking for GPO `"$ADFSUGPO`""
$objADFSUGPO = Get-GPO -All |
    Where-Object { $_.DisplayName -eq $ADFSUGPO }
if($null -eq $objADFSUGPO){
    Write-Host "Creating GPO `"$ADFSUGPO`""
    $objADFSUGPO = New-GPO -Name $ADFSUGPO
    $objADFSUGPO.GpoStatus = "ComputerSettingsDisabled"
    Write-Host "Linking GPO `"$ADFSUGPO`" to `"$($objADDomain.DistinguishedName)`""
    New-GPLink -Guid $objADFSUGPO.Id `
        -Target $objADDomain.DistinguishedName `
        -LinkEnabled Yes |
            Out-Null
}

Write-Host "Setting registry keys in GPO `"$ADFSUGPO`" for Modern Auth and SSOn"
Set-GPPrefRegistryValue -Guid $objADFSUGPO.Id `
    -Context User `
    -Key "HKCU\SOFTWARE\Microsoft\Office\16.0\Common\Identity" `
    -ValueName "EnableExchangeOnPremModernAuth" `
    -Value 1 `
    -Type DWord `
    -Action Update |
        Out-Null

Set-GPPrefRegistryValue -Guid $objADFSUGPO.Id `
    -Context User `
    -Key "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$(($ADFSFQDN -replace $ADFSFQDN.Split(".")[0]).TrimStart("."))\$($ADFSFQDN.Split(".")[0])" `
    -ValueName "https" `
    -Value 1 `
    -Type DWord `
    -Action Update |
        Out-Null

Set-GPPrefRegistryValue -Guid $objADFSUGPO.Id `
    -Context User `
    -Key "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains\$(($ADFSFQDN -replace $ADFSFQDN.Split(".")[0]).TrimStart("."))\$($ADFSFQDN.Split(".")[0])" `
    -ValueName "https" `
    -Value 1 `
    -Type DWord `
    -Action Update |
        Out-Null

# When cert auth / device registration is needed:
# Create DNS record / host zone for
# - certauth.<domain>.<tld>
# - enterpriseregistration.<domain>.<tld>

$tmpDom = ($ADFSFQDN -replace $ADFSFQDN.Split(".")[0]).Substring(1)
$tmpZone = Get-DnsServerZone -ComputerName $DNSServer |
    Where-Object { $_.ZoneName -eq $tmpDom }
if($null -eq $tmpZone){
    Write-Host "Creating DNS Zone `"$ADFSFQDN`""
    Add-DnsServerPrimaryZone -ComputerName $DNSServer `
        -Name $ADFSFQDN `
        -ReplicationScope Forest `
        -DynamicUpdate None
    
    Write-Host "Adding Record `".`" with `"$ADFSIPAddress`"to DNS Zone `"$ADFSFQDN`""
    Add-DnsServerResourceRecordA -ComputerName $DNSServer `
        -ZoneName $ADFSFQDN `
        -Name "." `
        -IPv4Address $ADFSIPAddress
} else{
    Write-Host "Adding Record `"$($ADFSFQDN.Split(".")[0])`" with `"$ADFSIPAddress`"to DNS Zone `"$($tmpZone.ZoneName)`""
    Add-DnsServerResourceRecordA -ComputerName $DNSServer `
        -ZoneName $tmpZone.ZoneName `
        -Name ($ADFSFQDN.Split(".")[0]) `
        -IPv4Address $ADFSIPAddress
}

if($null -eq (Get-KdsRootKey)){
    Write-Host "Adding new Kds root key"
    Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10) |
        Out-Null
}
$ADFSSvc = New-ADServiceAccount -Name $ADFSSvcAcc `
    -DnsHostName $ADFSFQDN `
    -ServicePrincipalNames "http/$ADFSFQDN" `
    -PassThru

$ADFSGroup = Get-ADGroup -Filter 'name -eq $ADFSAdminGroup'
if($null -eq $ADFSGroup){
    Write-Host "Creating ADFS admin group `"$ADFSAdminGroup`""
    $ADFSGroup = New-ADGroup -Name $ADFSAdminGroup `
        -DisplayName $ADFSAdminGroup `
        -Description $ADFSAdminGroup `
        -GroupScope Global `
        -PassThru
}
$ADFSAdmin = Get-ADUser -Filter 'name -eq $ADFSAdminAcc'
if($null -eq $ADFSAdmin){
    Write-Host "Creating ADFS Admin `"$ADFSAdminAcc`""
    $ADFSAdminPW = Read-Host "Enter Password for new User `"$ADFSAdminAcc`"" `
        -AsSecureString
    $ADFSAdmin = New-ADUser -Name $ADFSAdminAcc `
        -DisplayName $ADFSAdminAcc `
        -SamAccountName $ADFSAdminAcc `
        -UserPrincipalName "$ADFSAdminAcc@$env:USERDNSDOMAIN" `
        -AccountPassword $ADFSAdminPW `
        -Enabled $true `
        -PassThru
}

Add-ADGroupMember -Identity $ADFSGroup `
    -Members $ADFSAdmin.Name

# See:
# https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/deployment/install-ad-fs-delegated-admin
Write-Host "Preparing domain for ADFS installation / configuration"
[string]$GuId = [Guid]::NewGuid()
$InitialPath = -join(
    "CN=Microsoft,CN=Program Data,",
    $objADDomain.DistinguishedName
)
$OUPath = -join(
    "CN=ADFS,",
    $InitialPath
)
$OU = -join(
    "CN=",
    $GuId,
    ",",
    $OUPath
)

Write-Host "Checking for OU `"$OUPath`""
if($null -eq (Get-ADObject -Filter {distinguishedName -eq $OUPath})){
    Write-Host "Creating OU `"ADFS`" in `"$InitialPath`""
    New-ADObject -Name "ADFS" `
        -Type Container `
        -Path $InitialPath |
            Out-Null
}

Write-Host "Adding ACL `"Generic.Read`" for `"$ADFSAdminGroup`" to `"$OUPath`""
$acl = Get-Acl -Path "AD:$OUPath"
[System.DirectoryServices.ActiveDirectorySecurityInheritance]$adSecInEnum = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
$ace1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"GenericRead","Allow",$adSecInEnum

$acl.AddAccessRule($ace1)
Set-Acl -Path "AD:$OUPath" `
    -AclObject $acl

Write-Host "Creating OU `"$GuId`" in `"$OUPath`""
New-ADObject -Name $GuId `
    -Type Container `
    -Path $OUPath

Write-Host "Adding ACLs"
Write-Host " - Generic.Read"
Write-Host " - Create.Child"
Write-Host " - Write.Owner"
Write-Host " - Delete.Tree"
Write-Host " - Write.Dacl"
Write-Host " - Write.Property"
Write-Host "for $($ADFSSvcAcc) to `"$OU`""
[System.DirectoryServices.ActiveDirectorySecurityInheritance]$adSecInEnum = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
$ace1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSSvc.SID),"GenericRead","Allow",$adSecInEnum
$ace2 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSSvc.SID),"CreateChild","Allow",$adSecInEnum
$ace3 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSSvc.SID),"WriteOwner","Allow",$adSecInEnum
$ace4 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSSvc.SID),"DeleteTree","Allow",$adSecInEnum
$ace5 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSSvc.SID),"WriteDacl","Allow",$adSecInEnum
$ace6 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSSvc.SID),"WriteProperty","Allow",$adSecInEnum

$acl = Get-Acl -Path "AD:$OU"

$acl.AddAccessRule($ace1)
$acl.AddAccessRule($ace2)
$acl.AddAccessRule($ace3)
$acl.AddAccessRule($ace4)
$acl.AddAccessRule($ace5)
$acl.AddAccessRule($ace6)

$acl.SetOwner($ADFSSvc.SID)

Set-Acl -Path "AD:$OU" `
    -AclObject $acl

Write-Host "Adding ACLs"
Write-Host " - Generic.Read"
Write-Host " - Create.Child"
Write-Host " - Write.Owner"
Write-Host " - Delete.Tree"
Write-Host " - Write.Dacl"
Write-Host " - Write.Property"
Write-Host "for $($ADFSGroup) to `"$OU`""
[System.DirectoryServices.ActiveDirectorySecurityInheritance]$adSecInEnum = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
$ace1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"GenericRead","Allow",$adSecInEnum
$ace2 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"CreateChild","Allow",$adSecInEnum
$ace3 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"WriteOwner","Allow",$adSecInEnum
$ace4 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"DeleteTree","Allow",$adSecInEnum
$ace5 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"WriteDacl","Allow",$adSecInEnum
$ace6 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $($ADFSGroup.SID),"WriteProperty","Allow",$adSecInEnum

$acl = Get-Acl -Path "AD:$OU"

$acl.AddAccessRule($ace1)
$acl.AddAccessRule($ace2)
$acl.AddAccessRule($ace3)
$acl.AddAccessRule($ace4)
$acl.AddAccessRule($ace5)
$acl.AddAccessRule($ace6)

$acl.SetOwner($($ADFSGroup.SID))

Set-Acl -Path "AD:$OU" `
    -AclObject $acl

Write-Host "Allowing computer account `"$ADFSServerAcc`" and admin `"$ADFSAdminAcc`" to delegate / retrieve password of `"$ADFSSvcAcc`""
$ADFSServer = Get-ADComputer -Identity $ADFSServerAcc
$ADFSSvc = Get-ADServiceAccount $ADFSSvcAcc `
    -Properties PrincipalsAllowedToDelegateToAccount, PrincipalsAllowedToRetrieveManagedPassword
$PATDTA = $ADFSSvc.PrincipalsAllowedToDelegateToAccount
$PATDTA += $ADFSAdmin.SID.Value, $ADFSServer.SID.Value
$PATRMP = $ADFSSvc.PrincipalsAllowedToRetrieveManagedPassword
$PATRMP += $ADFSAdmin.SID.Value, $ADFSServer.SID.Value

Set-ADServiceAccount -Identity $ADFSSvc `
    -PrincipalsAllowedToDelegateToAccount $PATDTA `
    -PrincipalsAllowedToRetrieveManagedPassword $PATRMP

$adminConfig = @{"DKMContainerDn"=$OU}

Write-Host "Writing Admin Config `"$($adminConfig.Keys)`" with value `"$($adminConfig.DKMContainerDn)`" to `"C:\_install\ADFSAdminConfig.txt`""
Set-Content -Path "C:\_install\ADFSAdminConfig.txt" `
    -Value $OU `
    -Encoding UTF8 `
    -Confirm:$false `
    -Force

Write-Host "Copy `"C:\_install\ADFSAdminConfig.txt`" over to new ADFS Server!"
pause
Vorbereitung, Installation und Konfiguration am neuen ADFS Server

Zur Aktivierung der Modern Auth am Exchange Server Subscription Edition wird jetzt der ADFS Server mit dem Script „Install-ADFS.ps1“ installiert. Auch hier bitte die entsprechenden Anpassungen im oberen Part des Scripts in den Zeilen 13 bis 18 vornehmen. Für die ADFS Bereitstellung verwende ich (vorerst) ebenfalls ein selbst signiertes Zertifikat für die Kommunikation mit dem Web Service. Hier jetzt der Hauptdarsteller:

# Install-ADFS.ps1 
#
#

Write-Host "Installing RSAT AD tools"
Install-WindowsFeature RSAT-AD-Tools |
    Out-Null

$objADDomain = Get-ADDomain -Current LocalComputer
$DNSServer = $objADDomain.PDCEmulator

# Start: Edit as needed
$ADFSName = "Binford Tools ADFS"
$ADFSFQDN = "adfs.binfordtools.net"
$OutlookUrl = "https://extern.tooltime.com/"
$AutoDUrl = "https://autodiscover.tooltime.com/"
$OWA = "https://extern.tooltime.com/owa/"
$ECP = "https://extern.tooltime.com/ecp/"
# End: Edit as needed

$exchangeServerServiceFqdns = @($OutlookUrl, $AutoDUrl)

$ADFSSvcAcc = Get-ADServiceAccount -Filter 'DnsHostName -eq $ADFSFQDN'
$ADFSgMSA = -join(
    $objADDomain.NetBIOSName,
    "\",
    $ADFSSvcAcc.SamAccountName
)

$IPAddress = Resolve-DnsName -Name $env:COMPUTERNAME `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

if(-not (Test-Path -Path "C:\_install" -PathType Container)){
    New-Item -Path "C:\" `
        -Name "_install" `
        -Type Directory |
            Out-Null
}

# For cert auth / device registration:
# Add certauth. / .enterpriseregistration to list of DnsNames

Write-Host "Creating ADFS service communication certificate for `"$ADFSFQDN`""
$Cert = New-SelfSignedCertificate -Subject "CN=$ADFSFQDN, O=$env:USERDNSDOMAIN, C=DE" `
    -DnsName "$ADFSFQDN", $(-join($env:COMPUTERNAME, ".", $env:USERDNSDOMAIN)), $env:USERDNSDOMAIN, $env:COMPUTERNAME `
    -CertStoreLocation "cert:\LocalMachine\My" `
    -NotAfter (Get-Date).AddYears(10) `
    -KeyAlgorithm RSA `
    -KeyLength 2048 `
    -KeyExportPolicy Exportable

Write-Host "Exporting certificate to C:\_install\ADFSSCCert.cer"
Export-Certificate -Cert $Cert `
    -FilePath "C:\_install\ADFSSCCert.cer" |
        Out-Null

Write-Host "Importing certificate from C:\_install\ADFSSCCert.cer to trusted roots"
Import-Certificate -FilePath "C:\_install\ADFSSCCert.cer" `
    -CertStoreLocation Cert:\LocalMachine\Root |
        Out-Null

Install-WindowsFeature -Name ADFS-Federation `
    -IncludeAllSubFeature `
    -IncludeManagementTools |
        Out-Null
while(-not (Test-Path -Path "C:\_install\ADFSAdminConfig.txt" -PathType Leaf)){
   Write-Host "Please copy ADFSAdminConfig.txt from Domain Controller where preparation was made to C:\_install on this machine!"
   pause
}

$OU = Get-Content -Path "C:\_install\ADFSAdminConfig.txt"
$adminConfig = @{}
$adminConfig += @{
    "dkmContainerDn" = "$OU"
}

$ADFSAdminCred = Get-Credential -Message "Enter Credentials for $env:USERNAME" `
    -UserName $env:USERNAME

Install-AdfsFarm -AdminConfiguration $adminConfig `
    -CertificateThumbprint $Cert.Thumbprint `
    -FederationServiceName $ADFSFQDN `
    -FederationServiceDisplayName $ADFSName `
    -GroupServiceAccountIdentifier $ADFSgMSA `


$CertTS = (Get-ADFSCertificate Token-Signing).Certificate

Write-Host "Exporting token-signing certificate to C:\_install\ADFSTSCert.cer"
Export-Certificate -Cert $CertTS `
    -FilePath "C:\_install\ADFSTSCert.cer" |
        Out-Null

Write-Host "Importing token-signing certificate from C:\_install\ADFSTSCert.cer to trusted roots"
Import-Certificate -FilePath "C:\_install\ADFSTSCert.cer" `
    -CertStoreLocation Cert:\LocalMachine\Root |
        Out-Null

do{
    $inp = Read-Host "Enable Windows authentication for SSOn? (y/n)"
}until($inp -match "^(y|n)$")
if($inp -eq "y"){
    Write-Host "Setting global intranet authentication policy to `"FormsAuthentication`", `"WindowsAuthentication`""
    Set-AdfsGlobalAuthenticationPolicy -PrimaryIntranetAuthenticationProvider FormsAuthentication, WindowsAuthentication
} else{
    Write-Host "Setting global intranet authentication policy to `"FormsAuthentication`""
    Set-AdfsGlobalAuthenticationPolicy -PrimaryIntranetAuthenticationProvider FormsAuthentication
}

Write-Host "Setting ADFS properties `"Enable keep me signed in`", `"SSo lifetime 10h`" and `"Keep me signed in lifetime 24h`""
Set-AdfsProperties -EnableKmsi $true
Set-AdfsProperties -SsoLifetime 600
Set-AdfsProperties -KmsiLifetimeMins 1440

Write-Host "Adding Windows integrated user agent `"Mozilla/5.0`""
$ADFSWIAUAs = (Get-AdfsProperties).WIASupportedUserAgents
$ADFSWIAUAs += "Mozilla/5.0"
Set-AdfsProperties -WIASupportedUserAgents $ADFSWIAUAs

Write-Host "Creating ADFS application group `"Outlook"
New-AdfsApplicationGroup -Name "Outlook" `
    -ApplicationGroupIdentifier "Outlook" `
    -Disabled:$false

Write-Host "Adding ADFS scope descriptions `"EAS scope`", `"EWS scope`" and `"Offline Access`""
Add-AdfsScopeDescription -Name "EAS.AccessAsUser.All" `
    -Description "EAS scope"
Add-AdfsScopeDescription -Name "EWS.AccessAsUser.All" `
    -Description "EWS scope"
Add-AdfsScopeDescription -Name "offline_access" `
    -Description "Offline Access"

Write-Host "Adding ADFS native client application `"Outlook - Native application`""
Add-AdfsNativeClientApplication -Name "Outlook - Native application" `
    -ApplicationGroupIdentifier "Outlook" `
    -Identifier "d3590ed6-52b3-4102-aeff-aad2292ab01c" `
    -RedirectUri @("ms-appx-web://Microsoft.AAD.BrokerPlugin/d3590ed6-52b3-4102-aeff-aad2292ab01c","msauth.com.microsoft.Outlook://auth","urn:ietf:wg:oauth:2.0:oob")
Write-Host "Adding ADFS native client application `"iOS and macOS - Native mail application`""
Add-AdfsNativeClientApplication -Name "iOS and macOS - Native mail application" `
    -ApplicationGroupIdentifier "Outlook" `
    -Identifier "f8d98a96-0999-43f5-8af3-69971c7bb423" `
    -RedirectUri @("com.apple.mobilemail://oauth-redirect","com.apple.preferences.internetaccounts://oauth-redirect/","com.apple.Preferences://oauth-redirect/")

$issuanceTransformRules = @"
@RuleName = "ActiveDirectoryUserSID"
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid"]
 => issue(claim = c);

@RuleName = "ActiveDirectoryUPN"
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"]
 => issue(claim = c);

@RuleName = "AppIDACR"
 => issue(Type = "appidacr", Value = "2");

@RuleName = "SCP"
 => issue(Type = "scp", Value = "user_impersonation");

@RuleName = "SCPEAS"
 => issue(Type = "scp", Value = "EAS.AccessAsUser.All");

@RuleName = "SCPEWS"
 => issue(Type = "scp", Value = "EWS.AccessAsUser.All");

@RuleName = "offlineaccess"
 => issue(Type = "scp", Value = "offline_access");
"@
foreach ($fqdn in $exchangeServerServiceFqdns) {
    Write-Host "Adding ADFS Web Api for `"$fqdn`""
    Add-AdfsWebApiApplication -Name "Outlook - Web API ($((New-Guid).ToString("N")))" `
        -ApplicationGroupIdentifier "Outlook" `
        -Identifier $fqdn `
        -IssuanceTransformRules $issuanceTransformRules `
        -AccessControlPolicyName "Permit Everyone"
}

$clientRoleIdentifier = @("f8d98a96-0999-43f5-8af3-69971c7bb423", "d3590ed6-52b3-4102-aeff-aad2292ab01c")
Get-AdfsWebApiApplication -ApplicationGroupIdentifier "Outlook" |
    ForEach-Object{
        [string]$serverRoleIdentifier = $_.Identifier
        foreach($id in $clientRoleIdentifier){
            Write-Host "Granting ADFS application for $serverRoleIdentifier permission to $id"
            Grant-AdfsApplicationPermission -ClientRoleIdentifier $id `
                -ServerRoleIdentifier $serverRoleIdentifier `
                -ScopeNames "winhello_cert","email","profile","vpn_cert","logon_cert","user_impersonation","allatclaims","offline_access","EAS.AccessAsUser.All","EWS.AccessAsUser.All","openid","aza"
        }
    }

Write-Host "Adding ADFS relaying party trust for Outlook on the Web"
Add-AdfsRelyingPartyTrust -Name "Outlook on the web" `
    -Notes "This is a trust for $OWA" `
    -Identifier $OWA `
    -WSFedEndpoint $OWA `
    -IssuanceAuthorizationRules '@RuleTemplate = "AllowAllAuthzRule" => issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");' `
    -IssueOAuthRefreshTokensTo NoDevice

Set-AdfsRelyingPartyTrust -TargetName "Outlook on the web" `
    -IssuanceTransformRules '@RuleName = "ActiveDirectoryUserSID" c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] => issue(store = "Active Directory", types = ("http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid"), query = ";objectSID;{0}", param = c.Value); @RuleName = "ActiveDirectoryUPN" c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] => issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"), query = ";userPrincipalName;{0}", param = c.Value);'

Write-Host "Adding ADFS relaying party trust for Exchange control panel"
Add-AdfsRelyingPartyTrust -Name EAC -Notes "This is a trust for $ECP" `
    -Identifier $ECP `
    -WSFedEndpoint $ECP `
    -IssuanceAuthorizationRules '@RuleTemplate = "AllowAllAuthzRule" => issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");' `
    -IssueOAuthRefreshTokensTo NoDevice

Set-AdfsRelyingPartyTrust -TargetName EAC `
    -IssuanceTransformRules '@RuleName = "ActiveDirectoryUserSID" c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] => issue(store = "Active Directory", types = ("http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid"), query = ";objectSID;{0}", param = c.Value); @RuleName = "ActiveDirectoryUPN" c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] => issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"), query = ";userPrincipalName;{0}", param = c.Value);'

Write-Host "Copy both certificates (ADFSSCCert.cer / ADFSTSCert.cer) from C:\_install to Domain Controller and run script `"Import-ADFSCertificates.ps1`""
Write-Host "Server will reboot after pressing <ENTER>"
pause
Restart-Computer
Zwischenstopp und kurz zurück auf den / einen Domain Controller

Da ich mich für den Artikel entschieden habe, den ADFS Server ohne Domain Admin zu installieren, kann ich mit meinem Standard User mit lokalen Adminrechten auf dem ADFS Server keine GPOs bearbeiten. Daher geht es mit den beiden Zertifikaten (ADFSSCCert.cer für die Service Communication; ADFSTSCert.cer fürs Token Signing) zurück auf einen Domain Controller und dem Script „Import-ADFSCertificates.ps1“:

# Import-ADFSCertificates.ps1
#
#

$CertGPO = "C_Distribute Root certificates"
$CertTPs = @()

while(($Certs = Get-ChildItem -Path "C:\_install" -Filter ADFS*Cert.cer).Count -ne 2){
    Write-Host "Copy both certificates (ADFSSCCert.cer / ADFSTSCert.cer) from ADFS Server C:\_install to Domain Controller and press <ENTER>"
    pause
}

$objCertGPO = Get-GPO -All |
    Where-Object { $_.DisplayName -eq $CertGPO }
if($null -eq $objCertGPO){
    $objCertGPO = New-GPO -Name $CertGPO
    $objCertGPO.GpoStatus = "UserSettingsDisabled"
    Write-Host "Linking GPO `"$CertGPO`" to `"$($objADDomain.DistinguishedName)`""
    New-GPLink -Guid $objCertGPO.Id `
        -Target $objADDomain.DistinguishedName `
        -LinkEnabled Yes |
            Out-Null
}

foreach($Cert in $Certs){
    Write-Host "Importing certificate `"$($Cert.Name)`""
    $tmpCert = Import-Certificate -FilePath $Cert.FullName `
        -CertStoreLocation Cert:\LocalMachine\Root
    
    $certRegPath = "HKLM:\SOFTWARE\Microsoft\SystemCertificates\Root\Certificates\{0}" `
        -f $tmpCert.Thumbprint
    $certBlob = Get-ItemProperty -Path $certRegPath `
        -Name "Blob" |
            Select-Object Blob `
                -Expand Blob

    Write-Host "Adding certificate `"$($Cert.Name)`" with thumbprint `"$($tmpCert.Thumbprint)`" to GPO `"$CertGPO`""
    $certPolRegPath = "HKLM\SOFTWARE\Policies\Microsoft\SystemCertificates\Root\Certificates\{0}" `
        -f $tmpCert.Thumbprint
    Set-GPRegistryValue -Name $CertGPO `
        -Key $certPolRegPath `
        -ValueName "Blob" `
        -Type Binary `
        -Value $certBlob |
            Out-Null
}
Zuletzt aktivieren wir die Modern Auth am Exchange SE

Mit dem letzten Script „Configure-ExADFS-ModernAuth.ps1“ aktivieren wir die Modern Auth am Exchange Server SE. Das erstellte Authentication Profile weisen wir vorerst nur einem (oder ein paar) Test Usern zu. Auch im Script „Configure-ExADFS-ModernAuth.ps1“ bitte die Zeilen 8 bis 12 an die eigene Umgebung anpassen.

# Configure-ExADFS-ModernAuth.ps1
#
#

$objADDomain = Get-ADDomain -Current LocalComputer

# Start: Edit as needed
$ADFSName = "Binford Tools ADFS"
$ADFSFQDN = "adfs.binfordtools.net"
$OWA = "https://extern.tooltime.com/owa/"
$ECP = "https://extern.tooltime.com/ecp/"
$Users = @("Exch.Admin", "Test.User1", "Test.User2")
# End: Edit as needed

$ExchSRV = Get-ADComputer -Identity $env:COMPUTERNAME `
    -Properties DnsHostName

$ExchConUri = -join("http://", `
    $ExchSRV.DNSHostName , `
    "/PowerShell" `
)

Write-Host "Running gpupdate to fetch new GPO with certificates"
Invoke-Expression -Command "C:\Windows\system32\gpupdate.exe /force"

Write-Host "Connecting to Exchange `"$env:COMPUTERNAME` ($ExchConUri)"
$ExchSession = New-PSSession -ConfigurationName Microsoft.Exchange `
    -ConnectionUri $ExchConUri
Import-PSSession -Session $ExchSession -DisableNameChecking `
    -AllowClobber |
        Out-Null

Write-Host "Creating (temporary) authentication policy to block modern auth per default"
New-AuthenticationPolicy "Block Modern auth" `
    -BlockModernAuthWebServices `
    -BlockModernAuthActiveSync `
    -BlockModernAuthAutodiscover `
    -BlockModernAuthImap `
    -BlockModernAuthMapi `
    -BlockModernAuthOfflineAddressBook `
    -BlockModernAuthPop `
    -BlockModernAuthRpc

Write-Host "Setting `"Block Modern auth`" authentication policy as default"
Set-OrganizationConfig -DefaultAuthenticationPolicy "Block Modern auth"

Write-Host "Creating `"Allow Modern auth`" per user authentication policy"
New-AuthenticationPolicy "Allow Modern auth" `
    -BlockLegacyAuthActiveSync `
    -BlockLegacyAuthOfflineAddressBook `
    -BlockLegacyAuthRpc `
    -BlockLegacyAuthWebServices `
    -BlockLegacyAuthAutodiscover `
    -BlockLegacyAuthMapi

Write-Host "Creating new auth server"
New-AuthServer -Type ADFS `
    -Name $ADFSName `
    -AuthMetadataUrl "https://$ADFSFQDN/FederationMetadata/2007-06/FederationMetadata.xml" |
        Out-Null

Write-Host "Setting new auth server as default authorization endpoint"
Set-AuthServer -Identity $ADFSName `
    -IsDefaultAuthorizationEndpoint $true

Write-Host "Fetching ADFS signing certificate from trusted roots"
$ADFSSCert = Get-ChildItem -Path Cert:\LocalMachine\Root |
    Where-Object { $_.Subject -match "^CN=ADFS Signing - $ADFSFQDN$" }

Write-Host "Updating organization config for ADFS use and enabling OAuth2"
Set-OrganizationConfig -AdfsIssuer "https://$ADFSFQDN/adfs/ls/" `
    -AdfsAudienceUris $OWA, $ECP `
    -AdfsSignCertificateThumbprint $ADFSSCert.Thumbprint
Set-OrganizationConfig -OAuth2ClientProfileEnabled $true

Write-Host "Setting Active Sync authentication methods to support modern auth"
Get-ActiveSyncVirtualDirectory |
    Set-ActiveSyncVirtualDirectory -InternalAuthenticationMethods Ntlm, OAuth, Negotiate `
        -ExternalAuthenticationMethods Ntlm, OAuth, Negotiate

Write-Host "Enabling ADFS authentication for Exchange control panel (ECP)"
Get-EcpVirtualDirectory |
    Set-EcpVirtualDirectory -AdfsAuthentication $true `
        -BasicAuthentication $false `
        -DigestAuthentication $false `
        -FormsAuthentication $false `
        -OAuthAuthentication $false `
        -WindowsAuthentication $false

Write-Host "Enabling ADFS authentication for Outlook Web App (OWA)"
Get-OwaVirtualDirectory |
    Set-OwaVirtualDirectory -AdfsAuthentication $true `
        -BasicAuthentication $false `
        -DigestAuthentication $false `
        -FormsAuthentication $false `
        -OAuthAuthentication $false `
        -WindowsAuthentication $false

foreach($u in $Users){
    Write-Host "Assigning authentication policy `"Allow Modern auth`" to $u"
    Set-User -Identity $u `
        -AuthenticationPolicy "Allow Modern auth"
}
Weil es so schön ist: Bunte Bilder zum Abschluss

Ablauf bei deaktiviertem SSOn / Windows Authentifizierung:

  1. Aufruf der Outlook Web App (OWA) oder des Exchange Control Panels (ECP) im Browser
  2. Umleitung zum ADFS
  3. Anmeldung
  4. Umleitung zu OWA / zum ECP
  1. Beim Starten von Outlook (mit gesetzter Policy: GPS: Automatically configure profile based on Active Directory Primary SMTP address):

Ablauf bei aktiviertem SSOn / Windows Authentifizierung:

  1. Aufruf der Outlook Web App (OWA) oder des Exchange Control Panels (ECP) im Browser
  2. Uuund… Schwupps, angemeldet 🙂
  1. Beim Starten von Outlook (mit gesetzter Policy: GPS: Automatically configure profile based on Active Directory Primary SMTP address) kann man sich zumindest das Tippen des Passworts sparen und den „Sign in as current user“ wählen: 😉
Und warum der ganze Spaß? Ein Ausblick:

Nunja 🤷‍♂️, eigentlich ganz einfach. Dank dem ADFS sollte sich in die gesamte Authentifizierung auch eine Zwei-Faktor-Authentisierung bzw. eine Multi-Faktor-Authentisierung in alle (bzw. die wichtigsten) Exchange Server Protokolle (OWA, ECP, EAS, MAPI/HTTP und OAB) bspw. von ESET (Einfach zu verwaltende und sichere Multi-Faktor-Authentifizierung | ESET | ESET) oder auch Duo (Complete Identity Security & MFA Solutions | Duo Security) implementieren lassen. Dazu aber mehr im vermutlich nächsten oder übernächsten Artikel. 😅


Die gesuchte Lösung noch nicht gefunden oder benötigen Sie Hilfe bei anderen Themen aus meinem Blog? Nehmen Sie gerne Kontakt mit mir bzw. meinem Unternehmen Jan Mischo IT auf. Ich freue mich auf Ihre Anfrage: https://janmischo.it/kontakt/


+49 2801 7004300

info@janmischo.it

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.