Understanding SAML/SSO Integration on Windows Server 2019

Security Assertion Markup Language (SAML) 2.0 enables Single Sign-On (SSO) between an Identity Provider (IdP) and Service Providers (SPs). Active Directory Federation Services (AD FS) on Windows Server 2019 acts as a SAML 2.0 Identity Provider, allowing users to authenticate once with their on-premises Active Directory credentials and gain access to cloud SaaS applications such as Salesforce, ServiceNow, GitHub Enterprise, and hundreds of others without re-entering passwords.

Prerequisites

Before configuring SAML/SSO, you need: a Windows Server 2019 server with AD FS role installed and configured, a publicly trusted SSL certificate for the federation service URL, DNS records pointing the federation service FQDN to the AD FS server’s public IP, and an AD FS Web Application Proxy (WAP) if external users need access.


# Verify AD FS is installed and running
Get-WindowsFeature -Name ADFS-Federation
Get-Service adfssrv | Select-Object Name, Status, StartType

# Check the AD FS configuration
Get-AdfsProperties | Select-Object HostName, HttpsPort, HttpPort, Identifier

# Get the federation service name (used as issuer URL in SAML assertions)
(Get-AdfsSslCertificate)[0].Hostname

# Verify the AD FS endpoints
Get-AdfsEndpoint | Where-Object { $_.Enabled } | Select-Object FullUrl, Protocol, AddressPath

Obtaining the SAML Metadata and Certificates

Before configuring the Service Provider, collect the information it will need from your AD FS instance. The SP needs the federation metadata URL or individual certificate and endpoint details:


# AD FS Federation Metadata URL (share with the SP's admin)
# https://adfs.corp.com/FederationMetadata/2007-06/FederationMetadata.xml

# Get the token-signing certificate (export the public key for the SP)
$signingCert = Get-AdfsCertificate -CertificateType Token-Signing | 
    Where-Object { $_.IsPrimary }

# Export certificate to Base64 for sharing
$certBase64 = [Convert]::ToBase64String($signingCert.Certificate.RawData, 'InsertLineBreaks')
"-----BEGIN CERTIFICATE-----`n$certBase64`n-----END CERTIFICATE-----" |
    Out-File 'C:ADFSADFSTokenSigning.cer'

Write-Output "Token-Signing Certificate Thumbprint: $($signingCert.Certificate.Thumbprint)"
Write-Output "Valid Until: $($signingCert.Certificate.NotAfter)"

# Get AD FS SAML endpoints
Get-AdfsEndpoint | Where-Object { $_.Protocol -eq 'SAML 2.0/WS-Federation' }

Adding a Relying Party Trust for a SAML SP (Salesforce Example)

Each SaaS application that accepts SAML assertions needs a Relying Party Trust (RPT) configured in AD FS. This can be done from metadata or manually:


# Option 1: Import from the SP's federation metadata URL
Add-AdfsRelyingPartyTrust `
    -Name 'Salesforce Production' `
    -MetadataUrl 'https://yourinstance.my.salesforce.com/.well-known/samlidp.xml' `
    -AutoUpdateEnabled $true `
    -MonitoringEnabled $true `
    -Enabled $true

# Option 2: Configure manually (for SPs that don't publish metadata)
Add-AdfsRelyingPartyTrust `
    -Name 'ServiceNow Production' `
    -Identifier 'https://yourinstance.service-now.com' `
    -SamlEndpoint (
        New-AdfsSamlEndpoint `
            -Binding POST `
            -Protocol SAMLAssertionConsumer `
            -Uri 'https://yourinstance.service-now.com/navpage.do'
    ) `
    -RequestSigningCertificate @() `
    -IssuanceAuthorizationRules '@RuleTemplate = "AllowAllAuthzRule" => issue(Type = "http://schemas.microsoft.com/authorization/claims/permit", Value = "true");' `
    -Enabled $true `
    -EncryptionCertificateRevocationCheck None

# Verify the trust was created
Get-AdfsRelyingPartyTrust -Name 'Salesforce Production' | Select-Object Name, Identifier, Enabled

Configuring Claim Issuance Rules

Claim issuance rules define which AD attributes are sent to the SP in the SAML assertion. At minimum, most SPs need an email address as the NameID claim and additional attributes like DisplayName, department, and groups:


# Define claim rules using the claim rule language
$rules = @'
@RuleTemplate = "LdapClaims"
@RuleName = "Send LDAP Attributes as Claims"
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/emailaddress",
       "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
       "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
       "http://schemas.xmlsoap.org/claims/Group",
       "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
   ),
   query = ";mail,givenName,sn,tokenGroups,userPrincipalName;{0}",
   param = c.Value);

@RuleTemplate = "MapClaims"
@RuleName = "Transform UPN to NameID"
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
         Issuer = c.Issuer,
         OriginalIssuer = c.OriginalIssuer,
         Value = c.Value,
         ValueType = c.ValueType,
         Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] =
             "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
'@

Set-AdfsRelyingPartyTrust -TargetName 'Salesforce Production' `
    -IssuanceTransformRules $rules

# Verify the rules
(Get-AdfsRelyingPartyTrust -Name 'Salesforce Production').IssuanceTransformRules

Controlling Access with Authorization Rules

Restrict which AD users or groups can use the SAML SSO application:


# Allow only members of specific security group
$authRules = @'
@RuleTemplate = "Authorization"
@RuleName = "Permit Salesforce Users Group"
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid",
   Value == "S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-XXXX"]
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/permit",
         Value = "PermitUsersWithClaim");

