Show login page for every URL when not authenticated

Question

Friday, December 1, 2017 8:51 PM

My application needs users to be authenticated from the start. There is no public area. The very first thing that the user needs to do is log in. So this should be done right from the start page. It should also be done when the user isn't logged in and another direct URL is accessed, for example from a notification.

I don't like the idea of redirecting the user to another URL and dragging around the return URL as parameter. This adds more network requests and makes ugly and insecure URLs. The server knows exactly whether the user is logged in and can always show the login form on every URL. On successful login, the original URL doesn't have to change, the user is directly server the requested content.

How can I do that?

I could add a custom middleware that checks authentication. For unauthenticated requests, the login form would be shown instead of what MVC routing would bring up. That middleware would shortcut the pipeline, avoiding MVC. But I only know how to show that view through MVC routing. My middleware would need to come before routing. Also, the login view makes use of model binding which is an MVC feature IIRC.

Or is there a way to setup a special route that maps every URL to the AccountController/Login if the request isn't authenticated? I can remember that I have written a custom route (based on login instead of a template) in ASP.NET 4. But I can't find that for ASP.NET Core.

I'm not using Identity but CookieAuthentication, if that's relevant.

All replies (5)

Friday, December 1, 2017 9:18 PM

My application needs users to be authenticated from the start. There is no public area. The very first thing that the user needs to do is log in. So this should be done right from the start page. It should also be done when the user isn't logged in and another direct URL is accessed, for example from a notification.

I don't like the idea of redirecting the user to another URL and dragging around the return URL as parameter. This adds more network requests and makes ugly and insecure URLs. The server knows exactly whether the user is logged in and can always show the login form on every URL. On successful login, the original URL doesn't have to change, the user is directly server the requested content.

How can I do that?

Simple.  Return a login view if the user is not authenticated.  You'll want to create a custom authorization filter otherwise you'd have to do this in every action.

        public IActionResult Index()
        {
            if(User.Identity.IsAuthenticated)
            {
                return View();
            }
            else
            {
                return View("MustLoginView");
            }
        }

Friday, December 1, 2017 9:27 PM

Can you please show or point me to some sample code? I'm wondering whether the filter will only serve the user the login view or also handle that view's input in the correct controller action. The URL should never change to something like /Account/Login during that process. If an error occurs at login, the error message should also be displayed in the login view again.


Friday, December 1, 2017 9:37 PM

ygoe

Can you please show or point me to some sample code?

I did... see the previous post.

ygoe

I'm wondering whether the filter will only serve the user the login view or also handle that view's input in the correct controller action. The URL should never change to something like /Account/Login during that process.

An action can return any View and the URL will stay as is.  You can test this yourself rather easily just make sure the "SomeOtherView" is located in the Views/Shared folder.

        public IActionResult Index()
        {
            return View("SomeOtherView");
        }

You can also do this with custom middleware.

/en-us/aspnet/core/fundamentals/middleware?tabs=aspnetcore2x

ygoe

If an error occurs at login, the error message should also be displayed in the login view again.

Just make sure the login view explicitly posts to the Login action.

Keep in mind that you will lose the returnUrl functionality.  I would place the current URL in a hidden field so that it posts to the login method.  Also this design might have other challanges. 


Friday, December 8, 2017 7:27 PM

I couldn't find out what this authorization filter is in a reasonable amount of time. Meanwhile I've found and created another solution that hooks into the MVC routing. That means it's not a bare-metal middleware but can still make use of the MVC infrastructure.

Here's the class:

   public class LoginRouter : IRouter
    {
        private readonly IRouter defaultRouter;

        public LoginRouter(IRouter defaultRouter)
        {
            this.defaultRouter = defaultRouter;
        }

        public async Task RouteAsync(RouteContext context)
        {
            // Route unauthenticated users to the login action
            if (!context.HttpContext.User.Claims.Any())
            {
                context.RouteData.Values["controller"] = "Account";
                context.RouteData.Values["action"] = "Login";
                await defaultRouter.RouteAsync(context);
            }
        }

        public VirtualPathData GetVirtualPath(VirtualPathContext context)
        {
            return defaultRouter.GetVirtualPath(context);
        }
    }

And it can be used in Startup.Configure:

           app.UseMvc(routes =>
            {
                routes.Routes.Add(new LoginRouter(routes.DefaultHandler));
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

Tuesday, July 16, 2019 1:01 PM

In my opinion, the easiest way to accomplish this is to create a personalized project controller with the [Authorize] annotation and inherit all controllers from it, eg:

//your new controller

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace MyProject.Controllers {
    [Authorize]
    public class MyController : Controller { }

}

Now, you just need to inherit your controllers from MyController instead of Controller and it'll automatically redirect to login page if not authenticated, eg:

namespace MyProject.Controllers {
    public class HomeController : MyController {
        public IActionResult Index() {

            return View();
        }
}

PS. Don't forget, MyController must inherit from Controller :)