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 ?
Account/Logingoes to theIndexmethod ofLoginControllerand passes"Account"as to theprojectparameter - user3559349