Skip to content

Instantly share code, notes, and snippets.

@richardneililagan
Last active March 3, 2016 07:49
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save richardneililagan/5091035 to your computer and use it in GitHub Desktop.
Save richardneililagan/5091035 to your computer and use it in GitHub Desktop.
A bunch of classes for enabling CORS support for MVC 4 Web API, based off of Carlos Figueira's work. https://gist.github.com/richardneililagan/5091035/#comment-791095
namespace Mvc.Cors
{
using System.Linq;
using System.Web.Http.Filters;
public class CorsEnabledAttribute : ActionFilterAttribute
{
private string[] allowedDomains;
public CorsEnabledAttribute()
{
allowedDomains = new string[] { "*" };
}
public CorsEnabledAttribute(params string[] domains)
{
allowedDomains = domains;
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Request.Headers.Contains(Headers.Origin))
{
var origin = actionExecutedContext.Request.Headers.GetValues(Headers.Origin).FirstOrDefault();
// if origin is not empty, and the allowed domains is either * or contains the origin domain
// then allow the request
if (!string.IsNullOrEmpty(origin) && (allowedDomains.Contains(origin) || allowedDomains.Contains("*")))
{
actionExecutedContext.Response.Headers.Add(
Headers.AccessControlAllowOrigin, origin
);
}
}
}
}
}
namespace Mvc.Cors
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
internal class CorsPreflightActionDescriptor : HttpActionDescriptor
{
private HttpActionDescriptor originalAction;
private string accessControlRequestMethod;
private HttpActionBinding actionBinding;
public CorsPreflightActionDescriptor(HttpActionDescriptor originalAction, string accessControlRequestMethod)
{
this.originalAction = originalAction;
this.accessControlRequestMethod = accessControlRequestMethod;
this.actionBinding = new HttpActionBinding(this, new HttpParameterBinding[0]);
}
public override string ActionName
{
get { return this.originalAction.ActionName; }
}
public override Task<object> ExecuteAsync(HttpControllerContext controllerContext, IDictionary<string, object> arguments, System.Threading.CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add(Headers.AccessControlAllowMethods, this.accessControlRequestMethod);
var requestedHeaders = string.Join(
", ",
controllerContext.Request.Headers.GetValues(Headers.AccessControlRequestHeaders)
);
if (!string.IsNullOrEmpty(requestedHeaders))
{
response.Headers.Add(Headers.AccessControlAllowHeaders, requestedHeaders);
}
var tcs = new TaskCompletionSource<object>();
tcs.SetResult(response);
return tcs.Task;
}
public override Collection<HttpParameterDescriptor> GetParameters()
{
return this.originalAction.GetParameters();
}
public override Type ReturnType
{
get { return typeof(HttpResponseMessage); }
}
public override Collection<FilterInfo> GetFilterPipeline()
{
return this.originalAction.GetFilterPipeline();
}
public override Collection<IFilter> GetFilters()
{
return this.originalAction.GetFilters();
}
public override Collection<T> GetCustomAttributes<T>()
{
return this.originalAction.GetCustomAttributes<T>();
}
public override HttpActionBinding ActionBinding
{
get { return this.actionBinding; }
set { this.actionBinding = value; }
}
}
}
namespace Mvc.Cors
{
using System.Linq;
using System.Net.Http;
using System.Web.Http.Controllers;
internal class CorsPreflightActionSelector : ApiControllerActionSelector
{
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var originalRequest = controllerContext.Request;
var isCorsRequest = originalRequest.Headers.Contains(Headers.Origin);
if (originalRequest.Method == HttpMethod.Options && isCorsRequest)
{
var accessControlRequestMethod = originalRequest.Headers.GetValues(Headers.AccessControlRequestMethod).FirstOrDefault();
if (!string.IsNullOrEmpty(accessControlRequestMethod))
{
var modifiedRequest = new HttpRequestMessage(
new HttpMethod(accessControlRequestMethod),
originalRequest.RequestUri
);
controllerContext.Request = modifiedRequest;
HttpActionDescriptor actualDescriptor = base.SelectAction(controllerContext);
controllerContext.Request = originalRequest;
if (actualDescriptor != null && actualDescriptor.GetFilters().OfType<CorsEnabledAttribute>().Any())
{
return new CorsPreflightActionDescriptor(actualDescriptor, accessControlRequestMethod);
}
}
}
return base.SelectAction(controllerContext);
}
}
}
namespace Mvc.Cors
{
using System;
using System.Web.Http.Controllers;
public class CorsPreflightEnabledAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
controllerSettings.Services.Replace(
typeof(IHttpActionSelector), new CorsPreflightActionSelector()
);
}
}
}
namespace Mvc.Cors
{
internal static class Headers
{
public static string Origin = "Origin";
public static string AccessControlRequestMethod = "Access-Control-Request-Method";
public static string AccessControlRequestHeaders = "Access-Control-Request-Headers";
public static string AccessControlAllowMethods = "Access-Control-Allow-Methods";
public static string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
public static string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
}
}
using Mvc.Cors;
[CorsPreflightEnabled]
public class SampleApiController : ApiController
{
[CorsEnabled]
public string Get() {
return "This is accessible by everyone.";
}
[CorsEnabled("http://stackoverflow.com", "http://google.com")]
public string Post() {
return "This is accessible by calls originating from the domains specified above.";
}
}
@richardneililagan
Copy link
Author

This is based off of Carlos Figueira's work.

Cleaned it up a bit, updated the code to suit my own conventions, and added functionality for specifying allowed domains.

Usage

Decorate your Web API actions and/or controllers that are CORS-enabled with the [CorsEnabled] attribute.
By default, this allows CORS for all calling domains.

[CorsEnabled]
public string Get()

To specify allowed domains, just plug the allowed domains in the attribute constructor as a string[] params.

[CorsEnabled("http://github.com", "http://stackoverflow.com")]
public string Post()

For browsers that support CORS pre-flight (i.e. sending an HTTP OPTIONS call prior to the real HTTP call to "ask for permission"), decorating with the [CorsPreflightEnabled] attribute allows your application to intercept the incoming HTTP OPTIONS call and respond accordingly.

[CorsPreflightEnabled]
public class MyController : ApiController

Notes

If you're hosting your application in IIS, make sure that it's set to handle HTTP OPTIONS requests using the ISAPI handlers provided with ASP.NET.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment