Using a local ADFS with Exchange 15 Update 1

As it seems the explanations from my older post (see here: Using a local ADFS with Exchange) is okay for Exchange 15, but is not okay for Exchange 15 with CU1 proxying to a backend E14 SP3 Server (especially in the barrier-free version).

The problem, which we encountered happens, because our custom WSFederationAuthenticationModule still opens the request and the proxy does not like that at all.
So there need to be some adjustments to make it work again.

1) Use another realm in System.IdentityModel
2) Use another WSFederationAuthenticationModule
3) Switch off the rewriting of http://mail.contoso.com/ to http://mail.contoso.com/owa/ (redirecting is fine, but rewriting is baaaaaaaad).

1. Use another Realm
The base problem is opening the request to examine if there is any ADFS-related data present. To bypass this we need a way to identify request, which are not authenticated or are targeted to the WSFederation endpoint. Luckyly we can just change the realm to an non-present folder in Exchange.

<federationConfiguration>
    <cookieHandler requireSsl="false" />
    <wsFederation passiveRedirectEnabled="true"
                  issuer="https://login.contoso.de/adfs/ls/"
                  realm="https://mail.contoso.de/owa/adfs/" <!-- This here changed: /adfs/ is a non-existent path -->
                  requireHttps="true" />
</federationConfiguration>

Make sure you use this realm as WSFederation-Endpoint in your ADFS configuration.

To complete it we have to reconfigure the audience uris:

<audienceUris>
  <add value="https://mail.contoso.de/" /> <!-- probably you can remove this one, but it does not hurt to have it -->
  <add value="https://mail.contoso.de/owa/" />
  <add value="https://mail.contoso.de/owa/adfs/" />
</audienceUris>

2. WSFederationAuthenticationModule
The older custom WSFederationAuthenticaionModule will open the request, so it is not suitable. This here will fix the problem for (and that has to be assumed here) the realm being a non-existent address

namespace ZDV.IdentityModel.Services
{
    class ExchangeFederationAuthenticationModule : System.IdentityModel.Services.WSFederationAuthenticationModule
    {
        protected override void OnAuthenticateRequest(object sender, EventArgs args)
        {
            var skip = Thread.CurrentPrincipal != null;

            try
            {
                var passiveEndpoint = System.IdentityModel.Services.FederatedAuthentication
                                            .FederationConfiguration
                                            .WsFederationConfiguration
                                            .Realm;

                var passiveEndpointUri = new Uri(passiveEndpoint);
                skip = skip && Uri.Compare(passiveEndpointUri, HttpContext.Current.Request.Url,
                                           UriComponents.Path, UriFormat.UriEscaped,
                                           StringComparison.OrdinalIgnoreCase) != 0;
            }
            catch (Exception)
            {
                skip = false;
            }

            if (skip) return;

            base.OnAuthenticateRequest(sender, args);
        }
    }
}

Adjust the names in the web.config and

<location inheritInChildApplications="false">
    <system.webServer>
      <serverRuntime uploadReadAheadSize="0" />
      <modules>
        <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="" />
        <add name="WSFederationAuthenticationModule" type="ZDV.IdentityModel.Services.ExchangeFederationAuthenticationModule, ZDV.IdentityModel.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e09b5e339c6d1313" preCondition="" />
        <!-- Exchange other Module definitions -->
      </modules>
      <!-- Lots of other config -->
    <system.webServer>
</location>
<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity name="ZDV.IdentityModel.Services" publicKeyToken="e09b5e339c6d1313" culture="neutral" />
      <codeBase version="1.0.0.0" href="file:///C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\bin\ZDV.IdentityModel.Services.dll" />
    </dependentAssembly>
    <!-- Lots of dependentAssemblies here -->
  </assemblyBinding>
</runtime>

3. Remove Rewriting
The update adds an rewriting mechanism to the wwwroot, which should be removed and replaced by some redirections:

<configuration>
  <system.webServer>
        <rewrite>
            <rules>
                <clear />
                <rule name="Trailing Slash OWA ECP" stopProcessing="true">
                    <match url="(.*[^/])$" />
                    <conditions logicalGrouping="MatchAny" trackAllCaptures="false">
                        <add input="{URL}" pattern="^/owa$" />
                        <add input="{URL}" pattern="^/ecp$" />
                    </conditions>
                    <action type="Redirect" url="{R:1}/" />
                </rule>
                <!--
                <rule name="Redirect Logon Requests" stopProcessing="true">
                    <match url="^owa/auth/logon\.aspx$" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="https://mail.uni-mainz.de/owa/" />
                </rule> -->
                <rule name="Redirect Logout Requests" stopProcessing="true">
                    <match url="^owa/logoff\.owa$" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="https://login.uni-mainz.de/adfs/ls/?wa=wsignout1.0" />
                </rule>
                <rule name="Redirect Logout Requests (alt)" stopProcessing="true">
                    <match url="^owa/auth/logoff\.aspx$" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="https://login.uni-mainz.de/adfs/ls/?wa=wsignout1.0" />
                </rule>
                <rule name="Redirect mail.students" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
                        <add input="{HTTP_HOST}" pattern="mail.students.uni-mainz.de" />
                    </conditions>
                    <action type="Redirect" url="https://mail.uni-mainz.de/{R:1}" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
