Configure cors to allow all subdomains using ASP.NET Core (Asp.net 5, MVC6, VNext)

asp.net CoreCorsasp.net Core-Mvc

asp.net Core Problem Overview


I have cors setup correctly in an ASP.NET Core web app. Im using the following package...

"Microsoft.AspNet.Cors": "6.0.0-rc1-final"

and here is the startup.cs snippet...

public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
	services.AddCors
	(
		options =>
		{
			options.AddPolicy
			(
				CORSDefaults.PolicyName, 
				builder =>
				{
					//From config...
					var allowedDomains = new []{"http://aaa.somewhere.com","https://aaa.somewhere.com","http://bbb.somewhere.com","https://bbb.somewhere.com"};
					
					//Load it
					builder
						.WithOrigins(allowedDomains)
						.AllowAnyHeader()
						.AllowAnyMethod()
						.AllowCredentials();
				}
			);
		}
	);
}

This works great except that the list of subdomains to allow is growing fast and I want to allow all subdomains of "somewhere.com". Something like "*.somewhere.com". I cant seem to find any documentation on how to do this in the new ASP.NET Core (MVC6, ASP.NET5, VNext). All the docs/examples I'm finding that demonstrate how to do this are for earlier versions of MVC or WebApi. How can I achieve this in the new stack?

asp.net Core Solutions


Solution 1 - asp.net Core

This has now been implemented in version 2.0.0. In your ConfigureServices use the following:

options.AddPolicy("MyCorsPolicy",
   builder => builder
      .SetIsOriginAllowedToAllowWildcardSubdomains()
      .WithOrigins("https://*.mydomain.com")
      .AllowAnyMethod()
      .AllowCredentials()
      .AllowAnyHeader()
      .Build()
   );

Also, don't forget to call UseCors in your Configure call too:

app.UseCors("MyCorsPolicy");

Solution 2 - asp.net Core

I submitted a pull request to the ASP.NET team with this change so hopefully it will make it into the nuget package. Until then, I use this workaround.

Below you register cors as usual with the exception of having to register the WildCardCorsService class in the di container.

public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.TryAdd(ServiceDescriptor.Transient<ICorsService, WildCardCorsService>());
	services.AddCors
	(
		options =>
		{
			options.AddPolicy
			(
				CORSDefaults.PolicyName, 
				builder =>
				{
					builder
						.WithOrigins("http://*.withwildcardsubdomain.com", "http://nowildcard.com")
						.AllowAnyHeader()
						.AllowAnyMethod()
						.AllowCredentials();
				}
			);
		}
	);
}

Save this class locally in your solution. It is a copy and edit of the Microsoft.AspNet.Cors.CorsService.cs class to allow it to handle wildcard subdomains. If it finds a wildcard char '*' it will check if the root domain matches on allowed origins and actual origin. It does NOT support partial wildcard matching.

namespace Microsoft.AspNet.Cors.Infrastructure
{
	/// <summary>
	/// This ICorsService should be used in place of the official default CorsService to support origins 
	/// like http://*.example.comwhich will allow any subdomain for example.com
	/// </summary>
	public class WildCardCorsService : ICorsService
	{
		private readonly CorsOptions _options;

		/// <summary>
		/// Creates a new instance of the <see cref="CorsService"/>.
		/// </summary>
		/// <param name="options">The option model representing <see cref="CorsOptions"/>.</param>
		public WildCardCorsService(IOptions<CorsOptions> options)
		{
			if (options == null)
			{
				throw new ArgumentNullException(nameof(options));
			}

			_options = options.Value;
		}

		/// <summary>
		/// Looks up a policy using the <paramref name="policyName"/> and then evaluates the policy using the passed in
		/// <paramref name="context"/>.
		/// </summary>
		/// <param name="requestContext"></param>
		/// <param name="policyName"></param>
		/// <returns>A <see cref="CorsResult"/> which contains the result of policy evaluation and can be
		/// used by the caller to set appropriate response headers.</returns>
		public CorsResult EvaluatePolicy(HttpContext context, string policyName)
		{
			if (context == null)
			{
				throw new ArgumentNullException(nameof(context));
			}

			var policy = _options.GetPolicy(policyName);
			return EvaluatePolicy(context, policy);
		}

