Lightweight ASP.NET Background Processes

Imagine a tool which performs some long running background task on the server (on the order of several minutes) and wants to notify users of completion. This notification may take the form of an email, or it may be that the user remains on the website and receives a visual confirmation that the task is complete. The “industrial strength” solution to this is Signal R, which provides all sorts of client/server connectivity capabilities.

In many cases, this may be an excessive solution. Running a background task, with both backend and frontend notifications, is straightforward and doesn’t require a lot of wiring. This post shows one simple approach that will work with ASP.NET Web API or MVC.

Core Concept

Use a dictionary in application state to track background tasks and their status. Match each task to an ID for reportability.

This technique will not work if the runtime of the background task is too long compared to the application idle time-out on IIS. For this reason, the technique is appropriate only for non-essential tasks which don’t expect to run more than a few minutes.

Long Running Job Class

Initiating, checking, and maintaining jobs is handled by the long running jobs class. The class has static methods to create and check a job; and instance values of an ID (which can be sent to/from the client) and the task itself.

Non-static fields and methods are the easiest, primarily just wrappers around the ID and Task.

public class LongRunningJob
{

  public enum JobStatus { Running, Done, Failed }

  /// <summary>
  /// A unique identifier for this job.  Send this ID to the client, 
  /// and it may use it to query the status of the job.
  /// </summary>
  public readonly Guid ID;

  /// <summary>
  /// The actual background task.  Use a continuation to send email 
  /// or perform other server-side operations when the task completes.
  /// </summary>
  public readonly Task Task;
                
  protected LongRunningJob(Guid id, Task task)
  {
    ID = id;
    Task = task;
  }

  /// <summary>
  /// Returns a status based on the Task
  /// </summary>
  public JobStatus Status
  {
    get
    {
      if (Task.IsFaulted)
        return JobStatus.Failed;
      else if (Task.IsCompleted || Task.IsCanceled)
        return JobStatus.Done;
      else
        return JobStatus.Running;
    }

  }

}

The real work is done in the static methods which provide factory construction of a long running job, and status querying via the application state dictionary. The dictionary is accessed via a “Tasks” static property.

Starting a new job is done via the factory method:

/// <summary>
/// Start a new job.  The method provided will be placed into the task 
/// queue and may be started immediately.
/// </summary>
/// <param name="job">The method to run in the background</param>
/// <returns>An object containing the task and a unique identifier that
/// can be used to retrieve the job (and check its status)</returns>
public static LongRunningJob StartJob(Action job)
{
  Guid id = Guid.NewGuid();
  var task = Task.Factory.StartNew(job);
  var lrj = new LongRunningJob(id, task);
  Tasks.Add(id, lrj);
  return lrj;
}

Retrieving a job by ID queries the Tasks dictionary:

/// <summary>
/// Retrieve a job by ID.  If no matching job is found, 
/// returns null.
/// </summary>
/// <param name="id">The ID from LongRunningJob.ID</param>
/// <returns>The LongRunningJob, 
/// or null if no matching job found</returns>
public static LongRunningJob RetrieveJob(Guid id)
{
  if (Tasks.ContainsKey(id))
  {
    return Tasks[id];
  }
  else
  {
    return null;
  }
}

We define the application state dictionary of tasks via a property which instantiates it on first request.

protected static IDictionary<Guid, LongRunningJob> Tasks
{
  get
  {
    var dict = HttpContext.Current.Application["_LongRunningJob"] as 
      IDictionary<Guid, LongRunningJob>;
    if (dict == null)
    {
      dict = new Dictionary<Guid, LongRunningJob>();
      HttpContext.Current.Application["_LongRunningJob"] = dict;
    }
    return dict;
  }
}

Sample Usage

To demonstrate usage, we create a sample application which performs long-running jobs as sleeping threads of various lengths. Failures can be introduced by intentionally throwing exceptions.

Here is the sample Web API controller methods:

[HttpPost]
public Guid CreateRegularJob([FromBody] int seconds)
{
  var job = Models.LongRunningJob.StartJob(new Action(() => 
  {
    // TODO: some long running task
    System.Threading.Thread.Sleep(seconds * 1000);
  }));
  job.Task.ContinueWith(new Action<System.Threading.Tasks.Task>(t => {
    // TODO: send email notification of completion
  }));
  return job.ID;
}

[HttpPost]
public Guid CreateFailJob([FromBody] int seconds)
{
  var job = Models.LongRunningJob.StartJob(new Action(() =>
  {
    System.Threading.Thread.Sleep(seconds * 1000);
    throw new Exception();
  }));
  return job.ID;
}

