13

My IDE is Visual Studio 2017. I've got an Angular4 client talking to a WebAPI backend in Core, and CORS is working as configured EXCEPT for the PUT and POST methods. The GET method is subject to the same preflight OPTIONS method in Chrome that the PUT and POST methods are, but GET is working fine.

It appears that the IIS Express server in Visual Studio is not forwarding the requests to the Kestrel server. Both Methods work in Postman, but not when Angular4 makes the call. Here's the code:

Angular4 POST

post(api: string, object: any): Observable<any> {
        let body = JSON.stringify(object);

        let options = new RequestOptions({
            headers: this.headers,
            withCredentials: true
            });

        return this.http.post(this.server + api, body, options)
            .map((res: Response) => res.json())
            .catch((error: any) => Observable.throw(error.json().error) || 'Post server error');
    }

Startup.cs Configure

services.Configure<IISOptions>(options => 
     options.ForwardWindowsAuthentication = true);

services.AddCors(options => {
            options.AddPolicy("AllowAll", builder => {
                builder.WithOrigins("http://localhost:XXXX")
                .WithMethods("GE‌​T", "POST", "PUT", "DELETE", "OPTIONS")
                .WithHeaders("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization")
                .AllowCredentials();
            });
        });

Startup.cs ConfigureServices

app.UseCors("AllowAll");

IIS ApplicationHost.Config in Project

<anonymousAuthentication enabled="false" userName="" />
    <basicAuthentication enabled="false" />
    <clientCertificateMappingAuthentication enabled="false" />
    <digestAuthentication enabled="false" />
    <iisClientCertificateMappingAuthentication enabled="false"></iisClientCertificateMappingAuthentication>
    <windowsAuthentication enabled="true" >
      <providers>
        <add value="Negotiate" />
      </providers>
    </windowsAuthentication>

AND

<customHeaders>
    <clear />
    <add name="X-Powered-By" value="ASP.NET" />
    <add name="Access-Control-Allow-Origin" value="http://localhost:5000"/>
    <add name="Access-Control-Allow-Headers" value="Accept, Origin, Content-
        Type"/>
    <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, 
        OPTIONS"/>
    <add name="Access-Control-Allow-Credentials" value="true"/>
</customHeaders>

Response for GET

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: **Kestrel**
X-SourceFiles: =?UTF-8?B?QzpcZGV2cHJvamVjdHNcVFJXRC5IeWRyb21hcnRBZG1pblxKVF9BZGRUYWdNYW5hZ2VtZW50XFRSV0QuSHlkcm9NYXJ0LkFwcFxhcGlcdGFncw==?=
Persistent-Auth: true
X-Powered-By: ASP.NET
Access-Control-Allow-Origin: http://localhost:5000
Access-Control-Allow-Headers: Accept, Origin, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Credentials: true
Date: Fri, 14 Jul 2017 17:03:43 GMT

Response for POST

HTTP/1.1 401 Unauthorized
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-SourceFiles: =?UTF-8?B?QzpcZGV2cHJvamVjdHNcVFJXRC5IeWRyb21hcnRBZG1pblxKVF9BZGRUYWdNYW5hZ2VtZW50XFRSV0QuSHlkcm9NYXJ0LkFwcFxhcGlcdGFncw==?=
WWW-Authenticate: Negotiate
X-Powered-By: ASP.NET
Access-Control-Allow-Origin: http://localhost:5000
Access-Control-Allow-Headers: Accept, Origin, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Credentials: true
Date: Fri, 14 Jul 2017 17:05:11 GMT
Content-Length: 6095

So the big question is, what am I missing?

user15741
  • 1,392
  • 16
  • 27
xyntiacat
  • 468
  • 1
  • 5
  • 13

5 Answers5

10

Here is what worked for me...

First, add a CORS policy to ConfigureServices() in Startup.cs

