Handling CORS Preflight requests to ASP.NET MVC actions
Asked Answered
L

6

54

I'm trying to perform a cross-domain POST request to an ASP.NET MVC controller action. This controller action accepts & uses various parameters. The problem is that when the preflight request happens, the controller action actually attempts to execute & because the OPTIONS request doesn't pass any data, the controller action throws out a 500 HTTP error. If I remove the code that uses the parameter, or the parameter itself, the entire request chain is completed successfully.

An example of how this is implemented:

Controller Action

public ActionResult GetData(string data)
{
    return new JsonResult
    {
        Data = data.ToUpper(),
        JsonRequestBehavior = JsonRequestBehavior.AllowGet
    };
}

Client-side code

<script type="text/javascript">
        $(function () {
            $("#button-request").click(function () {
                var ajaxConfig = {
                    dataType: "json",
                    url: "http://localhost:8100/host/getdata",
                    contentType: 'application/json',
                    data: JSON.stringify({ data: "A string of data" }),
                    type: "POST",
                    success: function (result) {
                        alert(result);
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        alert('Error: Status: ' + textStatus + ', Message: ' + errorThrown);
                    }
                };

                $.ajax(ajaxConfig);
            });
        });
    </script>

Now, whenever the preflight request happens, it returns a 500 HTTP code, because the "data" parameter is null, seeing as the OPTIONS request doesn't pass any values.

The server application has been set up in my local IIS on port 8100 & the page running the client-side code is set up on port 8200 to mimic the cross-domain calls.

I have also configured the host (on 8100) with the following headers:

Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Origin: http://localhost:8200

One workaround I had found, was to check the HTTP method that executes the action & if it's a OPTIONS request to just return blank content, otherwise execute the action code. Like so:

public ActionResult GetData(string data)
{
    if (Request.HttpMethod == "OPTIONS") {
        return new ContentResult();
    } else {
        return new JsonResult
        {
            Data = data.ToUpper(),
            JsonRequestBehavior = JsonRequestBehavior.AllowGet
        };
    }
}

But this approach feels very clunky to me. I considered adding this sort of logic to an Attribute, but even this would mean decorating every action that will get called using CORS with it.

Is there a more elegant solution to getting this functionality to work?

Lahomalahore answered 29/11, 2012 at 11:7 Comment(1)
<modules runAllManagedModulesForAllRequests="true"></modules> Worked for me. Why is everything Microsoft so nuanced?Oireachtas
L
69

So I have found a solution that works. For each request, I check whether it's a CORS request & whether the request is coming in with the OPTIONS verb, indicating that it's the preflight request. If it is, I just send an empty response back (which only contains the headers configured in IIS of course), thus negating the controller action execution.

Then if the client confirms it's allowed to perform the request based on the returned headers from preflight, the actual POST is performed & the controller action is executed. And example of my code:

protected void Application_BeginRequest()
{
    if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
        Request.HttpMethod == "OPTIONS") {
        Response.Flush();
    }
}

As mentioned, this worked for me, but if anyone knows of a better way, or of any flaws in my current implementation, I would appreciate to hear about them.