		/// <inheritdoc />
		public CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
		{
			if (context == null)
			{
				throw new ArgumentNullException(nameof(context));
			}

			if (policy == null)
			{
				throw new ArgumentNullException(nameof(policy));
			}

			var corsResult = new CorsResult();
			var accessControlRequestMethod = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestMethod];
			if (string.Equals(context.Request.Method, Microsoft.AspNet.Cors.Infrastructure.CorsConstants.PreflightHttpMethod, StringComparison.Ordinal) &&
				!StringValues.IsNullOrEmpty(accessControlRequestMethod))
			{
				EvaluatePreflightRequest(context, policy, corsResult);
			}
			else
			{
				EvaluateRequest(context, policy, corsResult);
			}

			return corsResult;
		}

		public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result)
		{
			var origin = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.Origin];
			if (!OriginIsAllowed(origin, policy))
			{
				return;
			}

			AddOriginToResult(origin, policy, result);
			result.SupportsCredentials = policy.SupportsCredentials;
			AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders);
		}

		public virtual void EvaluatePreflightRequest(HttpContext context, CorsPolicy policy, CorsResult result)
		{
			var origin = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.Origin];
			if (!OriginIsAllowed(origin, policy))
			{
				return;
			}

			var accessControlRequestMethod = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestMethod];
			if (StringValues.IsNullOrEmpty(accessControlRequestMethod))
			{
				return;
			}

			var requestHeaders =
				context.Request.Headers.GetCommaSeparatedValues(Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestHeaders);

			if (!policy.AllowAnyMethod && !policy.Methods.Contains(accessControlRequestMethod))
			{
				return;
			}

			if (!policy.AllowAnyHeader &&
				requestHeaders != null &&
				!requestHeaders.All(header => Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase) ||
											  policy.Headers.Contains(header, StringComparer.OrdinalIgnoreCase)))
			{
				return;
			}

			AddOriginToResult(origin, policy, result);
			result.SupportsCredentials = policy.SupportsCredentials;
			result.PreflightMaxAge = policy.PreflightMaxAge;
			result.AllowedMethods.Add(accessControlRequestMethod);
			AddHeaderValues(result.AllowedHeaders, requestHeaders);
		}

		/// <inheritdoc />
		public virtual void ApplyResult(CorsResult result, HttpResponse response)
		{
			if (result == null)
			{
				throw new ArgumentNullException(nameof(result));
			}

			if (response == null)
			{
				throw new ArgumentNullException(nameof(response));
			}

			var headers = response.Headers;

			if (result.AllowedOrigin != null)
			{
				headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowOrigin] = result.AllowedOrigin;
			}

			if (result.VaryByOrigin)
			{
				headers["Vary"] = "Origin";
			}

			if (result.SupportsCredentials)
			{
				headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowCredentials] = "true";
			}

			if (result.AllowedMethods.Count > 0)
			{
				// Filter out simple methods
				var nonSimpleAllowMethods = result.AllowedMethods
					.Where(m =>
						!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleMethods.Contains(m, StringComparer.OrdinalIgnoreCase))
					.ToArray();

				if (nonSimpleAllowMethods.Length > 0)
				{
					headers.SetCommaSeparatedValues(
						Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowMethods,
						nonSimpleAllowMethods);
				}
			}

			if (result.AllowedHeaders.Count > 0)
			{
				// Filter out simple request headers
				var nonSimpleAllowRequestHeaders = result.AllowedHeaders
					.Where(header =>
						!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
					.ToArray();

				if (nonSimpleAllowRequestHeaders.Length > 0)
				{
					headers.SetCommaSeparatedValues(
						Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowHeaders,
						nonSimpleAllowRequestHeaders);
				}
			}

			if (result.AllowedExposedHeaders.Count > 0)
			{
				// Filter out simple response headers
				var nonSimpleAllowResponseHeaders = result.AllowedExposedHeaders
					.Where(header =>
						!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleResponseHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
					.ToArray();

				if (nonSimpleAllowResponseHeaders.Length > 0)
				{
					headers.SetCommaSeparatedValues(
						Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlExposeHeaders,
						nonSimpleAllowResponseHeaders);
				}
			}

			if (result.PreflightMaxAge.HasValue)
			{
				headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlMaxAge]
					= result.PreflightMaxAge.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture);
			}
		}

		protected virtual bool OriginIsAllowed(string origin, CorsPolicy policy)
		{
			if (!string.IsNullOrWhiteSpace(origin) &&
				(policy.AllowAnyOrigin ||
				 policy.Origins.Contains(origin) ||
				 IsWildCardSubdomainMatch(origin, policy)))
				return true;

			return false;
		}

		private void AddOriginToResult(string origin, CorsPolicy policy, CorsResult result)
		{
			if (policy.AllowAnyOrigin)
			{
				if (policy.SupportsCredentials)
				{
					result.AllowedOrigin = origin;
					result.VaryByOrigin = true;
				}
				else
				{
					result.AllowedOrigin = Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AnyOrigin;
				}
			}
			else
			{
				result.AllowedOrigin = origin;
			}
		}

		private static void AddHeaderValues(IList<string> target, IEnumerable<string> headerValues)
		{
			if (headerValues == null)
			{
				return;
			}

			foreach (var current in headerValues)
			{
				target.Add(current);
			}
		}

		private bool IsWildCardSubdomainMatch(string origin, CorsPolicy policy)
		{
			var actualOriginUri = new Uri(origin);
			var actualOriginRootDomain = GetRootDomain(actualOriginUri);

			foreach (var o in policy.Origins)
			{
				if (!o.Contains("*"))
					continue;

				// 1) CANNOT USE System.Text.RegularExpression since it does not exist in .net platform 5.4 (which the Microsoft.AspNet.Cors project.json targets)
				// 2) '*' char is not valid for creation of a URI object so we replace it just for this comparison
				var allowedOriginUri = new Uri(o.Replace("*", "SOMELETTERS"));
				if (allowedOriginUri.Scheme == actualOriginUri.Scheme &&
					actualOriginRootDomain == GetRootDomain(allowedOriginUri))
					return true;
			}

			return false;
		}

		private string GetRootDomain(Uri uri)
		{
			//Got this snippet here http://stackoverflow.com/questions/16473838/get-domain-name-of-a-url-in-c-sharp-net
			var host = uri.Host;
			int index = host.LastIndexOf('.'), last = 3;

			while (index > 0 && index >= last - 3)
			{
				last = index;
				index = host.LastIndexOf('.', last - 1);
			}

			return host.Substring(index + 1);
		}
	}

	/// <summary>
	/// Needed to copy these in since some of them are internal to the Microsoft.AspNet.Cors project
	/// </summary>
	public static class CorsConstants
	{
		/// <summary>The HTTP method for the CORS preflight request.</summary>
		public static readonly string PreflightHttpMethod = "OPTIONS";
		/// <summary>The Origin request header.</summary>
		public static readonly string Origin = "Origin";
		/// <summary>
		/// The value for the Access-Control-Allow-Origin response header to allow all origins.
		/// </summary>
		public static readonly string AnyOrigin = "*";
		/// <summary>The Access-Control-Request-Method request header.</summary>
		public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
		/// <summary>The Access-Control-Request-Headers request header.</summary>
		public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers";
		/// <summary>The Access-Control-Allow-Origin response header.</summary>
		public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
		/// <summary>The Access-Control-Allow-Headers response header.</summary>
		public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
		/// <summary>The Access-Control-Expose-Headers response header.</summary>
		public static readonly string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
		/// <summary>The Access-Control-Allow-Methods response header.</summary>
		public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods";
		/// <summary>The Access-Control-Allow-Credentials response header.</summary>
		public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
		/// <summary>The Access-Control-Max-Age response header.</summary>
		public static readonly string AccessControlMaxAge = "Access-Control-Max-Age";
		internal static readonly string[] SimpleRequestHeaders = new string[4]
		{
	  "Origin",
	  "Accept",
	  "Accept-Language",
	  "Content-Language"
		};
		internal static readonly string[] SimpleResponseHeaders = new string[6]
		{
	  "Cache-Control",
	  "Content-Language",
	  "Content-Type",
	  "Expires",
	  "Last-Modified",
	  "Pragma"
		};
		internal static readonly string[] SimpleMethods = new string[3]
		{
	  "GET",
	  "HEAD",
	  "POST"
		};
	}
}