<!-- We do not need this here!
  <location inheritInChildApplications="false">
    <system.webServer>
      <modules>
        <add name="OwaUrlModule" type="Microsoft.Exchange.HttpProxy.OwaUrlModule,Microsoft.Exchange.OwaUrlModule,Version=15.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35" preCondition="" />
      </modules>
    </system.webServer>
    <system.web>
      <machineKey validationKey="AutoGenerate,IsolateApps" />
      <compilation defaultLanguage="c#" debug="false">
        <assemblies>
          <add assembly="Microsoft.Exchange.OwaUrlModule, Version=15.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        </assemblies>
      </compilation>
    </system.web>
  </location>
  -->
</configuration>
Advertisements

Using a local ADFS with Exchange 2013

Be sure to read the updates for CU1 (which will run with E15 “baseline”, too): Adjustments for E15 CU1

Some weeks (or month) ago we set up some Exchange 2013 (E15) Servers. Four of them are frontend servers with (among other services) OWA and ECP running.
Since we have lots of non-domain computers here at the university, we try to use ADFS for authentication where possible (we had a running E14 setup as well).

While using ADFS with V14 wasn’t that easy, setting up E15 to use ADFS (V2.0) worked like a charm – nearly.
In my description below, i will only write about OWA, since ECP will be very similar.

Prequisites

  • A working installation of E15, configured for Windows-Integrated-Authentication with OWA
  • A working setup of ADFS (at least V2.0) running somewhere in your domain
  • The OWA Appliction-Pool-Accounts should have the right to impersonate users (in our setup OWA runs as SYSTEM)

Tools

  • Fiddler will be useful to debug
  • WinMerge was handy to synchronize the Web.config files
  • Visual-Studio (C#/VB Express will do)

1. Setting up the RelyingParty
Since Microsoft doesn’t support FedUtil anymore (or doesn’t support it for WIF on .NET 4.5) we will have to create the RelyingParty manually. Important are the following settings:

  1. ADFS 2.0 Profile
  2. Passive Federation Endpoint pointing to: https://mail.contoso.com/owa/

We will only submit two claims: UPN and Name.
Name will be used for entries into the IIS-Log, so use Windows-Account-Name or UPN for that.
UPN will be used to impersonate the user and only the UserPrincipalName from the active directory will be acceptable here.
Create a custom rule with the code below or use one of the templates to create that.


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", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
query = ";userPrincipalName,userPrincipalName;{0}",
param = c.Value);

2. Configuring <System.IdentityModel>

Straight forward here. Change this xml for your environment and add it to the location-Tag inside the Web.config.

<system.identityModel>
  <identityConfiguration>
    <securityTokenHandlers>
      <remove type="System.IdentityModel.Tokens.SamlSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <remove type="System.IdentityModel.Tokens.Saml2SecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <add type="System.IdentityModel.Tokens.SamlSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <samlSecurityTokenRequirement mapToWindows="true" />
      </add>
      <add type="System.IdentityModel.Tokens.Saml2SecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <samlSecurityTokenRequirement mapToWindows="true" />
      </add>
      <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </securityTokenHandlers>
    <audienceUris>
      <add value="https://mail.contoso.com/owa/" />
    </audienceUris>
    <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <trustedIssuers>
        <add thumbprint="WELLWHATANICECERTIFICATETHUMBPRINTYOUHAVE" name="https://login.contoso.com/adfs/services/trust" />
      </trustedIssuers>
    </issuerNameRegistry>
  </identityConfiguration>
</system.identityModel>
<system.identityModel.services>
  <federationConfiguration>
    <cookieHandler requireSsl="true" />
    <wsFederation passiveRedirectEnabled="true" issuer="https://login.contoso.com/adfs/ls/" realm="https://mail.contoso.com/owa/" requireHttps="true" />
  </federationConfiguration>
</system.identityModel.services>

3. Modifying other parts of the Web.Config
Now we have to get IIS to load System.IdentityModel and use it as authentication.
I will omit lot’s of code, so you probably have to look a little bit, where to insert it.

<configuration>
  <configSections>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  </configSections>

  <location inheritInChildApplications="false">
    <system.webServer>
      <modules>
        <!-- [ADFS] -->
	<add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="" />
	<add name="WSFederationAuthenticationModule" type="ZDV.IdentityModel.Services.WSFederationAuthenticationModule, ZDV.IdentityModel.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=publicToken" preCondition="" /> <!-- See below -->
          <!-- The default config would be the following, but this will lead to errors, if a user tries to append files with the HTML 4.1 file upload -->
          <!-- 
          <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="" />
          -->
        <!-- [/ADFS] --> 
        <remove name="ServiceModel" />
        <remove name="ServiceModel-4.0" />
        <remove name="Session" />
        <remove name="Profile" />
        <add name="HttpProxy" type="Microsoft.Exchange.HttpProxy.ProxyModule,Microsoft.Exchange.FrontEndHttpProxy,Version=15.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35" preCondition="" />
        <add name="cafe_exppw" />
      </modules>
      <!-- .. something here .. -->
    </system.webServer>
    <system.web>
      <machineKey decryptionKey="ABC" validationKey="ABC" /> <!-- Set the machine key farm wide, or else ADFS will not be able to use a non-sticky load-balancer environment -->
      <!-- .. something here .. -->
      <httpRuntime maxUrlLength="500" maxRequestLength="35000" requestValidationMode="4.5" /><!-- [ADFS]The requestValidationMode has to be WIF compatible[/ADFS] -->
      <!-- .. something here .. -->
    </system.web>
  </location>

  <appSettings>
    <!-- .. something here .. -->
    <!-- [ADFS] -->
    <add key="ida:FederationMetadataLocation" value="https://login.uni-mainz.de/FederationMetadata/2007-06/FederationMetadata.xml" />
    <add key="ida:Issuer" value="https://login.uni-mainz.de/adfs/ls/" />
    <add key="ida:ProviderSelection" value="productionSTS" />
    <!-- [/ADFS] -->
  </appSettings>

  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="ZDV.IdentityModel.Services" publicKeyToken="publicKeyToken" culture="neutral" />
        <codeBase version="1.0.0.0" href="file:///C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\bin\ZDV.IdentityModel.Services.dll" />
      </dependentAssembly>
      <!-- .. something here .. -->
    </assemblyBinding>
  </runtime>
</configuration>

As mentioned the upload by HTML 4.1 can make problems. It seems that this comes from loading the request inside the WSFederation-Authentication Module.
To bypass this, we overwrote the default module with our own:

using System;
using System.Linq;
using System.Threading;
using System.Web;

namespace ZDV.IdentityModel.Services
{
    public class WSFederationAuthenticationModule : System.IdentityModel.Services.WSFederationAuthenticationModule
    {
        protected override void OnAuthenticateRequest(object sender, EventArgs args)
        {
            bool skip = Thread.CurrentPrincipal != null;
            
            //skip = skip && Thread.CurrentPrincipal.Identity.IsAuthenticated;
            skip = skip && HttpContext.Current != null &&
                !HttpContext.Current.Request
                            .QueryString
                            .AllKeys
                            .Any(key => String.Equals(key, "wa", StringComparison.OrdinalIgnoreCase));
            skip = skip && (
                HttpContext.Current.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) &&
                !HttpContext.Current.Request
                           .Form
                           .AllKeys
                           .Any(key => String.Equals(key, "wa", StringComparison.OrdinalIgnoreCase)));

            if(skip) return;

            base.OnAuthenticateRequest(sender, args);
        }
    }
}

Build it and deploy it to the Exchange/V15/HttpProxy/bin Directory.

4. Redirections

Since E15 doesn’t know about ADFS now, the logout has to be redirected.
URL Rewrite (from IIS.net) will do the job for us with the following configuration:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <clear />
                <rule name="Trailing Slash OWA ECP" stopProcessing="true">
                    <match url="(.*[^/])$" />
                    <conditions logicalGrouping="MatchAny" trackAllCaptures="false">
                        <add input="{URL}" pattern="^/owa$" />
                        <add input="{URL}" pattern="^/ecp$" />
                    </conditions>
                    <action type="Redirect" url="{R:1}/" />
                </rule>
                <rule name="Redirect Logon Requests" stopProcessing="true">
                    <match url="^owa/auth/logon\.aspx$" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="https://mail.contoso.com/owa/" />
                </rule>
                <rule name="Redirect Logout Requests V14" stopProcessing="true">
                    <match url="^owa/auth/logoff\.aspx$" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="https://login.contoso.com/adfs/ls/?wa=wsignout1.0" />
                </rule>
                <rule name="Redirect Logout Requests V15" stopProcessing="true">
                    <match url="^owa/logoff\.owa$" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="https://login.contoso.com/adfs/ls/?wa=wsignout1.0" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>