[HttpGet]
public Models.LongRunningJob.JobStatus CheckStatus(Guid id)
{
  var job = Models.LongRunningJob.RetrieveJob(id);
  return job.Status;               
}

On the client side, sample jobs are created to POST’ing to the appropriate create jobs methods. Each ID is placed in a table, and a polling routine queries the status of each running ID each second until they complete or fail. The ajax calls return without blocking for the long running task to complete.

A frontend sample shows several jobs queued up to run simultaneously for various lengths:
longjobpoll

Future Work

  • Tasks can be extended to return data.
  • Old jobs should be removed from the dictionary to avoid memory leaks.

“Bolt-On” CSRF Protection in Intranet Web API Windows Authentication Scenarios

Web API is a powerful tool for constructing web services, but also for separating concerns of a web application. However, like any web application, security concerns must be addressed. One of the most common security concerns with authenticated operations is cross-site request forgery. In a traditional ASP.NET MVC application, we can use the built-in AntiForgeryToken mechanism to place a unique token within each page served to the client, and require that token be included in any requests back to the server. Since an attacker cannot access the contents of the actual served page, they are unable to acquire the token; and since the token is not a cookie or credential, the browser will not send it automatically. Thus, an attacking page cannot craft an effective CSRF attack.

When moving into the Web API world, things are a little more muddy. The general tack has been to take a different angle on authentication and authorization all together. For example, the use of certificates to sign each request, bearer tokens, oAuth, or similar approaches. However, for purely intranet applications, we may still wish to use Windows authentication. This means the end-user of a website which calls our API will automatically do so with the user’s windows credential. (It is also possible for server-side applications to use impersonation to call the API with a particular Windows service account, however, this is not subject to CSRF vulnerability).

There are also ways to make a joint MVC/Web API application use the MVC anti-forgery tokens, which is really neat, but only works if the API and application are one cohesive web application solution, as otherwise the Web API’s instance of ASP.NET would differ from the MVC instance of ASP.NET, and they would not share the set of valid anti-forgery tokens. It also doesn’t work if “plain” HTML/JS is being used to access the API.

However, by using Windows authentication, we open our API up to CSRF attackers. An intranet user may be using a web application which uses our API (and thus, carries their credential), while at the same time they are browsing evilattacksites.com which can construct a form with the intranet URL and post it to the API’s intranet address. This post will carry the user’s credentials with it, and execute a successful attack.

You might note that the API is on a intranet site and the attacker is (probably) external, so CORS might be suggested as an answer. However, CORS only controls what data can be read, it does not protect against CSRF submissions.

We want Web API CSRF protection that:

  • Works with any client (doesn’t require pages generated by MVC)
  • Works with Windows authentication/intranet
  • Is easy to “bolt-on” to existing Web API services AND clients

Closing the CSRF Vulnerability

The solution we will use is to provide a per-IP CSRF token that must be attached to the HTTP header and is validated on all POST/PUT/DELETE requests.

The technique here is to construct a message handler which will process all Web API requests before they go to the controller. It will validate the presence of a CSRF token when needed, and produce it when requested. Thus, there will be no changes to the controllers! The only server-side application change (besides importing the message handler), is to add it in the WebApiConfig Register method.

config.MessageHandlers.Add(new Infrastructure.CSRFMessageHandler());

On the client side, none of the individual requests to the API need to be altered. An initial login call (handled by the CSRFMessageHandler on the server) acquires the CSRF token and places it into the headers for all subsequent ajax calls. There will be no other changes needed to the client. However, clients which consume multiple Web API services secured in this way will have a more complex setup procedure. 🙂

$(function () {
   $.ajax("../api/login")
      .done(function (data) { $.ajaxSetup({ headers: 
         { "X-CSRF-Key": data } }) })
      .fail(function () { /* DO SOMETHING */ });
});

With no further changes, the API is now secure, and the clients receive and use the CSRF token to gain access. This protection can easily be “bolted on” to existing APIs and clients.

Implementation of CSRFMessageHandler

The real magic of this technique is in CSRFMessageHandler. This handler intercepts each call to the Web API before it is sent to the controller. There are two parts: the CSRF token verification, and the token generation.

First, the overall class structure of the handler. We prepare a RNGCryptoServiceProvider for generating the token, and extract the request context so we have access to the ASP.NET application state object, where the CSRF tokens will be stored (you could also store them in a database, or other repository).

public class CSRFMessageHandler : DelegatingHandler
{
   private static readonly RNGCryptoServiceProvider rng = 
      new RNGCryptoServiceProvider();
   public const string CSRF_HEADER_NAME = "X-CSRF-Key";      

