Reconfiguring CORS Policy in ASP.NET Core at Runtime
Have you ever created a runtime version of your app only to realize you needed to change the public properties? Lear how to easily do this with ASP.NET Core.
Join the DZone community and get the full member experience.
Join For FreeASP.NET Core comes with ready to use Cross-Origin Resource Sharing support in the form of Microsoft.AspNetCore.Cors
package. The usage is very straightforward, you just need to register the services, configure the policy and enable CORS either with middleware (for a whole pipeline or a specific branch), filter (globally for MVC) or attribute (at the MVC controller/action level). This is all nicely described in the documentation. But what if there is a need to reconfigure the policy at runtime?
Let's assume that there is an application which contains two APIs. One is considered "private" so only other applications from the same suite can use it, while the second is "public" and a client administrator should be able to configure it so it can be used with any 3rd party application.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("Private", builder =>
{
builder.WithOrigins("http://appone.suite.com, http://apptwo.suite.com");
...
});
options.AddPolicy("Public", builder =>
{
// Apply "public" policy (based on information read from storage etc.)
...
});
})
...;
}
...
}
The initial configuration of the policy is not an issue, the problem is the part when the admin decides to change the policy. The whole application shouldn't require a restart for changes to take effect, so the policy needs to be accessed and reconfigured.
How the Policy Can Be Accessed
The initialization code shows that the policies are being added to CorsOptions
. Internally, the IServiceCollection.AddCors
is plugging the options into the ASP.NET Core configuration framework by calling IServiceCollection.Configure
. This means that they can be retrieved with the help of Dependency Injection as IOptions
and considered a singleton. This is enough information to start building a service which will help with accessing the policy.
public class CorsPolicyAccessor : ICorsPolicyAccessor
{
private readonly CorsOptions _options;
public CorsPolicyAccessor(IOptions<CorsOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
}
}
From this point it's easy. The CorsOptions
exposes the GetPolicy
method and the DefaultPolicyName
property which can be used for exposing access to the policy.
public class CorsPolicyAccessor : ICorsPolicyAccessor
{
...
public CorsPolicy GetPolicy()
{
return _options.GetPolicy(_options.DefaultPolicyName);
}
public CorsPolicy GetPolicy(string name)
{
return _options.GetPolicy(name);
}
}
Now the new service can be registered (preferably after the AddCors
call).
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
...
})
.AddTransient<ICorsPolicyAccessor, CorsPolicyAccessor>()
...;
}
...
}
Exposing the Policy With MVC
With the help of the newly created ICorsPolicyAccessor
service and Dependency Injection, the CORS policy can now be reconfigured at runtime, for example from an ASP.NET Core MVC controller. For starters, let's create an action and view all the lists whose origins are in the Public policy.
public class OriginsController : Controller
{
private readonly ICorsPolicyAccessor _corsPolicyAccessor;
public OriginsController(ICorsPolicyAccessor corsPolicyAccessor)
{
_corsPolicyAccessor = corsPolicyAccessor;
}
[AcceptVerbs("GET")]
public IActionResult Manage()
{
return View(new OriginsModel(_corsPolicyAccessor.GetPolicy("Public").Origins));
}
}
public class OriginsModel
{
private readonly IList<string> _origins;
public IEnumerable<string> Origins => _origins;
public OriginsModel(IList<string> origins)
{
_origins = origins;
}
}
@model OriginsModel
<!DOCTYPE html>
<html>
...
<body>
<div>
<ul>
@foreach(var origin in Model.Origins)
{
<li>@origin</li>
}
</ul>
</div>
</body>
</html>
Navigating to the URL pointing at the action should result in a list of all the origins. This can be easily extended with the capabilities of adding and removing origins. First, the View Model should be changed so the list of origins can be used to generate a select
element.
public class OriginsModel
{
...
public List<SelectListItem> Origins => _origins.Select(origin => new SelectListItem
{
Text = origin,
Value = origin
}).ToList();
...
}
This allows you to add some inputs and forms to handle the operations (this could be done a lot nicer with AJAX but I want to keep things simple for the sake of clarity).
@model OriginsModel
<!DOCTYPE html>
<html>
...
<body>
<div>
<ul>
@foreach(var origin in Model.Origins)
{
<li>@origin.Text</li>
}
</ul>
<form asp-action="Add" method="post">
<fieldset>
<legend>Adding an origing</legend>
<input type="text" name="origin" />
<input type="submit" value="Add" />
</fieldset>
</form>
<form asp-action="Remove" method="post">
<fieldset>
<legend>Removing an origing</legend>
<select name="origin" asp-items="Model.Origins"></select>
<input type="submit" value="Remove" />
</fieldset>
</form>
</div>
</body>
</html>
The last thing to do is handling the Add and Remove actions. I'm going to use the PRG Pattern here which should allow a clear separation of responsibilities.
public class OriginsController : Controller
{
...
[AcceptVerbs("POST")]
public IActionResult Add(string origin)
{
_corsPolicyAccessor.GetPolicy("Public").Origins.Add(origin);
return RedirectToAction(nameof(Manage));
}
[AcceptVerbs("POST")]
public IActionResult Remove(string origin)
{
_corsPolicyAccessor.GetPolicy("Public").Origins.Remove(origin);
return RedirectToAction(nameof(Manage));
}
}
Testing this will show that, indeed, changes are being picked up immediately (although there is a small risk of a race involved). This simple demo shows how easy it is to reconfigure part of a policy. With this approach, any of CorsPolicy
public properties can be changed.
Published at DZone with permission of Tomasz Pęczek. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments