ADFS/C2WTS identity impersonation failure after an IIS reset

My team recently encountered an issue at a Relying Party website where identity impersonation in combination with ADFS authentication and the Claims to Windows Token service (C2WTS) fails after an IIS reset is performed.

Reproducing the problem

The problem is easily reproduced using the following steps:

  • Create an empty ASP.NET website and set up federation with ADFS
    • Ensure that a UPN claim is emitted by ADFS
  • Modify C:\Program Files\Windows Identity Foundation\v3.5\c2wtshost.exe.config to allow access to authenticated users

Code Snippet

  1.   <windowsTokenService>
  2.     <!--
  3.         By default no callers are allowed to use the Windows Identity Foundation Claims To NT Token Service.
  4.         Add the identities you wish to allow below.
  5.       -->
  6.     <allowedCallers>
  7.       <clear/>
  8.       <!-- <add value="NT AUTHORITY\Network Service" /> -->
  9.       <!-- <add value="NT AUTHORITY\Local Service" /> -->
  10.       <!-- <add value="NT AUTHORITY\System" /> -->
  11.        <add value="NT AUTHORITY\Authenticated Users" />
  12.     </allowedCallers>
  13.   </windowsTokenService>
  • Start the C2WTS service via start-run-services.msc
  • Modify the ASP.NET web.config file to enable identity impersonation

Code Snippet

  1.   <system.web>
  2.     <identity impersonate="true"/>
  3.     ...
  • Modify the ASP.NET web.config file to enable communication with the C2WTS service

Code Snippet

  1.   <microsoft.identityModel>
  2.     <service>
  3.       ...
  4.       <securityTokenHandlers>
  5.         <add type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
  6.           <samlSecurityTokenRequirement mapToWindows="true" useWindowsTokenService="true" />
  7.         </add>
  8.       </securityTokenHandlers>
  9.       ...
  10.     </service>
  11.   </microsoft.identityModel>
  • Log in via ADFS
  • Perform an IIS reset
  • Refresh the page

The following error will occur:

Either a required impersonation level was not provided, or the provided impersonation level is invalid. (Exception from HRESULT: 0x80070542)

This error is fairly catastrophic as the entire app pool is compromised until it is recycled. i.e. – it is not the currently logged in user that no longer has access, it is all users!

The solution

As stated previously, recycling the app pool resolves the issue but this is not a satisfactory solution.

To understand the solution it is necessary to go under the covers of Windows Identity Foundation and look at the innards of the Microsoft.IdentityModel.Tokens.Saml11.SAML11SecurityTokenHandler class.

Within the ValidateToken method the following code is executed:

Code Snippet

  1. if (this._samlSecurityTokenRequirement.MapToWindows)
  2. {
  3.     WindowsClaimsIdentity identity2 = WindowsClaimsIdentity.CreateFromUpn(this.FindUpn(claimsIdentity), "Federation", this._samlSecurityTokenRequirement.UseWindowsTokenService, base.Configuration.IssuerNameRegistry.GetWindowsIssuerName());
  4.     identity2.Claims.CopyRange(claimsIdentity.Claims);
  5.     claimsIdentity = identity2;
  6. }

As you can see, the code is conditional on the mapToWindows and useWindowsTokenService flags present in the microsoft.identityModel configuration section. This means that if both flags are true, the C2WTS service will be called every time the handler is invoked.

The problem though is that this handler is only invoked once when the SAML assertion from ADFS is first presented to the ASP.NET website. Every subsequent page hit instead invokes the Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler class which deserialises the claims collection held within a cookie. This handler does not, in the out of the box implementation, call the C2WTS service and so, after an IIS reset, even though the user still has access to the website based upon the cookie contents, all impersonation detail is lost and so the error occurs.

Some investigation of the SessionSecurityTokenHandler class reveals that it also uses a flag called useWindowsTokenService but it is false in the default implementation.

The solution is to remove the default SessionSecurityTokenHandler implementation and add in a new one where the useWindowsTokenService flag is set to true, as shown below:

Code Snippet

  1.   <microsoft.identityModel>
  2.     <service>
  3.       ...
  4.       <securityTokenHandlers>
  5.         <remove type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  6.         <add type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
  7.           <sessionTokenRequirement useWindowsTokenService="true"/>
  8.         </add>
  9.         <add type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
  10.           <samlSecurityTokenRequirement mapToWindows="true" useWindowsTokenService="true" />
  11.         </add>
  12.       </securityTokenHandlers>
  13.       ...
  14.     </service>
  15.   </microsoft.identityModel>

The logged in session will now survive an IIS reset.

Hope this helps.

Brad