0
votes

I'm trying to get this routing logic wired into this ASP MVC application of mine.

To make security easy, I want to make it that all URLs that get passed around have the project id before the controller name.

The requirements are as such:

  • Check if the URL contains a project ID
  • If so, check if the user has access to that project.
  • If any of the above requirements aren't met, redirect user to website.com/Account/Login

An example of an URL:

http://localhost:36923/112233/DataTableRows?id=P8dFd9o8DEitJpak6lGAbA

If the user does have access to the project ID 112233, the correct controller/action is called. If not, the action filter below correctly redirects the user to an error page. This, so far, works.

The issue is when the project ID is missing or whether the user does not have access to the specified project. The redirect to Account/Login makes a 404 error.

Here's a custom route I've created:

public sealed class ProjectAttributeRoute : Route
{
    public ProjectAttributeRoute(string url)
        : base(url, new MvcRouteHandler())
    {

    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var r = base.GetRouteData(httpContext);

        //No project id provided so redirect to login page.
        if (r == null)
        {
            r = new RouteData(this, this.RouteHandler);
            r.Values.Add("controller", "Account");
            r.Values.Add("action", "Login");
        }

        return r;
    }

}

My route config:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapCustomRoutes("ProjectRoute", "
        {project}/{controller}/{action}",
        defaults: new { action = "Index" }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Portal", action = "Index", id = 
        UrlParameter.Optional }
    );


    routes.MapRoute(
        name: "LoginRoute",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Account", action = "Login", id = UrlParameter.Optional }
        );

}

public static class RouteConfigExtensions
{
    public static void MapCustomRoutes(this RouteCollection routes, string name, string url, object defaults = null, object constraints = null)
    {
        routes.Add(name, new ProjectAttributeRoute(url)
        {
            Defaults = new RouteValueDictionary(defaults),
            Constraints = new RouteValueDictionary(constraints),
            DataTokens = new RouteValueDictionary()
        });
    }
}

The action filter that actually verifies whether the user has access to the project passed in the URL:

public class ProjectAccessFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

        var controller = filterContext.Controller as BaseApiController;

        //if no project provided in URL. TODO: Make an exception of /Account/Login
        if (!filterContext.RequestContext.RouteData.Values.ContainsKey("project"))
        {
            UrlHelper urlHelper = new UrlHelper(filterContext.RequestContext);
            filterContext.HttpContext.GetOwinContext().Authentication.SignOut();
            filterContext.Result = new RedirectResult(urlHelper.Action("Login", "Account"));
            return;
        }

        var urlProjectId = filterContext.RequestContext.RouteData.Values["project"].ToString();
        List<ProjectModel> userProjects = new List<ProjectModel>();

        //the data layer which is called in GetProjectsFromCurrentUser is async, so I have to do this...
        var t = Task.Run(async () =>
        {
            return (await controller.GetProjectsFromCurrentUser()).ToList();
        });

        userProjects = t.Result;

        if (!userProjects.Any(x => x.Id == urlProjectId))
        {
            UnhandledExceptionViewModel viewModel = new UnhandledExceptionViewModel();

            viewModel.ExceptionMessage = $"User has no access to project '{urlProjectId}'";
            var result = new ViewResult
            {
                ViewData = new ViewDataDictionary(viewModel),
                ViewName = "Error"

            };

            filterContext.Result = result;

        }

    }
}

Any obvious misconfiguration of routes ?

2
All 3 route definitions mean that if the route contains 2 or 3 segments it will match the 1st route so Account/Login goes to the Index method of LoginController and passes "Account" as to the project parameter - user3559349

2 Answers

0
votes

Please check in the Global.asax.cs file Application_Start() method your custom route filtering should get registered

 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
 RouteConfig.RegisterRoutes(RouteTable.Routes);
0
votes

I ended up using this route configuration:

routes.MapRoute(
    name: "NoProjectRoute",
    url: "{controller}/{action}",
    defaults: new { action = "Index"  },
    constraints: new { controller = new NoProjectRouteConstraint() }
);


routes.MapCustomRoutes("ProjectRoute", 
    "{project}/{controller}/{action}",
    defaults: new { action = "Index" }
);

routes.MapRoute(
    name: "Default",
    url: "{project}/{controller}/{action}",
    defaults: new { controller = "Portal", action = "Index" }
);

With a custom contraint:

public class NoProjectRouteConstraint : IRouteConstraint
{
    //all the controllers that are exempt from having project id in URL...
    public const string c_ExemptControllers = "account|filedownload|restful|ping|public";


    private readonly string _controller;
    private readonly string _action;


    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        var m = c_ExemptControllers.Split('|').ToList();

        var controllerName = (values.ContainsKey("controller")) ? values["controller"].ToString() : "";
        var actionName = (values.ContainsKey("action")) ? values["action"].ToString() : "";
        var result = false;

        if (controllerName == string.Empty || actionName == string.Empty)
            return false;

        if (c_ExemptControllers.Contains(controllerName.ToLower()))
        {
            result = true;
        }


        return result;
    }
}