In this post, we are going to talk about enabling CORS in ASP.NET Core Web Applications.
Before we understand how to enable CORS in ASP.NET Core applications, we need to understand an important concept in the web application security model called the Same-Origin Policy.
VIDEO: Implement CORS in ASP.NET Core Web API.
What Is CORS?
Let’s learn what the term same-origin actually means first.
About Same-Origin and What It Means
Two URLs are considered to have the same origin if they have the same URL scheme (HTTP or HTTPS), same hostname (domain name), and same port number (the endpoint at which applications talk to each other).
What Is the Same-Origin Policy?
The Same-origin policy states that a Web browser will only allow communication between two URLs if they belong to the same origin. That is, the client app (https://example.com
) cannot communicate with the server app (https://example.net
) as they belong to a different origin. This policy exists to isolate potentially malicious scripts from harming other documents on the web.
But there are some cases where Cross-Domain scripting is desired and the idea of the open web makes it compelling.
Cross-Origin Resource Sharing is a mechanism to bypass the Same-Origin policy of a Web browser. Specifically, a server app uses additional HTTP headers to tell a browser that it is fine to load documents (from its origin) in a few selected client apps (of different origin).
But how does CORS actually work?
Let’s have a look at the image. There is a client app (https://example.com
) sending a GET request to a server app (https://example.net
) for some resource:
All modern browsers set the Origin
header automatically, which indicates the domain of the site is making the request.
If the server allows Cross-origin requests from the Origin (https://example.com
), it sets the Access-Control-Allow-Origin
header with its value matching the origin header’s value from the request.
In another way, if the server doesn’t include this header, the request fails. The browser shall receive the response data, but this data shall not be accessible to the client.
That’s how a simple CORS request works.
Preflight Requests
Sometimes, instead of a simple GET request, a client may need to send requests like PUT, DELETE, etc. For such requests, the browser sends an additional request (an OPTIONS request) called a Preflight request. This is done just before the actual request to make sure that the original request succeeds. If it does, the browser sends the actual request.
This diagram illustrates such a scenario:
Actual request
Creating an Application Without CORS
In the demo, let’s create two projects. The first one is going to be a Web API project, and the second one is going to be a Blazor WebAssembly project.
Let’s create a Web API project that will be our server. Once we create this project, we are going to modify the launchSettings. json
file:
{ "profiles": { "CorsServerApp": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": false, "launchUrl": "weatherforecast", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
As you can see, this app will run on the https://localhost:5001 URI.
Next, let’s create a Client app. We are going to create a new Blazor WebAssembly standalone project. Of course, we have to modify the launchSettings.json
file as well:
{ "profiles": { "CorsClientApp": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:5011;http://localhost:5010", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
Okay. We have both a client and the server app running and they belong to different origins. The server app will run on port 5001 and the client app on port 5011.
To make cross-origin requests, we first have to modify the Program.cs
file of the client’s app:
using CorsClientApp; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri("https://localhost:5001") }); await builder.Build().RunAsync();
Here, we just set up the base address for the HttpClient, which is the URI of our Web API.
Then, we have to open the Pages/FetchData.razor
file, and modify the @code part:
@code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast"); } ... }
Since we already modified the base address of the HttpClient, by adding this change, we want to send our GET request to the https://localhost:5001/weatherforecast
URI, which is the exact address of the GET action inside the WeatherForecast controller in the server app.
Since we haven’t enabled CORS on the server app, the server app doesn’t set any headers and the browser rejects the response.
So, if we start both applications, navigate to the client app, and open the Fetchdata page, we are going to see the error message:
So, we can see that the request is blocked by CORS policy and our client app is not allowed to fetch the data from the required resource.
Enabling CORS in ASP.NET Core Middleware
Now that we have seen the Same-Origin policy in action, let’s see how we can enable CORS in ASP.NET Core.
To do that, let’s open the Program.cs
file in the server app and modify it:
var builder = WebApplication.CreateBuilder(args); const string policyName = "CorsPolicy"; builder.Services.AddCors(options => { options.AddPolicy(name: policyName, builder => { builder.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddControllers(); ... app.UseCors(policyName); app.UseAuthorization(); app.MapControllers(); app.Run();
First, we create a private variable to hold the name of our CORS policy. Then, we call the AddCors
method that adds cross-origin resource-sharing services to the service collection. This method accepts an Action delegate as a parameter where we can configure the CORS options. For the CORS options, we call the AddPolicy method, which adds a new policy to the configuration and also accepts an Action delegate as a parameter where we can set up the methods to build our policy.
With this policy, we enable access from any origin AllowAnyOrigin
, allow any header in a request AllowAnyHeader
, and allow any method AllowAnyMethod
.
We can agree that this policy is not the strict one, but it is a basic setup that will remove the CORS issue for our client application.
Also, we call the UseCors
method and pass the policy name to add the CORS middleware to the application’s pipeline. We must call this method after the UseRouting
method and before the UseAuthorization
method.
Now, with these changes in place, we can start both of our apps and navigate to the FetchData
page:
There we go. We have enabled CORS in our application and removed the issue we were facing before.
Default Policies for CORS in ASP.NET Core
In the case where we don’t want to use multiple named policies but a single default policy, we can replace the AddPolicy
method with the AddDefaultPolicy
method:
builder.Services.AddCors(options => { options.AddDefaultPolicy(builder => { builder.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); });
As you can see, we are not providing the name of the policy. Additionally, we don’t have to provide the policy name in the UseCors
method:
app.UseAuthorization(); app.UseCors(); app.MapControllers();
For the rest of the article, we are going to stick with the named CORS policy configuration.
Enabling CORS in ASP.NET Core With Attributes
Instead of enabling CORS at the entire application level, we can also enable CORS at the controller level or the action level. To be able to do that, we have to use the [EnableCors]
attribute:
- We can use just the
[EnableCors]
attribute on top of the controller or the action, and it will implement a default CORS policy - Or we can use the
[EnableCors("Policy name")]
attribute, to apply a named CORS policy
By using the named policy with the [EnableCors]
attribute, we can apply different policies to different controllers or actions.
So, in our example, if we want, we can add one more named policy for the CORS configuration:
const string policyName = "CorsPolicy"; const string anotherPolicyName = "AnotherCorsPolicy"; builder.Services.AddCors(options => { options.AddPolicy(name: policyName, builder => { builder.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); options.AddPolicy(name: anotherPolicyName, builder => { builder.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); });
Also, we have to modify:
app.UseAuthorization(); app.UseCors(); app.MapControllers();
This time, we are not specifying the policy name since we are using more than one policy in our application.
Finally, to see the [EnableCors]
attribute in action, we are going to modify the WeatherForecastController
:
[EnableCors("CorsPolicy")] [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } [EnableCors("AnotherCorsPolicy")] [HttpGet("anotherPolicyExample")] public IActionResult GetTempAboveLimit() { return Ok("This action is protected with another named CORS policy"); }
As you can see for the first action, we are enabling CORS using the old named policy – CorsPolicy. But for the other action, we are using a new policy – AnotherCorsPolicy.
Okay, now we know how to apply different policies while enabling CORS. However, it is not a good practice to use two different named policies that apply the same policy options. In this situation, only one policy would be sufficient.
So, let’s see how we can configure different policy options and make sense of using different named policies in our application.
Configuring CORS Policy
We can use various options while adding policies to the CORS configuration.
Right now, for both policies, we are allowing any origin, any header, and any method. But let’s say, we want to allow only our client application to access the first GET action from the WeatherForecast
controller, and some other client (on port 5021) to access the second action:
builder.Services.AddCors(options => { options.AddPolicy(name: policyName, builder => { builder.WithOrigins("https://localhost:5011") .AllowAnyHeader() .AllowAnyMethod(); }); options.AddPolicy(name: anotherPolicyName, builder => { builder.WithOrigins("https://localhost:5021") .AllowAnyHeader() .AllowAnyMethod(); }); });
This time, we are using the WithOrigins
method, which accepts a params string[] origins
as a parameter. This means, if we want to allow access to multiple origins for a single policy, we can just add multiple comma-separated URIs as an argument: WithOrigins("First URI", "Second URI")
.
Configuring CORS for Multiple Subdomains
One more thing. When we want to allow access for a client that has a main domain and multiple subdomains, we don’t have to add all the URIs with subdomains as arguments. What we can do is use the wildcard in the WithOrigins
method followed by the SetIsOriginAllowedToAllowWildcardSubdomains
method:
opt.AddPolicy(name: policyName, builder => { builder.WithOrigins("https://*.code-maze.com") .SetIsOriginAllowedToAllowWildcardSubdomains(); });
By using the SetIsOriginAllowedToAllowWildcardSubdomains
method, we allow origins to match a configured wildcard domain when evaluating if the origin is allowed.
Enabling CORS for Specific Methods
If we take a look at our controller, we can see that we have only GET actions inside. Well, we can specify that as well in the CORS configuration:
builder.Services.AddCors(options => { options.AddPolicy(name: policyName, builder => { builder.WithOrigins("https://localhost:5011") .AllowAnyHeader() .WithMethods("GET"); }); options.AddPolicy(name: anotherPolicyName, builder => { builder.WithOrigins("https://localhost:5021") .AllowAnyHeader() .WithMethods("GET"); }); });
Now, instead of AllowAnyMethod
, we are using WithMethods
, which accepts params string[] methods
as a parameter. Of course, this means if we want to allow access to multiple methods, we need to add them as multiple comma-separated strings: WithMethods("PUT", "DELETE", "GET")
.
Enabling CORS for Specific Headers
In the same way that we use the WithMethods
method to specify access for single or multiple methods, we can use the WithHeaders
method to allow specific headers to be sent in a CORS request:
opt.AddPolicy(name: anotherPolicyName, builder => { builder.WithOrigins("https://localhost:5021") .WithHeaders(HeaderNames.ContentType, HeaderNames.Accept) .WithMethods("GET"); });
Additionally, when we want to expose some headers, we can use the WithExposedHeaders
method. For example, in our Blazor WebAssembly Pagination with ASP.NET Core Web API article, we create a pagination functionality on the Web API’s side and add the required information inside the X-Pagination
header:
[HttpGet] public async Task<IActionResult> Get([FromQuery] ProductParameters productParameters) { var products = await _repo.GetProducts(productParameters); Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(products.MetaData)); return Ok(products); }
But for our client application to be able to access that information inside the header, we have to expose it in the CORS configuration:
opt.AddPolicy(name: anotherPolicyName, builder => { builder.WithOrigins("https://localhost:5021") .WithHeaders(HeaderNames.ContentType, HeaderNames.Accept) .WithMethods("GET") .WithExposedHeaders("X-Pagination"); });
And that’s it for our CORS configuration.
Additional read: You can see the CORS in action with .NET Core and Angular projects by reading the .NET Core tutorial.
Using CORS With Minimal API
So far, we have applied CORS policies to the controller’s action. We can use CORS with minimal API as well.
Let’s add a minimal API endpoint to our Program.cs
:
app.MapControllers(); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/minimal/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast") .WithOpenApi(); app.Run(); internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }
Here, we define a new endpoint /minimal/weatherforecast
that would respond to the GET request and return some random weather data.
We can apply our (previously defined) policies to this endpoint:
app.MapGet("/minimal/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast") .WithOpenApi() .RequireCors(policyName);
All we have to do is to use the RequireCors
method.
How to Disable CORS
Finally, a small note on disabling CORS. Sometimes we need to disable CORS on the action or the controller level.
To do that, we can use the [DisableCors] attribute:
[DisableCors] [HttpGet] public async Task<IActionResult> Get([FromQuery] ProductParameters productParameters) { var products = await _repo.GetProducts(productParameters); Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(products.MetaData)); return Ok(products); }
That’s it for now. Let’s recap what we’ve learned.
Conclusion
Think of CORS as a relaxation attempt to the more restrictive Same-Origin policy. On one side, there is a growing need for security on the web. And on the other, there is a need to integrate web services. CORS provides rich tools for our need for an open and secure web, and ASP.NET Core allows us to take advantage of CORS in our cross-platform web applications.