Manage CORS policy dynamically

Xueyuan Dai

Brief

We introduced CORS support in ASP.NET Web API a few months ago. Out of the box it supports configuring CORS policy by attributes. It is a very intuitive and powerful way but lacks flexibility at runtime. Imaging your service allows a 3rd party to consume your service. You need the capability of updating the allowing origins list without compiling and deploying your service each time the list changes.

In following this article, I show you two examples of dynamically managing your CORS policy.

  1. Manage your CORS allowed origins in a SQL database.
  2. Manage your CORS allowed origins in the web.config file.

Prerequisites

  1. Visual Studio Express 2013 Preview for Web or Visual Studio 2013 Preview.
  2. QUnit.
  3. Bootstrap (optional)

Set up the test environment

CORS Service

Create a WebAPI project. It comes with a default ValuesController. Mark the controller with EnableCors attribute:

   1:      [EnableCors("*", "*", "*")]
   2:      public class ValuesController : ApiController
   3:      {
   4:          // GET api/values
   5:          public IEnumerable<string> Get()
   6:          {
   7:              return new string[] { "value1", "value2" };
   8:          }
   9:   
  10:          // GET api/values/5
  11:          public string Get(int id)
  12:          {
  13:              return "value";
  14:          }
  15:   
  16:          // POST api/values
  17:          public void Post([FromBody]string value)
  18:          {
  19:          }
  20:   
  21:          // PUT api/values/5
  22:          public void Put(int id, [FromBody]string value)
  23:          {
  24:          }
  25:   
  26:          [DisableCors]
  27:          // DELETE api/values/5
  28:          public void Delete(int id)
  29:          {
  30:          }
  31:      }

Add following line to the App_StartWebApiConfig.cs file to enable CORS.

   1:  config.EnableCors();

Now your CORS service is set up.

Test client

The client code can’t stay in the same service as CORS service since it requires two services hosted at different origins so the CORS mechanism will kick in.

CORS is also affected by the choice of browsers[i]. So the tests need to be run in browser. Therefore I write the test in QUnit[ii].

Create an empty ASP.NET project. Adding following files:

Index.html

   1:  <!DOCTYPE html>
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:  <head>
   4:      <title>CORS Test</title>
   5:      <link href="Content/bootstrap.css" rel="stylesheet" />
   6:  </head>
   7:  <body>
   8:      <div class="container">
   9:          <h1>CORS Test Client</h1>
  10:          <p>Input the url to the CORS service in following form.</p>
  11:          <form action="Test.html" method="get" class="form-inline">
  12:              <input type="text" name="url" class="input-xxlarge" />
  13:              <input type="submit" name="submit" class="btn"/>
  14:          </form>
  15:      </div>
  16:  </body>
  17:  </html>

Test.html

   1:  <!DOCTYPE html>
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:  <head>
   4:      <title>CORS Test</title>
   5:      <link href="Content/qunit.css" rel="stylesheet" />
   6:  </head>
   7:  <body>
   8:      <div id="qunit"></div>
   9:      <div id="qunit-fixture"></div>
  10:      <script src="Scripts/jquery-2.0.2.js"></script>
  11:      <script src="Scripts/qunit.js"></script>
  12:      <script src="Scripts/test.js"></script>
  13:  </body>
  14:  </html>

Scriptstest.js

   1:  function getParameterByName(name) {
   2:      name = name.replace(/[[]/, "\[").replace(/[]]/, "\]");
   3:      var regex = new RegExp("[\?&]" + name + "=([^&#]*)"),
   4:          results = regex.exec(location.search);
   5:      return results == null ? "" : decodeURIComponent(results[1].replace(/+/g, " "));
   6:  }
   7:   
   8:  test("Cross domain get request", function () {
   9:      var testUrl = getParameterByName("url");
  10:   
  11:      var xhr = new XMLHttpRequest();
  12:      xhr.open("GET", testUrl + "/api/Values", false);
  13:      xhr.send();
  14:   
  15:      var retval = JSON.parse(xhr.responseText);
  16:   
  17:      equal(2, retval.length);
  18:      equal("value1", retval[0]);
  19:      equal("value2", retval[1]);
  20:  });

Notes:

  1. The project requires QUnit.js and QUnit.css. It also includes Bootstrap. Bootstrap is optional.
  2. Index.html is the front page. It accepts a URL to which this test client will sent CORS request to.
  3. Test.html contains QUnit test fixture.
  4. Test.js contains the test case and a utility function to get parameter from query string.

What now?

Now you have both CORS service and test client. Host them separately in IIS Express or Azure:

clip_image001

clip_image002

Visit the test client and input the CORS service URL:

clip_image004

The page will navigate to the test page once you submit the URL:

clip_image006

Notice the test passes since the CORS service accepts request from all origins.

Manage your CORS allowed origin in database

The goal is to save the allowed origin list in database and make CORS components to visit the database at runtime. We will introduce a data model, CRUD views to manage the database and a new CORS attribute to mark your endpoints.

The advantage of using database is that it’s powerful. You can change the policy at runtime without restart the service. The downside is that database is sometime overkill especially when your service is too simple to add a database.

Create management page

Model

I used Entity Framework for the database. First, let’s create the simplest model:

   1:      public class AllowOrigin
   2:      {
   3:          public int Id { get; set; }
   4:   
   5:          public string Name { get; set; }
   6:   
   7:          public string Origin { get; set; }
   8:      }
  • Id is the key for database index;
  • Name is used to categorize the origin. Multiple origins can have the same name so as to apply them to a specified endpoint;
  • Origin is the allowed URL origin.

Create Controller and Views by scaffolding

Now, let’s use the powerful ASP.NET scaffolding to create controller and views for managing allowed origin list.

clip_image008

Let’s add a MVC 5 Controller with read/write actions and views, using Entity Framework.

clip_image009

Once views and controller are created let’s update the _Layout.cshtml file by adding a link to the navigation bar:

   1:  <div class="nav-collapse collapse">
   2:    <ul class="nav">
   3:      <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li>
   4:      <li>@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)</li>
   5:      <li>@Html.ActionLink("CORS Admin", "Index", "CorsAdminOrigin")</li>
   6:    </ul>
   7:  </div>

Now the CORS origin management page is ready to go:
clip_image011

Extend CORS to read allow origin from database

We will take advantage of one CORS extension point to accomplish our goal:

ICorsPolicyProvider

Instances that implement this interface will create a CORS policy based on a given http request. A CORS policy is a rule deciding how the CORS engine will process the CORS request

   1:  namespace System.Web.Http.Cors
   2:  {
   3:      /// <summary>
   4:      /// Provides an abstraction for getting the <see cref="CorsPolicy"/>.
   5:      /// </summary>
   6:      public interface ICorsPolicyProvider
   7:      {
   8:          /// <summary>
   9:          /// Gets the <see cref="CorsPolicy"/>.
  10:          /// </summary>
  11:          /// <param name="request">The request.</param>
  12:          /// <param name="cancellationToken">The cancellation token.</param>
  13:          /// <returns>The <see cref="CorsPolicy"/>.</returns>
  14:          Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken);
  15:      }
  16:  }

Out of box EnableCorsAttribute implements ICorsPolicyProvider. It turns all of your settings in the CORS attribute to CORS policy.

AllowCors

We won’t use the EnableCors attribute. Instead we create a new attribute:

   1:  public class AllowCorsAttribute : Attribute, ICorsPolicyProvider
   2:  {
   3:      private string _configName;
   4:   
   5:      public AllowCorsAttribute(string name = null)
   6:      {
   7:          _configName = name ?? "Default";
   8:      }
   9:   
  10:      public string ConfigName
  11:      {
  12:          get { return _configName; }
  13:      }
  14:   
  15:      public Task<CorsPolicy> GetCorsPolicyAsync(
  16:          HttpRequestMessage request,
  17:          CancellationToken cancellationToken)
  18:      {
  19:          using (var db = new CorsContext())
  20:          {
  21:              var origins = db.AllowOrigins.Where(o => o.Name == ConfigName).ToArray();
  22:   
  23:              var retval = new CorsPolicy();
  24:              retval.AllowAnyHeader = true;
  25:              retval.AllowAnyMethod = true;
  26:              retval.AllowAnyOrigin = false;
  27:   
  28:              foreach (var each in origins)
  29:              {
  30:                  retval.Origins.Add(each.Origin);
  31:              }
  32:   
  33:              return Task.FromResult(retval);
  34:          }
  35:      }
  36:  }

