CORS with credential support on Azure

Azure hosting service provides an easy & friendly portal for managing all its Azure resources. It has a dedicated page for configuring all CORS Origins for each app-service.

Advantage of using the Azure CORS:

  1. It automatically handles & responds back to the CORS preflight request before reaching the actual application.
  2. You can maintain CORS origin as part of the ARM templates, so that you can automate deployment of multiple instance.
  3. Easily manageable by non-technical guy, once he is aware of how to white-list CORS origins.
  4. Zero development or maintenance required by the application developers, as things are completely handled by Azure CORS service. 

Limitation of Azure's default CORS module:

Though the Azure CORS looks awesome, like a one-stop perfect solution. It does have a critical limitation which is intentionally left out.

It doesn't support credentials in the CORS headers, and it causes a major blockade for Applications which runs on different sub-domains planning to use Cookies as a means of Single Sign On medium. So when you try to use withCredentials flag in XHR requests, browsers will simply complain about the missing CORS header and block the subsequent cross origin requests.

So feature request related to this issue was already raised by the user to Azure team and it was simply declined.
*ReferenceAccess-Control-Allow-Credentials not set in credentialed CORS request

The main reasons that I suspect for this denial are

  1. Performance - Implementing a feature to verify origins and apply different headers based on specified condition for each, will increase the unwanted computation/load requirement on all Azure applications that just look for a basic CORS support - Access-Control-Allow-Origin : * or <origin>.
  2. Security - Azure team doesn't want to blindly add allow credential header (Access-Control-Allow-Credentials) for all CORS origins, as it may unknowingly open up the security risk for sites.

The recommendation from Azure team is to add a Custom CORS module.

There are few solutions available from which you can select one, based on your requirement and development flexibility

  1. Azure website extension for official IIS CORS module:
    It doesn't exits at time of writing this article, but I strongly believe that an Azure website extension will be developed & released in future. This module has great flexibility for configuring the rules and it also off-loads the work to the IIS rather than the need for application to do the checks.

  2. Microsoft's CORS nuget package:
    These packages helps the developers to decorate the actions or controllers with [EnableCors] attribute with the corresponding policy. This provides greater flexibility as it can return different CORS headers based on the mentioned CORS policy for each actions or controller. You can learn more about it in the following article 

    *reference - Enable Cross-Origin Requests (CORS) in ASP.NET Core

    Pick your Nuget flavor from below
  3. Simply custom headers to web.config:
    If you want to avoid the pain of implementing/customizing your application, and just prefer to simply add a single trusted origin and directly hardcode add all required CORS header in the web.config. You can add simply copy paste following headers 
    <httpProtocol>
       <customHeaders>
         <clear />
         <add name="X-Powered-By" value="ASP.NET" />
    	<remove name="Access-Control-Allow-Origin" />
    	<add name="Access-Control-Allow-Origin" value="http://www.domain.com" />
    	<add name="Access-Control-Allow-Headers" value="Content-Type" />
    	<add name="Access-Control-Allow-Credentials" value="true" />
       </customHeaders>
    </httpProtocol>

     

  4. Implement a custom CORS HttpModule:
    Implementing the custom CORS module, allows you to add CORS headers to static assets like CSS, JavaScripts and images (used in canvas). It also allows you to customize as much as possible, without the need to decorate actions and controllers.

    Web.Config
    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true">
          <remove name="CorsModule"/>
          <add name="CorsModule" type="Davidsekar.HttpModules.CorsModule"/>
        </modules>
    </system.webServer>?

    HttpModules/CorsModule.cs
    namespace Davidsekar.HttpModules
    {
        using System;
        using System.Collections.Generic;
        using System.Configuration;
        using System.Linq;
        using System.Net;
        using System.Web;
        /// <summary>
        /// Http module for enabling 
        /// </summary>
        /// <seealso cref="System.Web.IHttpModule" />
        public class CorsModule : IHttpModule
        {
            private readonly HashSet<string> _allowedOrigin,
                _allowedOriginWithCredentials,
                _allowedMethods,
                _allowedHeaders;
    
            private int _preflightMaxAge;
            private CorsResult _corsResult;
    
            public CorsModule()
            {
                _allowedOrigin = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                _allowedOriginWithCredentials = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                _allowedMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                _allowedHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            }
    
            /// <summary>
            /// Initializes a module and prepares it to handle requests.
            /// </summary>
            /// <param name="context">An <see cref="T:System.Web.HttpApplication" /> that provides access to the methods, properties, and events common to all application objects within an ASP.NET application</param>
            public void Init(HttpApplication context)
            {
                context.BeginRequest += Application_BeginRequest;
                context.EndRequest += Application_EndRequest;
    
                ParseConfigHeaders("AllowedOrigins", _allowedOrigin);
                ParseConfigHeaders("AllowedOriginsWithCredentials", _allowedOriginWithCredentials);
                ParseConfigHeaders("AccessControlAllowMethods", _allowedMethods);
                ParseConfigHeaders("AccessControlAllowHeaders", _allowedHeaders);
                _preflightMaxAge = Convert.ToInt32(ConfigurationManager.AppSettings["AccessControlMaxAge"]);
            }
    
            /// <summary>
            /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule" />.
            /// </summary>
            public void Dispose()
            {
            }
    
            /// <summary>
            /// Handles the BeginRequest event of the Application control.
            /// </summary>
            /// <param name="sender">The source of the event.</param>
            /// <param name="eventArgs">The <see cref="EventArgs"/> instance containing the event data.</param>
            private void Application_BeginRequest(object sender, EventArgs eventArgs)
            {
                if (!(sender is HttpApplication application)) return;
    
                var context = application.Context;
                var isPreflightRequest = ValidateRequest(context.Request);
    
    
                if (isPreflightRequest)
                {
                    // OPTIONS request need not run the actual action, return as soon as we have required headers
                    context.Response.StatusCode = (int)HttpStatusCode.NoContent;
                    context.Response.End();
                }
            }
    
            /// <summary>
            /// Handles the EndRequest event of the Application control.
            /// </summary>
            /// <param name="source">The source of the event.</param>
            /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
            private void Application_EndRequest(object source, EventArgs e)
            {
                if (!(source is HttpApplication application)) return;
    
                ApplyCorsResult(application.Context.Response);
            }
    
            private void ParseConfigHeaders(string settingName, HashSet<string> headers)
            {
                var commaSeparatedText = ConfigurationManager.AppSettings[settingName];
                if (!string.IsNullOrEmpty(commaSeparatedText))
                {
                    foreach (var header in commaSeparatedText.Split(','))
                        headers.Add(header);
                }
            }
    
            /// <summary>
            /// Validates the request.
            /// </summary>
            /// <param name="currentRequest">The current request.</param>
            /// <returns>true, if its a CORS preflight request</returns>
            private bool ValidateRequest(HttpRequest currentRequest)
            {
                _corsResult = new CorsResult();
                if (currentRequest == null) return false;
    
                var origin = currentRequest.Headers.Get(CorsConstants.Origin);
                _corsResult.IsCrossOrigin = _allowedOrigin.Contains(origin, StringComparer.InvariantCultureIgnoreCase);
    
                if (_allowedOriginWithCredentials.Contains(origin, StringComparer.InvariantCultureIgnoreCase))
                {
                    _corsResult.IsCrossOrigin = true;
                    _corsResult.SupportsCredentials = true;
                }
    
                if (_corsResult.IsCrossOrigin)
                {
                    _corsResult.AllowedOrigin = origin;
    
                    foreach (var methods in _allowedMethods)
                        _corsResult.AllowedMethods.Add(methods);
    
                    foreach (var header in _allowedHeaders)
                        _corsResult.AllowedHeaders.Add(header);
    
                    _corsResult.PreflightMaxAge = _preflightMaxAge;
                }
    
                return currentRequest.HttpMethod.Equals(CorsConstants.PreflightHttpMethod,
                    StringComparison.InvariantCultureIgnoreCase);
            }
    
            /// <summary>
            /// Applies the cors result.
            /// </summary>
            /// <param name="currentResponse">The current response.</param>
            private void ApplyCorsResult(HttpResponse currentResponse)
            {
                if (currentResponse == null || !_corsResult.IsCrossOrigin) return;
    
                currentResponse.Headers[CorsConstants.AccessControlAllowOrigin] = _corsResult.AllowedOrigin;
                currentResponse.Headers[CorsConstants.AccessControlAllowMethods] =
                    string.Join(",", _corsResult.AllowedMethods);
    
                currentResponse.Headers[CorsConstants.AccessControlAllowHeaders] =
                    string.Join(",", _corsResult.AllowedHeaders);
                currentResponse.Headers[CorsConstants.AccessControlMaxAge] = _corsResult.PreflightMaxAge.ToString();
    
                if (_corsResult.SupportsCredentials)
                    currentResponse.Headers[CorsConstants.AccessControlAllowCredentials] = "true";
    
                // Add 'Vary' header based on origin, so that CDNs & browsers will know that
                // the CORS headers can differ based on the origin that makes the request.
                currentResponse.AppendHeader("Vary", "Origin");
            }
        }
    
        internal class CorsResult
        {
            public bool IsCrossOrigin { get; set; } = false;
            public string AllowedOrigin { get; set; }
            public bool SupportsCredentials { get; set; }
            public IList<string> AllowedMethods { get; } = new List<string>();
            public IList<string> AllowedHeaders { get; } = new List<string>();
            public IList<string> AllowedExposedHeaders { get; } = new List<string>();
            public int PreflightMaxAge { get; set; }
        }
    
        internal class CorsConstants
        {
            public static readonly string PreflightHttpMethod = "Options";
            public static readonly string Origin = "Origin";
            public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
            public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers";
    
            public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
            public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods";
            public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
            public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
            public static readonly string AccessControlMaxAge = "Access-Control-Max-Age";
        }
    }?


*Note:
CORS is ever evolving and most modern browsers support all CORS header & still changes are in progress. So picking your manual solution/implementation will require you to stay updated with the latest happenings and make required changes to your CorsModule as and when required.

Related Posts