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>
Leave a comment

6 Comments

  1. dotnetlabs

     /  May 16, 2013

    Thanks for this post, can you just specified path of web.config that we must modify.

    Reply
    • The Files are located in C:\Program Files\Microsoft\Exchange\V15\HttpProxy\owa (or ecp).
      Be careful, my article has some backdraws with E15 CU1. I will update it next week.

      Reply
  2. Jonas

     /  July 19, 2013

    Is it possible to do a similar integration with ADFS 1.0. I’m currently in a project where I’m dependent on ADFS 1.0 as the STS.

    Reply
    • Well, the whole system stands and falls with the “mapToWindows” Attribute of the TokenHandler and as i see it, that is independent of the IdentityProvider since it is part of .NET itself.

      Reply
  3. yeldar

     /  October 22, 2013

    hi. i try reproduce your configuration, but after all steps owa didn’t redirect me to adfs and have error 500, but no errors on events. I use exchange 2013 + adfs 2.0. Do your really do this SSO scheme for OWA 13 and other applications with adfs?

    Reply
    • Yes we are using this on a farm with 4 Servers. On OWA and ECP.
      Switch on failed Request tracing to see, where it fails.

      Reply

Leave a reply to Thomas Cancel reply