0

I have a scenario where an app needs to authenticate a user by calling an API and sending a user token to verify user identity. I started working on a custom authentication handler based on the following tutorials:

Tutorial 1

Tutorial 2

I have a very basic example which right now simply fails the authentication just to make sure it works:

public class SoleAuthenticationHandler : AuthenticationHandler<SoleAuthenticationOptions>
{
    private readonly ISoleApiService _soleApiService;

    public SoleAuthenticationHandler(
        IOptionsMonitor<SoleAuthenticationOptions> options, 
        ILoggerFactory logger, 
        UrlEncoder encoder, 
        ISystemClock clock, ISoleApiService soleApiService) 
        : base(options, logger, encoder, clock)
    {
        _soleApiService = soleApiService;
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        return Task.FromResult(AuthenticateResult.Fail("You are not authorized to access this resource."));
    }
}

This works as intended, controller actions decorated with [Authorize] attribute are intercepted and 401 is thrown. My questions are the following:

  1. How am I supposed to handle the 401 once it happens? For example let's say I want to redirect a user to a friendly page that says "you're not authorized please login". Is that something done in the handler or elsewhere? What is the proper process here? Looking at Microsoft docs for the AuthenticationHandler there is a method called BuildRedirectUri but providing that method with a uri does not really change anything - the page still returns a 401.
  2. As it stands now in order for this to work I need to decorate controllers/actions with [Authorize] attribute. Is there a way to do this globally so that I don't have to specifically authorize each controller and/or action?
Marko
  • 11,003
  • 8
  • 43
  • 55
  • What are you using for the front end? If it's a SPA like Angular your front end should check the error status for a 401 and redirect – raklos Nov 11 '19 at 08:26
  • @raklos No it's a typical .net mvc website the only difference is that it works within a larger portal application and it calls portal's api to authenticate. If the portal does not authenticate you then I need the AuthorizationHandler to redirect to the portal login. That's really the gist of it. – Marko Nov 11 '19 at 11:25

1 Answers1

0

We had/have the customErrors pages in ASP.NET web forms and MVC 5.x to redirect users automatically to the specified error pages when a certain statusCode is issued:

<customErrors mode="On" defaultRedirect="error">
        <error statusCode="404" redirect="error/notfound" />
        <error statusCode="403" redirect="error/forbidden" />
</customErrors>

Here in ASP.NET Core we can simulate these pages this way:

First add a new ErrorController to handle specific statusCodes (id's here) and then return custom views for different conditions:

public class ErrorController : Controller
    {
        private readonly ILogger<ErrorController> _logger;

        public ErrorController(ILogger<ErrorController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index(int? id)
        {
            var logBuilder = new StringBuilder();

            var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
            logBuilder.AppendLine($"Error {id} for {Request.Method} {statusCodeReExecuteFeature?.OriginalPath ?? Request.Path.Value}{Request.QueryString.Value}\n");

            var exceptionHandlerFeature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
            if (exceptionHandlerFeature?.Error != null)
            {
                var exception = exceptionHandlerFeature.Error;
                logBuilder.AppendLine($"<h1>Exception: {exception.Message}</h1>{exception.StackTrace}");
            }

            foreach (var header in Request.Headers)
            {
                var headerValues = string.Join(",", value: header.Value);
                logBuilder.AppendLine($"{header.Key}: {headerValues}");
            }
            _logger.LogError(logBuilder.ToString());

            if (id == null)
            {
                return View("Error");
            }

            switch (id.Value)
            {
                case 401:
                case 403:
                    return View("AccessDenied");
                case 404:
                    return View("NotFound");

                default:
                    return View("Error");
            }
        }
    }

Now it's time to connect this controller to the built-in error handling middlewares of ASP.NET Core:

public void Configure(IApplicationBuilder app)
        {
            if (env.IsDevelopment())
            {
                app.UseDatabaseErrorPage();
                app.UseDeveloperExceptionPage();
            }
            app.UseExceptionHandler("/error/index/500");
            app.UseStatusCodePagesWithReExecute("/error/index/{0}");

About your second question, just define your filter/Authorize attribute globally.

VahidN
  • 16,098
  • 7
  • 63
  • 110
  • Yeah this is sort of the same setup that I have the only thing different is I don't have the app.UseExceptionHandler("") call because I have a single controller/action handling all errors. But when I wire up that custom authentication handler once the handler returns 401 nothing else fires. I'm starting to think that it might be the order of things in the startup pipeline. – Marko Nov 15 '19 at 02:05