Lahomalahore answered 30/11, 2012 at 13:24 Comment(14)
Hi Carl - should this method still be pushing the request into the target controller's logic? In case you have a moment, I posted a question: #24254302Gorgon
Hi there! Have left a comment on your question. If you don't come right, I'll try & recreate it this evening after work.Lahomalahore
Cors sure is fun. I'm having an issue with tinyurl.com/nxz65ac I'm implemented on my WebAPI. File uploads work fine when they're small, I can upload multiples just fine but when I selected a larger file say around 50MB and try to upload, I get the "No 'Access-Control-Allow-Origin' header is present on the requested resource." in Chrome. Event though if I select multiple files, the smaller ones are uploaded fine but the large file makes the browser stop. I'd create a new question but I don't know how to tag users like you that have solved painful Cors issues.. anyone know how?Baggett
It wasn't a CORS issue anyhow but it was weird Chrome was stating that it was. There are two areas for file size limits in WebAPI and I had to set both: stackoverflow.com/a/17324840Baggett
This goes into the global.asax & is one of the application-level events you can hook into.Lahomalahore
I've added an answer that plugs this code into the OWIN pipeline, for those not using Global.asaxBegorra
This didn't quite work for me but debugging and inspecting led me to this small change. Add the specific headers and be sure to explicitly name the Access-Control-Allow-Headers (an asterisk did not suffice). if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS") { Response.Headers.Add("Access-Control-Allow-Origin", "*"); Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, Session"); Response.Flush(); }Dosimeter
@MarcusCunningham Was the site getting hosted from IIS? When I ran this piece of code, IIS had returned all the CORS headers in the response. If this was done from something like IIS Express, this might not have been the case, as it wasn't configured with the headers. I only ask, since having the headers coded in will work, but it does remove the management of these headers from the infrastructure team, if they are looking after IIS.Lahomalahore
@CarlHancke Good question. When I was debugging it was IIS Express. I cannot remember offhand if I tested it without the coded headers in our IIS7.5 environment. I'll revisit it again soon and get back to you.Dosimeter
2016 and for some reason, THIS was my issue. I can't wait until we can migrate to ASP.net Core. Thank you Carl! And to anyone else reading...I wouldn't be surprised if this is your solution.Miley
seems like you experienced this on IIS express. Did you ever try out your solution on IIS without anonymous authentication enabled?Diversion
Header keys are case insensitive and I've encountered cases where jQuery in Chrome will send "Origin" for HTTP and "origin" for HTTPS. It's unlikely just this combination of jQuery and Chrome that do this though. I've updated your answer to check case-insensitively to account for this discrepancy.Kamp
After doing this, I get this error: Server cannot append header after HTTP headers have been sent. It's frustrating.Casino
@Mahmoodvcs Was this code added to the Application_BeginRequest event in Global.asax? A silly question, but your error leads me to believe that the code is executing later in the request life cycle, after ASP.NET has already written the headers to the response stream.Lahomalahore
B
12

expanding on Carl's answer, i took his code and plugged it into my OWIN pipeline:

app.Use((context, next) =>
{
     if (context.Request.Headers.Any(k => k.Key.Contains("Origin")) && context.Request.Method == "OPTIONS")
     {
         context.Response.StatusCode = 200;
         return context.Response.WriteAsync("handled");
     }

     return next.Invoke();
});

Just add this to the beginning (or anywhere before you register the WebAPI) of your IAppBuilder in Startup.cs

Begorra answered 31/7, 2015 at 19:46 Comment(0)
K
7

Here is how I handled the preflight/CORS issues with ASP.Net Web Api. I simply added the Microsoft.AspNet.WebApi.Cors Nuget package to my Web project.Then in my WebApiConfig.cs file I added this line:

config.EnableCors(new ApplicationCorsPolicy());

and created a custom PolicyProvider class

public class ApplicationCorsPolicy : Attribute, ICorsPolicyProvider
{
    public async Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var corsRequestContext = request.GetCorsRequestContext();
        var originRequested = corsRequestContext.Origin;

        if (await IsOriginFromAPaidCustomer(originRequested))
        {
            // Grant CORS request
            var policy = new CorsPolicy
            {
                AllowAnyHeader = true,
                AllowAnyMethod = true
            };
            policy.Origins.Add(originRequested);
            return policy;
        }
        // Reject CORS request
        return null;
    }

    private async Task<bool> IsOriginFromAPaidCustomer(string originRequested)
    {
        // Do database look up here to determine if origin should be allowed.
        // In my application I have a table that has a list of domains that are
        // allowed to make API requests to my service. This is validated here.
        return true;
    }
}

