===========================================================================
Updated 10 September 2013: tested with Windows 2012 R2 RTM and the script functions as in R2 Preview. Outlook Anywhere bug in the Preview code has been fixed and Outlook now works with RTM. Updated the script to correct Autodiscover ExternalURL
===========================================================================

In this post I will be discussing deploying a highly available Windows 2012 R2 Preview ADFS and Web Application Proxy solution using only PowerShell. This was done as a proof of concept to compare the time taken as well as complexity to build and configure a Reverse Proxy solution to replace a UAG 2010 array. Those of you who have worked with UAG will know that just installing and applying UAG updates and Service Packs takes over an hour – before you even get to the application configuration which is totally manual. For a client recently with 6 trunks publishing Exchange, Lync, Office Web Apps, FIM and SharePoint with the same applications published on multiple domain names it would take over 8 hours of mind numbing, RSI inducing and error prone manual input. The configuration I have built here is for Exchange 2013, Lync 2013 and Office Web Apps Server only, but adding SharePoint and FIM would add about 30 seconds to the installation time.

The other purpose of these scripts is to get something that is repeatable and easily configurable which gives certainty of the configuration and avoids human error. Note that this is a proof of concept so I have not put in much error checking – all code should be tested in a lab environment.

The end result on the ADFS side is an ADFS farm with two servers and a Non-Claims-Aware Relying Party Trust for Exchange.

ADFS Relying Party Trusts 2

And on the Web Application Proxy side we have a cluster of servers with two ADFS preauthenticated applications and 10 Pass-through applications.

WAP Exchange and Lync Applications

Background Reading

These scripts are based on information, functions and script extracts from some of my previous blog posts so if you want more information and background then I recommend you review these:

Windows 2012 R2 Preview Web Application Proxy – Exchange 2013 Publishing Tests for the Web Application Proxy overview

Publish Lync 2013 with 2012 R2 Preview Web Application Proxy for the Lync configuration and SNI requirements

SSL SAN Certificate Request and Import from PowerShell for the PowerShell based certificate requests

Outlook Anywhere NTLM SSO with UAG 2010 KCD for the KCD configuration

Overview of the Scripts

For ease of installation I have split this into four PowerShell scripts which do the following:

Setup-ADFSserver1.ps1 which is run on the first internal ADFS server
  1. User input of a password for exporting the ADFS certificate to .PFX with private key
  2. User input of the credentials for the ADFS service account
  3. Either import the ADFS certificate from a PFX file, or if used for testing – generate a certificate request .ini file for ADFS, requests a certificate from an online CA and exports the certificate as a PFX file to a file share
  4. Install an ADFS farm using WID
  5. Add a non claims aware relying party trust for Exchange using a permit all issuance rule
Setup-ADFSserver2.ps1 which is run on the secondary ADFS server
  1. User input of the password for importing the ADFS certificate
  2. User input of the credentials for the ADFS service account
  3. Import the ADFS certificate
  4. Add the server as an additional node to the ADFS farm
Setup-WAPserver1.ps1 which is run on the first Web Application Proxy server
  1. User input of the password for importing the ADFS certificate
  2. User input of a password for exporting the Web Application Proxy certificate to .PFX with private key
  3. User input of the credentials for an administrator of the ADFS server (not the ADFS service account)
  4. Import the ADFS certificate
  5. Either import the Web Application Proxy certificate from a PFX file, or if used for testing – generate a certificate request .ini file for the Web Application Proxy, requests a certificate from an online CA and exports the certificate as a PFX file to a file share
  6. Install the Web Application Proxy and add it as an ADFS proxy
  7. Add application publishing rules for Lync and Office Web Apps Server
  8. Add application publishing rules for Exchange
  9. Add SPN for KCD (Kerberos Constrained Delegation) to allow the WAP server to delegate credentials for the Exchange server
  10. Set the default certificate binding for clients that do not support SNI (Server Name Indication)
Setup-WAPserver2.ps1 which is run on the secondary WAP server
  1. User input of the password for importing the ADFS certificate
  2. User input of the password for importing the Web Application Proxy certificate to .PFX with private key
  3. User input of the credentials for an administrator of the ADFS server (not the ADFS service account)
  4. Import the ADFS certificate
  5. Import the WAP certificate
  6. Install the Web Application Proxy and add it as an ADFS proxy
  7. Add SPN for KCD (Kerberos Constrained Delegation) to allow the WAP server to delegate credentials for the Exchange server
  8. Set the default certificate binding for clients that do not support SNI (Server Name Indication)

In my testing the first ADFS server took on average 2 minutes 15 seconds, the second ADFS server 2 minutes 15 seconds, the first WAP server 2 minutes 45 and the second WAP server 2 minutes 30. That is a total of 9 minutes and 45 seconds for a highly available ADFS and Reverse Proxy solution which is a whole lot better than configuring UAG.

Note that I am giving the option of using an internal Certificate Authority for the deployment by specifying ‘-OnlineRequest’ in the scripts. This was done to help me with testing, however in the real world you would use a public certificate on the Web Application Proxy for the publishing rules as well as ADFS. I normally use the same public ADFS certificate on the internal ADFS server to keep the configuration consistent and allow non-domain joined clients and mobile devices connecting via internal Wifi to connect without having to import an internal root certificate.