Enjoy!

Solution 3 - asp.net Core

The out-of-the-box CorsService uses policy.Origins.Contains(origin) to evaluate a request. So, it does not look like there is a trivial way to do what you require, because the List must contain the origin. You could implement your own ICorsService, inherit what the out-of-the-box CorsService already provides, and tweak the methods to handle the *.mydomain.com wildcard.

Edit Here is what I accomplished using yo aspnet to generate a 1.0.0-rc1-update2 Web Api project. It works. Register your service in Startup.cs (see CorsServiceCollectionExtensions for details.)

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();

        services.TryAdd(
            ServiceDescriptor.Transient<ICorsService, MyCorsService>());

        services.TryAdd(
            ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
    }

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(minLevel: LogLevel.Verbose);

        app.UseCors(corsPolictyBuilder =>
        {
            corsPolictyBuilder.WithOrigins("*.mydomain.com");
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync(
                $"Is Cors? {context.Request.Headers.ContainsKey(CorsConstants.Origin)}");
        });
    }
}

Here is the service, awaiting your implementation. You can either copy/paste or inherit from CorsService.

public class MyCorsService : CorsService, ICorsService
{
    private ILogger _logger;
    
    public MyCorsService(IOptions<CorsOptions> options, ILogger<MyCorsService> logger)
        : base(options)
    {
        _logger = logger;
        _logger.LogInformation("MyCorsService");
    }

