Configure cors to allow all subdomains using ASP.NET Core (Asp.net 5, MVC6, VNext)
asp.net CoreCorsasp.net Core-Mvcasp.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 :)