I have an MVC app where I have an Admin area. When user is not logged in I'm redirecting the request to login page with custom AuthorizeAttribute.
public class UserAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var user = AdminGlobals.CurrentUser;
if (user == null || !user.IsActive)
{
return false;
}
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var user = AdminGlobals.CurrentUser;
var context = HttpContext.Current;
//Do not redirect if the request is already redirecting
if (context.Response.IsRequestBeingRedirected) return;
if (user == null || !user.IsActive)
{
context.Response.Redirect("/login", true);
}
else
{
context.Response.Redirect("/unauthorized", true);
}
}
}
Controller and action:
[UserAuthorize]
public ActionResult Index()
{
return View();
}
And in the view I have:
Hello #AdminGlobals.CurrentUser.Title
When the application is in debug mode I'm having NullReferenceExceptions for inside the view and also the layout page since it tries to use same variable #AdminGlobals.CurrentUser which is null. I know I can easily prevent this but I cannot understand why the view is rendered when the request is not authorized. Is it possible to use RedirectToAction inside AuthorizeAttribute to prevent rendering of the view?
I'm not sure that using Response in a filter is the way to go. Typically, you leverage the filterContext to perform your redirect. For example:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var user = AdminGlobals.CurrentUser;
var context = filterContext.HttpContext;
//Do not redirect if the request is already redirecting
if (context.Response.IsRequestBeingRedirected) return;
var routeData = new RouteValueDictionary(new {
controller = "Home",
action = "Unauthorized"
});
if (user == null || !user.IsActive)
{
routeData = new RouteValueDictionary(new { controller = "Home", action = "Login" });
}
filterContext.Result = new RedirectToRouteResult("Default", routeData);
}
I'm assuming from your code that both actions are on the HomeController; otherwise, update the controller name to the correct value.
"Default," in the constructor for the RedirectToRouteResult object, is the name of the route in your RouteConfig that you want to be applied.
Also, use the HttpContext from the filerContext.
Related
i had previously been handling roles/access rights with my own classes and if statements, in which the original URL of user was saved incase he is not even logged in, Once user logs in he is redirected to his original page. Now below is my old code. I am having difficulty to use same logic in my custom authorize attribute. please guide. Thank you
(Old method) Wrapper in every action method
[HttpGet]
public ActionResult Index(string DealType)
{
User user = Session["CurrentUser"] as User;
if (user != null)
{
if (user.IsInRole(RoleType.MASTER) || user.IsInRole(RoleType.VIEW))
{
// Money Shot
List<Deal> deals = dataBase.Deals.Where(d => d.DealType.Equals(DealType)).ToList();
return View(deals);
}
else
{
return PartialView("_unauthorize");
}
}
else
{
// I need to handle this part in custom attribute
return RedirectToAction("Login", "User", new { RedirectURL= string.Format("/{0}/{1}", "Deal", "Index") });
}
}
and in my login action method i used this
public ActionResult Login(User model){
//Code of matching username and password...
//Validations/ exceptions handling of incorrect passwords
if (!string.IsNullOrEmpty(RedirectURL))
{
return Redirect(RedirectURL);
}
else
{
return RedirectToAction("Index", "Home");
}
}
Now since i learned about custom attributes i applied them like below
public class AuthorizeUserAttribute : AuthorizeAttribute
{
public AuthorizeUserAttribute(params RoleType[] roleTypes)
{
AccessLevels = roleTypes;
}
// Custom property
public RoleType[] AccessLevels { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
User user = HttpContext.Current.Session["CurrentUser"] as User;
if (user != null)
{
if (user.IsInRole(AccessLevels))
{
return true;
}
else
{
return false;
}
}
else
{
//redirect URL should be save here but this is boolean method!
return false;
}
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
controller = "User",
action = "Unauthorised"
})
);
}
}
i used them like this
[AuthorizeUser(RoleType.DELETE, RoleType.ADMIN)]
Now issue is that if user has completely not even logged in the URL which he was accessing should be saved and once he logs in he should be redirected to where he came from. Hope i explained it well.
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
RedirectToRouteResult routeData = null;
var returnUrl = string.Empty;
if(filterContext.HttpContext.Request.Url != null)
returnUrl = filterContext.HttpContext.Request.Url.LocalPath;
if (CurrentUser == null)
routeData = new RedirectToRouteResult(
new RouteValueDictionary(new {controller = "Account", action = "Login", returnUrl = returnUrl}));
else
routeData = new RedirectToRouteResult(
new RouteValueDictionary(new {controller = "Error", action = "AccessDenied"}));
filterContext.Result = routeData;
}
In the code above (inside of your custom AuthorizeAttribute) you can capture the return URL using the available request information.
This will make your returnUrl available within the Request.QueryString[] dictionary.
On your Login view you'll need to put something like this to make it actionable.
#{
ViewBag.ReturnUrl = Request.QueryString["returnUrl"];
}
and then in your login form:
#using (Html.BeginForm("Login", "Account", new {returnUrl = ViewBag.ReturnUrl}, FormMethod.Post, new{#class="form-horizontal form-material", #onsubmit="return loading_event();", #id="loginForm"}))
I have a custom auth set up so that a user is stored as a session variable. Once they go through the Account/LogIn process I store the details returned from a 3rd party API as a user in the session like this:
Session["User"] = new UserViewModel(result);
I want to check the user is present before every controller action so I have made a BaseController with the following check in it:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (Session["User"] != null)
base.OnActionExecuting(filterContext);
else
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { action = "LogIn", controller = "Account" }));
Each of the controllers then inherits from the BaseController so that it redirects to the Log In page if there is no user. I don't inherit from the BaseController for the AccountController so that it doesn't get in an infinite loop of checking and redirecting, but I also want to have specific pages not check for log in. Is there any way to do this, i.e. write an exception rule in the same way that you might have [AllowAnonymous]?
You could use a filter on those methods as:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class ActionCheckAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower().Trim();
string actionName = filterContext.ActionDescriptor.ActionName.ToLower().Trim();
// this is just a sample.. you can implement any logic you want
if (!actionName.StartsWith("your method name") && !controllerName.StartsWith("your controller name"))
{
var session1 = HttpContext.Current.User.Identity.Name;
HttpContext ctx = HttpContext.Current;
//Redirects user to login screen if session has timed out
if (session1 == null)
{
base.OnActionExecuting(filterContext);
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new
{
controller = "Account",
action = "LogOff"
}));
}
}
}
}
then on the controllers put the attribute as:
[ActionCheck]
public class MyController : Controller
{
public ActionResult Index()
{
return View();
}
}
or on specific action methods as:
[ActionCheck]
public Actionresult SomeMethod()
{
return View();
}
Put the [Authorize(Roles="admin")] on a view, it works as the user is redirected, however, they are always redirected to the login view despite the fact they are already logged in.
How can I change this so it goes to an error page or something similar?
you can create a custom authorize attribute like this
public class CustomAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
base.HandleUnauthorizedRequest(filterContext);
}
else
{
filterContext.Result = new RedirectToRouteResult(new
RouteValueDictionary(new { controller = "Home", action = "UnAuthorized" }));
}
}
}
and use it like this
[CustomAuthorize(Roles="admin")]
Hope this helps
Instead of the duplicate offered I used code from the question: Prevent FormsAuthenticationModule of intercepting ASP.NET Web API responses and modified accordingly:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeCustom : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
else
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
base.HandleUnauthorizedRequest(filterContext);
}
}
}
Then just created a view for the "/Error/Unauthorized" route and changed the attribute [Authorize] to [AuthorizeCustom]. Now unauthorized people will be redirected to login as expected and people who aren't in roles are redirected to a custom view.
My MVC 3 webapp has different parts which require to check whether the user is a user or an admin, they all get access to the same pages, except some pages have controllers (buttons and textboxes) that only admins can see. I do that check by putting the user's access level into my viewmodel and doing a check:
#if (Model.UserAccess != "Viewer")
{
[do something]
}
I check in my action what access the logged in user has. If the session were to timeout I redirect them to the logon page.
My action is called from a Project page view and loaded as a partial:
#{Html.RenderAction("CategoryList", "Home", new { categoryId = Model.CategoryId });}
And my Controller:
public PartialViewResult CategoryList(int categoryid = 0)
{
var useraccess = GetUseraccess();
[blablabla stuff done here]
var model = new CategoryViewModel()
{
CategoryList = categorylist
UserAccess = useraccess
};
return PartialView(model);
}
public string GetUseraccess()
{
var useraccess = "viewer"; //default
var check = SessionCheck();
if (check == "expired")
{
ModelState.AddModelError("", "Session expired. Please login again.");
Response.Redirect("/Account/LogOn");
}
else if (check == "admin")
{
useraccess = "admin";
}
return useraccess;
}
public string SessionCheck()
{
if (Session["UserAccess"] == null)
{
return "expired";
}
else if ((string)Session["UserAccess"] == "admin")
{
return "admin";
}
else // viewer
{
return "viewer";
}
}
Now that works fine. However I've been trying to implement a custom attribute that would check the session's expiration before the controller is fired:
public class CheckUserAccessSessionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var useraccess = filterContext.HttpContext.Session["UserAccess"];
if ((string)useraccess == null)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Account", action = "LogOn" }));
}
}
}
[CheckUserAccessSession]
public PartialViewResult CategoryList(int categoryid = 0)
{
[same stuff as above here]
}
I get the error
Exception Details: System.InvalidOperationException: Child actions are not allowed to perform redirect actions.
I understand why the error happens. But I haven't found how to go around it. Similarly I'd like to send to my action some data from another custom attribute but that also isn't working because of the RedirectToRouteResult.
Your issue here that is you're returning a PartialViewResult on that action method, and by definition the output of that action method is going to be only a portion of the full request served by IIS. You should be doing permissions checking on the action method that serves the full view in which the partial view is incorporated.
Technically when you're calling Response.Redirect in your initial implementation you're breaking far away from the ASP.NET MVC design conventions; even though it works, it's bad design.
Use this piece of code. It will work.
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
PartialViewResult result = new PartialViewResult();
result.ViewName = "noaccess";
filterContext.Result = result;
}
I'm need to create a aspnet mvc app that has following verification http://domain.com/accounta/controller/view/id, this account has to be checked once in the database is validated and if it should continue in the url, otherwise the customer will be redirected to a page of nonexistent account, the problem I found is that in every controller method I'll have to be validated? There is a more peaceful for it?
ex:
public ActionResult Index()
{
if ((host != null) && (host.IndexOf(".") < 0))
{
sessao = SessionController.GetInstance();
if (sessao.Conta.dsHost != null)
{
return View(sessao.Conta);
}
else
{
using (var contexto = new ThalentoEntities())
{
sessao.Conta = contexto.TH_Conta.Single(q => q.dsHost == host && q.flAtivo == true);
if (sessao.Conta.dsHost != null)
return View(sessao.Conta);
else
return Redirect("/erro/no_account");
}
}
}
else
{
return Redirect("/erro/no_account");
}
return View();
}
abovethe code of each method in controllers..
and bellow code of global.asax
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { host= UrlParameter.Optional, controller = "principal", action = "index", id = UrlParameter.Optional }
);
You can use AuthorizeAttribute. Example:
public class CustomAuthorizeAttrinute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// override standard error result
if (filterContext.Result is HttpUnauthorizedResult)
{
string url = "~/account/logon";
if (filterContext.HttpContext.Request != null)
url += "?rb=" + filterContext.HttpContext.Request.RawUrl;
if (LoginLib.IsLogged())
LoginLib.Logout();
filterContext.Result = new RedirectResult(url);
}
}
}
public class AdminAuthorizeAttribute : CustomAuthorizeAttrinute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return LoginLib.IsLogged<Admin>();
}
}
And then in controller
[AdminAuthorize]
public ActionResult Index()
{
var model = new FooModel();
model.Secret = "This is for admins only!";
return View(model);
}
I'd start with the routing - you should teach the routing engine to recognize the account in the url, here's how:
routes.MapRoute(
"AccountUrl",
"{account_name}/{controller}/{action}/{id}",
new { host= UrlParameter.Optional, account_name = "", controller = "principal", action = "index", id = UrlParameter.Optional }
);
You should add this code before the the "Default" route in your Global.asax.
Then you'll need to figure out a way to execute the account validation logic before each action. You can achieve this with Filters. Here's a reference code for your case:
public class ValidateAccountAttribute: FilterAttribute, IActionFilter {
public void OnActionExecuting(ActionExecutingContext filterContext) {
if (filterContext.RouteData.Values.ContainsKey("account_name") ||
!IsAccountExists((string)filterContext.RouteData.Values["account_name"]))
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new {controller = "account", action = "login"}));
}
private bool IsAccountExists(string accountName) {
// TODO: Implement
throw new NotImplementedException();
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
}
}
It just validates the account_name routing value and redirects to login page if it's null. You can change the redirect url to whatever you need.
This filter can be applied globally (which is probably not what you need), to specific action or whole controller.
Hope that helps.