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.
Related
I have an MVC app which has the following route config
In Global.ascx
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
In the RouteConfig.cs I have
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Now if i type in the browser , https://localhost/users this will take me to the
UsersController and call the Index() ActionResult. In there i do a check to see if the
user has access to the view or not as follows:
public ActionResult Index()
{
if (<User has access condition check>)
{
return View();
}
return View("~/Views/PermissionError.cshtml");
}
The issue is that I have about 30 pages in my app that the user can browse to by typing in the broswer url.
So instead of doing the check in every Index ActionResult , is there a way i can do the check in my route config or somewhere else that does the permission check and if they are allowed to view the page it will continue to the page
else it will show the error page ?
is there a way i can do the check in my route config or somewhere else that does the permission check
Yes, that might write a customer AuthorizeAttribute to make it.
You can try to write a customer AuthorizeAttribute and register that to global filter setting.
Here is the sample code which you can edit by your real logic.
public class AuthorizeBrowsingAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var isAnonAllowed = filterContext.ActionDescriptor.IsDefined(
typeof(AllowAnonymousAttribute), true) ||
filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(
typeof(AllowAnonymousAttribute), true);
// user did't get
if (!<User has access condition check> && !isAnonAllowed)
{
filterContext.Result = new RedirectResult("~/Views/PermissionError.cshtml");
}
}
}
The code register our customer AuthorizeAttribute
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AuthorizeBrowsingAttribute());
}
If there are some page which you don't want to do permission check you can add AllowAnonymous attribtue on the view method.
[AllowAnonymous]
public ActionResult NoPermission()
{
}
I am learning ASP.Net MVC 5 and I came up to a case where I need to restrict access to controller action under some situations. Suppose I have 5 actions in my controller and I want to restrict two of them in certain scenarios.How to achieve this I know we have inbuilt attributes like [Authorize]. Can I create user-defined restrictions to the controller actions.
Something like:
[SomeRule]
public ActionResult Index()
{
return View();
}
And if I could create a function or class named "SomeRule" and then add some rules there.Can I add a function/method/class where I can add some logic and restrict the access and redirect to a genreal page if condition does not match. I am a beginner please guide me.
What you'd want to do is create a custom Action Filter, which would allow you to define custom logic within your action to determine if a given user could / could not access the decorated action:
public class SomeRuleAttribute : System.Web.Mvc.ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// Define some condition to check here
if (condition)
{
// Redirect the user accordingly
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Account" }, { "action", "LogOn" } });
}
}
}
You can also extend these even further and set properties on them as well if you need to apply some values to check against where the attribute is defined:
public class SomeRule: ActionFilterAttribute
{
// Any public properties here can be set within the declaration of the filter
public string YourProperty { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// Define some condition to check here
if (condition && YourProperty == "some value")
{
// Redirect the user accordingly
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Account" }, { "action", "LogOn" } });
}
}
}
This would look like the following when used:
[SomeRule(YourProperty = "some value")]
public ActionResult YourControllerAction()
{
// Code omitted for brevity
}
I have a class called PasswordChangeChecker.csthat has a method that returns from the database whether or not a user has changed their password. The signature for that method is:
public bool IsPasswordChangedFromInitial(string IdOfUser)
where IdOfUser is the Id field from the Identity framework User. If it returns true, that means the change password page should not be displayed, otherwise, it should navigate to the change password form. Once the user successfully changes their password, the database flag gets set appropriately, and they should not be prompted for a password change again (unless manually forced to by an admin). How can I put this method in the RouteConfig.cs file, where currently I have:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace IdentityDevelopment
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
How can I add a conditional construct into the defaults parameter so I can use the IsPasswordChangedFromInitial method to decide whether or not to go to the password change page? That page is at /Account/ChangePassword.
EDIT
As per the comments, the appropriate action methods for my specific need are (I have omitted irrelevant code):
Login post action:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel details, string returnUrl)
{
if (ModelState.IsValid)
{
AppUser user = await UserManager.FindAsync(details.Name,
details.Password);
if (user == null)
{
AppUser userByName = await UserManager.FindByNameAsync(details.Name);
if(userByName == null)
{
ModelState.AddModelError("", "Invalid username.");
}
else
{
//If this else is reached, it means the password entered is invalid.
//code for incrementing count of failed attempts and navigation to lock out page if needed
}
}
else
{
if(user.LockedOut)
{
//navigate to locked out page
}
else
{
PasswordChangeChecker PassCheck = new PasswordChangeChecker();
string userId = user.Id.ToString();
bool proceed = PassCheck.IsPasswordChangedFromInitial(userId);
//Authorize the user
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(LocationClaimsProvider.GetClaims(ident));
ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties
{
IsPersistent = false
}, ident);
//persist login into db code
if (proceed)
{
//reset failed logins count
return Redirect(returnUrl);
}
else
{
return ChangePassword();
}
}
}
}
ViewBag.returnUrl = returnUrl;
return View(details);
}
ChangePassword() get action:
[HttpGet]
[Authorize]
public ActionResult ChangePassword()
{
return View();
}
Somehow the view returned is the view in the RouteConfig.cs instead of the ChangePassword.cshtml page.
Thank you.
I would do it with global action filters
you can make a action filter with method
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (showChagePwPage)
{
//redirect to the change password page
filterContext.Result = new RedirectToActionResult("ChangePassword", "Account");
}
base.OnActionExecuting(filterContext);
}
and then adding it into global action filter by
GlobalFilters.Filters.Add(yourFilterContext);
After several days of this hellish exercise similar to yours, where I was trying to route users at login, I realized that I wasn't going to be able to get the value of the UserId while in the login of the Account controller. I did some experimenting and came up with this approach that solved my problem.
I create an ActionResult in my Home controller and called it Purgatory (of course I renamed it to something more suitable once it proved functional). There, I stuffed all my login logic for routing the logged-in user to their respective page upon login.
Then, in the RedirectToLocal in Account controller, I changed the
return RedirectToAction("Index", "Home");
to
return RedirectToAction("Purgatory", "Home");
So now when a user logs in, if the returnTo param isn't set to a particular page, the returnTo param will be null and when it gets to the RedirectToLocal, it'll drop to what used to be the redirect to the home page, which will now go into purgatory.
This sounds like a good time to use an action filter, which you can apply globally or per controller/action.
Here's a simple example:
using System;
using System.Web.Mvc;
using System.Web.Routing;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class VerifyPasswordChangedAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if(!filterContext.ActionDescriptor.ActionName.Equals("changepassword", StringComparison.OrdinalIgnoreCase))
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
var userName = filterContext.HttpContext.User.Identity.Name;
PasswordChangeChecker PassCheck = new PasswordChangeChecker();
if (!PassCheck.IsPasswordChangedFromInitial(userName))
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "account", action = "changepassword", area = string.Empty }));
}
}
}
base.OnActionExecuting(filterContext);
}
}
I would modify your IsPasswordChangedFromInitial method to simply use the authenticated user's username, rather than trying to figure out how to get access to a UserManager instance in an action filter. Otherwise, assuming you're using the OWIN-based ASP.NET Identity, add a claim to store that user.Id field when you create your ClaimsIdentity, so that you don't have to keep looking it up.
The outermost conditional handles the case of this being a global filter - without it, you would get an infinite redirect.
UPDATE
Not a duplicate as per above as that question routes to an explicit view, not a controller/action
I am using a custom authorisation (at controller level) to ensure that a user can only access specific functions of an application (held in an external access control system).
Below is the AuthorizeAttribute class
public class MyCustomAuth : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
string ctrl = (string)filterContext.RouteData.Values["controller"];
bool isAuth = GetAuthorizedFunctions(HttpContext.Current.User).Any(f => f.Controller.Equals(ctrl, StringComparison.InvariantCultureIgnoreCase));
if (!isAuth)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Contact", action = "Index", AuthMsg = "Sorry, unauthorized" }));
}
}
}
Bascially, if the user requests a function that they are not authorised to access, then I redirect then to the "Contact" page to show a suitable message.
However, in the above, the AuthMessage is encoded into the URL string..
http://localhost/HotelRequests/Contact?AuthMsg=Sorry%2C%20unauthorized
How can I pass this message without it being shown in the URL, preferable as a ViewModel required for the Contact page.
TempData has more life than ViewBag or ViewData
filterContext.Controller.TempData["AuthMsg "] = "Sorry, unauthorized";
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Contact", action = "Index" }));
Read it in Contact's Index method as..
ViewBag.Message = TempData["AuthMsg "].ToString();
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.