   protected override async Task<HttpResponseMessage> SendAsync(
      HttpRequestMessage request, CancellationToken cancellationToken)
   {            
      var method = request.Method.Method.Trim().ToUpper();

      // Extract the context from the request property
      // so that application "state" can be accessed
      var context = ((HttpContextBase)request.
         Properties["MS_HttpContext"]);

      // PART ONE: On "login" request, create token.  
      // Put this here so no need to modify controller.
      ... see below
      // PART TWO: For update methods, enforce the CSRF key
      ... see below

      return await base.SendAsync(request, cancellationToken);
   }
}

When the client sends a “login” request (any request ending in /login), it will be intercepted by the handler and a CSRF token will be created and returned. If desired, you could check the dictionary to see if the client already has a key and reuse it. You could send back the username along with the key. You could set an expiration time for the key and make a process to remove old (expired) keys. Some additional improvement is definitely possible.

if (method == "GET" && 
   request.RequestUri.AbsolutePath.ToUpper().EndsWith("/LOGIN"))
{
   var keys = context.Application[CSRF_HEADER_NAME] as 
      IDictionary<string, string>;
   if (keys == null)
   {
      keys = new Dictionary<string, string>();
      context.Application[CSRF_HEADER_NAME] = keys;
   }

   byte[] bkey = new byte[16];
   rng.GetBytes(bkey);
   var key = Convert.ToBase64String(bkey); 
   keys[context.Request.UserHostAddress] = key;
   return new HttpResponseMessage(HttpStatusCode.OK) 
   { 
      Content = new StringContent(key) 
   };
}

For all modification requests (POST, PUT, DELETE), the handler will validate that the appropriate key is included in the headers.

if (method == "POST" || method == "PUT" || method == "DELETE")
{
   HttpResponseMessage response = request.CreateErrorResponse(
      HttpStatusCode.Forbidden,
      "POST/PUT/DELETE require valid and matching anti-CSRF key, use Login method");

   string key = null;
                
   if (request.Headers.Contains(CSRF_HEADER_NAME))
      key = request.Headers.GetValues(CSRF_HEADER_NAME).Single();

   var keys = context.Application[CSRF_HEADER_NAME] as 
      IDictionary<string, string>;
   string ipaddr = context.Request.UserHostAddress;
   
   // match to the key for user's IP address
   if (keys == null ||
      !keys.ContainsKey(ipaddr) ||
      keys[ipaddr] != key) throw new HttpResponseException(response);
}

Upgrading MVC 3 in Visual Studio 2013

Visual Studio 2013 does not support MVC 3 in a “first-class” way. Therefore, it is desirable to upgrade MVC 3 projects to MVC 4. A tool exists which is very useful to accomplish this, but some additional steps might be necessary (as we found).

Primary Web MVC 3 Project Upgrade

Open the project in Visual Studio 2013. Select the Web project. Open the Package Manager Console:
mvc2013_1

“Install” the UpgradeMvc3ToMvc4 package. Be sure “Default Project” (top bar of console) is set to the correct project!

mvc2013_2

This should complete the initial upgrade. However, you might receive a dependency error:

mvc2013_3

In this case, you can remove the conflict (replace Microsoft.AspNet.Razor with whatever the dependency error is):

Uninstall-Package Microsoft.AspNet.Razor -Force

Uninstall the package indicated in the “already referencing” error. Ignore the warnings about breaking packages. Then try the upgrade again. You may need to uninstall several packages before the install succeeds.

Upgrading Unit Test Project References

If any unit test projects or other dependency projects exist, they will need to be upgraded as well. Change “default project” on the top bar of the package manager console to these projects and run the install command again.

Some errors may happen here, generally this is OK. The main point is to update the references automatically. If this does not succeed, these references are relatively easy to update manually. Make them match those in the web project (which was already upgraded).
Attempt to “Rebuild Solution”. It should be successful (no errors). Check for Model intellisense in the view. Should work. Run all tests. Should pass.

Reset Windows Authentication Mode

Go to web project’s primary Web.config (in the root of the web project).

In the “AppSettings” section, add the following two lines:

<add key="autoFormsAuthentication" value="false" />
<add key="enableSimpleMembership" value="false"/>

This is to switch back to Windows authentication. Otherwise the app will try to use forms authentication even if it previously used Windows authentication.

Ensure IIS Express is set to Windows authentication

If this is your first web project in Visual Studio 2013, you may also need to configure IIS express to support Windows Authentication. Follow these instructions: http://stackoverflow.com/a/19515891.

You only need to do that once per machine.