20

I have a WebAPI 2 REST service running with Windows Authentication. It is hosted separately from the website, so I've enabled CORS using the ASP.NET CORS NuGet package. My client site is using AngularJS.

So far, here's what I've been through:

  1. I didn't have withCredentials set, so the CORS requests were returning a 401. Resolved by adding withCredentials to my $httpProvider config.
  2. Next, I had set my EnableCorsAttribute with a wildcard origin, which isn't allowed when using credentials. Resolved by setting the explicit list of origins.
  3. This enabled my GET requests to succeed, but my POST issued a preflight request, and I hadn't created any controller actions to support the OPTIONS verb. To resolve this, I've implemented a MessageHandler as a global OPTIONS handler. It simply returns 200 for any OPTIONS request. I know this isn't perfect, but works for now, in Fiddler.

Where I'm stuck - my Angular preflight calls aren't including the credentials. According to this answer, this is by design, as OPTIONS requests are designed to be anonymous. However, the Windows Authentication is stopping the request with a 401.

I've tried putting the [AllowAnonymous] attribute on my MessageHandler. On my dev computer, it works - OPTIONS verbs do not require authentication, but other verbs do. When I build and deploy to the test server, though, I am continuing to get a 401 on my OPTIONS request.

Is it possible to apply [AllowAnonymous] on my MessageHandler when using Windows Authentication? If so, any guidance on how to do so? Or is this the wrong rabbit hole, and I should be looking at a different approach?

UPDATE: I was able to get it to work by setting both Windows Authentication and Anonymous Authentication on the site in IIS. This caused everything to allow anonymous, so I've added a global filter of Authorize, while retaining the AllowAnonymous on my MessageHandler.

However, this feels like a hack...I've always understood that only one authentication method should be used (no mixed). If anyone has a better approach, I'd appreciate hearing about it.

Michal Levý
  • 14,671
  • 1
  • 27
  • 41
Dave Simione
  • 1,411
  • 2
  • 23
  • 28
  • You should probably add a tag such as 'selfhost' or 'owin' as this isn't tied down to something like IIS. :) – peteski Nov 24 '15 at 21:16
  • I used this guide https://www.codeproject.com/Articles/1119206/How-to-Enable-Cross-Origin-Request-in-ASP-NET-Web (published in 2016), which is very similar with most of the answers below – jyrkim May 16 '18 at 13:57

9 Answers9

16

I used self-hosting with HttpListener and following solution worked for me:

  1. I allow anonymous OPTIONS requests
  2. Enable CORS with SupportsCredentials set true
var cors = new EnableCorsAttribute("*", "*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
var listener = appBuilder.Properties["System.Net.HttpListener"] as HttpListener;
if (listener != null)
{
    listener.AuthenticationSchemeSelectorDelegate = (request) => {
    if (String.Compare(request.HttpMethod, "OPTIONS", true) == 0)
    {
        return AuthenticationSchemes.Anonymous;
    }
    else
    {
        return AuthenticationSchemes.IntegratedWindowsAuthentication;
    }};
}
Marcos Dimitrio
  • 5,592
  • 3
  • 33
  • 56
Igor Tkachenko
  • 448
  • 6
  • 13
  • 3
    This should be marked as the answer, the AuthenticationSchemeSelectorDelegate is exactly what you need to ensure that OPTIONS requests are globally excluded from any auth settings. Bravo, this answer is also EXACTLY what I needed. (I'd suggest that 'selfhost' tag is added to the question). – peteski Nov 24 '15 at 21:11
  • 9
    How do i get appBuilder instance in webApiConfig.cs Register Method? – Rajesh Mishra May 27 '16 at 12:05
  • Main problem with this answer is question directly mentions he is using IIS. (I understand it was probably added later but still. Updated question tags...) – Michal Levý May 05 '21 at 06:30
14

I have struggled for a while to make CORS requests work within the following constraints (very similar to those of the OP's):

  • Windows Authentication for all users
  • No Anonymous authentication allowed
  • Works with IE11 which, in some cases, does not send CORS preflight requests (or at least do not reach global.asax BeginRequest as OPTIONS request)

My final configuration is the following:

web.config - allow unauthenticated (anonymous) preflight requests (OPTIONS)

<system.web>
    <authentication mode="Windows" />
    <authorization>
        <allow verbs="OPTIONS" users="*"/>
        <deny users="?" />
    </authorization>
</system.web>

global.asax.cs - properly reply with headers that allow caller from another domain to receive data

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    if (Context.Request.HttpMethod == "OPTIONS")
    {
        if (Context.Request.Headers["Origin"] != null)
            Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);

        Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, MaxDataServiceVersion");
        Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");

        Response.End();
    }
}

CORS enabling

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // all requests are enabled in this example. SupportsCredentials must be here to allow authenticated requests          
        var corsAttr = new EnableCorsAttribute("*", "*", "*") { SupportsCredentials = true };
        config.EnableCors(corsAttr);
    }
}

