Optimizing Kerberos Delegation Performance When Talking to a Legacy System

Anyone who has ever dealt with Kerberos delegation knows it is anything but trivial to set up everything correctly. All the intricate nuances related to granting necessary permissions and properly configuring SPNs can bog down anyone. But, sometimes, even all of that is only a half of the story – especially when you’re dealing with a legacy ERP system as the back-end. The other half is exactly something I would like to share today.

I’ll start with a known fact that Kerberos authentication is request-based. That is, every request to a remote system needs to bear a valid Kerberos ticket in the HTTP Authorization header. One might wonder what’s the big deal with this: it’s 2017 so several extra kilobytes of payload (yes, Kerberos tickets are not as small as one might think and can be up to 16 Kb in size) should make no difference. Well, the problem is mainly not with the payload size per se, but with having to validate the incoming Kerberos ticket for each and every request, which obviously does not scale well. However, please be warned that the ticket size can take its toll too, as http.sys may become upset with HTTP request sizes larger than 16 Kbytes.

So, to mitigate the performance degradation, smart guys at Microsoft added a special setting to IIS called “authPersistNonNTLM” that enables Kerberos-based authentication to happen just once per a TCP connection. For the curious reader, there’s an excellent blog post that discusses the internals of this setting at great length. Now, you might think – shall we simply turn this setting on and call it a day? Well, not so fast, my friend, not so fast – we’re dealing with a legacy system here. To be more precise, “legacy” means a system that only supports HTTP 1.0. Which means: no keep-alives, which, in turn, means no chance of re-using a TCP connection for multiple HTTP requests, which, consequently, renders the “authPersistNonNTLM” setting completely useless. Doh!

Optimizing Kerberos delegation performance when talking to a legacy system

A curious mind would ask: is there anything that still could be optimized in such a pessimistic scenario? And, it turns out, the answer is yes: we still could squeeze in some more performance by employing a technique called “pre-authenticate”. The idea behind this technique is very simple: instead of doing an extra round-trip to the server after receiving a “401 Unauthorized” response, we should beforehand add a valid Kerberos ticket to each outgoing request, thus eliminating unnecessary roundtrips. Now, that would be easy if WCF supported pre-authentication out of the box. Unfortunately, for some strange reason, it does not, so we are left alone with a task of obtaining a valid Kerberos session ticket and adding it as a properly encoded Authorize header at hand. Preferably, without having to resort to low-level SSPI APIs – because, you know, there be dragons 🙂

After much trial and error, we’ve come up with something like this:

public dynamic CallSoapMethod<TService>(Func<TService, Task<dynamic>> 
     func, string endPointName)
{
  dynamic service = Activator.CreateInstance(typeof(TService), endPointName);

  using (new OperationContextScope(service.InnerChannel))
  {
    HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty();
    var contextChannel = (IContextChannel)service.InnerChannel;
    var remoteAddressUri = contextChannel.RemoteAddress.Uri;

    ICredentials credentials = CredentialCache.DefaultCredentials;
    NetworkCredential credential = credentials.GetCredential
    (remoteAddressUri, "Kerberos");

    KerberosSecurityTokenProvider tokenProvider = 
    new KerberosSecurityTokenProvider(
      "HTTP/your-service-SPN",

      System.Security.Principal.TokenImpersonationLevel.Impersonation,
            credential);

      KerberosRequestorSecurityToken securityToken = tokenProvider.GetToken(
            TimeSpan.FromMinutes(1)) as KerberosRequestorSecurityToken;

      var securityTokenRequest = securityToken.GetRequest();
      string serviceToken = Convert.ToBase64String(securityTokenRequest);

      requestMessage.Headers["Authorization"] = "Negotiate " + serviceToken;
      OperationContext.Current.OutgoingMessageProperties
      [HttpRequestMessageProperty.Name] = requestMessage;

      return func.Invoke(service);
  }
}

 

IMPORTANT: You must have the following in your web.config for the web site / web service that is calling the legacy system:

<system.web>
    <authentication mode="Windows" />
    <identity impersonate="true" />
</system.web>

 

or else it would be impossible to make Kerberos identity delegation work.

Share article: