Using WSFederationAuthenticationModule with Classic ASP

I’ve recently been working with a client that is updating an existing Classic ASP application to ASP.NET MVC 5. As part of the upgrade, some of the systems currently hosted on an internal web server are being moved over to Windows Azure and the existing Windows Authentication in place, over to single sign-on (SSO) with Windows Azure Active Directory and Office 365, to leverage a cloud-based identity solution and minimise on-premises infrastructure.

Authentication for all

If you haven’t checked out Windows Identity Foundation (WIF) yet, you should give it a look. Initially released as a standalone download for .NET 3.5, it’s now been rolled into the .NET 4.5 framework and is a set of classes for implementing claims-aware and claims-based identity applications. Amongst other things, it allows for Identity delegation, Federated Identity and Federated Authentication scenarios, the latter of which is what we will be concerned with here.

Since the update to the Classic ASP application is being performed piecemeal, both the existing application and the new MVC application need to function together harmoniously and ideally be secured by the same authentication and authorisation scheme. This is easily achieved by including the Classic ASP application code within an ASP.NET application and setting the Application Pool to run in Integrated Pipeline mode. With an application running in Integrated Pipeline mode, any application that can be hosted on IIS can be enhanced with managed .NET code to implement new features such as URL rewriting or, as in this case, authentication and authorisation, through the use of HTTP modules and handlers written in any .NET Framework supported language.

So, here is the scenario

  1. Classic ASP application files are contained within a directory within an ASP.NET MVC 5 application.
  2. Application is running in Integrated Pipeline mode on IIS 8.5
  3. Authentication for both Classic ASP and the MVC application is being handled with WIF by registering the WSFederationAuthenticationModule in the <modules> section of <system.webServer> in web.config and setting all managed modules to run for all requests. The web.config also contains the details for WS-Federation needed for WIF Federated Authentication in the <federationConfiguration> of <system.identityModel.services>.
  4. All routes within the application are set to require the request to be authenticated using the <authorization> section of <system.web> in web.config

 

Now, making a GET request to an existing Classic ASP page correctly sends the user to the Office 365 sign on page to allow the user to authenticate for the application. Once the the user has signed in to Office 365, the user is redirected back to the application and can now access the originally requested resource.

Body Check

For GET requests, this worked great with the out of the box configuration. When making a POST request from a Classic ASP page however, things were going awry; every time the Classic ASP page attempted to access the POSTed form values using Request.Form(“value”), an ASP error similar to the following occurred

Request object error 'ASP 0101 : 80004005'

Unexpected error

/somepage.asp, line X, line Y

The function returned |

Query string parameters could be accessed via the Request object without problem which indicated something may be messing with the Request body before it is passed to the ASP code. Since all managed modules are running for all requests and given an understanding of how the WSFederation Authentication Module works, the best place to start looking would be in the WSFederationAuthenticationModule code.

Using DotPeek to decompile WSFederationAuthenticationModule we can see the following in the InitializeModule(HttpApplication context)

protected override void InitializeModule(HttpApplication context)
{
  SessionAuthenticationModule.CheckForCurrent();
  context.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
  context.EndRequest += new EventHandler(this.OnEndRequest);
  context.PostAuthenticateRequest += new EventHandler(this.OnPostAuthenticateRequest);
}

Three event handlers are registered, let’s start by looking at OnAuthenticateRequest

protected virtual void OnAuthenticateRequest(object sender, EventArgs args)
{
  HttpRequestBase request = new HttpContextWrapper(HttpContext.Current).Request;
  if (!this.CanReadSignInResponse(request))
    return;
  try
  {
    this.SignInWithResponseMessage(request);
  }
  catch (Exception ex)
  {
    if (Fx.IsFatal(ex))
    {
      throw;
    }
    else
    {
      if (DiagnosticUtility.ShouldTrace(TraceEventType.Warning))
        TraceUtility.TraceString(TraceEventType.Warning, SR.GetString("ID8020", new object[1]
        {
          (object) ex
        }), new object[0]);
      ErrorEventArgs args1 = new ErrorEventArgs(ex);
      this.OnSignInError(args1);
      if (args1.Cancel)
        return;
      throw;
    }
  }
} 

A check is made to see if we can read the sign in response. Let’s see what that check is

public virtual bool CanReadSignInResponse(HttpRequestBase request, bool onPage)
{
  if (request == null)
    throw DiagnosticUtil.ExceptionUtil.ThrowHelperArgumentNull("request");
  if (string.Equals(request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
  {
    if (this.IsSignInResponse(request))
      return true;
  }
  else
  {
    SignOutCleanupRequestMessage outCleanupMessage = WSFederationAuthenticationModule.GetSignOutCleanupMessage(request);
    if (outCleanupMessage != null)
    {
      if (DiagnosticUtility.ShouldTrace(TraceEventType.Information))
        TraceUtility.TraceEvent(TraceEventType.Information, 786438, "CanReadSignInResponse", (TraceRecord) new WSFedMessageTraceRecord((FederationMessage) outCleanupMessage), (object) null);
      this.SignOut(true);
      HttpResponse response = HttpContext.Current.Response;
      if (!string.IsNullOrEmpty(outCleanupMessage.Reply))
      {
        string signOutRedirectUrl = this.GetSignOutRedirectUrl(outCleanupMessage);
        if (onPage)
        {
          response.Redirect(signOutRedirectUrl);
        }
        else
        {
          response.Redirect(signOutRedirectUrl, false);
          HttpContext.Current.ApplicationInstance.CompleteRequest();
        }
      }
      else
      {
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.ClearContent();
        response.BinaryWrite(WSFederationAuthenticationModule._signOutImage);
        if (onPage)
          response.End();
        else
          HttpContext.Current.ApplicationInstance.CompleteRequest();
      }
    }
  }
  return false;
}

The method checks to see if this is a POST request and if it is, to run this.IsSignInResponse(request)). Let’s see what this does

public virtual bool IsSignInResponse(HttpRequestBase request)
{
  if (request == null)
    throw DiagnosticUtil.ExceptionUtil.ThrowHelperArgumentNull("request");
  if (request.Form == null || !StringComparer.Ordinal.Equals(request.Unvalidated.Form["wa"], "wsignin1.0"))
    return false;
  if (string.IsNullOrEmpty(request.Unvalidated.Form["wresult"]))
    return !string.IsNullOrEmpty(request.Unvalidated.Form["wresultptr"]);
  else
    return true;
}

And here is the culprit! If the request is a POST request, the request’s Form collection is inspected for a key named wa. Reading from the form collection is going to read the Request Body, which might be fine to do if the request is going to be passed to .NET code following this, but may cause problems for other applications hosted in IIS such as PHP or in this case ASP, since the request stream following the read may no longer be in a fit state to be read by the subsequent code. In this instance, The Classic ASP framework is unable to instantiate Request.Form successfully, resulting in a '80004005' error.

No Need to Read

Thankfully, the solution to the problem is rather straightforward as WSFederationAuthenticationModule is not a sealed class and implements a lot of its methods as virtual, allowing us to override them in a class deriving from WSFederationAuthenticationModule. We only need to override one method to ensure that the Request body isn’t read when we know that the request is not a sign in response:

public class PreserveRequestBodyWSFederationAuthenticationModule : WSFederationAuthenticationModule
{
    public override bool IsSignInResponse(HttpRequestBase request)
    {
        if (request == null) 
        {
             throw new ArgumentNullException("request");
        }
	
        var url = request.Path;

        if (Path.HasExtension(url) &&
            Path.GetExtension(url).Equals(".asp", StringComparison.InvariantCultureIgnoreCase) &&
            request.HttpMethod == "POST")
        {
            return false;
        }

        return base.IsSignInResponse(request);
    }
}

With an updated authentication and authorization process in place, effort can now be focused on upgrading the other parts of the legacy codebase.

Comments

comments powered by Disqus