services.AddCors(o => o.AddPolicy("CORSPolicy", builder =>
        {
            builder.AllowAnyOrigin()
                   .AllowAnyMethod()
                   .AllowAnyHeader()
                   .AllowCredentials();
        }));

and use in the Configure() method

app.UseCors("CORSPolicy");

Next, either add Authorize attributes to your controllers or add them globally. I went with globally. To do that, add the following after the services.AddCors() method above in the ConfigureServices() method (you can have other stuff inside that predicate, too)

            services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                             .RequireAuthenticatedUser()
                             .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        }); 

Then, I added this little nugget (it's what made everything "just work" for me). Basically, it allows preflight requests to pass through IIS without authenticating.

services.AddAuthentication(IISDefaults.AuthenticationScheme);

I think you'll have to add the following to your usings, too

using Microsoft.AspNetCore.Server.IISIntegration;

You should also tell the app to use authentication. Do this in the Configure() method below app.UseCors("CORSPolicy")

app.UseAuthentication();

Finally, make sure you have anonymousAuthentication and windowsAuthentication set to true in your applicationhost.config file. If you don't know what that file is, it configures your IIS settings for the app. You can find it in the hidden .vs folder at the root of your project inside the config folder.

<authentication>
    <anonymousAuthentication enabled="true" userName="" />
    <basicAuthentication enabled="false" />
    <clientCertificateMappingAuthentication enabled="false" />
    <digestAuthentication enabled="false" />
    <iisClientCertificateMappingAuthentication enabled="false"></iisClientCertificateMappingAuthentication>
    <windowsAuthentication enabled="true">
      <providers>
        <add value="Negotiate" />
        <add value="NTLM" />
      </providers>
    </windowsAuthentication>
  </authentication>

I don't know if this is the case for everyone, but I have two <authentication></authentication> sections in my applicationhost.config file. I changed both to be on the safe side and have not tried changing one and not the other.


**

TLDR;

**

STEP 1)

public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(o => o.AddPolicy("CORSPolicy", builder =>
        {
            builder.AllowAnyOrigin()
                   .AllowAnyMethod()
                   .AllowAnyHeader()
                   .AllowCredentials();
        }));

        services.AddAuthentication(IISDefaults.AuthenticationScheme);

        services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                             .RequireAuthenticatedUser()
                             .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        }); 
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseCors("CORSPolicy");

        app.UseAuthentication();

        app.UseMvc();
    }

STEP 2) in applicationhost.config You may have to do this in TWO places in the file

<authentication>
      <anonymousAuthentication enabled="true" />
      <windowsAuthentication enabled="true" />
    </authentication>
Burning Hippo
  • 787
  • 1
  • 20
  • 47
  • Using .Net Core 2.1, Intellisense is not showing the `services.AddAuthentication` or `app.UseAuthentication`. Is there soemthing more than `using Microsoft.AspNetCore.Server.IISIntegration` that I must add to my list of `using` statements? – Zarepheth Jan 08 '19 at 15:57
  • 1
    In 2018 people should use IIS CORS module, https://blogs.iis.net/iisteam/introducing-iis-cors-1-0 not such hack. – Lex Li Apr 08 '19 at 00:43
6

Got it. Okay, so basically, what was happening was a preflight OPTIONS request doesn't have authorization on it, so it was by default and design, failing since I had disabled anonymous authentication and enabled windows authentication. I had to allow anonymous authentication to occur to both the client and the web api so that the OPTIONS requests could get through unscathed. This, however, leaves a huge security hole that I had to resolve. Since I had opened the door to the OPTIONS requests, I had to close that door somehow for the POST, PUT and DELETE requests. I did this by creating an authorization policy that only allowed in authenticated users. My final code is as follows:

Angular 4 Post

Note the use of withCredentials in the options.

post(api: string, object: any): Observable<any> {
    let body = JSON.stringify(object);

    let options = new RequestOptions({
        headers: this.headers,
        withCredentials: true
        });

    return this.http.post(this.server + api, body, options)
        .map((res: Response) => res.json())
        .catch((error: any) => Observable.throw(error.json().error) || 'Post server error');
}