AllowCors attribute derives from System.Attribute and implement ICorsPolicyProvider, therefore it will be picked up by AttributeBasedPolicyProviderFactory. However it accepts only one parameter for its name. The rest of the settings of the policy are read from the database.

In this sample, it only loads allowed origin from database. It is for the sake of simplicity. In a real scenario all settings can be saved in database.

How does it work

Attributes your ValuesController

Replace the EnableCors attribute with AllowCors attribute for ValuesController:

   1:  [AllowCors("Values")]
   2:  public class ValuesController : ApiController
   3:  {

Notice I gave a name “Values” for this scope.

A negative test

Now open both test client and the CORS service (remember to redeploy your services) Run your test client you will notice that the tests failed:

clip_image013

The error reads:

SEC7120: Origin http://corstestclient.azurewebsites.net not found in Access-Control-Allow-Origin header.
Test.html

This is expected since test client is not on the allowed list.

Add allowed origin

So go to the CORS service and add test client to allowed list. Remember the name of the CORS policy to apply is “Values”.

clip_image014

Rerun your test client. Now it passes!

Manage your CORS allowed origin in web.config

The goal of this sample is to show you how to manage CORS setting in web.config.

There are multiple benefits to use web.config. First, it doesn’t need recompile and fully redeployed. Second, it’s so simple that you just need a notepad to update the configuration. Last, if you’re using Azure web site, the portal allow you update the settings on the flight.

There a few downside of web.config. It requires service to be started to change the policy. And it doesn’t fit the situation you need configure endpoints differently.

Adding settings in web.config

   1:  <appSettings>
   2:      <add key="webpages:Version" value="3.0.0.0" />
   3:      <add key="webpages:Enabled" value="false" />
   4:      <add key="PreserveLoginUrl" value="true" />
   5:      <add key="ClientValidationEnabled" value="true" />
   6:      <add key="UnobtrusiveJavaScriptEnabled" value="true" />
   7:      <add key="cors:allowOrigins" value="http://localhost:40861"/>

Extend CORS to read allow origin from web.config

This is quite straightforward if you have gone through the database sample first:

   1:  public class AllowCorsAttribute : Attribute, ICorsPolicyProvider
   2:  {
   3:      private const string keyCorsAllowOrigin = "cors:allowOrigins";
   4:   
   5:      private CorsPolicy _policy;
   6:   
   7:      public Task<CorsPolicy> GetCorsPolicyAsync(
   8:          HttpRequestMessage request, 
   9:          CancellationToken cancellationToken)
  10:      {
  11:          if (_policy == null)
  12:          {
  13:              var retval = new CorsPolicy();
  14:              retval.AllowAnyHeader = true;
  15:              retval.AllowAnyMethod = true;
  16:              retval.AllowAnyOrigin = false;
  17:   
  18:              var value = ConfigurationManager.AppSettings[keyCorsAllowOrigin];
  19:   
  20:              if (!string.IsNullOrEmpty(value))
  21:              {
  22:                  foreach (var one in from v in value.Split(';')
  23:                                      where !string.IsNullOrEmpty(v)
  24:                                      select v)
  25:                  {
  26:                      retval.Origins.Add(one);
  27:                  }
  28:              }
  29:   
  30:              _policy = retval;
  31:          }
  32:   
  33:          return Task.FromResult(_policy);
  34:      }
  35:  }

Note: You don’t need to reload policy for every request. Web.config doesn’t change during the life time of a service.

How it works

Put your client origin in the web.config and start the service. Run your test client. It just works.

clip_image015

Configure in Azure Web Site Portal

Here’s a powerful integration sample if you’re using Azure Web Sites to host your service. In the Azure Web Site portal you can configure your application settings:

clip_image017

Now you can change the allow list in browser.


[i] http://caniuse.com/cors

[ii] http://qunitjs.com/


0 comments

Discussion is closed.

Feedback usabilla icon