DEV Community

Niels Swimburger.NET πŸ”
Niels Swimburger.NET πŸ”

Posted on • Originally published at swimburger.net on

Configure CORS using AppSettings or Custom Configuration Sections in ASP.NET Web API

Skip the basics and go straight to the 'AppSettingsCorsAttribute' implementation or the 'ConfigCorsPolicyAttribute' implementation.

Browsers don't allow you to make AJAX requests from one origin to another, also referred to as 'Cross Origin Resource Sharing' (CORS). An origin in this context means the combination of domain, protocol, and port.

If CORS wasn't a thing, any website could make AJAX requests to your bank's website. If you happen to be signed in, websites could potentially make transactions without your knowledge. Luckily CORS does exists and won't allow this.

This security measure does mean it is harder for the front-end of websites to communicate with a back-end hosted on a different origin. The back-end can explicitly allow cross-origin resource requests by using the following headers:

Access-Control-Allow-Origin
A comma separated list of origins you want to allow cross origin requests from. Instead of allowing origins explicitly, you can use '*' as a wildcard for all websites. You cannot use the wildcard option in combination with Access-Control-Allow-Credentials.
Access-Control-Expose-Headers
A comma separated list of headers that the client can access when receiving the response. Instead of allowing headers explicitly, you can use '*' as a wildcard to expose all headers.
Access-Control-Allow-Headers
A comma separated list of headers that the client can access when receiving the response. Instead of allowing headers explicitly, you can use '*' as a wildcard to allow all headers.
Access-Control-Allow-Methods
A comma separated list of HTTP methods that the client are allowed to use for the CORS request. Instead of allowing methods explicitly, you can use '*' as a wildcard to allow all methods.
Access-Control-Allow-Credentials
By default, CORS requests will not pass credentials. The client needs to explicitly set the property withCredentials to true on XMLHttpRequest objects. Additionally, the server also needs to return the following header Access-Control-Allow-Credentials: true.
Access-Control-Max-Age
Before sending the actual CORS request, the browser will send a pre-flight request to check the CORS headers. This header will determine how long the pre-flight request will be cached

There's a lot more details to how CORS functions and how implementations differ among browsers which is very well document by Mozilla.

Add CORS support to ASP.NET Web API

Install the following package into your Web API project:

Install-Package Microsoft.AspNet.WebApi.Cors

Enter fullscreen mode Exit fullscreen mode

Call the EnableCors function on your HttpConfiguration on startup. Usually this is done in the WebApiConfig.Register function:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace AppSettingsCors.WebApi
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.EnableCors();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Add the following attribute to the controller or action you want to enable CORS for:

[EnableCors(origins: "https://localhost:44310", headers: "\*", methods: "\*")]

Enter fullscreen mode Exit fullscreen mode

Now webpages hosted on 'https://localhost:44310' can make AJAX requests to your controller/action.

You can also define CORS globally by passing the attribute to EnableCors:

var cors = new EnableCorsAttribute("https://localhost:44310", "\*", "\*");
config.EnableCors(cors);

Enter fullscreen mode Exit fullscreen mode

For more details on how to use the Microsoft provided CORS support, check out 'Enable cross-origin requests in ASP.NET Web API 2'.

Use AppSettings to configure CORS

All code in this article can be found on this GitHub repository.

The attributes provided by the CORS library work well, but you do have to hardcode the values into the attribute. Only constants are allowed in attributes, so when you have to change the parameters, you have to update the attribute parameters manually and recompile. Having to recompile to update the CORS policy may be a deal breaker if:

  • If you have to move your API to a different origin (domain, protocol, and port)
  • If you have you need to change CORS policy when deploying your app to a different location (DEV vs staging vs prod)
  • If you have an API used by more and more clients over time.

Instead of hardcoding the CORS policy into the attribute, you can create your own attribute implementing the ICorsPolicyProvider interface.

The library will automatically pick up on the attribute and call the interface method Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken).

Add the following class to your project:

using System;
using System.Configuration;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Cors;
using System.Web.Http.Cors;