@RuleTemplate = "Authorization"
@RuleName = "Deny All Others"
=> issue(Type = "http://schemas.microsoft.com/authorization/claims/deny",
         Value = "DenyUsersWithClaim");
'@

Set-AdfsRelyingPartyTrust -TargetName 'Salesforce Production' `
    -IssuanceAuthorizationRules $authRules

# Get the SID for the group to fill in above
(Get-ADGroup -Identity 'SG-Salesforce-Users').SID.Value

Installing and Configuring the Web Application Proxy

The WAP allows external users to authenticate via AD FS without requiring a VPN. It runs on a DMZ server running Windows Server 2019:


# On the WAP server (in DMZ)
Install-WindowsFeature -Name Web-Application-Proxy -IncludeManagementTools

# Configure WAP pointing to internal AD FS
Install-WebApplicationProxy `
    -FederationServiceTrustCredential (Get-Credential 'CORPadmin') `
    -CertificateThumbprint $cert.Thumbprint `
    -FederationServiceName 'adfs.corp.com'

# Publish AD FS to the web via WAP
Add-WebApplicationProxyApplication `
    -BackendServerUrl 'https://adfs.corp.com/adfs/' `
    -ExternalCertificateThumbprint $cert.Thumbprint `
    -ExternalUrl 'https://adfs.corp.com/adfs/' `
    -Name 'AD FS' `
    -ExternalPreAuthentication PassThrough

# Verify WAP health
Get-WebApplicationProxyHealth
Get-WebApplicationProxyApplication | Select-Object Name, ExternalUrl, BackendServerUrl

Testing SAML Authentication


# Test AD FS health
Test-AdfsServerHealth | Select-Object Name, Result, Detail | Format-Table -AutoSize

# Test a specific relying party trust
Test-AdfsRelyingPartyTrust -Name 'Salesforce Production'

# Review the AD FS audit log for authentication events
Get-WinEvent -LogName 'AD FS/Admin' -MaxEvents 50 | 
    Where-Object { $_.Id -in 1200,1201,1202,1203 } |
    Select-Object TimeCreated, Id, Message

# Enable verbose AD FS logging for debugging
Set-AdfsProperties -AuditLevel Verbose
# Then check Security event log for Event IDs 299, 500, 501, 1200, 1201

# Decode a SAML response for debugging (base64 decode the SAMLResponse POST parameter)
$encoded = 'PHNhbWxwOlJlc3BvbnNl...'  # from browser dev tools
[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded))

Certificate Renewal for AD FS


# Check certificate expiry dates
Get-AdfsCertificate | Select-Object CertificateType, IsPrimary, 
    @{N='Thumbprint';E={$_.Certificate.Thumbprint}},
    @{N='NotAfter';E={$_.Certificate.NotAfter}}

# Enable automatic certificate rollover (default is on, verify)
Get-AdfsProperties | Select-Object AutoCertificateRollover

# Force certificate rollover manually (30 days before expiry by default)
Update-AdfsCertificate -CertificateType Token-Signing -Urgent

# After rollover, update all relying party trusts with the new certificate
# Most SPs can re-import your metadata URL to pick up the new cert automatically

Conclusion

AD FS on Windows Server 2019 provides a production-ready SAML 2.0 Identity Provider that integrates with virtually any cloud application supporting federated SSO. By carefully authoring claim issuance rules to pass the correct attributes, scoping access with authorization rules, and deploying the Web Application Proxy for external users, organizations deliver a seamless single sign-on experience while keeping credentials entirely on-premises and under their control.