I have read numerous posts where people have had similar issues but have not found a working solution. I have a MVC 4 site, I do not want to remove caching from the entire website as I want to cache the pages. When the user clicks the logoff button it successfully logs off and redirects to the login page, however when the user clicks the back button it shows a previously viewed "restricted page" which you should only be able to see if logged in. I understand that this is because the browser has cached the page client side. I have tried a number of solutions and as mentioned earlier none of them work. Currently my logoff has the following code:
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
Session.Abandon();
Session.Clear();
// clear authentication cookie
HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
cookie1.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(cookie1);
// clear session cookie (not necessary for your current problem but i would recommend you do it anyway)
HttpCookie cookie2 = new HttpCookie("ASP.NET_SessionId", "");
cookie2.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(cookie2);
// Invalidate the Cache on the Client Side
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(-1));
Response.Cache.SetNoStore();
Response.AppendHeader("Pragma", "no-cache");
// send an expired cookie back to the browser
var ticketExpiration = DateTime.Now.AddDays(-7);
var ticket = new FormsAuthenticationTicket(
1,
// replace with username if this is the wrong cookie name
FormsAuthentication.FormsCookieName,
DateTime.Now,
ticketExpiration,
false,
String.Empty);
var cookie = new System.Web.HttpCookie("user")
{
Expires = ticketExpiration,
Value = FormsAuthentication.Encrypt(ticket),
HttpOnly = true
};
Response.Cookies.Add(cookie);
return RedirectToAction("Login", "Account");
}
If you want to apply the "no cache on browser back" behavior on all pages then you should put the following it in global.asax.
protected void Application_BeginRequest()
{
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.UtcNow.AddHours(-1));
Response.Cache.SetNoStore();
}
hope it helps someone !
You could use the hash change event on the browser window, to trigger an ajax request on postback, this would obviously fail as your logged out. From there you could trigger the browser to do anything you like.
Add below lines of code to Global.asax.cs file.
protected void Application_BeginRequest()
{
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.UtcNow.AddHours(-1));
Response.Cache.SetNoStore();
}
Response.ClearHeaders();
Response.AddHeader("Cache-Control", "no-cache, no-store, max-age=0,
must-revalidate");
Response.AddHeader("Pragma", "no-cache");
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Buffer = true;
Response.ExpiresAbsolute = DateTime.Now.AddDays(-1);
Response.Expires = 0;
Related
I am attempting to write a 301 redirect scheme using a custom route (class that derives from RouteBase) similar to Handling legacy url's for any level in MVC. I was peeking into the HttpResponse class at the RedirectPermanent() method using reflector and noticed that the code both sets the status and outputs a simple HTML page.
this.StatusCode = permanent ? 0x12d : 0x12e;
this.RedirectLocation = url;
if (UriUtil.IsSafeScheme(url))
{
url = HttpUtility.HtmlAttributeEncode(url);
}
else
{
url = HttpUtility.HtmlAttributeEncode(HttpUtility.UrlEncode(url));
}
this.Write("<html><head><title>Object moved</title></head><body>\r\n");
this.Write("<h2>Object moved to here.</h2>\r\n");
this.Write("</body></html>\r\n");
This is desirable, as in my experience not all browsers are configured to follow 301 redirects (although the search engines do). So it makes sense to also give the user a link to the page in case the browser doesn't go there automatically.
What I would like to do is take this to the next level - I want to output the result of an MVC view (along with its themed layout page) instead of having hard coded ugly generic HTML in the response. Something like:
private void RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.Clear();
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
// Output a Custom View Here
response.Write(The View)
response.End();
}
How can I write the output of a view to the response stream?
Additional Information
In the past, we have had problems with 301 redirects from mydomain.com to www.mydomain.com, and subsequently got lots of reports from users that the SSL certificate was invalid. The search engines did their job, but the users had problems until I switched to a 302 redirect. I was actually unable to reproduce it, but we got a significant number of reports so something had to be done.
I plan to make the view do a meta redirect as well as a javascript redirect to help improve reliability, but for those users who still end up at the 301 page I want them to feel at home. We already have custom 404 and 500 pages, why not a custom themed 301 page as well?
It turns out I was just over-thinking the problem and the solution was much simpler than I had envisioned. All I really needed to do was use the routeData to push the request to another controller action. What threw me off was the extra 301 status information that needed to be attached to the request.
private RouteData RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.CacheControl = "no-cache";
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "Redirect301";
routeData.Values["url"] = destinationUrl;
return routeData;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Do stuff here...
if (this.IsDefaultUICulture(cultureName))
{
var urlWithoutCulture = url.ToString().ToLowerInvariant().Replace("/" + cultureName.ToLowerInvariant(), "");
return this.RedirectPermanent(urlWithoutCulture, httpContext);
}
// Get the page info here...
if (page != null)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = page.ContentType.ToString();
result.Values["action"] = "Index";
result.Values["id"] = page.ContentId;
}
return result;
}
I simply needed to return the RouteData so it could be processed by the router!
Note also I added a CacheControl header to prevent IE9 and Firefox from caching the redirect in a way that cannot be cleared.
Now I have a nice page that displays a link and message to the user when the browser is unable to follow the 301, and I will add a meta redirect and javascript redirect to the view to boot - odds are the browser will follow one of them.
Also see this answer for a more comprehensive solution.
Assuming you have access to the HttpContextBase here's how you could render the contents of a controller action to the response:
private void RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.Clear();
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
// Output a Custom View Here (HomeController/SomeAction in this example)
var routeData = new RouteData();
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "SomeAction";
IController homeController = new HomeController();
var rc = new RequestContext(new HttpContextWrapper(context), routeData);
homeController.Execute(rc);
response.End();
}
This will render SomeAction on HomeController to the response.
I am using filter that check if user is log in then don't cache the previous page. The code for this is like,
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
....
if (isRegisteredUser.equalsIgnoreCase(("1"))) {
sessionID = UUID.randomUUID().toString();
session.setMaxInactiveInterval(1800);
Cookie userCookie = new Cookie("userCookie", "loginUser");
userCookie.setPath("/");
httpServletResponse.addCookie(userCookie);
Cookie sessionCookie = new Cookie("WITSessionCookie", sessionID);
sessionCookie.setMaxAge(60*30);
sessionCookie.setPath("/");
httpServletResponse.addCookie(sessionCookie);
if (!httpServletRequest.getRequestURI().startsWith(httpServletRequest.getContextPath() + ResourceHandler.RESOURCE_IDENTIFIER)) { // Skip JSF resources (CSS/JS/Images/etc)
httpServletResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
httpServletResponse.setHeader("Pragma", "no-cache"); // HTTP 1.0.
httpServletResponse.setDateHeader("Expires", 0); // Proxies.
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
...
} //end of doFilter()
But the problem is, if on any page user click on any button or link, and I return null for that action, and then I click on browser back button, then it says page expires. How can I prevent this? It is fine, that the page is not caching, but why it expires my page when I click on any button or any link and return null for that action?
Thanks
That's normal and specified behaviour when the enduser attempts to re-send a POST request which is not cached in the browser. You need to implement the post-redirect-get pattern. Send a redirect to the destination page in the action method. Or, better, make it an ajax submit instead. This is by the way regardless of whether you return to the same view by returning null or not.
For my website i want following behaviors for secured controller(or action)
if a user makes a normal request redirect to login page (which i have easily able to do)
if request is Ajax type Request.IsAjaxRequest()==true, return status code 401
How can i create a filter for this??
public class MyCustomAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
//if ajax request set status code and end Response
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.End();
}
base.HandleUnauthorizedRequest(filterContext);
}
}
Create a filter like above, it will return status code 401 for unauthorized request if request is made thru ajax.
If you are using jQuery you can do as below
jQuery.ajax({
statusCode: {
401: function() {
alert('unauthrized');
},
/*other options*/
});
In addition to the accepted answer, I needed to put this line of code in to prevent FormsAuthentication from redirecting to the login page..
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
I then removed
filterContext.HttpContext.Response.End();
var unauthorizedResult = new JsonResult
{
Data = new ErrorResult() {Success = 0, Error = "Forbidden"},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
// status code
filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
// return data
filterContext.Result = unauthorizedResult;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
Your problem is not with AJAX request, your problem is returning HTTP 401 Unauthorized response, because you use forms authentication. This response code tells the framework that it should redirect the user-agent to your login page with a HTTP 302 response instead. That's why it was easy to setup the "normal" request redirect - it's done automatically.
To answer your question, I had similar problem and the solution I ended up with was not using forms authentication. I implemented a custom authorization attribute that handles both cases manually instead. I'm not sure if this is the best approach, but it does work. I'm interested in what others think of this solution or what other solutions there are.
Fortunately, you can still use the FormsAuthentication class to handle cookies for you, but you have to delete the forms authentication configuration from your Web.config file. When the user logs in you use FormsAuthentication.SetAuthCookie to, well, set a cookie (you are probably doing this already). Second, in your authorization attribute, you get the cookie from the request and use FormsAuthentication.Decrypt to decrypt it. If it exists and is valid, you set the user in the HttpContext based on this cookie, because forms authentication won't do it for you anymore. If it doesn't you either redirect to the login page or return 401, depending on whether it's an AJAX call or not.
You can use ajaxonly to restrain access to ajax actionresult
You can just return a HttpUnauthorizedResult.
Note: This could cause the MVC framework to return you to the login page.
public ActionResult FailResult()
{
return new HttpUnauthorizedResult();
}
a simple way is to do a check in the SignIn action
public ActionResult SignIn()
{
if (Request.IsAjaxRequest())
{
// you could return a partial view that has this script instead
return Content("<script>window.location = '" + Url.Action("SignIn", "Account") + "'</script>");
}
...
return View();
I am having a problem where a cookie I am setting is being lost directly after RedirectToAction() is called. Is something going on behind the scenes that invalidates the current request and creates a new one that causes the cookie to be lost before it has been persisted to disk?
I understand that if you want data to be available after the redirect you need to use TempData, but is that the same for cookies? If so, doesn't that seem ugly to have to store the cookie value in TempData and then write the cookie later?
Update:
I just realized that the cookie is lost at the end of the request, it doesn't matter if I call RedirectToAction(). So now the question is why wont the cookie persist accros two requests? (I update the code below to show what I am doing now)
public ActionResult DoSomething()
{
Response.Cookies["SomeCookie"].Value = "Jarified";
Response.Cookies["SomeCookie"].Expires = DateTime.UtcNow.AddDays(3);
return View("SomeView");
}
Update
I created a new MVC project using the default template. I modified the HomeController/Index action to have the code below. The first time I hit the view "Cookie Not Found" is printed as expected. However, every subsequent time the same message is printed. If I remove the line that sets the expiration date then everything works just fine. I guess the real question here is why does making the cookie persistent cause the browser to throw it away? Is there a trick to making cookies persistent in MVC?
public ActionResult Index()
{
HttpCookie cookie = Request.Cookies["temp"];
if (cookie == null)
{
ViewData["Message"] = "Cookie Not Found";
Response.Cookies["temp"].Value = "Welcome to ASP.NET MVC!";
Response.Cookies["temp"].Expires = DateTime.UtcNow;
}
else
{
ViewData["Message"] = cookie.Value;
}
return View();
}
The reason that
Response.Cookies["temp"].Expires = DateTime.UtcNow;
is not working is that it sets a cookie with an expires in the past (at least for me).
It works when I change it to
Response.Cookies["temp"].Expires = DateTime.UtcNow.AddDays(3);
Setting a cookie expires property to the past will delete the cookie.
This code works for me:
public ActionResult Index() {
HttpCookie cookie = Request.Cookies["temp"];
if (cookie == null) {
ViewData["Message"] = "Cookie Not Found";
Response.Cookies["temp"].Value = "This is a cookie: Welcome to ASP.NET MVC!";
Response.Cookies["temp"].Expires = DateTime.UtcNow.AddDays(3);
} else {
return RedirectToAction("Something");
}
return View();
}
public ActionResult Something() {
HttpCookie cookie = Request.Cookies["temp"];
ViewData["Message"] = cookie.Value;
return View();
}
Take a look at this article. It should address your issue!
http://stephenwalther.com/blog/archive/2008/07/08/asp-net-mvc-tip-15-pass-browser-cookies-and-server-variables-as-action-parameters.aspx
var newCookie = new HttpCookie("myCookie", cookieValue);
newCookie.Expires = DateTime.Now.AddDays(10);
Response.AppendCookie(newCookie);
return RedirectToAction("Index");
Here's a one liner for setting a cookie and expiration date
Response.Cookies.Add(new HttpCookie("myCookie", "cookie value")
{ Expires = DateTime.Now.AddDays(1)});
Here is My Code To Log In
var expire = DateTime.Now.AddDays(7);
// Create a new ticket used for authentication
var ticket = new FormsAuthenticationTicket(
1, // Ticket version
username, // Username to be associated with this ticket
DateTime.Now, // Date/time issued
expire, // Date/time to expire
true, // "true" for a persistent user cookie (could be a checkbox on form)
roles, // User-data (the roles from this user record in our database)
FormsAuthentication.FormsCookiePath); // Path cookie is valid for
// Hash the cookie for transport over the wire
var hash = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, hash) { Expires = expire };
// Add the cookie to the list for outbound response
Response.Cookies.Add(cookie);
Here Is My Code To Check The Roles. It is a custom IHTTP Module
if (HttpContext.Current.User == null) return;
if (!HttpContext.Current.User.Identity.IsAuthenticated) return;
if (!(HttpContext.Current.User.Identity is FormsIdentity)) return;
// Get Forms Identity From Current User
var id = (FormsIdentity)HttpContext.Current.User.Identity;
// Get Forms Ticket From Identity object
var ticket = id.Ticket;
// Retrieve stored user-data (our roles from db)
var userData = ticket.UserData;
var roles = userData.Split(',');
// Create a new Generic Principal Instance and assign to Current User
Thread.CurrentPrincipal = HttpContext.Current.User = new GenericPrincipal(id, roles);
Here is my Code To Log Out
FormsAuthentication.SignOut();
Response.Cookies.Remove(FormsAuthentication.FormsCookieName);
Session.Clear();
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.Now.AddSeconds(-1));
Response.Cache.SetNoStore();
Response.AppendHeader("Pragma", "no-cache");
return View("SignIn");
This is crazy. I have two bald spots now.
1) shouldn't your call to Response.Cookies.Remove(FormsAuthentication.FormsCookieName); be Response.Cookies.Remove(whatever-the-user-name-is);?
2) try sending an expired cookie back to the browser.
FormsAuthentication.SignOut();
// replace with username if this is the wrong cookie name
Response.Cookies.Remove(FormsAuthentication.FormsCookieName);
Session.Clear();
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.Now.AddSeconds(-1));
Response.Cache.SetNoStore();
Response.AppendHeader("Pragma", "no-cache");
// send an expired cookie back to the browser
var ticketExpiration = DateTime.Now.AddDays(-7);
var ticket = new FormsAuthenticationTicket(
1,
// replace with username if this is the wrong cookie name
FormsAuthentication.FormsCookieName,
DateTime.Now,
ticketExpiration,
false,
String.Empty);
var cookie = new System.Web.HttpCookie("user")
{
Expires = ticketExpiration,
Value = FormsAuthentication.Encrypt(ticket),
HttpOnly = true
};
Response.Cookies.Add(cookie);
return View("SignIn");
You can not directly delete a cookie on a client's computer. When you calls the Cookies.Remove method the cookie is deleted on a server side. To delete the cookie on a client's side it's necessary to set the cookie's expiration date to a past date.
HttpCookie cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie != null)
{
cookie.Expires = DateTime.Now.AddDays(-1);
HttpContext.Current.Response.Cookies.Add(cookie);
}
I hope this helps you.
If you want to apply the "no cache on browser back" behavior on all pages then you should put it in global.asax.
protected void Application_BeginRequest()
{
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetExpires(DateTime.UtcNow.AddHours(-1));
Response.Cache.SetNoStore();
}
hope it helps someone !