Prerequisites

  • Four Windows 2012 R2 Preview servers which are joined to the domain and use internal DNS
  • In order to publish Exchange OWA and ECP applications with ADFS authentication, the Web Application Proxy servers must be domain joined in order to perform KCD. This makes the scripts easier too as they can place and retrieve certificates from a share and request online certificates from the enterprise Certificate Authority
  • Certificate PFX files for ADFS and Web Application Proxy exported as PFX files with a private key (unless OnlineRequest is specified in which case the script will request certificates from an Enterprise online Certificate Authority)
  • The ADFS farm FQDN (adfs2.showcase.kloud.com.au in this case) DNS A record must exist in DNS before configuration. As an aside – this should be an A record not a CNAME to allow domain joined clients with the ADFS FQDN in the IE trusted sites to authenticate automatically
  • Exchange and Lync already configured
  • One or more load balancers to spread the traffic load across the Web Application Proxy ‘cluster’ and the ADFS farm servers and perform health checks to provide high availability

Scripts

First, a note about the credential prompts. I could have entered the credentials into the script as plain text and converted them to a secure string, but I think that is bad practice as you are dealing with certificates and service accounts where the passwords will not change. Leaving text files with passwords on disk in plain text is a bad thing. Another option is to input the credentials once and save them to the registry in an encrypted form but that is not really relevant here as you will typically run the script only once per server. A third option would be to use remote PowerShell and input credentials once before running the configuration on all servers.

Edit the ‘### Variables’ section of each script and run from an elevated PowerShell prompt.

Setup-ADFSserver1.ps1

Usage:

  • .\Setup-ADFSserver1.ps1
  • .\Setup-ADFSserver1.ps1 –OnlineRequest
param (
	[switch]$OnlineRequest
)

$Start = get-date

