ASP. Net Core 2.2 with cookie authentication: how to avoid page redirect when not authorized for API only controllers

Viewed 741

My ASP .Net Core 2.2 web application has some controllers that return View(s) and some controllers that return a json result as they are just API controllers.

My website uses cookie authentication, with this declaration:

services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.AccessDeniedPath = "/Pages/AccessDenied";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Strict;
    });

In the Configuration section:

// ...
app.UseAuthentication();
app.UseDefaultFiles();
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Pages}/{action=Index}/{id?}");
});

My PagesController just inherits from Microsoft.AspNetCore.Mvc.Controller and for some pages I put [Authorize] attribute. When a non authorized user tries to open the page it gets correcly redirected to my public AccessDenied page. Everything's good.

BUT, I have also an ApiController, which inherits from Microsoft.AspNetCore.Mvc.ControllerBase, has an attribute [ApiController] and also a [Authorize(Roles = "Administrator")]. I use this controller to communicate with javascript in my pages. The problem is that when a non authorized user tries to invoke the methods on this controller, I don't want it to respond with a HTTP 200 page (the AccessDenied one), but I want that it returns just an HTTP 401 Unauthorized because it is an API.

How can I achieve such a different behavior keeping the cookie authentication?

Thanks!

1 Answers

The simplest way to obtain this behavior is writing a custom class which derives from CookieAuthenticationEvents and override both RedirectToLogin and RedirectToAccessDenied.

If you are not familiar with the CookieAuthenticationEvents class you can start from this example in the docs. Basically it is a plug point to add custom behavior for the cookie authentication.

First of all you need a way to identify your AJAX requests. The simplest way to do so is using a common prefix in the request path for all of your AJAX requests. As an example you can decide that all of your AJAX request paths will start by /api.

You can write the following custom CookieAuthenticationEvents class:

public sealed class CustomCookieAuthenticationEvents : CookieAuthenticationEvents 
{
  public override Task RedirectToAccessDenied(
            RedirectContext<CookieAuthenticationOptions> context)
  {
    if (context is null)
    {
      throw new ArgumentNullException(nameof(context));
    }

    if (IsAjaxRequest(context.HttpContext))
    {
      context.Response.StatusCode = StatusCodes.Status403Forbidden;
      return Task.CompletedTask;
    }
    else 
    {
      // non AJAX requests behave as usual
      return base.RedirectToAccessDenied(context);
    }
  }

  public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
  {
    if (context is null)
    {
      throw new ArgumentNullException(nameof(context));
    }

    if (IsAjaxRequest(context.HttpContext))
    {
      context.Response.StatusCode = StatusCodes.Status401Unauthorized;
      return Task.CompletedTask;
    }
    else 
    {
      // non AJAX requests behave as usual
      return base.RedirectToLogin(context);
    }
  }

  private static bool IsAjaxRequest(HttpContext httpContext) 
  {
    var requestPath = httpContext.Request.Path;
        return requestPath.StartsWithSegments(new PathString("/api"), StringComparison.OrdinalIgnoreCase);
  }
}

Now you can register your custom CookieAuthenticationEvents with the authentication services inside your Startup.ConfigureServices method:

services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.AccessDeniedPath = "/Pages/AccessDenied";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.EventsType = typeof(CustomCookieAuthenticationEvents);
    });

services.AddSingleton<CustomCookieAuthenticationEvents>();
Related