Kjensen
Kjensen

Reputation: 12374

How do I disable/enable authentication at runtime in Asp.Net Core 2.2?

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.

Attempted Solution

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.

How do I disable authorization when the site is NOT in maintenance mode (at runtime)?

Notes:

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

Answers (3)

Micka&#235;l Derriey
Micka&#235;l Derriey

Reputation: 13704

Yes you can!

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:

  • an authorization policy is made of one or more requirements
  • all of the requirements must be satisfied for a policy to succeed

Our goal is then to create a requirement which is satisfied if any of the following statements is true:

  • the maintenance mode is not enabled, or
  • the user is authenticated

Let's see the code!

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:

  • you can redefine the default policy, used when "empty" [Authorize] attributes are used, or
  • create an explicit policy that you'll have to reference in your attributes, like [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

Dongdong
Dongdong

Reputation: 2498

Since current pages need work perfectly with anonymous mode, then authentication should NOT be in Controller level.

I think your requests are:

  1. If a Maintancer login system,

    • run extra code to show maintance elements(switch button or others) on page, so Maintancer can switch page with different mode, and do maintancer actions
  2. If user visit site anonymously, anonymous-mode elements will render to browser
  3. If user login but not an Maintancer, normal-user-mode elements will render to browser

To resolve those, The key is to block unauthorized user to visit Maintancer ACTIONS, instead of controller.

my suggestions are:

  1. in _Layout.cshtml page, check if Maintancer Login, then enject switch button
  2. in the actions or pages that could visit anornymously, check if "Maintancer Login" && IsMaintenanceMode, then show Maintancer-authorized elements, like Delete Post, Edit Content, ...
  3. in Controller.Actions that works only for Maintancer(like Delete Post), add [Authorize(Roles="Maintancer")] or [Authorize(Policy="Maintancer")] or you customized authorize.

Upvotes: 0

Ryan
Ryan

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

Related Questions