Startup.cs

Added CORS, added an authentication policy, used CORS.

(under ConfigureServices)

services.AddCors(options =>
        {
            options.AddPolicy("AllowSpecificOrigin",
                builder => builder.WithOrigins("http://localhost:5000")
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
        });

and

services.AddAuthorization(options =>
        {
            options.AddPolicy("AllUsers", policy => policy.RequireAuthenticatedUser());
        });

and

(under Configure)

app.UseCors("AllowSpecificOrigin");

Controller

Added authorization referencing the policy created in the Startup.

[Authorize(Policy = "AllUsers")]
    [Route("api/[controller]")]    
    public class TagsController : ITagsController

applicationhost.config for IISExpress

<authentication>
        <anonymousAuthentication enabled="false" userName="" />
        <basicAuthentication enabled="false" />
        <clientCertificateMappingAuthentication enabled="false" />
        <digestAuthentication enabled="false" />
        <iisClientCertificateMappingAuthentication enabled="false"></iisClientCertificateMappingAuthentication>
        <windowsAuthentication enabled="true">
          <providers>
            <add value="Negotiate" />
            <add value="NTLM" />
          </providers>
        </windowsAuthentication>
      </authentication>

I totally removed the custom headers.

This solution allowed all 4 verbs to work as expected and I was able to use the identifying information in the httpContext.User object to log information to the database.

Once I deploy this to IIS, I expect will have to add the forwardWindowsAuthToken to the web.config:

<aspNetCore processPath=".\TRWD.HydroMart.App.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true" />

and

this to the startup in ConfigureServices:

services.Configure<IISOptions>(options => {
        options.ForwardWindowsAuthentication = true;
    });
xyntiacat
  • 468
  • 1
  • 5
  • 13
  • Also figured out that you have to disable the WebDavModule for the API application so that PUT and DELETE requests can get through. Do that by adding inside the tags on the web.config for the application. – xyntiacat Jul 28 '17 at 16:21
  • Several months after this answer, Microsoft gave an official solution, https://blogs.iis.net/iisteam/introducing-iis-cors-1-0 Configure IIS CORS module and all such hacks become unnecessary. – Lex Li Apr 08 '19 at 00:44
5

I got it working with very simple approach. I uses dotNet Core 2.0.

In configureServices method of startup

services.AddCors(options =>
        {
            options.AddPolicy("app-cors-policy",
                builder =>
                {
                    builder
                        .AllowAnyOrigin()
                        .AllowAnyHeader()
                        .AllowAnyMethod()
                        .AllowCredentials()
                    ;
                });

        });


In configure method of startup

app
   .UseCors("app-cors-policy") //Must precede UseMvc
   .UseMvc();

Then, enable both - Anonymous authentication and Windows authentication as well. This allows OPTIONS requests (preflight) which are anonymous always, to go through. All xhr requests has to be withCredentials else will fail.

jaymjarri
  • 3,048
  • 1
  • 18
  • 7
2

To Allow anonymous preflight options you can add the following to your web.config

<security>
   <authorization>
       <remove users="*" roles="" verbs="" />
       <add accessType="Allow" users="" roles="mydomain/mygroup" />
       <add accessType="Allow" users="?" verbs="OPTIONS" />
   </authorization>
</security>

Note: the use of a group to limit the access for authenticated users and limiting the access of anonymous users to the OPTIONS verb.

Add the cors package Microsoft.AspNetCore.Cors if you haven't already.

And configure it.

A good guide for configuring CORS can be found here:

0

And now there is this: https://www.iis.net/downloads/microsoft/iis-cors-module

"The Microsoft IIS CORS Module is an extension that enables web sites to support the CORS (Cross-Origin Resource Sharing) protocol."

Woopdidoo