I'm surprised I haven't come across this before, but I'm attempting to find a way to redirect to the default route post-authentication based on a user's role.
To set up an example, let's say there are two roles, Admin and Tester. An Admin's default route should be admin/index and the AdminController shouldn't be accessible to a Tester. A Tester's default route should be test/index and the TestController shouldn't be accessible to Admin.
I looked into route constraints, but apparently they can only be used to determine whether a route is valid. Then I attempted to try to call RedirectToAction after logging in, but that got a bit messy with return URLs and another reason that made it even more of a no-no which I can't remember at the moment.
I've landed on the following which I've implemented in my BaseController, but it's less than optimal to execute this on every controller action:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.Controller.GetType() == typeof(TestController) &&
User.IsInRole("Admin"))
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Admin", action = "Index" }));
else if (filterContext.Controller.GetType() == typeof(AdminController) &&
User.IsInRole("Tester"))
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Test", action = "Index" }));
}
else
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Error", action = "AccessDenied" }));
}
}
Is there a best practice for handling the default route based on user role?
using Microsoft.AspNet.Identity;
[Authrorize]
public class AdminController : Controller{
/* do your stuff in here. If your View is not actually a big difference from the tester view and you will only Hide some objects from the tester or viceversa, I suggest you use the same View but make a different methods inside the Controller. Actually you don't need to make AdminController and Tester Controller at all. */
}
// you can just do this in one Controller like
[Authorize(Roles="Admin")]
public ActionResult DetailsForAdmin(int id)
{
var myRole = db.MyModelsAccounts.Find(id);
return View("Admin", myRole); //<- Admin returning View
}
[Authorize(Roles="Test")]
public ActionResult DetailsForTester
I think you get the Idea.
Related
I have a custom authorization header that checks to see if the current user has a certain permission before allowing the user to call the controller action.
[HasPermission(Permission.ViewPage]
public ActionResult Index()
{
return View();
}
The HasPermission class inherits from AuthorizeAttribute and overrides OnAuthorization like this:
public override void OnAuthorization(AuthorizationContext context)
{
if (!Permissions.IsUserInPermission(Permission))
{
context.Result = new ViewResult{ ViewName = "Forbidden" };
}
}
This works great with just about everything, except partial views.
When I put the authorization attribute on an action that returns a partial, the Forbidden view is returned, as expected, but it has the full layout. The full layout has all of the other elements on the page like the menu, so it looks like an iframe into another version of the site.
Is there a way to return a partial view when authorization fails on a controller action that returns a partial?
Or am I just doing this the wrong way?
Mark the action that is returning a PartialViewResult as [ChildActionOnly] then you can check for the context.Controller.ControllerContext.IsChildAction property in your filter OnAuthorization method
if (context.Controller.ControllerContext.IsChildAction)
{
context.Result = new PartialViewResult();
}
else
{
context.Result = new ViewResult();
}
In my MVC Application I have my routes defined as follows:
routes.MapRoute(
"Category_default",
"{lang}/Category/{categoryid}/{controller}/{action}/{id}",
new { lang = "en", action = "Index", id = UrlParameter.Optional, categoryid = -1 }
).DataTokens.Add("area", "Category");
routes.MapRoute(
name: "Default",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "home", action = "index", lang = "en", id = UrlParameter.Optional }
);
The application works fine. However, the system administrator just brought to my knowledge that those users who don't have access to the category (in other words, they are logically not associated with it) can also see the data just by switching the categoryid parameter, which is no surprise since I haven't put any check there.
What's the efficient way of checking if the user has privileges over this category or not. In the system I have a User object with User.AllowedCategories List which contains integer values of all the ids the user has access to.
The category area has about 20 controllers (therefore 20 views). Should I put a logic to check on every view? Or I can do it with minimum coding / or can I put this logic globally?
You can achieve that in 3 ways,
Method 1: Global Filters
In FilterConfig.cs:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ValidateUserFilter());
}
In ValidateUserFilter.cs
public class ValidateUserFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
//Controllers to avoid validation
if ((new[] { "<<CONTROLLER1>>", "<<CONTROLLER2>>" }).Any(x => x == filterContext.ActionDescriptor.ControllerDescriptor.ControllerName))
{
return;
}
if (!User.AllowedCategories.Any(x => x == FilterContext.RequestContext.RouteData.Values["id"]))
{
//Redirect user to unauthorized page.
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "Controller", "<<CONTROLLER_NAME>>" }, { "Action", "<<ACTION_NAME>>" } });
}
}
}
Method 2: FilterAttribute
public class ValidateControllerAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (!User.AllowedCategories.Any(x => x == filterContext.RequestContext.RouteData.Values["id"]))
{
//Redirect user to unauthorized page.
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "Controller", "<<CONTROLLER_NAME>>" }, { "Action", "<<ACTION_NAME>>" } });
//OR, You can redirect to 403 response
//throw new HttpException((int)System.Net.HttpStatusCode.Forbidden, "You do not have permission to view this page");
}
}
}
You have to add this attribute in every controller which you want to
validate
Ex:
[ValidateController]
public class MyControllerController : Controller
Method 3: ActionMethodSelectorAttribute
public class ValidateActionAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (!User.AllowedCategories.Any(x => x == controllerContext.RequestContext.RouteData.Values["id"]))
{
//Redirect user to unauthorized page.
controllerContext.HttpContext.Response.Clear();
controllerContext.HttpContext.Response.Redirect("~/<<CONTROLLER_NAME>>/<<ACTION_NAME>>");
return false;
//OR, You can redirect to 403 response
//throw new HttpException((int)System.Net.HttpStatusCode.Forbidden, "You do not have permission to view this page");
/*OR,
controllerContext.HttpContext.Response.StatusCode = 403;
return true;*/
}
}
}
You have to add this attribute in every action which you want to
validate
Ex:
[ValidateAction]
public ActionResult Index()
{
return View();
}
If I got it right, essentially the problem is that, unauthorized user is able to access parts of application, which it should not.
You can use Authorize attribute on your action methods, OR on the Controller class, depending on what areas you want to have authorization. If your design allows, you can also consider creating a base controller and apply Authorize attribute on that, if you do not want repetitions.
Now specify the valid users for these controllers, using Roles OR Users parameters
[Authorize(Roles = "Valid Roles", Users = "Valid Users")]
If the default Authorize attribute doesn't suffice your needs you can always create your own custom attribute for authorization.
So my project requirements changed and now I think I need to build my own action filter.
So, this is my current login controller:
public class LoginController : Controller
{
// GET: Login
public ActionResult Index()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model)
{
string userName = AuthenticateUser(model.UserName, model.Password);
if (!(String.IsNullOrEmpty(userName)))
{
Session["UserName"] = userName;
return View("~/Views/Home/Default.cshtml");
}
else
{
ModelState.AddModelError("", "Invalid Login");
return View("~/Views/Home/Login.cshtml");
}
}
public string AuthenticateUser(string username, string password)
{
if(password.Equals("123")
return "Super"
else
return null;
}
public ActionResult LogOff()
{
Session["UserName"] = null;
//AuthenticationManager.SignOut();
return View("~/Views/Home/Login.cshtml");
}
}
And this is my action filter attempt:
public class AuthorizationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (HttpContext.Current.Session["UserName"] != null)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary{{ "controller", "MainPage" },
{ "action", "Default" }
});
}
base.OnActionExecuting(filterContext);
}
}
I have already added it to FilterConfig, but when I login it does not load Default.cshtml it just keeps looping the action filter. The action result for it looks like this:
//this is located in the MainPage controller
[AuthorizationFilter]
public ActionResult Default()
{
return View("~/Views/Home/Default.cshtml");
}
So, what would I need to add in order to give authorization so only authenticated users can view the applicationĀ“s pages? Should I use Session variables or is there another/better way of doing this using? I am pretty much stuck with AuthenticateUser(), since what happens there now is just a simple comparison like the one we have there now.
Thank you for your time.
Create an AuthorizeAttribute with your logic in there:
public class AuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
{
// Don't check for authorization as AllowAnonymous filter is applied to the action or controller
return;
}
// Check for authorization
if (HttpContext.Current.Session["UserName"] == null)
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
As long as you have the Login URL Configured in your Startup.Auth.cs file, it will handle the redirection to the login page for you. If you create a new MVC project it configures this for you:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(
new CookieAuthenticationOptions {
// YOUR LOGIN PATH
LoginPath = new PathString("/Account/Login")
}
);
}
}
Using this you can decorate your controllers with [AuthorizationFilter] and also [AllowAnonymous] attributes if you want to prevent the authorization from being checked for certain Controllers or Actions.
You might want to check this in different scenarios to ensure it provides tight enough security. ASP.NET MVC provides mechanisms that you can use out of the box for protecting your applications, I'd recommend using those if possible in any situation. I remember someone saying to me, if you're trying to do authentication/security for yourself, you're probably doing it wrong.
Since your attribute is added to the FilterConfig, it will apply to ALL actions. So when you navigate to your MainPage/Default action it will be applying the filter and redirecting you to your MainPage/Default action (and so on...).
You will either need to:
remove it from the FilterConfig and apply it to the appropriate actions / controllers
or add an extra check in the filter so that it doesn't redirect on certain routes
I am working on an ASP.NET MVC application. For some reason, everytime I think I understand routing, something pops up that I don't understand. Currently, I have two routes that I can't seem to figure out. My directory structure looks like the following
- Views
- Internal
- Profile
- Index.cshtml
- Input
- Page1.cshtml
In my global.asax.cs file, I have added the following mappings:
routes.MapRoute(
"UserProfileInfo",
"{controller}/profile",
new { controller = "Internal", action = "UserProfileInfo" }
);
routes.MapRoute(
"Page1",
"{controller}/input/page1",
new { controller = "Internal", action = "Page1" }
);
In MyController, I have the following:
public ActionResult UserProfileInfo()
{
return View("~/Views/internal/profile/Index.cshtml");
}
public ActionResult Page1()
{
return View("~/Views/internal/input/Page1.cshtml");
}
I want to store my actions in a single controller. I thought I had everything setup properly. But I continue to get a 404. What am I doing wrong?
Remove the "Controller" suffix from the controller name in your calls to MapRoute to create a mapping to a class called InternalController. The Controller suffix is appended by the framework when looking for a matching implementation. e.g.:
routes.MapRoute(
"UserProfileInfo",
"{controller}/profile",
new { controller = "Internal", action = "UserProfileInfo" }
);
I have multiple controllers with different actions (no "Index" actions). The actions I consider "default" actions, are named differently. I want to create default routes for their names and have the first available action (from my list of default actions) executed if only the controller name is provided in the route.
So, for example, I have the following actions which I want to consider default and want checked for their existence in a controller in this order:
List()
Draw()
ViewSingle()
The routing should somehow search for /{controller} and then take the first available action from the list above as default action, e.g.:
/ControllerA -> ControllerA.List()
/ControllerB -> ControllerB.Draw()
/ControllerC -> ControllerC.ViewSingle()
/ControllerD -> ControllerD.Draw()
/ControllerE -> ControllerE.List()
Is this possible? I tried creating additional Default actions like this but couldn't get it to work:
routes.MapRoute("Default1", "{controller}/{action}",
new { controller = UrlParameter.Optional, action = "List" }
routes.MapRoute("Default2", "{controller}/{action}",
new { controller = UrlParameter.Optional, action = "Draw" }
routes.MapRoute("Default3", "{controller}/{action}",
new { controller = UrlParameter.Optional, action = "ViewSingle" }
Help?
I think you got something wrong about the default route. Those controller and action parameters are there for convention. If URL has a controller name in it, it will route to suitable controller. Same thing is true for Index methods. It just provides a default value for that.They are already optional and that's why. I think you don't need routes here: You could try to place your default functions in Index methods and it would work.Just to be clear:
public class ControllerA:Controller
{
public ActionResult Index()
{
return List();
}
public ActionResult List()
{
//List function
}
}
public class ControllerB:Controller
{
public ActionResult Index()
{
return Draw();
}
public ActionResult Draw()
{
//Draw function
}
}
I think it would work. But if you want to go on with creating routes, you create each route like this:
routes.MapRoute("Default1", "ControllerA",
new { controller = ControllerA, action = "List" }
Notice that ControllerA is not in curly braces,it's a static text. So you create routes for each controller. Keep controller naming convention in mind.
I agree with Narsil. You should write a constant to url. -like controller name- 'cause MVC cannot resolve this routes and cannot figure out what you want to show.
eg.
routes.MapRoute(null, "ControllerA/{action}",
new { controller = "ControllerA", action = "List" }
routes.MapRoute(null, "ControllerB/{action}",
new { controller = "ControllerB", action = "Draw" }
routes.MapRoute(null, "ControllerC/{action}",
new { controller = "ControllerC", action = "ViewSingle" }