namespace AppSettingsCors.WebApi
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class AppSettingsCorsAttribute : Attribute, ICorsPolicyProvider
    {
        private readonly CorsPolicy policy;

        public AppSettingsCorsAttribute(
            string allowedOriginsAppSettingName,
            string allowedHeadersAppSettingName,
            string allowedMethodsAppSettingName,
            string supportsCredentialsAppSettingName = null
        )
        {
            policy = new CorsPolicy();
            ConfigureOrigins(allowedOriginsAppSettingName);
            ConfigureHeaders(allowedHeadersAppSettingName);
            ConfigureMethods(allowedMethodsAppSettingName);
            ConfigureSupportsCredentials(supportsCredentialsAppSettingName);
        }

        private void ConfigureOrigins(string allowedOriginsAppSettingName)
        {
            if (string.IsNullOrEmpty(allowedOriginsAppSettingName))
            {
                throw new ArgumentNullException(nameof(allowedOriginsAppSettingName));
            }

            var origins = ConfigurationManager.AppSettings[allowedOriginsAppSettingName];

            if (string.IsNullOrEmpty(origins))
            {
                throw new Exception($"CORS Origins AppSetting is null or empty: {allowedOriginsAppSettingName}");
            }
            else if (origins == "\*")
            {
                policy.AllowAnyOrigin = true;
            }
            else
            {
                foreach (var origin in origins.Split(','))
                {
                    policy.Origins.Add(origin.Trim());
                }
            }
        }

        private void ConfigureHeaders(string allowedHeadersAppSettingName)
        {

            if (string.IsNullOrEmpty(allowedHeadersAppSettingName))
            {
                throw new ArgumentNullException(nameof(allowedHeadersAppSettingName));
            }

            var headers = ConfigurationManager.AppSettings[allowedHeadersAppSettingName];

            if (string.IsNullOrEmpty(headers))
            {
                throw new Exception($"CORS Headers AppSetting is null or empty: {allowedHeadersAppSettingName}");
            }
            else if (headers == "\*")
            {
                policy.AllowAnyHeader = true;
            }
            else
            {
                foreach (var header in headers.Split(','))
                {
                    policy.Headers.Add(header.Trim());
                }
            }
        }

        private void ConfigureMethods(string allowedMethodsAppSettingName)
        {
            if (string.IsNullOrEmpty(allowedMethodsAppSettingName))
            {
                throw new ArgumentNullException(nameof(allowedMethodsAppSettingName));
            }

            var methods = ConfigurationManager.AppSettings[allowedMethodsAppSettingName];

            if (string.IsNullOrEmpty(methods))
            {
                throw new Exception($"CORS Methods AppSetting is null or empty: {allowedMethodsAppSettingName}");
            }
            else if (methods == "\*")
            {
                policy.AllowAnyMethod = true;
            }
            else
            {
                foreach (var verb in methods.Split(','))
                {
                    policy.Methods.Add(verb.Trim());
                }
            }
        }

        private void ConfigureSupportsCredentials(string supportsCredentialsAppSettingName)
        {
            if (string.IsNullOrEmpty(supportsCredentialsAppSettingName))
            {
                return;
            }

            var supportsCredentialsString = ConfigurationManager.AppSettings[supportsCredentialsAppSettingName];

            if (string.IsNullOrEmpty(supportsCredentialsString))
            {
                throw new Exception($"CORS SupportsCredentials AppSetting is null or empty: {supportsCredentialsAppSettingName}");
            }

            if (!bool.TryParse(supportsCredentialsString, out bool supportsCredentials))
            {
                throw new Exception($"CORS SupportsCredentials AppSetting is cannot be parsed as boolean: {supportsCredentialsString}");
            }

            policy.SupportsCredentials = supportsCredentials;
        }

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

Enter fullscreen mode Exit fullscreen mode

The attribute above will accept the AppSetting keys in the constructor and extract the CORS policy configuration from the configuration AppSettings.

Note: The less commonly used CORS headers are not configurable by the above attribute. The CorsPolicy class does support them, so if you need them you can add support by extending the attribute.

Replace the EnableCors attribute with the following attribute:

[AppSettingsCors("AllowedOriginsCors\_A", "AllowedHeadersCors\_A", "AllowedMethodsCors\_A")]

Enter fullscreen mode Exit fullscreen mode

Add the following AppSettings to your web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  ...
  <appSettings>
    ...
    <add key="AllowedOriginsCors\_A" value="https://localhost:44310" />
    <add key="AllowedHeadersCors\_A" value="\*" />
    <add key="AllowedMethodsCors\_A" value="\*" />
    ...
  </appSettings>
  ...
</configuration>

Enter fullscreen mode Exit fullscreen mode

Now you can update the CORS policy without having to recompile, though the IIS website will be recycled when you modify the web.config file.

A big advantage of using AppSettings is that many platforms such as Azure App Service allow you to override the AppSettings with App Service Configuration or App Configuration.

To make the CORS policy even more reusable, you can create attributes inheriting from AppSettingsCorsAttribute and specify the AppSetting keys in the constructor as shown below:

using System;

namespace AppSettingsCors.WebApi
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class CorsPolicyAAttribute : AppSettingsCorsAttribute
    {
        public CorsPolicyAAttribute() : base(
            "AllowedOriginsCors\_A",
            "AllowedHeadersCors\_A",
            "AllowedMethodsCors\_A"
        )
        { }
    }

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class CorsPolicyBAttribute : AppSettingsCorsAttribute
    {
        public CorsPolicyBAttribute() : base(
            "AllowedOriginsCors\_B",
            "AllowedHeadersCors\_B",
            "AllowedMethodsCors\_B",
            "SupportsCredentialsCors\_B"
        )
        { }
    }
}

