How to Configure Angular 6 with .NET Core 2 to allow CORS from any host?

Possible Issues:

  • In your startup.cs.Configure() method, does app.UseCors() precede app.useMVC()?
  • Does the Http Request URL contain a trailing slash (/)?
  • Does the Http Request include Credentials?
  • Is the browser receiving a Http Response from the API?
  • Does the Response include a 'Access-Control-Allow-Origin' header?
  • When you send the request from Postman, does the Http Response include the 'Access-Control-Allow-Origin' header as well as a body containing the data?

Gotchas

Firefox requires a certificate to be installed for your API in order to send Http Request utilizing the HTTPS protocol.

Test your API with Postman and your browser Developer Tool. Notice 2 Http requests. The Http 200 is a “pre-flight” to see what CORS options are available.

  • If your API (.NET) throws a HTTP 500 (Internal Server Error), it will return a developer exception page and Postman will display a “no ‘Access-Control-Allow-Origin’ header is present on the requested resource” message - this is mis-leading.
  • The actual problem in this case is that the developer exception page cannot be returned from the server to a client (browser) running on a different Origin.

I would like to respectfully point out the following:

  • The CORS specification states that setting origins to '*' (all origins) is invalid if the Access-Control-Allow-Credentials header is present.
  • AllowAnyOrigin() is not recommended in production unless you intend to allow anyone to utilize your API AND you will not implement credentials.
  • You do not need to configure CORS at the controller level with the EnableCors attribute when utilizing multiple CORS policies.
  • Configuring multiple CORS policies with ASP.NET Core is easy(see tutorial below)

The following articles are worth reviewing:

ASP.NET Core 2.2 does not permit allowing credentials with AllowAnyOrigin()

Enable Cross-Origin Requests (CORS) in ASP.NET Core

key points (tldr;):

  • CORS Middleware must precede any defined endpoints in your app configuration.
  • The URL must be specified without a trailing slash (/).
  • If the browser sends credentials but the response doesn't include a valid Access-Control-Allow-Credentials header, the browser doesn't expose the response to the app, and the cross-origin request fails.
  • The CORS specification also states that setting origins to "*" (all origins) is invalid if the Access-Control-Allow-Credentials header is present.
  • If a browser supports CORS, it sets these headers automatically for cross-origin requests.
  • If the response doesn't include the Access-Control-Allow-Origin header, the cross-origin request fails.

CORS Tutorial: (2) Angular Clients + ASP.NET Core

  1. Create Visual Studio Solution

    md c:\s\a
    cd c:\s\a
    c:\s\a>dotnet new sln -n solutionName
    
  2. Create ASP.NET Core Project

    c:\s\a>md s
    c:\s\a>cd s
    c:\s\a\s>dotnet new webapi -o api -n api
    
  3. API Launch Settings and CORS Configuration

launchSettings.json

Clone Development Profile to Staging Profile

{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iis": {
    "applicationUrl": "http://localhost:myIISApiPortNumber",
    "sslPort": myIISApiSSLPortNumber
},
"iisExpress": {
    "applicationUrl": "http://localhost:myIISExpressApiPortNumber",
    "sslPort": myIISExpressApiSSLPortNumber
}
},
"profiles": {
"Development (IIS Express)": {
    "commandName": "IISExpress",
    "launchBrowser": true,
    "launchUrl": "api/values",
    "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development"
    }
},
"Staging (IIS Express)": {
    "commandName": "IISExpress",
    "launchBrowser": true,
    "launchUrl": "api/values",
    "applicationUrl": "https://localhost:myIISExpressApiSSLPortNumber;http://localhost:myIISExpressApiPortNumber",
    "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Staging"
    }
},
"Production (IIS)": {
    "commandName": "IIS",
    "launchBrowser": true,
    "launchUrl": "api/values",
    "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Production"
    },
    "applicationUrl": "https:localhost:myIISApiSSLPortNumber;http://localhost:myIISApiPortNumber"
}
}
}

startup.cs

Add CORS Configuration

public class Startup
{
    public IConfiguration Configuration { get; }

    public IServiceCollection _services { get; private set; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        _services = services;
        RegisterCorsPolicies();
        services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseCors("DevelopmentCorsPolicy");
            app.UseDeveloperExceptionPage();
        }
        else if (env.IsStaging())
        {
            app.UseCors("StagingCorsPolicy");
        }
        else
        {
            app.UseCors("ProductionCorsPolicy");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseMvc(); // CORS middleware must precede any defined endpoints
    }