protected void Application_Start()
{
    GlobalConfiguration.Configure(WebApiConfig.Register);
}
Graham
  • 6,577
  • 17
  • 55
  • 76
Alexei - check Codidact
  • 17,850
  • 12
  • 118
  • 126
  • This helped! I ended up not adding the authorization element to the web.config and it still worked. I believe the Application_AuthenticateRequest override eliminated the need to allow for authentication when the OPTION method is called for. – MadMoai Nov 13 '18 at 21:54
  • @MadMoai - I will try without the authorization, although I think it is needed when only Windows Authentication is enabled in IIS and Options must be enabled for non-authenticated request. Thanks. – Alexei - check Codidact Nov 14 '18 at 06:50
  • As per [Microsoft](https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api#custom-cors-policy-providers), The CORS spec states that setting origins to "*" is invalid if SupportsCredentials is true. – WorkSmarter Mar 04 '20 at 17:44
  • @WorkSmarter This is true but it turns out that current version of CORS libraries for WebAPI 2 (`Microsoft.AspNet.WebApi.Cors.5.2.7`) do not return "Access-Control-Allow-Origin: *" with this settings but use the value from the Origin header of the request... – Michal Levý May 05 '21 at 14:39
3

This is a much simpler solution -- a few lines of code to allow all "OPTIONS" requests to effectively impersonate the app pool account. You can keep Anonymous turned Off, and configure CORS policies per normal practices, but then add the following to your global.asax.cs:

            protected void Application_AuthenticateRequest(object sender, EventArgs e)
            {
                if (Context.Request.HttpMethod == "OPTIONS" && Context.User == null)
                {
                    Context.User = System.Security.Principal.WindowsPrincipal.Current;
                }
            }
willman
  • 1,151
  • 1
  • 7
  • 20
2

I solved it in a very similar way but with some details and focused on oData service

I didn't disable anonymous authentication in IIS since i needed it to POST request

And I've added in Global.aspx (Adding MaxDataServiceVersion in Access-Control-Allow-Headers) the same code than above

protected void Application_BeginRequest(object sender, EventArgs e)
{
    if ((Context.Request.Path.Contains("api/") || Context.Request.Path.Contains("odata/")) && Context.Request.HttpMethod == "OPTIONS")
    {
        Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
        Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,MaxDataServiceVersion");
        Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");
        Context.Response.End();
    }
} 

and WebAPIConfig.cs

public static void Register(HttpConfiguration config)
{
   // Web API configuration and services
   var cors = new EnableCorsAttribute("*", "*", "*");
   cors.SupportsCredentials = true;
   config.EnableCors(cors);


   config.Routes.MapHttpRoute(
       name: "DefaultApi",
       routeTemplate: "api/{controller}/{id}",
       defaults: new { id = RouteParameter.Optional }
   );
}

and AngularJS call

$http({
       method: 'POST',
        url: 'http://XX.XXX.XXX.XX/oData/myoDataWS.svc/entityName',
        withCredentials: true,
        headers: {
            'Content-Type': 'application/json;odata=verbose',
            'Accept': 'application/json;odata=light;q=1,application/json;odata=verbose;q=0.5',
            'MaxDataServiceVersion': '3.0'
        },
        data: {
            '@odata.type':'entityName',
            'field1': 1560,
            'field2': 24,
            'field3': 'sjhdjshdjsd',
            'field4':'wewewew',
            'field5':'ewewewe',
            'lastModifiedDate':'2015-10-26T11:45:00',
            'field6':'1359',
            'field7':'5'
        }
    });
Alexei - check Codidact
  • 17,850
  • 12
  • 118
  • 126
2

In our situation:

  • Windows Authentication
  • Multiple CORS origins
  • SupportCredentials set to true
  • IIS Hosting

we found that the solution was elsewhere:

In Web.Config all we had to do was to add runAllManagedModulesForAllRequests=true

<modules runAllManagedModulesForAllRequests="true">

We ended up to this solution by looking into a solution on why the Application_BeginRequest was not being triggered.

The other configurations that we had:

in Web.Config

    <authentication mode="Windows" />
    <authorization>
      <allow verbs="OPTIONS" users="*" />
      <deny users="?"/>
    </authorization>

in WebApiConfig

        private static string GetAllowedOrigins()
        {
            return ConfigurationManager.AppSettings["CorsOriginsKey"];
        }

        public static void Register(HttpConfiguration config)
        {
            //set cors origins
            string origins = GetAllowedOrigins();
            var cors = new EnableCorsAttribute(origins, "*", "*");
            config.EnableCors(cors);

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
       }

BTW "*" cors origin is not compatible with Windows Authentication / SupportCredentials = true

https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api#pass-credentials-in-cross-origin-requests

tetius
  • 405
  • 1
  • 4
  • 7
1

Dave,

After playing around with the CORS package, this is what caused it to work for me: [EnableCors(origins: "", headers: "", methods: "*", SupportsCredentials=true)]

I had to enable SupportsCredentials=true. Origins,Headers, and Methods are all set to "*"

jr3
  • 914
  • 3
  • 14
  • 27
  • I have had this set from the beginning, otherwise the server won't accept the credentials header. Thanks for the attempt, though. – Dave Simione Dec 27 '14 at 20:39
  • 1
    http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api Section: Passing Credentials in Cross-Origin Requests The CORS spec also states that setting origins to "*" is invalid if SupportsCredentials is true. You should supply the origins you want to allow rather than using "*" – peteski Nov 24 '15 at 21:15
1

disable anonymous authentication in IIS if you don't need it.

Than add this in your global asax:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    if ((Context.Request.Path.Contains("api/") || Context.Request.Path.Contains("odata/")) && Context.Request.HttpMethod == "OPTIONS")
    {
        Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
        Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");
        Context.Response.End();
    }
} 