See, the Cors framework allows you to add your own logic for determining which origins are allowed, etc. This is very helpful if you are exposing a REST API to the outside world and the list of people (origins) who can access your site are in a controlled environment like a database. Now, if you are simply allowing all origins (which might not be such a good idea in all cases) you can just do this in WebApiConfig.cs to enable CORS globally:

config.EnableCors();

Just like Filters and Handlers in WebApi you can also add class or method level annotations to your controllers like so:

[EnableCors("*, *, *, *")]

Note that the EnableCors attribute has a constructor that accepts the following parameters

  1. List of Origins Allowed
  2. List of request headers allowed
  3. List of HTTP methods allowed
  4. List of response headers allowed

You can specify statically at each controller/end point who is allowed to access what resource.

Update 06/24/2016: I should mention that I have the following in my Web.config. It looks like these may not be the defaults for everyone.

<system.webServer>
    <handlers>
        <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
        <remove name="OPTIONSVerbHandler" />
        <remove name="TRACEVerbHandler" />
        <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
        </handlers>
</system.webServer>

Source: Microsoft

Kragh answered 5/5, 2016 at 9:37 Comment(0)
E
7

The accepted answer works like a charm, but I found that the request was actually being passed down to the controller. I was receiving a 200 status code, but the response body contained a lot of HTML with an exception from the controller. So instead of using Response.Flush(), I found it was better to use Response.End(), which does stop the execution of the request. This alternative solution would look like this:

EDIT: fixed a typo carried from the original answer.

protected void Application_BeginRequest()
{
    if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
        Request.HttpMethod == "OPTIONS") {
        Response.End();
    }
}
Exeter answered 25/5, 2017 at 20:14 Comment(2)
where do you put this function?Aminopyrine
That looks like it's in Global.asax.cs in the root folder of the application.Crochet
D
3

This may be a red herring. I have recently got CORS working fine without jumping through any of the hoops that you are doing.

This was done using a combination of Thinktecture.IdentityModel nuget package, and more importantly... REMOVAL of all references to WebDAV. This includes removing the webdav module from IIS, and ensuring the following lines in your web config:

<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="WebDAVModule" />
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
    <handlers>
      <remove name="WebDAV" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

Then you can just use thinktecture to configure your CORS from your Global.asax using a static class like this:

public class CorsConfig
{
    public static void RegisterCors(HttpConfiguration httpConfiguration)
    {
        var corsConfig = new WebApiCorsConfiguration();
        corsConfig.RegisterGlobal(httpConfiguration);

        corsConfig.ForAllResources().AllowAllOriginsAllMethodsAndAllRequestHeaders();
    }
}

SOURCE: http://brockallen.com/2012/06/28/cors-support-in-webapi-mvc-and-iis-with-thinktecture-identitymodel/

Dramaturgy answered 30/11, 2012 at 13:36 Comment(3)
I was looking at Thinktecture during my research, but this seemed like such a simple thing to do, I wanted to find a way without third party libraries to achieve it. Still, thanks for taking the time to post your answer :)Lahomalahore
No problems, sometimes it's more beneficial and fun to do it yourself :)Dramaturgy
Thank you! Your answer pointed me in the right direction. CORS was working for me, but I accidentily removed the ExtensionlessUrlHandler stuff from my web.config which broke my CORS (I was supposed to remove the exceptionless stuff ....)Carabineer
B
3

None of these answers worked for me, but the following webconfig settings did. The two key settings for me were setting Access-Control-Allow-Headers to Content-Type and commenting out the line that removes the OPTIONSVerbHandler:

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"></modules>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type" />
      </customHeaders>
    </httpProtocol>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <!--<remove name="OPTIONSVerbHandler" />-->
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  </system.webServer>
Bel answered 1/8, 2016 at 6:57 Comment(1)
It is important to note that the above code enables CORS for every origin. Typically, you want to try and limit the origins if at all possible. Did you try config.EnableCors() in your WebApiConfig.cs file?Kragh

© 2022 - 2024 — McMap. All rights reserved.