    public override void ApplyResult(
        CorsResult result, HttpResponse response)
    {
        _logger.LogInformation("ApplyResult");
        base.ApplyResult(result, response);
    }

    public override void EvaluateRequest(
        HttpContext context, CorsPolicy policy, CorsResult result)
    {
        _logger.LogInformation("EvaluateRequest");
        base.EvaluateRequest(context, policy, result);
    }

    public override void EvaluatePreflightRequest(
        HttpContext context, CorsPolicy policy, CorsResult result)
    {
        _logger.LogInformation("EvaluatePreflightRequest");
        base.EvaluatePreflightRequest(context, policy, result);
    }
}

Solution 4 - asp.net Core

SetIsOriginAllowedToAllowWildcardSubdomains function works well when the wildcard character is specified at the first part of the subdomains for e.g. https:\\*.modules.features.releaseversion.companyname.com but the same function doesn't give the desired result when the wildcard character is specified at any other part of the subdomain for e.g. https:\\environment.modules.*.releaseversion.companyname.com or https:\\*.modules.*.releaseversion.companyname.com or https:\\environment.*.*.releaseversion.companyname.com

Below code is inspired from the @Shaun Luttin and @sjdirect code snippet

We just wanted to extend the behaviour of the Microsoft.AspNetCore.Cors.Infrastructure.CorsService class to enable usage of wildcard character specified anywhere in the URL

Below class performs the CORS check to allow wild card subdomains. Copy this class locally into the desired project. It's an extended version of the Microsoft.AspNetCore.Cors.Infrastructure.CorsService to enable wild card support for subdomains.

    using Microsoft.AspNetCore.Cors.Infrastructure;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using Microsoft.Extensions.Primitives;
    using System;
    using System.Text.RegularExpressions;

namespace Microsoft.AspNetCore.Cors.Infrastructure
{
	public class CORSServiceWildCardSupport : CorsService, ICorsService
	{
		private readonly CorsOptions _options;
		private readonly ILogger _logger;
		public CORSServiceWildCardSupport(IOptions<CorsOptions> options, ILoggerFactory loggerFactory) : base(options, loggerFactory)
		{
			_options = options.Value;
			_logger = loggerFactory.CreateLogger<CorsService>();
		}

		public new virtual CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
		{
			if (context == null)
			{
				throw new ArgumentNullException("context");
			}
			if (policy == null)
			{
				throw new ArgumentNullException("policy");
			}
			if (policy.AllowAnyOrigin && policy.SupportsCredentials)
			{
				throw new ArgumentException(Resource.InsecureConfiguration, "policy");
			}
			IHeaderDictionary headers = context.Request.Headers;
			StringValues origin = headers[CorsConstants.Origin];

			bool num = string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase);
			bool flag = num && headers.ContainsKey(CorsConstants.AccessControlRequestMethod);

