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" />
Related
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
}
}
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);
}
What caching mechanism does OutputCacheAttribute use and how can i extend it?
It uses ASP.NET WebForm caching, and you can extend it by overriding OnResultExecuting.
Refer to:
http://forums.asp.net/t/1703571.aspx/1?Extending+OutputCache+Attribute+in+asp+net+mvc+3
Code extract from above link.
public override void OnResultExecuting(ResultExecutingContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (!filterContext.IsChildAction) {
// we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
using (OutputCachedPage page = new OutputCachedPage(_cacheSettings)) {
page.ProcessRequest(HttpContext.Current);
}
}
}
private sealed class OutputCachedPage : Page {
private OutputCacheParameters _cacheSettings;
public OutputCachedPage(OutputCacheParameters cacheSettings) {
// Tracing requires Page IDs to be unique.
ID = Guid.NewGuid().ToString();
_cacheSettings = cacheSettings;
}
protected override void FrameworkInitialize() {
// when you put the <%# OutputCache %> directive on a page, the generated code calls InitOutputCache() from here
base.FrameworkInitialize();
InitOutputCache(_cacheSettings);
}
}
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'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.