    private void RegisterCorsPolicies()
    {
        string[] localHostOrigins = new string[] {
        "http://localhost:4200", "http://localhost:3200"};

        string[] stagingHostOrigins= new string[] {
        "http://localhost:4200"};

        string[] productionHostOrigins = new string[] {
        "http://yourdomain.net", "http://www.yourdomain.net",
        "https://yourdomain.net", "https://www.yourdomain.net"};

        _services.AddCors(options =>    // CORS middleware must precede any defined endpoints
        {
            options.AddPolicy("DevelopmentCorsPolicy", builder =>
            {
                builder.WithOrigins(localHostOrigins)
                        .AllowAnyHeader().AllowAnyMethod();
            });
            options.AddPolicy("StagingCorsPolicy", builder =>
            {
                builder.WithOrigins(stagingHostOrigins)
                        .AllowAnyHeader().AllowAnyMethod();
            });
            options.AddPolicy("ProductionCorsPolicy", builder =>
            {
                builder.WithOrigins(productionHostOrigins)
                        .AllowAnyHeader().AllowAnyMethod();
            });
        //options.AddPolicy("AllowAllOrigins",
        //    builder =>
        //    {
        // WARNING: ASP.NET Core 2.2 does not permit allowing credentials with AllowAnyOrigin() 
        // cref: https://docs.microsoft.com/en-us/aspnet/core/migration/21-to-22?view=aspnetcore-2.2&tabs=visual-studio
            //        builder.AllowAnyOrigin()
            //               .AllowAnyHeader().AllowAnyMethod();
            //    });
            //options.AddPolicy("AllowSpecificMethods",
            //    builder =>
            //    {
            //        builder.WithOrigins(productionHostOrigins)
            //               .WithMethods("GET", "POST", "HEAD");
            //    });
            //options.AddPolicy("AllowSpecificHeaders",
            //    builder =>
            //    {
            //        builder.WithOrigins(productionHostOrigins)
            //               .WithHeaders("accept", "content-type", "origin", "x-custom-header");
            //    });
            //options.AddPolicy("ExposeResponseHeaders",
            //    builder =>
            //    {
            //        builder.WithOrigins(productionHostOrigins)
            //               .WithExposedHeaders("x-custom-header");
            //    });
            //options.AddPolicy("AllowCredentials",
            // WARNING: ASP.NET Core 2.2 does not permit allowing credentials with AllowAnyOrigin() cref: https://docs.microsoft.com/en-us/aspnet/core/migration/21-to-22?view=aspnetcore-2.2&tabs=visual-studio
            //    builder =>
            //    {
            //        builder.WithOrigins(productionHostOrigins)
            //               .AllowCredentials();
            //    });
            //options.AddPolicy("SetPreflightExpiration",
            //    builder =>
            //    {
            //        builder.WithOrigins(productionHostOrigins)
            //               .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
            //    });
        });
    }
}
  1. Start Debugging API with Development (IIS Express) profile

Set breakpoint on ValuesController.Get()

  1. Test API with Postman:

    https://localhost:myApiPortNumber/api/values

    • Access-Control-Allow-Origin header and values should be in response
  2. Create Angular application

    c:\s\a\s>ng new Spa1 --routing (will automatically create Spa folder)
    
  3. Start Spa1 application

    c:\s\a\s>cd Spa1
    c:\s\a\s\Spa1>Ng serve
    
  4. Browse to http://localhost:4200/

    • Spa1 should start successfully
  5. Implement CORs in Spa1

app.module.ts

  • Import HttpClientModule
import { HttpClientModule } from '@angular/common/http';

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    HttpClientModule,
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts

  • Import HttpClient

  • Add getValues() method with CORS Request

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent implements OnInit {
  title = 'Spa1';
  values: any;
  apiUrl: string = environment.apiUrl;
  valuesUrl = this.apiUrl + "values";

  constructor(private http: HttpClient) { }

  ngOnInit() {
    this.getValues();
  }
  getValues() {
    this.http.get(this.valuesUrl).subscribe(response => {
      this.values = response;
  }, error => {
  console.log(error);
});
}
}

app.component.html

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<h2>Values</h2>
<p *ngFor="let value of values">
  {{value}}
</p>
<router-outlet></router-outlet>

environment.ts

export const environment = {
  production: false,
  apiUrl: 'https://localhost:myApiPortNumber/api/'
};
  1. Start Spa1 application

    c:\s\a\s\Spa1>Ng serve
    
  2. Browse to http://localhost:4200/

    • Chrome & Edge: should successfully make CORs requests now
    • Safari: I have not tested
    • Firefox: may block request due to un-trusted certificate.

One way of remediating the Firefox block:

FireFox | Options | Privacy & Security | Security | Certificates | [View Certificates] :

Certificate Manager | [Add Exception]:

   add localhost

CORS Test

  1. Clone Spa1

    c:\s\a\s>xcopy /s /i Spa1 Spa2
    
  2. Refactor title of Spa2

app.component.ts

export class AppComponent implements OnInit {
  title = 'Spa2';
}
  1. Start Spa2 application on port 3200

    c:\s\a\s\Spa2>ng serve --port 3200
    
  2. Browse to http://localhost:3200/

    • The values array should render on the web page
  3. Stop Debugging API with Development (IIS Express) profile

  4. Start Debugging API with Staging (IIS Express) profile

  5. Browse to http://localhost:4200/

    • The values array should render on the web page.
  6. Browse to http://localhost:3200/

    • The values array should not render on the web page.
  7. inspect Http Response with Developer Tools:

    • Access-Control-Allow-Origin header and values should not be in response

Add the following code snippet in the method ConfigureServices. Edit this for allowing only custom headers.

// Add service and create Policy with options
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy",
                builder => builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials()
                );
        });

Add the following in Configure method

app.UseCors("CorsPolicy");

Add 'EnableCors' attribute to the controller.

[EnableCors("CorsPolicy")]

Register cors in services

services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder
                    .SetIsOriginAllowed((host) => true)
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials());
            });

add cors middleware

app.UseCors("CorsPolicy");