Make sure that where you enable cors you also enable the credential usage, like:

public static void Register(HttpConfiguration config)
{
   // Web API configuration and services
   var cors = new EnableCorsAttribute("*", "*", "*");
   cors.SupportsCredentials = true;
   config.EnableCors(cors);

   // Web API routes
   config.MapHttpAttributeRoutes();

   config.Routes.MapHttpRoute(
       name: "DefaultApi",
       routeTemplate: "api/{controller}/{id}",
       defaults: new { id = RouteParameter.Optional }
   );
}

As you can see I enable CORS globally and using the application BeginRequest hook I authenticate all the OPTIONS requests for the api (Web Api) and the odata requests (if you use it).

This works fine with all browsers, in the client side remember to add the xhrFiled withCredentials like shown below.

$.ajax({
    type : method,
    url : apiUrl,
    dataType : "json",
    xhrFields: {
        withCredentials: true
    },
    async : true,
    crossDomain : true,
    contentType : "application/json",
    data: data ? JSON.stringify(data) : ''
}).....

I'm trying to find another solution avoiding to use the hook but without success until now, I would use the web.config configuration to do something like the following: WARNING THE CONFIGURATION BELOW DOESN'T WORK!

  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <authentication mode="Windows" />
    <authorization>
      <deny verbs="GET,PUT,POST" users="?" />
      <allow verbs="OPTIONS" users="?"/>
    </authorization>
  </system.web>
  <location path="api">
    <system.web>
      <authorization>
        <allow users="?"/>
      </authorization>
    </system.web>
  </location>
Norcino
  • 3,271
  • 4
  • 16
  • 31
0

Other solutions I found on the web didn't work for me or seemed too hacky; in the end I came up with a simpler and working solution:

web.config:

<system.web>
    ...
    <authentication mode="Windows" />
    <authorization>
        <deny users="?" />
    </authorization>
</system.web>

Project properties:

  1. Turn on Windows Authentication
  2. Turn off Anonymous Authentication

Setup CORS:

[assembly: OwinStartup(typeof(Startup))]
namespace MyWebsite
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCors(CorsOptions.AllowAll);

This requires Microsoft.Owin.Cors assembly that is avaliable on NUget.

Angular initialization:

$httpProvider.defaults.withCredentials = true;
Vedran
  • 8,380
  • 4
  • 43
  • 57
0

This is my solution.

Global.asax*

protected void Application_BeginRequest(object sender, EventArgs e)
{
    if(!ListOfAuthorizedOrigins.Contains(Context.Request.Headers["Origin"])) return;

    if (Request.HttpMethod == "OPTIONS")
    {
        HttpContext.Current.Response.Headers.Remove("Access-Control-Allow-Origin");
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
        HttpContext.Current.Response.StatusCode = 200;
        HttpContext.Current.Response.End();
    }

    if (Request.Headers.AllKeys.Contains("Origin"))
    {
        HttpContext.Current.Response.Headers.Remove("Access-Control-Allow-Origin");
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
    }
}
Dblock247
  • 4,603
  • 6
  • 35
  • 61