Enter fullscreen mode Exit fullscreen mode

Now you can simply decorate your controller and actions with CorsPolicyAAttribute or CorsPolicyBAttribute. Here's an example:

using System.Web.Http;

namespace AppSettingsCors.WebApi.Controllers
{
    public class PolicyDemoController : ApiController
    {
        [HttpGet]
        [CorsPolicyA, CorsPolicyB]
        public string AB()
        {
            return "Hello A & B";
        }

        [HttpGet]
        [CorsPolicyA]
        public string A()
        {
            return "Hello A";
        }

        [HttpGet]
        [CorsPolicyB]
        public string B()
        {
            return "Hello B";
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Use a Custom Configuration Section to configure CORS

All code in this article can be found on this GitHub repository.

Using AppSettings to configure CORS is a huge improvement over hardcoding, though you may prefer a more purpose made configuration section.

Add the following code to your project:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Xml;

namespace AppSettingsCors.WebApi
{
    public class CorsPoliciesSection : ConfigurationSection
    {
        [ConfigurationProperty("", IsDefaultCollection = true)]
        [ConfigurationCollection(typeof(CorsPolicyCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
        public CorsPolicyCollection CorsPolicies
        {
            get => (CorsPolicyCollection)base[""];
            set => this["Policies"] = value;
        }
    }

    public class CorsPolicyCollection : ConfigurationElementCollection
    {
        protected override ConfigurationElement CreateNewElement() => new CorsPolicyElement();

        protected override object GetElementKey(ConfigurationElement element) => (element as CorsPolicyElement).Key;
    }

    public class CorsPolicyElement : ConfigurationElement
    {
        [ConfigurationProperty("key", Options = ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey)]
        public string Key
        {
            get { return (string)base["key"]; }
            set { base["key"] = value; }
        }

        [ConfigurationProperty("AllowedOrigins", Options = ConfigurationPropertyOptions.IsRequired)]
        public TextContentConfigurationElement AllowedOrigins
        {
            get { return (TextContentConfigurationElement)base["AllowedOrigins"]; }
            set { base["AllowedOrigins"] = value; }
        }

        [ConfigurationProperty("AllowedHeaders", Options = ConfigurationPropertyOptions.IsRequired)]
        public TextContentConfigurationElement AllowedHeaders
        {
            get { return (TextContentConfigurationElement)base["AllowedHeaders"]; }
            set { base["AllowedHeaders"] = value; }
        }

        [ConfigurationProperty("AllowedMethods", Options = ConfigurationPropertyOptions.IsRequired)]
        public TextContentConfigurationElement AllowedMethods
        {
            get { return (TextContentConfigurationElement)base["AllowedMethods"]; }
            set { base["AllowedMethods"] = value; }
        }

        [ConfigurationProperty("SupportsCredentials", Options = ConfigurationPropertyOptions.IsRequired)]
        public TextContentConfigurationElement SupportsCredentials
        {
            get { return (TextContentConfigurationElement)base["SupportsCredentials"]; }
            set { base["SupportsCredentials"] = value; }
        }
    }

    public class TextContentConfigurationElement : ConfigurationElement
    {
        protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
        {
            TextContent = reader.ReadElementContentAsString();
        }

        public string TextContent { get; private set; }
    }
}

Enter fullscreen mode Exit fullscreen mode

This configuration section will allow you to create CORS policies in a more structured way in your configuration file.

Add the following attribute to your project:

using System;
using System.Linq;
using System.Configuration;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Cors;
using System.Web.Http.Cors;

namespace AppSettingsCors.WebApi
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class ConfigCorsPolicyAttribute : Attribute, ICorsPolicyProvider
    {
        private readonly CorsPolicy policy;
        private readonly string policyKey;

        public ConfigCorsPolicyAttribute(string policyKey)
        {
            policy = new CorsPolicy();
            this.policyKey = policyKey;
            var policiesSection = (CorsPoliciesSection)ConfigurationManager.GetSection("CorsPolicies");
            var corsPolicyElement = policiesSection.CorsPolicies.OfType<CorsPolicyElement>().FirstOrDefault(e => e.Key == policyKey);
            ConfigureOrigins(corsPolicyElement.AllowedOrigins.TextContent);
            ConfigureHeaders(corsPolicyElement.AllowedHeaders.TextContent);
            ConfigureMethods(corsPolicyElement.AllowedMethods.TextContent);
            ConfigureSupportsCredentials(corsPolicyElement.SupportsCredentials.TextContent);
        }

        private void ConfigureOrigins(string origins)
        {
            if (string.IsNullOrEmpty(origins))
            {
                throw new ArgumentNullException(nameof(origins), $"CORS Origins is null or empty for policy {policyKey}");
            }
            else if (origins == "\*")
            {
                policy.AllowAnyOrigin = true;
            }
            else
            {
                foreach (var origin in origins.Split(','))
                {
                    policy.Origins.Add(origin.Trim());
                }
            }
        }

        private void ConfigureHeaders(string headers)
        {
            if (string.IsNullOrEmpty(headers))
            {
                throw new ArgumentNullException(nameof(headers), $"CORS Headers is null or empty for policy {policyKey}");
            }
            else if (headers == "\*")
            {
                policy.AllowAnyHeader = true;
            }
            else
            {
                foreach (var header in headers.Split(','))
                {
                    policy.Headers.Add(header.Trim());
                }
            }
        }

        private void ConfigureMethods(string methods)
        {
            if (string.IsNullOrEmpty(methods))
            {
                throw new ArgumentNullException(nameof(methods), $"CORS Methods is null or empty for policy {policyKey}");
            }
            else if (methods == "\*")
            {
                policy.AllowAnyMethod = true;
            }
            else
            {
                foreach (var verb in methods.Split(','))
                {
                    policy.Methods.Add(verb.Trim());
                }
            }
        }

        private void ConfigureSupportsCredentials(string supportsCredentialsString)
        {
            if (string.IsNullOrEmpty(supportsCredentialsString))
            {
                throw new ArgumentNullException(nameof(supportsCredentialsString), $"CORS SupportsCredentials is null or empty for policy {policyKey}");
            }

            if (!bool.TryParse(supportsCredentialsString, out bool supportsCredentials))
            {
                throw new Exception($"CORS SupportsCredentials is cannot be parsed as boolean: {supportsCredentialsString}");
            }

            policy.SupportsCredentials = supportsCredentials;
        }

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

Enter fullscreen mode Exit fullscreen mode

This attribute will read the custom configuration section and configure the CORS policy from the config file.

Update your web.config with the following custom configuration section:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  ...
  <configSections>
    <section name="CorsPolicies" type="AppSettingsCors.WebApi.CorsPoliciesSection, AppSettingsCors.WebApi"/>
  </configSections>
  <CorsPolicies>
    <add key="Policy\_C">
      <AllowedOrigins>https://localhost:44310, https://localhost:7777</AllowedOrigins>
      <AllowedHeaders>*</AllowedHeaders>
      <AllowedMethods>*</AllowedMethods>
      <SupportsCredentials>false</SupportsCredentials>
    </add>
  </CorsPolicies>
  ...
</configuration>

Enter fullscreen mode Exit fullscreen mode

Now you can decorate your controllers and actions using the ConfigCorsPolicyAttribute by passing in the key of the policy into the constructor. Here's an example:

[HttpGet]
[ConfigCorsPolicyAttribute("Policy\_C")]
public string C()
{
    return "Hello C";
}

Enter fullscreen mode Exit fullscreen mode

With this approach you have a more structured configuration, but the drawback is that you cannot override the policies like you could with AppSettings in Azure.

Note: The less commonly used CORS headers are not configurable in the implementation above. The CorsPolicy class does support them, so if you need them you can add support for them in the attribute.

Summary

In this article you learned the very basics of CORS and how to add CORS support to ASP.NET Web API using

  • out-of-the box EnableCors attribute
  • Custom AppSettingsCors attribute that pulls the policy configuration from web.config AppSettings
  • Custom ConfigCorsPolicy attribute that pulls the policy configuration from a web.config custom configuration section

Warning : Although CORS headers allow you to use a wildcard (*), it is not recommended. Please explicitly specify which origins you want to allow if possible.

Top comments (0)