			CorsResult result = new CorsResult
			{
				IsPreflightRequest = flag,
				IsOriginAllowed = IsWildCardSubdomainMatch(origin, policy)
			};
			if (flag)
			{
				EvaluatePreflightRequest(context, policy, result);
			}
			else
			{
				EvaluateRequest(context, policy, result);
			}
			return result;
		}

		protected virtual IsWildCardSubdomainMatch(string origin, CorsPolicy policy)
		{
			var actualOrigin = new Uri(origin);
			foreach (var o in policy.Origins)
			{
				if (IsWildcardMatch(actualOrigin, o))
				{
					return true;
				}
			}
			return false;
		}

		private bool IsWildcardMatch(Uri actualOrigin, string wildcardUri)
		{
			if (!wildcardUri.StartsWith(actualOrigin.Scheme))
			{
				return false;
			}
			var wildcardUriMinusScheme = wildcardUri.Replace(actualOrigin.Scheme + "://", "");
			var regexFirstStage = wildcardUriMinusScheme.Replace(".", "\\.");
			var regexAllowedHosts = "^" + regexFirstStage.Replace("*", ".*") + "$";
			var actualOriginMinusScheme = actualOrigin.OriginalString.Replace(actualOrigin.Scheme + "://", "");
			var isMatch = Regex.IsMatch(actualOriginMinusScheme, regexAllowedHosts);
			return isMatch;
		}
	}
}

From the above class function IsWildCardSubdomainMatch or IsWildcardMatch can be extended based on the requirement, for our requirement we just needed to perform string comparison.

Register the CORSServiceWildCardSupport class into the dependency container using the below extension class. Copy the class locally into the desired project

using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.DependencyInjection
{
    public static class CORSServiceCollectionExtensions
    {
        public static IServiceCollection AddCORSWithWildCardSupport(this IServiceCollection services)
        {
            if (services == null)
            {
                throw new ArgumentNullException("services");
            }
            services.AddOptions();
            services.TryAdd(ServiceDescriptor.Transient<ICorsService, CORSServiceWildCardSupport>());
            services.TryAdd(ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
            return services;
        }

        public static IServiceCollection AddCORSWithWildCardSupport(this IServiceCollection services, Action<CorsOptions> setupAction)
        {
            if (services == null)
            {
                throw new ArgumentNullException("services");
            }
            if (setupAction == null)
            {
                throw new ArgumentNullException("setupAction");
            }
            services.AddCORSWithWildCardSupport();
            services.Configure(setupAction);
            return services;
        }
    }
}

Register the CORS from the Startup class

public void ConfigureServices(IServiceCollection services)
        {
            try
            {
                string[] whitelist = {"https:\\environment.modules.*.releaseversion.companyname.com","https:\\*.modules.*.releaseversion.companyname.com","https:\\environment.*.*.releaseversion.companyname.com"};

                services.AddCORSWithWildCardSupport(o => o.AddPolicy(Resource.CorsPolicyName, builder =>
                {
                    builder.WithOrigins(whitelist)
                           .AllowAnyMethod()
                           .AllowAnyHeader()
                           .AllowCredentials();
                }));

                services.AddControllers();

                services.AddMvc(option => option.EnableEndpointRouting = false)
                    .SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

                services.AddAuthentication("Windows");
            }
            catch(Exception ex)
            {
                Logger.Error($"Failed to start due to {ex.Message}.");
            }
        }

services.AddControllers method also registers ICORSService into the dependency container hence always use AddCORS prior to AddControllers

Enjoy :)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsjdirectView Question on Stackoverflow
Solution 1 - asp.net CoreLloyd PowellView Answer on Stackoverflow
Solution 2 - asp.net CoresjdirectView Answer on Stackoverflow
Solution 3 - asp.net CoreShaun LuttinView Answer on Stackoverflow
Solution 4 - asp.net CoreAnilkumarView Answer on Stackoverflow