Dynamic CORS origins from appsettings using Web API 2.2 Cross-Origin Support

by John Nye

27 Jun
2014

Recently I wanted to expose part of a site to allow you to retrieve the most recent posts as part of a web api request from another domain. I wanted to allow a sister site to make a web api request to get the most recent blog posts. Luckily this is supported via a handy nuget package

PM> Install-Package Microsoft.AspNet.WebApi.Cors

This allows you access to an attribute that you can decorate your controllers/actions to make it available to pre defined types.

Extending Web Api Cors using AppSettings

This package in conjuction with an excellent article by Brock Allen on MSDN helped me to create a new attribute (below) that allowed me to read valid origins from the web.config appsettings section, meaning I can use the attribute and have different allowed origins for different flavours of the site or even different build configurations.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class EnableCorsByAppSettingAttribute : Attribute, ICorsPolicyProvider
{
    const string defaultKey = "cors:AllowedOrigins";
    private readonly string rawOrigins;
    private CorsPolicy corsPolicy;

    /// <summary>
    /// By default uses "cors:AllowedOrigins" AppSetting key
    /// </summary>
    public EnableCorsByAppSettingAttribute() 
        : this(defaultKey) // Use default AppSetting key
    {
    }

    /// <summary>
    /// Enables Cross Origin
    /// </summary>
    /// <param name="appSettingKey">AppSetting key that defines valid origins</param>
    public EnableCorsByAppSettingAttribute(string appSettingKey)
    {
        // Collect comma separated origins
        this.rawOrigins = ConfigurationManager.AppSettings[appSettingKey];
        this.BuildCorsPolicy();
    }

    /// <summary>
    /// Build Cors policy
    /// </summary>
    private void BuildCorsPolicy()
    {
        bool allowAnyHeader = String.IsNullOrEmpty(this.Headers) || this.Headers == "*";
        bool allowAnyMethod = String.IsNullOrEmpty(this.Methods) || this.Methods == "*";

        this.corsPolicy = new CorsPolicy
            {
                AllowAnyHeader = allowAnyHeader,
                AllowAnyMethod = allowAnyMethod,
            };

        // Add origins from app setting value
        this.corsPolicy.Origins.AddCommaSeperatedValues(this.rawOrigins);
        this.corsPolicy.Headers.AddCommaSeperatedValues(this.Headers);
        this.corsPolicy.Methods.AddCommaSeperatedValues(this.Methods);
    }

    public string Headers { get; set; }
    public string Methods { get; set; }

    public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, 
                                               CancellationToken cancellationToken)
    {
        return Task.FromResult(this.corsPolicy);
    }
}

The above code takes advantage of a simple extension method called AddCommaSeperatedValues, which is defined as follows

internal static class CollectionExtensions
{
    public static void AddCommaSeperatedValues(this ICollection<string> current, string raw)
    {
        if (current == null)
        {
            return;
        }

        var valuesToAdd = raw.SplitCommaSeperatedValues();
        foreach (var value in valuesToAdd)
        {
            current.Add(value);
        }
    }
}

This in turn relies on another string extension method which turns a comma separated string into a collection of values

internal static class StringExtensions
{
    /// <summary>
    /// Splits a comma delimited string into a new collection
    /// </summary>
    /// <param name="raw">Comma delimited string of values to split</param>
    /// <returns></returns>
    public static IEnumerable<string> SplitCommaSeperatedValues(this string raw)
    {
        if (string.IsNullOrWhiteSpace(raw))
        {
            return Enumerable.Empty<string>();
        }

        return raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                  .Select(s => s.Trim())
                  .ToList();
    }
}

Using the [EnableCorsByAppSetting] attribute

Using this new attribute is simple. Simply add the attribute to the controller or action method you desire.

In the controller

We can either use the default app setting key by using the default constructor

[EnableCorsByAppSetting]
public class PostsController : ApiController
{
    public IEnumerable<PostSummaryViewModel> Get()
    {
        // Return recent posts
        // Code ommited
    }    
}

Or define our own appSetting to use by providing that appSetting key

[EnableCorsByAppSetting("cors:PostsOrigins")]
public class PostsController : ApiController
{
    public IEnumerable<PostSummaryViewModel> Get()
    {
        // Return recent posts
        // Code ommited
    }    
}

Now that is set up we simply need to add the origins to the appsetting.

In the web.config

Below we are defining the origin that is allowed during development

<appSettings>
    <add key="cors:AllowedOrigins" value="http://localhost:52811" />
</appSettings>

And if we want a different origin when in a release build, when the site goes live for example, we can simply add a transform to the web.release.config file

In web.release.config

<appSettings>
    <add key="cors:AllowedOrigins" value="http://www.mylivesite.com" 
         xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
</appSettings>

Thanks again to Brock Allen. I hope this is useful to those reading this. If you have any questions or comments, please use the comments for below and I will endeavor to respond promptly.

Comments 0 * Be the first to comment!

Leave a message...

20 Jul
2017