Reputation: 12374
A website is per default anonymous access only.
The admin has a button to switch the site into maintenance mode, which should enable authorization using the built-in CookieAuthentication (flip a bit in a database, not relevant for this post).
In order to make that work, I first configured cookie authentication (in startup.cs):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/auth/login");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}
Then on relevant controllers, I put an [Authorize]
attribute.
[Authorize]
public class HomeController : Controller
{
//removed
}
This works perfectly - cookie auth kicks in when authorize-attribute is present. So far so good.
Now I want to disable authorization at runtime when maintenance mode is off.
This is what I ended up with after a lot of trial and error and research.
public void OnAuthorization(AuthorizationFilterContext context)
{
IMaintenanceModeDataService ds = context.HttpContext.RequestServices.GetService<IMaintenanceModeDataService>();
if (!ds.IsMaintenanceModeEnabled)
{
//Maintenance mode is off, no need for authorization
return;
}
else
{
ClaimsPrincipal user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
//when the user is authenticated, we don't need to do anything else.
return;
}
else
{
//we're in maintenance mode AND the user is not
//It is outside the scope of this to redirect to a login
//We just want to display maintenancemode.html
context.Result = new RedirectResult("/maintenancemode.html");
return;
}
}
}
[MaintenanceModeAwareAuthorize]
public class HomeController : Controller
{
//removed
}
This works great when the site is in maintenance mode.
When the site is NOT in maintenance mode, the cookie authentication still kicks in and requires auth. I could remove that and try to implement my own auth, but that would be stupid, when we already have perfectly well-crafted solutions built-in.
Q: Why not handle this by doing x (which requires serverside access to config, environment vars, server or similar)?
A: Because this needs to be immediately accessible to non-technical admin-users by clicking a button in the backend.
Upvotes: 3
Views: 3967
Reputation: 13704
The authorization system in ASP.NET Core is extensible and you can implement your scenario easily with poliy-based authorization.
Two main things to know to get going:
Our goal is then to create a requirement which is satisfied if any of the following statements is true:
The first step is to create our requirement:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirement : IAuthorizationRequirement
{
}
We then have to implement the handler for this requirement, which will determine if it's satisfied or not. The good news is handlers support dependency injection:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler : AuthorizationHandler<MaintenanceModeDisabledOrAuthenticatedUserRequirement>
{
private readonly IMaintenanceModeDataService _maintenanceModeService;
public MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler(IMaintenanceModeDataService maintenanceModeService)
{
_maintenanceModeService = maintenanceModeService;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MaintenanceModeDisabledOrAuthenticatedUserRequirement requirement)
{
if (!_maintenanceModeService.IsMaintenanceModeEnabled || context.User.Identities.Any(x => x.IsAuthenticated))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Next, we need to create an authorization policy that uses this requirement, and you have 2 choices here:
[Authorize]
attributes are used, or[Authorize(Policy = "<your-policy-name>")]
There's no right or wrong answer; I'd pick the first option is my application had only one authorization policy, and the second one if it had several of them. We'll see how to do both:
services
.AddAuthorization(options =>
{
// 1. This is how you redefine the default policy
// By default, it requires the user to be authenticated
//
// See https://github.com/dotnet/aspnetcore/blob/30eec7d2ae99ad86cfd9fca8759bac0214de7b12/src/Security/Authorization/Core/src/AuthorizationOptions.cs#L22-L28
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement())
.Build();
// 2. Define a specific, named policy that you can reference from your [Authorize] attributes
options.AddPolicy("MaintenanceModeDisabledOrAuthenticatedUser", builder => builder
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement()));
});
Next, you need to register the requirement handler as an IAuthorizationHandler
, as indicated in the official docs
// The lifetime you pick is up to you
// You just need to remember that it's got a dependency on IMaintenanceModeDataService, so if you
// registered the implementation of IMaintenanceModeDataService as a scoped service, you shouldn't
// register the handler as a singleton
// See this captive dependency article from Mark Seeman: https://blog.ploeh.dk/2014/06/02/captive-dependency/
services.AddScoped<IAuthorizationHandler, MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler>();
The final step is to apply the [Authorize]
attributes on your controllers/actions as needed.
// 1. If you redefined the default policy
[Authorize]
public IActionResult Index()
{
return View();
}
// 2. If you defined an explicit policy
[Authorize(Policy = "MaintenanceModeDisabledOrAuthenticatedUser")]
public IActionResult Index()
{
return View();
}
Upvotes: 8
Reputation: 2498
Since current pages need work perfectly with anonymous
mode, then authentication should NOT be in Controller
level.
I think your requests are:
If a Maintancer
login system,
maintance elements
(switch button or others) on page, so Maintancer
can switch page with different mode, and do maintancer actions
anonymously
, anonymous-mode elements
will render to browserMaintancer
, normal-user-mode elements
will render to browserTo resolve those, The key is to block unauthorized user to visit Maintancer ACTIONS
, instead of controller
.
my suggestions are:
Maintancer Login
, then enject switch button
IsMaintenanceMode
, then show Maintancer-authorized elements
, like Delete Post
, Edit Content
, ...Maintancer
(like Delete Post
), add [Authorize(Roles="Maintancer")]
or [Authorize(Policy="Maintancer")]
or you customized authorize.Upvotes: 0
Reputation: 20106
I am afraid that could not be done .The accept of authorization is different from authentication, when context.HttpContext.User.Identity.IsAuthenticated
is false
, it will always redirect to login page.
It's better to have actions that must or may require authorization in a controller together, and unauthorized actions in a separate controller with [AllowAnonymous]
.
if (!user.IsMaintenanceModeEnabled)
{
context.Result = new RedirectResult("Another controller with [AllowAnonymous]");
return;
}
Upvotes: 1