Question: Is OnAuthorization called for every request? If not, where can I check for session timeout before Authorize attributes are called?
Background:
I need a way to determine if session exists and has not timed out. I want to do this on every method call for every controller in my app. I need to do this before authorization filters are called because I keep a hashset of permissions in session and my authorize attribute looks at these permissions. I need to do this for every request, regardless of whether an authorize attribute is applied or not.
A couple answers I've read (one cited below) state to override OnActionExecuting in a base controller. This makes sense however I've found OnActionExecuting is not calleed until after AuthorizeCore in the filter attribute is called.
The approach I've arrived at thus far is to check for session in a base controller and check for permission in an authorize attribute.
BaseController.cs:
protected override void OnAuthorization(AuthorizationContext filterContext)
{
// This base does two things:
// 1.) Ensures that Session exists
// 2.) Ensures that the Security object exists and is initalized.
HttpContextBase httpContext = filterContext.HttpContext;
// Check if session exists
if (httpContext.Session == null)
filterContext.Result = Redirect(core.SecurityConstants.SessionTimeoutRedirectURL);
else
{
if(!Security.IsInitialized())
filterContext.Result = Redirect(core.SecurityConstants.PermissionDeniedRedirectURL);
else if (httpContext.Session.IsNewSession) // check if a new session id was generated
{
// If it says it is a new session, but an existing cookie exists, then it must have timed out
string sessionCookie = httpContext.Request.Headers["Cookie"];
if ((null != sessionCookie) && (sessionCookie.IndexOf("ASP.NET_SessionId") >= 0))
{
if (httpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
httpContext.Response.End();
}
filterContext.Result = Redirect(core.SecurityConstants.SessionTimeoutRedirectURL);
}
}
}
base.OnAuthorization(filterContext);
}
SecurityAttribute.cs:
public class SecurityAttribute : AuthorizeAttribute
{
public Permission Permission { get; set; }
public SecurityAttribute(Permission permission)
{
Permission = permission;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
base.AuthorizeCore(httpContext);
return Security.HasPermission(Permission);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
base.HandleUnauthorizedRequest(filterContext);
//if (filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
//{
filterContext.RequestContext.HttpContext.Response.Redirect(SecurityConstants.PermissionDeniedRedirectURL);
//}
}
}
References
When OnAuthorization method is called?
With ASP.NET MVC redirect to login page when session expires
Maybe following works for you, add it in Global.asax
protected void Application_AcquireRequestState()
{
if (Context.Session!=null && Context.Session.IsNewSession)
{
//do something
}
}
Related
I have register a global filter to authorize requests that need a cookie but I have a controller that needs to be public so I add [AllowAnonymous] attribute to the controller methods but my filter still fires and keeps redirecting. I'm not sure the best way to fix this issue.
Do I need to modify my onauthorization method to look for the [AllowAnonymous] attribute?
public class CookieAuthFilter : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies.Get("token");
if (cookie == null)
{
filterContext.Result = new RedirectResult("/Home/Index");
}
}
}
Do I need to modify my onauthorization method to look for the [AllowAnonymous] attribute?
You could, but it would be simpler just to move your logic so the base OnAuthorize method (which contains the logic to scan for [AllowAnonymous]) is unmodified.
public class CookieAuthFilter : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return httpContext.Request.Cookies.Get("token") != null;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectResult("/Home/Index");
}
}
It is better to use AuthorizeCore to return true if the user is authorized, and use HandleUnauthorizedRequest for the redirect.
OnAuthorization also contains some additional logic to help it deal with output caching that you should leave in place.
I have initialized method in my base controller class which is called whenever any action method is executed. On every action method, I want to check my session and if it is null it should redirect to the login page.
public class BaseController : Controller
{
protected IDataRepository _appData = new DataRepository();
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
if (SessionFactory.CurrentAdminUser == null)
{
RedirectToLogin();
}
}
}
public ActionResult RedirectToLogin()
{
return RedirectToAction("AdminLogin", "Admin");
}
it's calling this method but not redirecting to admin login method and keeps execution on and call method which is in flow so error will come.
In short i want to check whenever my application session gets null its should rediect to login page and its not convenient to check on all methods.please suggest me some good way.
You're calling RedirectToLogin() which in turn simply returns a RedirectToActionResult that is not being used and does not affect the flow of the action.
Try this instead:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting();
if (SessionFactory.CurrentAdminUser == null)
filterContext.Result = new RedirectResult(Url.Action("AdminLogin", "Admin"));
}
Alternatively, if you insist on overriding Initialize:
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
if (SessionFactory.CurrentAdminUser == null)
{
requestContext.HttpContext.Response.Clear();
requestContext.HttpContext.Response.Redirect(Url.Action("AdminLogin", "Admin"));
requestContext.HttpContext.Response.End();
}
}
Also, check the [Authorize] filter, it may better suit your needs. See here.
A simpler approach:
public void RedirectToLogin()
{
RedirectToAction("AdminLogin", "Admin").ExecuteResult(this.ControllerContext);
}
I'm currently writing an Admin MVC 3 site, and each user only has access to certain parts of the site.
The areas of my site are the same as the user Roles, so what I would like to do is the put the AuthorizeAttribute on each area, using the area's name as the parameter in the Role.
So far I've got this to work when I'm hard coding the checking of each area, but I would like to just loop through all areas and apply the Authorize filter.
(i'm using this as my custom FilterProvider - http://www.dotnetcurry.com/ShowArticle.aspx?ID=578)
My code so far ("Gcm" is one of my areas, and is also a Role) :
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
// for all controllers, run AdminAuthorizeAttribute to make sure they're at least logged in
filters.Add(ObjectFactory.GetInstance<AdminAuthorizeAttribute>());
AdminAuthorizeAttribute gcmAuthroizeAttribute = ObjectFactory.GetInstance<AdminAuthorizeAttribute>();
gcmAuthroizeAttribute.Roles = "Gcm";
var provider = new FilterProvider();
provider.Add(
x =>
x.RouteData.DataTokens["area"] != null && x.RouteData.DataTokens["area"].ToString() == "Gcm"
? gcmAuthroizeAttribute
: null);
FilterProviders.Providers.Add(provider);
}
Does anyone know how to get all the areas of my application, so I can just loop through them, rather than hard coding each area?
Or if anyone has a better idea of how to Authorize per area, that would be appreciated to.
Thanks for your help
Saan
You could you make a base controller for each area, and put the authorize attribute over the base class. That way you can pass the area parameter in for each area's base controller.
Here is an example of a Authorize Attribute override i have created. I needed my authorize function to support to types of member ship so you might not want to get too into the inner workings of the functions, but AuthorizeCore is where the main logic occures. In my case i am checking it against a entity datacontext.
Usage:
[AjaxAuthorize(AjaxRole = "Administrators")]
public JsonResult SaveAdministrativeUser(v.... )
Code:
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
private class HttpAuthorizeFailedResult : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 403. Membership.Provider.Name == "UnitArchiveMembershipProvider"
context.HttpContext.Response.StatusCode = context.HttpContext. User.Identity is WindowsIdentity ? 401 : 403;
}
}
public string AjaxRole { get; set;}
public AjaxAuthorizeAttribute()
{
AjaxRole = "Users";
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (string.IsNullOrEmpty(MvcApplication.Config.DBSettings.Database))
{
return true;
}
//When authorize parameter is set to false, not authorization should be performed.
UnitArchiveData db = DataContextFactory.GetWebRequestScopedDataContext<UnitArchiveData>(MvcApplication.Config.DBSettings.GetConnectionString());
if (httpContext.User.Identity.IsAuthenticated)
{
login_data user = db.login_datas.Where(n => n.EmailAddress == httpContext.User.Identity.Name).FirstOrDefault();
if (user != null)
{
return user.cd_login_role.RoleName == "Administrators" || user.cd_login_role.RoleName == AjaxRole;
}
}
return false;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 403 error.
filterContext.Result = new HttpAuthorizeFailedResult();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
When I was investigating a separate issue, I came across How to pass parameters to a custom ActionFilter in ASP.NET MVC 2?
That attribute example can be altered to check for the current Controller's area.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
RouteData routeData = filterContext.RouteData;
// check if user is allowed on this page
if (SessionFactory.GetSession().Contains(SessionKey.User))
{
User user = (User)SessionFactory.GetSession().Get(SessionKey.User);
string thisArea = routeData.DataTokens["area"].ToString();
// if the user doesn't have access to this area
if (!user.IsInRole(thisArea))
{
HandleUnauthorizedRequest(filterContext);
}
}
// do normal OnAuthorization checks too
base.OnAuthorization(filterContext);
}
}
I then apply my custom authorize attribute to all controllers like this in Global.asax:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
// for all controllers, run CustomAuthorizeAttribute to make sure they're at logged in and have access to area
filters.Add(ObjectFactory.GetInstance<CustomAuthorizeAttribute>());
}
Thanks for all who replied
Saan
I want to use TempData to store messages between Post and followed redirect but TempData are always empty.
I have BaseContoller offering some infrastructure for passing TempData. Simplified code looks like:
public abstract class BaseController : Controller
{
public const string AuditMessagesKey = "AuditMessages";
private List<InformationMessage> _informationMessages = new List<InformationMessage>();
protected BaseController()
{
// I also tried this in overriden Initialize
ViewData[AuditMessagesKey] = GetAuditMessages();
}
protected void AddAuditMessage(InformationMessage message)
{
if (message == null)
return;
_informationMessages.Add(message);
}
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
base.OnResultExecuting(filterContext);
if (filterContext.Result is RedirectToRouteResult)
{
// I see that messages are stored into TempData
TempData[AuditMessagesKey] = _informationMessages;
// This also doesn't help
// TempData.Keep(AuditMessagesKey);
}
}
private ICollection<InformationMessage> GetAuditMessages()
{
// TempData are always empty here
var messages = TempData[AuditMessagesKey] as List<InformationMessage>;
if (messages == null)
{
messages = new List<InformationMessage>();
}
return messages;
}
}
Action method looks like:
[HttpPost]
public ActionResult CancelEdit(RequestSaveModel model)
{
AddAuditMessage(new InformationMessage
{
Message = String.Format(Messages.RequestEditationCanceled, model.Title),
Severity = MessageSeverity.Information
});
return RedirectToAction("Detail", new { Id = model.Id});
}
Application is tested on VS Development web server. There are no Ajax calls and I removed all Html.RenderAction calls from my master page. I can see that TempData are accessed only once per request in GetAuditedMessages and stored only once in OnResultExecuting. Nothing overwrites the data. Session state is allowed.
The code is little bit simplified. We are also using antiforgery token, custom filters for authorization and for action selection but it should not affect TempData behavior.
I don't understand it. I used TempData before in test application and it worked fine.
The problem I see in your code is that you are trying to retrieve the data from TempData in the controller's constructor - which is before it is available.
Move the call to GetAuditMessages() into an OnActionExecuting method, and it will be accessible.
public abstract class BaseController : Controller
{
public const string AuditMessagesKey = "AuditMessages";
private List<InformationMessage> _informationMessages = new List<InformationMessage>();
protected BaseController()
{
// TempData is not available yet
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
ViewData[AuditMessagesKey] = GetAuditMessages();
base.OnActionExecuting(filterContext);
}
protected void AddAuditMessage(InformationMessage message)
{
if (message == null)
return;
_informationMessages.Add(message);
}
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
base.OnResultExecuting(filterContext);
if (filterContext.Result is RedirectToRouteResult)
{
// I see that messages are stored into TempData
TempData[AuditMessagesKey] = _informationMessages;
// This also doesn't help
// TempData.Keep(AuditMessagesKey);
}
}
private ICollection<InformationMessage> GetAuditMessages()
{
var messages = TempData[AuditMessagesKey] as List<InformationMessage>;
if (messages == null)
{
messages = new List<InformationMessage>();
}
return messages;
}
}
I think this is what's happening:
In CancelEdit, your RedirectToAction is returned, and the framework redirects to "Detail". In your Detail method, the ActionExecuting fires, but it's filterContext.Result is not your RedirectToAction result - it's a new result (actually, no result as of yet).
Do you need the check for "filterContext.Result is RedirectToRouteResult"? It seems that you will only have those messages added before you perform a redirect.
in my Solution I forgot to remove HttpCookies for my development
it just work on published site in Https
<httpCookies httpOnlyCookies="true" requireSSL="true" />
I'd like [Authorize] to redirect to loginUrl unless I'm also using a role, such as [Authorize (Roles="Admin")]. In that case, I want to simply display a page saying the user isn't authorized.
What should I do?
Here is the code from my modified implementation of AuthorizeAttribute; I named it SecurityAttribute. The only thing that I have changed is the OnAuthorization method, and I added an additional string property for the Url to redirect to an Unauthorized page:
// Set default Unauthorized Page Url here
private string _notifyUrl = "/Error/Unauthorized";
public string NotifyUrl {
get { return _notifyUrl; } set { _notifyUrl = value; }
}
public override void OnAuthorization(AuthorizationContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext)) {
HttpCachePolicyBase cachePolicy =
filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null);
}
/// This code added to support custom Unauthorized pages.
else if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (NotifyUrl != null)
filterContext.Result = new RedirectResult(NotifyUrl);
else
// Redirect to Login page.
HandleUnauthorizedRequest(filterContext);
}
/// End of additional code
else
{
// Redirect to Login page.
HandleUnauthorizedRequest(filterContext);
}
}
You call it the same way as the original AuthorizeAttribute, except that there is an additional property to override the Unauthorized Page Url:
// Use custom Unauthorized page:
[Security (Roles="Admin, User", NotifyUrl="/UnauthorizedPage")]
// Use default Unauthorized page:
[Security (Roles="Admin, User")]
Extend the AuthorizeAttribute class and override HandleUnauthorizedRequest
public class RoleAuthorizeAttribute : AuthorizeAttribute
{
private string redirectUrl = "";
public RoleAuthorizeAttribute() : base()
{
}
public RoleAuthorizeAttribute(string redirectUrl) : base()
{
this.redirectUrl = redirectUrl;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
string authUrl = this.redirectUrl; //passed from attribute
//if null, get it from config
if (String.IsNullOrEmpty(authUrl))
authUrl = System.Web.Configuration.WebConfigurationManager.AppSettings["RolesAuthRedirectUrl"];
if (!String.IsNullOrEmpty(authUrl))
filterContext.HttpContext.Response.Redirect(authUrl);
}
//else do normal process
base.HandleUnauthorizedRequest(filterContext);
}
}
Usage
[RoleAuthorize(Roles = "Admin, Editor")]
public class AccountController : Controller
{
}
And make sure you add your AppSettings entry in the config
<appSettings>
<add key="RolesAuthRedirectUrl" value="http://mysite/myauthorizedpage" />
</appSettings>
The easiest way I've found is to extend and customize the AuthorizeAttribute so that it does something different (i.e., not set an HttpUnauthorizedResult) when the Role check fails. I've written an article about this on my blog that you might find useful. The article describes much what you are wanting, though it goes further and allows the user who "owns" the data to also have access to the action. I think it should be fairly easy to modify for your purposes -- you'd just need to remove the "or owner" part.