### Variables
$CertificateADFSsubject = "adfs2.showcase.kloud.com.au"
$ADFSdisplayName = "Kloud Showcase ADFS 2"
$ADFSuser = "kloud\ADFSfarm2012R2"
$CertificateLocalPath = "C:\Certificates\"
$CertificateRemotePath = "\\dc01.kloud.internal\certificates\"
$RelyingPartyTrustExchangeName = "Exchange"
$RelyingPartyTrustExchangeURI = "https://ex2010-01.kloud.internal/owa"
$RelyingPartyTrustExchangeIssuanceRule = "@RuleTemplate = `"AllowAllAuthzRule`""    # Escape quotes
$RelyingPartyTrustExchangeIssuanceRule += "`n => issue(Type = `"http://schemas.microsoft.com/authorization/claims/permit`", Value = `"true`");"    # New line and escape quotes
# Only for Online Request
$CertificateAuthority = "dc01.kloud.internal\kloud-Showcase-CA"

### Preparation
$PfxPasswordADFS = Get-Credential "PFX password" -message "Enter the password for the ADFS PFX certificate"
if ($PfxPasswordADFS -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$ADFSuserCredential = Get-Credential $ADFSuser -message "Enter the password for the ADFS service account"
if ($ADFSuserCredential -eq $null) {
	write-host "No password entered, exiting"
	exit
}

if ((Test-Path $CertificateLocalPath) -eq $false) {
	mkdir $CertificateLocalPath
}

$CertificateADFS = "$CertificateADFSsubject.pfx"
$CertificateADFSremotePath = $CertificateRemotePath + $CertificateADFS

$CertificateADFSsubjectCN = "CN=" + $CertificateADFSsubject
cd $CertificateLocalPath

function New-CertificateRequest {
	param (
		[Parameter(Mandatory=$true, HelpMessage = "Please enter the subject beginning with CN=")]
		[ValidatePattern("CN=")]
		[string]$subject,
		[Parameter(Mandatory=$false, HelpMessage = "Please enter the SAN domains as a comma separated list")]
		[array]$SANs,
		[Parameter(Mandatory=$false, HelpMessage = "Please enter the Online Certificate Authority")]
		[string]$OnlineCA,
		[Parameter(Mandatory=$false, HelpMessage = "Please enter the Online Certificate Authority")]
		[string]$CATemplate = "WebServer"
	)

	### Preparation
	$subjectDomain = $subject.split(',')[0].split('=')[1]
	if ($subjectDomain -match "\*.") {
		$subjectDomain = $subjectDomain -replace "\*", "star"
	}
	$CertificateINI = "$subjectDomain.ini"
	$CertificateREQ = "$subjectDomain.req"
	$CertificateRSP = "$subjectDomain.rsp"
	$CertificateCER = "$subjectDomain.cer"

	### INI file generation
	new-item -type file $CertificateINI -force
	add-content $CertificateINI '[Version]'
	add-content $CertificateINI 'Signature="$Windows NT$"'
	add-content $CertificateINI ''
	add-content $CertificateINI '[NewRequest]'
	$temp = 'Subject="' + $subject + '"'
	add-content $CertificateINI $temp
	add-content $CertificateINI 'Exportable=TRUE'
	add-content $CertificateINI 'KeyLength=2048'
	add-content $CertificateINI 'KeySpec=1'
	add-content $CertificateINI 'KeyUsage=0xA0'
	add-content $CertificateINI 'MachineKeySet=True'
	add-content $CertificateINI 'ProviderName="Microsoft RSA SChannel Cryptographic Provider"'
	add-content $CertificateINI 'ProviderType=12'
	add-content $CertificateINI 'SMIME=FALSE'
	add-content $CertificateINI 'RequestType=PKCS10'
	add-content $CertificateINI '[Strings]'
	add-content $CertificateINI 'szOID_ENHANCED_KEY_USAGE = "2.5.29.37"'
	add-content $CertificateINI 'szOID_PKIX_KP_SERVER_AUTH = "1.3.6.1.5.5.7.3.1"'
	add-content $CertificateINI 'szOID_PKIX_KP_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2"'
	if ($SANs) {
		add-content $CertificateINI 'szOID_SUBJECT_ALT_NAME2 = "2.5.29.17"'
		add-content $CertificateINI '[Extensions]'
		add-content $CertificateINI '2.5.29.17 = "{text}"'

		foreach ($SAN in $SANs) {
			$temp = '_continue_ = "dns=' + $SAN + '&"'
			add-content $CertificateINI $temp
		}
	}

	### Certificate request generation
	if (test-path $CertificateREQ) {del $CertificateREQ}
	certreq -new $CertificateINI $CertificateREQ

	### Online certificate request and import
	if ($OnlineCA) {
		if (test-path $CertificateCER) {del $CertificateCER}
		if (test-path $CertificateRSP) {del $CertificateRSP}
		certreq -submit -attrib "CertificateTemplate:$CATemplate" -config $OnlineCA $CertificateREQ $CertificateCER

		certreq -accept $CertificateCER
	}
}

### Script

if ($OnlineRequest) {

    # Certificate creation
    New-CertificateRequest -subject $CertificateADFSsubjectCN -OnlineCA $CertificateAuthority

    # Certificate export
    $ADFScertificate = dir Cert:\LocalMachine\My | where {$_.subject -match $CertificateADFSsubject}
    if (($ADFScertificate.count) -gt 1) {
	    write-host -foregroundcolor Yellow "`n`nDuplicate certifcates! Delete all certificates with the subject $CertificateADFSsubject"
	    exit
    }
    Export-PfxCertificate -Cert $ADFScertificate -FilePath $CertificateADFSremotePath -Password $PfxPasswordADFS.Password
}
else {
    Import-PfxCertificate –FilePath $CertificateADFSremotePath -CertStoreLocation cert:\localMachine\my -Password $PfxPasswordADFS.Password
}

# ADFS Install
Add-WindowsFeature ADFS-Federation -IncludeManagementTools
Import-Module ADFS
$CertificateThumbprint = (dir Cert:\LocalMachine\My | where {$_.subject -match $CertificateADFSsubjectCN}).thumbprint
Install-AdfsFarm -CertificateThumbprint $CertificateThumbprint -FederationServiceDisplayName $ADFSdisplayName -FederationServiceName $CertificateADFSsubject -ServiceAccountCredential $ADFSuserCredential

# ADFS Non Claims Aware Relying Party Trust
Add-AdfsNonClaimsAwareRelyingPartyTrust -Name $RelyingPartyTrustExchangeName -Identifier $RelyingPartyTrustExchangeURI -IssuanceAuthorizationRules $RelyingPartyTrustExchangeIssuanceRule

$Finish = get-date
$Elapsed = $finish - $start
"Elapsed time: {0:mm} minutes and {0:ss} seconds" -f $Elapsed

Setup-ADFSserver2.ps1

Usage: .\Setup-ADFSserver2.ps1

$Start = get-date

### Variables
$CertificateADFSsubject = "adfs2.showcase.kloud.com.au"
$ADFSprimaryServer = "2012R2ADFS1.kloud.internal"
$ADFSuser = "kloud\ADFSfarm2012R2"
$CertificateLocalPath = "C:\Certificates\"
$CertificateRemotePath = "\\dc01.kloud.internal\certificates\"

### Preparation
$PfxPasswordADFS = Get-Credential "PFX password" -message "Enter the password for the PFX certificate"
if ($PfxPasswordADFS -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$ADFSuserCredential = Get-Credential $ADFSuser -message "Enter the password for the ADFS service account"
if ($ADFSuserCredential -eq $null) {
	write-host "No password entered, exiting"
	exit
}

$CertificateADFS = "$CertificateADFSsubject.pfx"
$CertificateADFSremotePath = $CertificateRemotePath + $CertificateADFS

### Script
# Import ADFS Certificate
if ((Test-Path $CertificateLocalPath) -eq $false) {
	mkdir $CertificateLocalPath
}
cd $CertificateLocalPath
Import-PfxCertificate –FilePath $CertificateADFSremotePath -CertStoreLocation cert:\localMachine\my -Password $PfxPasswordADFS.Password
$CertificateThumbprint = (dir Cert:\LocalMachine\My | where {$_.subject -match $ADFSfqdn}).thumbprint

# ADFS Install
Add-WindowsFeature ADFS-Federation -IncludeManagementTools
Import-Module ADFS
Add-AdfsFarmNode -CertificateThumbprint $CertificateThumbprint -ServiceAccountCredential $ADFSuserCredential -PrimaryComputerName $ADFSprimaryServer -PrimaryComputerPort 80

$Finish = get-date
$Elapsed = $finish - $start
"Elapsed time: {0:mm} minutes and {0:ss} seconds" -f $Elapsed

Setup-WAPserver1.ps1

Usage:

  • .\Setup-WAPserver1.ps1
  • .\Setup-WAPserver1.ps1 –OnlineRequest
param (
	[switch]$OnlineRequest
)

$Start = get-date

### Variables
# WAP and ADFS
$CertificateWAPsubject = "mail.showcase.kloud.com.au"
$CertificateADFSsubject = "adfs2.showcase.kloud.com.au"
$CertificateWAPsans = "mail.showcase.kloud.com.au","autodiscover.showcase.kloud.com.au","lyncws.showcase.kloud.com.au","lyncdiscover.showcase.kloud.com.au","meet.showcase.kloud.com.au","dialin.showcase.kloud.com.au","owas.showcase.kloud.com.au","teams.showcase.kloud.com.au","my.showcase.kloud.com.au"
$ADFSadministrator = "kloud\administrator"
$CertificateRemotePath = "\\dc01.kloud.internal\certificates\"
$CertificateLocalPath = "C:\Certificates\"
# Lync
$LyncDomain = "showcase.kloud.com.au"
$LyncWebServicesRoot = "lyncws."
$OfficeWebAppsRoot = "owas."
# Exchange
$AdfsExchangeRelyingParty = "Exchange"
$ExchangeExternalURL = "https://mail.showcase.kloud.com.au/"
$ExchangeAutodiscoverURL = "https://autodiscover.showcase.kloud.com.au/"
$ExchangeInternalURL = "https://ex2010-01.kloud.internal/"
$ExchangeSPN = "http/ex2010-01.kloud.internal"
#SNI
$SniIPport = "0.0.0.0:443" # IP and port to bind to. 0.0.0.0:443 matches all
# Only for Online Request
$CertificateAuthority = "dc01.kloud.internal\kloud-Showcase-CA"

### Preparation
$PfxPasswordADFS = Get-Credential "ADFS PFX password" -message "Enter the password for the PFX certificate"
if ($PfxPasswordADFS -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$PfxPasswordWAP = Get-Credential "WAP PFX password" -message "Enter the password for the PFX certificate"
if ($PfxPasswordWAP -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$ADFScredentials = Get-Credential $ADFSadministrator -message "Enter the password for the ADFS service account"
if ($ADFScredentials -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$CertificateADFS = "$CertificateADFSsubject.pfx"
$CertificateWAP = "$CertificateWAPsubject.pfx"
$CertificateADFSremotePath = $CertificateRemotePath + $CertificateADFS
$CertificateWAPremotePath = $CertificateRemotePath + $CertificateWAP
$CertificateWAPsubjectCN = "CN=" + $CertificateWAPsubject

function New-CertificateRequest {
	param (
		[Parameter(Mandatory=$true, HelpMessage = "Please enter the subject beginning with CN=")]
		[ValidatePattern("CN=")]
		[string]$subject,
		[Parameter(Mandatory=$false, HelpMessage = "Please enter the SAN domains as a comma separated list")]
		[array]$SANs,
		[Parameter(Mandatory=$false, HelpMessage = "Please enter the Online Certificate Authority")]
		[string]$OnlineCA,
		[Parameter(Mandatory=$false, HelpMessage = "Please enter the Online Certificate Authority")]
		[string]$CATemplate = "WebServer"
	)

	### Preparation
	$subjectDomain = $subject.split(',')[0].split('=')[1]
	$CertificateINI = "$subjectDomain.ini"
	$CertificateREQ = "$subjectDomain.req"
	$CertificateRSP = "$subjectDomain.rsp"
	$CertificateCER = "$subjectDomain.cer"

	### INI file generation
	new-item -type file $CertificateINI -force
	add-content $CertificateINI '[Version]'
	add-content $CertificateINI 'Signature="$Windows NT$"'
	add-content $CertificateINI ''
	add-content $CertificateINI '[NewRequest]'
	$temp = 'Subject="' + $subject + '"'
	add-content $CertificateINI $temp
	add-content $CertificateINI 'Exportable=TRUE'
	add-content $CertificateINI 'KeyLength=2048'
	add-content $CertificateINI 'KeySpec=1'
	add-content $CertificateINI 'KeyUsage=0xA0'
	add-content $CertificateINI 'MachineKeySet=True'
	add-content $CertificateINI 'ProviderName="Microsoft RSA SChannel Cryptographic Provider"'
	add-content $CertificateINI 'ProviderType=12'
	add-content $CertificateINI 'SMIME=FALSE'
	add-content $CertificateINI 'RequestType=PKCS10'
	add-content $CertificateINI '[Strings]'
	add-content $CertificateINI 'szOID_ENHANCED_KEY_USAGE = "2.5.29.37"'
	add-content $CertificateINI 'szOID_PKIX_KP_SERVER_AUTH = "1.3.6.1.5.5.7.3.1"'
	add-content $CertificateINI 'szOID_PKIX_KP_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2"'
	if ($SANs) {
		add-content $CertificateINI 'szOID_SUBJECT_ALT_NAME2 = "2.5.29.17"'
		add-content $CertificateINI '[Extensions]'
		add-content $CertificateINI '2.5.29.17 = "{text}"'

		foreach ($SAN in $SANs) {
			$temp = '_continue_ = "dns=' + $SAN + '&"'
			add-content $CertificateINI $temp
		}
	}

	### Certificate request generation
	if (test-path $CertificateREQ) {del $CertificateREQ}
	certreq -new $CertificateINI $CertificateREQ

	### Online certificate request and import
	if ($OnlineCA) {
		if (test-path $CertificateCER) {del $CertificateCER}
		if (test-path $CertificateRSP) {del $CertificateRSP}
		certreq -submit -attrib "CertificateTemplate:$CATemplate" -config $OnlineCA $CertificateREQ $CertificateCER

		certreq -accept $CertificateCER
	}
}

### Script

# Import ADFS Certificate
if ((Test-Path $CertificateLocalPath) -eq $false) {
	mkdir $CertificateLocalPath
}
cd $CertificateLocalPath
Import-PfxCertificate –FilePath $CertificateADFSremotePath -CertStoreLocation cert:\localMachine\my -Password $PfxPasswordADFS.Password

if ($OnlineRequest) {
	# Request Web Application Proxy certificate
	New-CertificateRequest -subject $CertificateWAPsubjectCN -SANs $CertificateWAPsans -OnlineCA $CertificateAuthority

	# Export Web Application Proxy certificate
	$WAPcertificate = dir Cert:\LocalMachine\My | where {$_.subject -match $CertificateWAPsubject}
	if (($WAPcertificate.count) -gt 1) {
		write-host -foregroundcolor Yellow "`n`nDuplicate certifcates! Delete all certificates with the subject $CertificateWAPsubject"
		exit
	}
	Export-PfxCertificate -Cert $WAPcertificate -FilePath $CertificateWAPremotePath -Password $PfxPasswordWAP.Password
}
else {
Import-PfxCertificate –FilePath $CertificateWAPremotePath -CertStoreLocation cert:\localMachine\my -Password $PfxPasswordWAP.Password
}

# Add Web Application Proxy Role
Install-WindowsFeature Telnet-Client, RSAT-AD-PowerShell, Web-Application-Proxy -IncludeManagementTools

# Web Application Proxy Configuration Wizard
$CertificateADFSThumbprint = (dir Cert:\LocalMachine\My | where {$_.subject -match $CertificateADFSsubject}).thumbprint
Install-WebApplicationProxy -CertificateThumbprint $CertificateADFSThumbprint -FederationServiceName $CertificateADFSsubject -FederationServiceTrustCredential $ADFScredentials

## Web Application Proxy Applications
$CertificateWAPThumbprint = (dir Cert:\LocalMachine\My | where {$_.subject -match $CertificateWAPsubject}).thumbprint

# Lync
Add-WebApplicationProxyApplication -Name 'Lync Web Services' -ExternalPreAuthentication PassThrough -ExternalUrl "https://$LyncWebServicesRoot$LyncDomain/" -BackendServerUrl ("https://"+$LyncWebServicesRoot+$Lyncdomain+":4443/") -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Lync Lyncdiscover' -ExternalPreAuthentication PassThrough -ExternalUrl "https://lyncdiscover.$LyncDomain/" -BackendServerUrl ("https://lyncdiscover."+$LyncDomain+":4443/") -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Lync Dialin' -ExternalPreAuthentication PassThrough -ExternalUrl "https://dialin.$LyncDomain/" -BackendServerUrl ("https://dialin."+$LyncDomain+":4443/") -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Lync Meet' -ExternalPreAuthentication PassThrough -ExternalUrl "https://meet.$LyncDomain/" -BackendServerUrl ("https://meet."+$LyncDomain+":4443/") -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Office Web Apps Server' -ExternalPreAuthentication PassThrough -ExternalUrl "https://$OfficeWebAppsRoot$LyncDomain/" -BackendServerUrl ("https://"+$OfficeWebAppsRoot+$LyncDomain+"/") -ExternalCertificateThumbprint $CertificateWAPThumbprint

# Exchange

Add-WebApplicationProxyApplication -Name 'Exchange OWA' -ExternalPreAuthentication ADFS -ADFSRelyingPartyName $AdfsExchangeRelyingParty -ExternalUrl $ExchangeExternalURL"owa/" -BackendServerUrl $ExchangeInternalURL"owa/" -BackendServerAuthenticationSpn $ExchangeSPN -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Exchange ECP' -ExternalPreAuthentication ADFS -ADFSRelyingPartyName $AdfsExchangeRelyingParty -ExternalUrl $ExchangeExternalURL"ecp/" -BackendServerUrl $ExchangeInternalURL"ecp/" -BackendServerAuthenticationSpn $ExchangeSPN -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Exchange Autodiscover' -ExternalPreAuthentication PassThrough -ExternalUrl $ExchangeAutodiscoverURL"autodiscover/" -BackendServerUrl $ExchangeInternalURL"autodiscover/" -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Exchange Outlook Anywhere' -ExternalPreAuthentication PassThrough -ExternalUrl $ExchangeExternalURL"rpc/" -BackendServerUrl $ExchangeInternalURL"rpc/" -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Exchange OAB' -ExternalPreAuthentication PassThrough -ExternalUrl $ExchangeExternalURL"OAB/" -BackendServerUrl $ExchangeInternalURL"OAB/" -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Exchange EWS' -ExternalPreAuthentication PassThrough -ExternalUrl $ExchangeExternalURL"EWS/" -BackendServerUrl $ExchangeInternalURL"EWS/" -ExternalCertificateThumbprint $CertificateWAPThumbprint
Add-WebApplicationProxyApplication -Name 'Exchange ActiveSync' -ExternalPreAuthentication PassThrough -ExternalUrl $ExchangeExternalURL"Microsoft-Server-ActiveSync/" -BackendServerUrl $ExchangeInternalURL"Microsoft-Server-ActiveSync/" -ExternalCertificateThumbprint $CertificateWAPThumbprint

## Exchange SPN for KCD

Import-Module -Name ActiveDirectory
$WAPserver = Get-ADComputer (hostname)
Set-ADObject $WAPserver.DistinguishedName -Add @{"msDS-AllowedToDelegateTo" = $ExchangeSPN}
Set-ADObject $WAPserver.DistinguishedName -Replace @{"userAccountControl" = 16781312}

# Set default SNI binding
sleep 15 # sleep for 15 seconds to allow WAP bindings to be set
$certsList = @()
$certs = netsh http show sslcert | where {$_ -match ":port" -or $_ -match "Certificate Hash" -or $_ -match "Application ID"}
$certs = $certs.replace(" : ","#").replace(" ","")
$certs | foreach {
	if ($_ -match ":port") {
		$tempObject = New-Object -Type PSObject
		$tempObject | Add-Member -Type noteproperty -Name Binding -value $_.split("#")[1]
	}
	elseif ($_ -match "CertificateHash") {
		$tempObject | Add-Member -Type noteproperty -Name CertificateHash -value ($_.split("#")[1]-replace " ")
	}
	else {
		$tempObject | Add-Member -Type noteproperty -Name ApplicationID -value ($_.split("#")[1]-replace " ")
		$certsList += $tempObject
	}
}

$match = $certsList | where {$_.Binding -match $CertificateWAPsubject}
$CertificateHash = $match.CertificateHash
$ApplicationID = $match.ApplicationID

write-host -ForegroundColor Green "`n`nDefault bindings before changes"
netsh http show sslcert | where {$_ -match "ip:port"}

$command = "http add sslcert ipport=$SniIPport certhash=$CertificateHash appid=$ApplicationID"
write-host -ForegroundColor Yellow "`n`nAdding Certificate"
$command | netsh

write-host -ForegroundColor Yellow "`n`nDefault bindings after changes. Check that $SniIPport exists"
netsh http show sslcert | where {$_ -match "ip:port"}

$Finish = get-date
$Elapsed = $finish - $start
"Elapsed time: {0:mm} minutes and {0:ss} seconds" -f $Elapsed

Setup-WAPserver2.ps1

Usage: .\Setup-WAPserver2.ps1

$Start = get-date

### Variables
$CertificateWAPsubject = "mail.showcase.kloud.com.au"
$CertificateADFSsubject = "adfs2.showcase.kloud.com.au"
$ADFSadministrator = "kloud\administrator"
$CertificateRemotePath = "\\dc01.kloud.internal\certificates\"
$CertificateLocalPath = "C:\Certificates\"
$ExchangeSPN = "http/ex2010-01.kloud.internal"
$SniIPport = "0.0.0.0:443" # IP and port to bind to. 0.0.0.0:443 matches all

### Preparation
$PfxPasswordADFS = Get-Credential "ADFS PFX password" -message "Enter the password for the PFX certificate"
if ($PfxPasswordADFS -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$PfxPasswordWAP = Get-Credential "WAP PFX password" -message "Enter the password for the PFX certificate"
if ($PfxPasswordWAP -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$ADFScredentials = Get-Credential $ADFSadministrator -message "Enter the password for the ADFS service account"
if ($ADFScredentials -eq $null) {
	write-host "No password entered, exiting"
	exit
}
$CertificateADFS = "$CertificateADFSsubject.pfx"
$CertificateWAP = "$CertificateWAPsubject.pfx"
$CertificateADFSremotePath = $CertificateRemotePath + $CertificateADFS
$CertificateWAPremotePath = $CertificateRemotePath + $CertificateWAP
$CertificateWAPsubjectCN = "CN=" + $CertificateWAPsubject

### Script

# Import ADFS Certificate
if ((Test-Path $CertificateLocalPath) -eq $false) {
	mkdir $CertificateLocalPath
}
cd $CertificateLocalPath
Import-PfxCertificate –FilePath $CertificateADFSremotePath -CertStoreLocation cert:\localMachine\my -Password $PfxPasswordADFS.Password

# Import WAP Certificate
Import-PfxCertificate –FilePath $CertificateWAPremotePath -CertStoreLocation cert:\localMachine\my -Password $PfxPasswordWAP.Password

# Add Web Application Proxy Role
Install-WindowsFeature Telnet-Client, RSAT-AD-PowerShell, Web-Application-Proxy -IncludeManagementTools

# Web Application Proxy Configuration Wizard
$CertificateADFSThumbprint = (dir Cert:\LocalMachine\My | where {$_.subject -match $CertificateADFSsubject}).thumbprint
Install-WebApplicationProxy -CertificateThumbprint $CertificateADFSThumbprint -FederationServiceName $CertificateADFSsubject -FederationServiceTrustCredential $ADFScredentials

## Exchange SPN for KCD
Import-Module -Name ActiveDirectory
$WAPserver = Get-ADComputer (hostname)
Set-ADObject $WAPserver.DistinguishedName -Add @{"msDS-AllowedToDelegateTo" = $ExchangeSPN}
Set-ADObject $WAPserver.DistinguishedName -Replace @{"userAccountControl" = 16781312}

# Set default SNI binding
sleep 15 # sleep for 15 seconds to allow WAP bindings to be set
$certsList = @()
$certs = netsh http show sslcert | where {$_ -match ":port" -or $_ -match "Certificate Hash" -or $_ -match "Application ID"}
$certs = $certs.replace(" : ","#").replace(" ","")
$certs | foreach {
	if ($_ -match ":port") {
		$tempObject = New-Object -Type PSObject
		$tempObject | Add-Member -Type noteproperty -Name Binding -value $_.split("#")[1]
	}
	elseif ($_ -match "CertificateHash") {
		$tempObject | Add-Member -Type noteproperty -Name CertificateHash -value ($_.split("#")[1]-replace " ")
	}
	else {
		$tempObject | Add-Member -Type noteproperty -Name ApplicationID -value ($_.split("#")[1]-replace " ")
		$certsList += $tempObject
	}
}

$match = $certsList | where {$_.Binding -match $CertificateWAPsubject}
$CertificateHash = $match.CertificateHash
$ApplicationID = $match.ApplicationID

write-host -ForegroundColor Green "`n`nDefault bindings before changes"
netsh http show sslcert | where {$_ -match "ip:port"}

$command = "http add sslcert ipport=$SniIPport certhash=$CertificateHash appid=$ApplicationID"
write-host -ForegroundColor Yellow "`n`nAdding Certificate"
$command | netsh

write-host -ForegroundColor Yellow "`n`nDefault bindings after changes. Check that $SniIPport exists"
netsh http show sslcert | where {$_ -match "ip:port"}

$Finish = get-date
$Elapsed = $finish - $start
"Elapsed time: {0:mm} minutes and {0:ss} seconds" -f $Elapsed

Add the servers to the load balancer, external DNS entries and you should be able to login to OWA and join a Lync meeting.

Category:
ADFS, Communication and Collaboration, Exchange, Lync
Tags:
, , ,

Join the conversation! 17 Comments

  1. […] Updated PowerShell deployment of Web Application Proxy blog after testing with RTM – see https://blog.kloud.com.au/2013/08/14/powershell-deployment-of-web-application-proxy-and-adfs-in-under… […]

  2. Marc,
    it looks like your scripts will save a lot of time, to get Exchange and Lync external services working.

    I have two Questions:
    Is it possible to use your scripts without any problems on Servers 2012R2 in different Language Version than english, please?
    You are publishing Exchange 2013 and Lync 2013, but will it also work if we are using the older Exchange 2010 and Lync 2010? Must I have a look to anything?

    Thanks
    Andreas

    • I have not tried it on different languages. I think most of it will work as the PowerShell cmdlets should be the same but you may hit issues with the functions and netsh command structure. Let me know how it goes.

      I have done some basic testing with Exchange 2010 OWA and Outlook Anywhere with Windows 2012 R2 RTM which worked. I haven’t tested Lync 2010 – it is just pass through so should work fine.

      • Thanks for answering my questions. Right now I went through the installation steps. Do I understand it the right way, that the AD-FS Service is not allowed to install together with the Web Application Proxy at the same Host?
        If so, is it a must to use the AD-FS on a Windows 2012R2 Server, or could it be used on a Windows Server 2008R2, instead of 2012R2? Is it right, that the Host where AD-FS is installed to must have one Interfaces that is direct facing to the Internet (DMZ)?
        I am sorry for this questions, but I am new with that Service.

      • Web Application Proxy and the ADFS farm must be on different hosts. Web Application Proxy requires Windows 2012 R2 ADFS, it will not work with a previous ADFS version. There are ADFS proxies for older versions of ADFS that will provide the ADFS authentication for external clients but they cannot be a reverse proxy.

        The ADFS farm server will not have an external/DMZ interface – that is what the WAP is for. The WAP server needs to be accessible from the Internet, but this can be a public interface, NAT or via a load balancer.

      • I am very appreciated for your great help. Hopefully you can answer me also this questions. Both Servers, the WAP and the AD-FS must be domain members? Pointing to the internal interface of Lync is to the Lync Director or to the Lync Edgeservers internal IP? Many thanks!!

      • The WAP server does not have to be domain joined to publish pass-through applications like Lync or act as an ADFS proxy. It does need to be domain joined if you want to publish non-claims aware applications using Kerberos Constrained Delegation. For the Lync question you need to publish both the Lync External Web Services URL for the pool and the Director pool External Web Services URL. These servers will usually only have a single network interface and you will publish port 4443. The Edge server is not involved in the reverse proxy scenario. See this for more information http://blog.schertz.name/2011/03/publishing-lync-director-web-services/

        For your other comment that was posted as a different user: remote support isn’t something offered as part of the blog. If you need support I think you should contact Microsoft or find a Microsoft partner in Germany who can assist. I suggest not using the script if it is showing language errors – just install ADFS on a domain joined machine and the Web Application Proxy on another server (either domain joined or not) using the wizards.

  3. I hadn’t see your scripts until now and I can’t wait to use them.

    I installed using the wizard. Do you know how the ADFS ProxyTrust certificates that are autogenerated by the WAP configuration wizard function? I have them in the private store on each of two WAP servers. I was expecting to see one of each of these certs in the AdfsTrustedDevices store on the ADFS servers, but on the load balanced ADFS farm servers I see both certificates multiple times on the first server, and erratic results on the second server in the farm.

    I seemingly get a new one every time I run the wizard and they are typically only valid for either 5 or 20 days. My main problem is that they seem to expire and I lose trust. Is this the WAP Certificate you are referring to in your scripts or are you referring to the certificate used by your applications? Or are they one in the same for your scripts?

    • The WAP certificate I was referring to is the reverse proxy certificate that has the published application FQDNs. I do not do anything with the ADFS ProxyTrust certificate.

      The ADFS ProxyTrust certificates are essentially device certificates and should be visible on the AdfsTrustedDevices computer certificate store on the ADFS farm server (and I would expect them to be on all farm members). They should be updated automatically as part of the trust process and you will see event ID 396 in the WAP ‘AD FS / Admin’ Event Log saying ‘The trust between the federation server proxy and the Federation Service was renewed successfully.’ When the servers are continually running I haven’t had an issue with losing trust but I did with some VMs that were off when the trust renewal should have taken place.

      One strange thing I noticed is that on the WAP server I don’t see any ADFS ProxyTrust valid certificates – the 20 day valid certificate expired yesterday 30 September. However the ADFS server has an ADFS ProxyTrust certificate for that WAP server valid from 23 September (the last time the trust was renewed) to 13 October. ADFS authentication does work through the WAP server so the certificate is not showing up.

  4. Hi Marc, is it still a requirement to install the ADFS Role on a DC?

    • It was never a requirement, but apparently is recommended now with 2012 R2. You can install it on a member server, even if you don’t have any 2012 R2 domain controllers

  5. Hi Marc,
    Great post thanks. You mention that “In order to publish Exchange OWA and ECP applications with ADFS authentication, the Web Application Proxy servers must be domain joined in order to perform KCD”
    As most people intend putting their WAP servers in DMZ they will not be willing to put domain joined servers in DMZ so I can see this as been a show stopper.

    • That will be a problem for some people, but KCD requires the server performing KCD to be domain joined. The TMG recommendation has always been to domain join and UAG required being domain joined too. So it is nothing new for Microsoft revers proxy products. What most of our clients do is have an external DMZ and an internal DMZ. The server has two interfaces and they can restrict traffic via the firewall.

      If you just want unauthenticated pass through traffic and, ADFS and claims based authentication it does not need to be domain joined. The Exchange team have stated that they do not require pre-authentication and the traffic can be passed to Exchange to authenticate.

  6. Hello Marc,

    We are running an Exchange 2012 SP3 farm for customers. Currently all customers use our active directory as account storage and login using their UPN. However, we got a request from a prospect if it’s possible if they use our hosted exchange mailboxes but with their own usernames + passwords. So my question is; could we deliver this through ADFS Web App proxy? We are already runing 2012 R2 domain controllers. What’s not clear to me is how this will work if the customers account will change his/her password, will it still work on our end?
    And is it a problem if the SAM Accountnames do not match? UPN ofcourse does.

    Tnx!

    • This will depend on whether you have an AD forest trust in place. If you use a trust you can have linked mailboxes and Exchange can authenticate directly and you could use Web Application Proxy in pass through mode or any other reverse proxy. If you setup ADFS and use WAP as an ADFS proxy, you can chain the ADFS STS and redirect to their ADFS for authentication for claims aware applications in the passive profile. The authentication is always going to the customer ADFS and using their Active Directory credentials.
      Exchange is not claims aware though so I’m not sure how you would do this. Office 365 is doing something similar but they have hooks into the authentication stack that I haven’t seen publicly available. In order for Outlook and ActiveSync to work it takes the authentication credentials and proxies authentication to ADFS.

      • Ok but what’s the benefit of exchange publishing through ADFS then? You authenticate always on the same AD domain, with already local useraccounts..

      • The ADFS / Web Application Proxy is just doing pre-authentication against the domain controllers in the domain the ADFS server is a member of. If you have a trust in place you can authenticate against domain controllers in another forest. You don’t have to have ADFS / WAP to do it.

Comments are closed.

%d bloggers like this: