152

Smashed my head against this a bit too long. How do I prevent a user from browsing a site's pages after they have been logged out using FormsAuthentication.SignOut? I would expect this to do it:

FormsAuthentication.SignOut();
Session.Abandon();
FormsAuthentication.RedirectToLoginPage();

But it doesn't. If I type in a URL directly, I can still browse to the page. I haven't used roll-your-own security in a while so I forget why this doesn't work.

GEOCHET
  • 21,119
  • 15
  • 74
  • 98
Jason
  • 8,400
  • 10
  • 56
  • 69
  • That code is fine as is ... clicking back in the browser does not revisit the page on the server it simply reloads the local cached version of the page. All of the solutions below seem to ignore that fact and not actually do anything more than you are doing here. In short ... there is no answer to this question that will solve the user looking at their cache as to date I don't believe there is a way to clear cache in say ... js or with a server side instruction. – War Aug 04 '16 at 16:22
  • 1
    This answer offers some ways to check, especially if you're site is failing PEN tests: https://stackoverflow.com/questions/31565632/invalidate-aspx-authentification-cookie – Tyler S. Loeper Mar 19 '18 at 19:17

23 Answers23

218

Users can still browse your website because cookies are not cleared when you call FormsAuthentication.SignOut() and they are authenticated on every new request. In MS documentation is says that cookie will be cleared but they don't, bug? Its exactly the same with Session.Abandon(), cookie is still there.

You should change your code to this:

FormsAuthentication.SignOut();
Session.Abandon();

// 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)
SessionStateSection sessionStateSection = (SessionStateSection)WebConfigurationManager.GetSection("system.web/sessionState");
HttpCookie cookie2 = new HttpCookie(sessionStateSection.CookieName, "");
cookie2.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(cookie2);

FormsAuthentication.RedirectToLoginPage();

HttpCookie is in the System.Web namespace. MSDN Reference.

dana
  • 17,267
  • 6
  • 64
  • 88
Igor Jerosimić
  • 13,621
  • 6
  • 44
  • 53
  • 18
    This works for me. However it's worth noting that if the Domain property has been set on the FormsAuthentication cookie when logging in, it will also need to be set when expirying the cookie on when you log out – Phil Hale Jun 17 '11 at 16:32
  • @Phil Hale - thanks for the reminder re: the domain property - forgot about that! – Mustafa Shabib Aug 09 '11 at 20:22
  • 5
    don't forget! cookie1.Path = FormsAuthentication.FormsCookiePath; – Alexander Taylor Jul 30 '12 at 12:06
  • 8
    Also don't forget **cookie1.HttpOnly = true;** – Dmitry Zaets Aug 22 '12 at 15:58
  • 6
    This seems like a better solution to me: **Response.Cookies[FormsAuthentication.FormsCookieName].Expires = DateTime.Now.AddDays(-1);** – Randy H. Mar 04 '13 at 22:22
  • 2
    Just as a FYI i noticed that if the Path is set for .ASPXAUTH the FormsAuthentication.FormsCookiePath will not pick it up. So you will need to set cookie1.Path with a variable saved somewhere. – Donny V. Mar 08 '13 at 17:09
  • 7
    @RandyH. Overriding the existing FormsAuthentication cookie with a new empty cookie ensures that even if the client winds back their system clock, they will still not be able to retrieve any user data from the cookie. – Tri Q Tran Apr 30 '13 at 23:18
  • 2
    Depending on where it appears, FormsAuthentication.RedirectToLoginPage() may need to be followed by Response.End(). – vwfreak May 22 '14 at 19:50
  • Great piece of code, helped me too for same issue. Also, I used the [MSDN article](http://msdn.microsoft.com/en-us/library/system.web.configuration.sessionstatesection%28v=vs.110%29.aspx) for retrieving session cookie name just in case something other than ASP.NET_SessionId is being used – joym8 May 24 '14 at 20:09
  • 3
    It has been about 5 years now since the above response was written. Does the above code still work with visual studio 2013? Can you just copy paste that into the logout_button_Click event? Thank you. – Micro Sep 11 '14 at 18:23
  • Does using the `LoginStatus` control to logout the user clear these cookies? ``? If not, how can I add the above code to execute too when I click the "logout" button above? – Micro Sep 15 '14 at 16:09
  • I can't believe that in my years of devving, I've only experienced this issue now. @MicroR - it worked for me with copy/paste – Adam Hey Jan 09 '15 at 12:54
  • Another don't forget: `Context.User = null` – Jefferey Cave Jan 10 '15 at 19:58
  • 1
    this doesn't work to immediately invalidate a cookie. – user3036342 Feb 12 '15 at 13:56
  • 7
    If you need to do this, then you have some configuration problems, since `FormsAuthentication.SignOut()` does this unless your authentication is not configured correctly. But still, if you want to reset the expire date of a cookie, at lest do it right. `HttpContext.Current.Request.Cookies.AllKeys.ToList().ForEach(s => HttpContext.Current.Request.Cookies[s].Expires = DateTime.Now.AddDays(-1))` will iterate over all cookies and have their expire date reset, so nothing is left behind when you logout. – Nuno Agapito Apr 29 '15 at 12:40
  • The Response.Cookies.Add method allows duplicate cookies in the cookie collection. Use the Response.Cookies.Set method to ensure the uniqueness of cookies in the cookie collection.(from msdn: https://msdn.microsoft.com/en-us/library/system.web.httpcookiecollection.set.aspx) – Amir Chatrbahr May 26 '15 at 05:07
  • 11
    Can someone combine all these comments into the answer? – David Sep 15 '15 at 13:57
  • 2
    Session.Abandon() does abort the current session but it does not clear the session identifier cookie. By design, the session identifer can be reused to start a new session on the next request. This behaviour can be disabled using the regenerateExpiredSessionId configuration parameter. – Oskar Berggren Apr 25 '16 at 03:24
  • 1
    FormsAuthentication.SignOut() doesn't work if there is any error after the statement. I found your solution works, but I would prefer the following approach .. public ActionResult Logout() { FormsAuthentication.SignOut(); return Redirect("~/"); } – Aji Jul 12 '16 at 11:59
  • Hi dana, I have implemented windows authentication into my application for login but issue is that when I logout from application its not properly logging out still user is logged in when refresh browser. I have tried your above solution but no luck. Please help me out – पone Aug 23 '21 at 07:42
24

Using two of the above postings by x64igor and Phil Haselden solved this:

1. x64igor gave the example to do the Logout:

  • You first need to Clear the Authentication Cookie and Session Cookie by passing back empty cookies in the Response to the Logout.

    public ActionResult LogOff()
    {
        FormsAuthentication.SignOut();
        Session.Clear();  // This may not be needed -- but can't hurt
        Session.Abandon();
    
        // Clear authentication cookie
        HttpCookie rFormsCookie = new HttpCookie( FormsAuthentication.FormsCookieName, "" );
        rFormsCookie.Expires = DateTime.Now.AddYears( -1 );
        Response.Cookies.Add( rFormsCookie );
    
        // Clear session cookie 
        HttpCookie rSessionCookie = new HttpCookie( "ASP.NET_SessionId", "" );
        rSessionCookie.Expires = DateTime.Now.AddYears( -1 );
        Response.Cookies.Add( rSessionCookie );
    

2. Phil Haselden gave the example above of how to prevent caching after logout:

  • You need to Invalidate the Cache on the Client Side via the Response.

        // Invalidate the Cache on the Client Side
        Response.Cache.SetCacheability( HttpCacheability.NoCache );
        Response.Cache.SetNoStore();
    
        // Redirect to the Home Page (that should be intercepted and redirected to the Login Page first)
        return RedirectToAction( "Index", "Home" ); 
    }
    
justdan23
  • 560
  • 7
  • 9
  • 1
    Wasted whole day at work to solve this problem. Once logged in the logout button started to call wrong action in controller (Login not Logout). Thank you, this solved the problem. Development enviroment: ASP.NET 4.51 MVC 5.1 – Ako Jan 22 '14 at 20:11
  • 1
    Good answer! Humble suggestion: Use the forma for clearing session cookies *x64igor* used: `SessionStateSection sessionStateSection = (SessionStateSection)WebConfigurationManager.GetSection("system.web/sessionState"); HttpCookie sessionCookie = new HttpCookie(sessionStateSection.CookieName, "");`. In general, the session cookie name is not `"ASP.NET_SessionId"`. – seebiscuit Dec 27 '16 at 17:56
20

Sounds to me like you don't have your web.config authorization section set up properly within . See below for an example.

<authentication mode="Forms">
  <forms name="MyCookie" loginUrl="Login.aspx" protection="All" timeout="90" slidingExpiration="true"></forms>
</authentication>
<authorization>
  <deny users="?" />
</authorization>
jwalkerjr
  • 1,779
  • 4
  • 18
  • 20
  • This is a much simpler solution, i would mark this as an answer. As i got one version of code working on different servers on one i didn't need to set additional properties you added here and on other i did. So modifying code shouldn't be correct solution, modifying configuration is better. – Vladimir Bozic Dec 20 '16 at 12:19
  • 1
    By default the slidingExpiration is set to true (https://msdn.microsoft.com/library/1d3t3c61(v=vs.100).aspx). And this will finally result in the cookie getting invalid after x minutes as set in timeout - and not when user is logged off via SignOut(). So this won't result in the desired behaviour to log a user off using FormsAuthentication. Please correct me if I am wrong. – OlafW Apr 10 '17 at 09:47
14

The key here is that you say "If I type in a URL directly...".

By default under forms authentication the browser caches pages for the user. So, selecting a URL directly from the browsers address box dropdown, or typing it in, MAY get the page from the browser's cache, and never go back to the server to check authentication/authorization. The solution to this is to prevent client-side caching in the Page_Load event of each page, or in the OnLoad() of your base page:

Response.Cache.SetCacheability(HttpCacheability.NoCache);

You might also like to call:

Response.Cache.SetNoStore();
Phil Haselden
  • 2,876
  • 3
  • 30
  • 25
12

I've struggled with this before too.

Here's an analogy for what seems to be going on... A new visitor, Joe, comes to the site and logs in via the login page using FormsAuthentication. ASP.NET generates a new identity for Joe, and gives him a cookie. That cookie is like the key to the house, and as long as Joe returns with that key, he can open the lock. Each visitor is given a new key and a new lock to use.

When FormsAuthentication.SignOut() is called, the system tells Joe to lose the key. Normally, this works, since Joe no longer has the key, he cannot get in.

However, if Joe ever comes back, and does have that lost key, he is let back in!

From what I can tell, there is no way to tell ASP.NET to change the lock on the door!

The way I can live with this is to remember Joe's name in a Session variable. When he logs out, I abandon the Session so I don't have his name anymore. Later, to check if he is allowed in, I simply compare his Identity.Name to what the current session has, and if they don't match, he is not a valid visitor.

In short, for a web site, do NOT rely on User.Identity.IsAuthenticated without also checking your Session variables!

Glen Little
  • 6,951
  • 4
  • 46
  • 68
  • 8
    + 1, I think this is called 'Cookie replay attack'. There is an article on limitations of FormsAuthentication.SignOut: http://support.microsoft.com/kb/900111 – Dmitry Dec 04 '13 at 19:03
  • 3
    For anyone looking to follow the link above, it is dead. You can try using the WaybackMachine to get a copy of this page here, but it IMMEDIATELY tries to redirect the user. http://web.archive.org/web/20171128133421/https://support.microsoft.com/en-us/help/900111/the-formsauthentication-signout-method-does-not-prevent-cookie-reply-a – Synctrex Apr 05 '19 at 17:25
7

After lots of search finally this worked for me . I hope it helps.

public ActionResult LogOff()
{
    AuthenticationManager.SignOut();
    HttpContext.User = new GenericPrincipal(new GenericIdentity(string.Empty), null);
    return RedirectToAction("Index", "Home");
}

<li class="page-scroll">@Html.ActionLink("Log off", "LogOff", "Account")</li>
  • I've been developing web applications for years in PHP. So I am new to MVC... I admit I love it, BUT who would have thought something so simple as logging someone off would be so difficult? I tried every other script on this page down the line and this is the only one that worked. Thanks for posting! – Anthony Griggs Feb 10 '17 at 00:18
6

This Answer is technically identical to Khosro.Pakmanesh. I'm posting it to clarify how his answer differs from other answers on this thread, and in which use case it can be used.

In general to clear a user-session, doing

HttpContext.Session.Abandon();
FormsAuthentication.SignOut();

will effectively log out the user. However, if in the same Request you need to check Request.isAuthenticated (as may often happen in an Authorization Filter, for example), then you will find that

Request.isAuthenticated == true

even _after you did HttpContext.Session.Abandon() and FormsAuthentication.SignOut().

The only thing that worked was doing

AuthenticationManager.SignOut();
HttpContext.User = new GenericPrincipal(new GenericIdentity(string.Empty), null);

That effectively sets Request.isAuthenticated = false.

Community
  • 1
  • 1
seebiscuit
  • 4,905
  • 5
  • 31
  • 47
6

This works for me

public virtual ActionResult LogOff()
    {
        FormsAuthentication.SignOut();
        foreach (var cookie in Request.Cookies.AllKeys)
        {
            Request.Cookies.Remove(cookie);
        }
        foreach (var cookie in Response.Cookies.AllKeys)
        {
            Response.Cookies.Remove(cookie);
        }
        return RedirectToAction(MVC.Home.Index());
    }
Korayem
  • 12,108
  • 5
  • 69
  • 56
3

I've tried most answers in this thread, no luck. Ended up with this:

protected void btnLogout_Click(object sender, EventArgs e)
{
    FormsAuthentication.Initialize();
    var fat = new FormsAuthenticationTicket(1, "", DateTime.Now, DateTime.Now.AddMinutes(-30), false, string.Empty, FormsAuthentication.FormsCookiePath);
    Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(fat)));
    FormsAuthentication.RedirectToLoginPage();
}

Found it here: http://forums.asp.net/t/1306526.aspx/1

stoffen
  • 570
  • 6
  • 14
3

The code you posted looks like it should correctly remove the forms authentication token, so it is possible that the folders/pages in question are not actually protected.

Have you confirmed that the pages cannot be accessed before a login has occured?

Can you post the web.config settings and login code that you are using?

Abram Simon
  • 3,239
  • 21
  • 17
3

I have been writing a base class for all of my Pages and I came to the same issue. I had code like the following and It didn't work. By tracing, control passes from RedirectToLoginPage() statement to the next line without to be redirected.

if (_requiresAuthentication)
{
    if (!User.Identity.IsAuthenticated)
        FormsAuthentication.RedirectToLoginPage();

    // check authorization for restricted pages only
    if (_isRestrictedPage) AuthorizePageAndButtons();
}

I found out that there are two solutions. Either to modify FormsAuthentication.RedirectToLoginPage(); to be

if (!User.Identity.IsAuthenticated)
    Response.Redirect(FormsAuthentication.LoginUrl);

OR to modify the web.config by adding

<authorization>
  <deny users="?" />
</authorization>

In the second case, while tracing, control didn't reach the requested page. It has been redirected immediately to the login url before hitting the break point. Hence, The SignOut() method isn't the issue, the redirect method is the one.

I hope that may help someone

Regards

Wahid Shalaly
  • 2,047
  • 1
  • 18
  • 29
  • 2
    Also you could call Response.End() just after calling FormsAuthentication.RedirectToLoginPage() – murki Oct 21 '09 at 16:50
  • I think there's a bit of a miscommunication on MS's part. You have to lock people out if you want them returned to the login page. Otherwise the framework will gleefully allow you access. So you have to say solution #2 in this post. – Josh Robinson Nov 15 '10 at 18:50
3

I just tried some of the suggestions here and while I was able to use the browser back button, when I clicked on a menu selection the [Authorize] token for that [ActionResult] sent me right back to the login screen.

Here is my logout code:

        FormsAuthentication.SignOut();
        Response.Cookies.Remove(FormsAuthentication.FormsCookieName);
        Response.Cache.SetExpires(DateTime.Now.AddSeconds(-1));
        HttpCookie cookie = HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
        if (cookie != null)
        {
            cookie.Expires = DateTime.Now.AddDays(-1);
            Response.Cookies.Add(cookie);
        }

Although the back function on the browser took me back and displayed the secured menu (I am still working on that) I was not able to do anything that was secured in the app.

Hope this helps

DonH
  • 31
  • 1
2

This started happening to me when I set the authentication > forms > Path property in Web.config. Removing that fixed the problem, and a simple FormsAuthentication.SignOut(); again removed the cookie.

stema
  • 90,351
  • 20
  • 107
  • 135
BPM
  • 21
  • 1
1

I just had the same problem, where SignOut() seemingly failed to properly remove the ticket. But only in a specific case, where some other logic caused a redirect. After I removed this second redirect (replaced it with an error message), the problem went away.

The problem must have been that the page redirected at the wrong time, hence not triggering authentication.

Peder Skou
  • 33
  • 4
1

Just try to send a session variable when you press log in. And on the welcome page, first check whether that session is empty like this in the page load or in the Init Event:

if(Session["UserID"] == null || Session["UserID"] == "")
{
    Response.Redirect("Login.aspx");
}
David Cain
  • 16,484
  • 14
  • 65
  • 75
Devrishi
  • 11
  • 1
1

I wanted to add some information to help understand the problem. Forms Authentication allows for storing user data either in a cookie, or in the query string of the URL. The method your site supports can be configured in the web.config file.

According to Microsoft:

The SignOut method removes the forms-authentication ticket information from the cookie or the URL if CookiesSupported is false.

At the same time, they say:

One of the HttpCookieMode values that indicates whether the application is configured for cookieless forms authentication. The default is UseDeviceProfile.

Lastly, regarding UseDeviceProfile, they say:

If the CookieMode property is set to UseDeviceProfile, the CookiesSupported property will return true if the Browser for the current Request supports both cookies and redirecting with cookies; otherwise, the CookiesSupported property will return false.

Piecing this all together, depending on the user's browser, the default configuration may result in CookiesSupported being true, which means the SignOut method doesn't clear the ticket from the cookie. This seems counter-intuitive and I don't know why it works this way -- I would expect SignOut to actually sign the user out under any circumstances.

One way to make the SignOut work by itself is to change the cookie mode to "UseCookies" (i.e. cookies are required) in the web.config file:

<authentication mode="Forms">
  <forms loginUrl="~/Account/SignIn" cookieless="UseCookies"/>
</authentication>

According to my tests, doing this makes SignOut work by itself at the cost of your site now requiring cookies to function properly.

RogerMKE
  • 667
  • 4
  • 12
  • I think you're reading that wrong. Regarding SignOut(), I'm pretty sure that what they mean is that it will be cleared from the URL if CookiesSupported is false, otherwise from the cookie. I.e. they should have written "The SignOut method removes the forms-authentication ticket information from the cookie or, if CookiesSupported is false, from the URL." – Oskar Berggren Apr 25 '16 at 03:14
1

For me, the following approach works. I think if there is any error after the "FormsAuthentication.SignOut()" statement, SingOut doesn't work.

public ActionResult SignOut()
    {
        if (Request.IsAuthenticated)
        {
            FormsAuthentication.SignOut();

            return Redirect("~/");
        }
        return View();
     }
Aji
  • 193
  • 2
  • 6
1

It could be that you are logging in from one subdomain (sub1.domain.com) and then trying to logout from a different subdomain (www.domain.com).

jorsh1
  • 291
  • 4
  • 13
1

I am having a similar issue now and I believe the problem in my case as well as the original poster is because of the redirect. By default a Response.Redirect causes an exception which immediately bubbles up until it is caught and the redirect is immediately executed, I am guessing that this is preventing the modified cookie collection from being passed down to the client. If you modify your code to use:

Response.Redirect("url", false);

This prevents the exception and seems to allow the cookie to be properly sent back to the client.

0

For MVC this works for me:

        public ActionResult LogOff()
        {
            FormsAuthentication.SignOut();
            return Redirect(FormsAuthentication.GetRedirectUrl(User.Identity.Name, true));
        }
anovo
  • 1
0

Are you testing/seeing this behaviour using IE? It's possible that IE is serving up those pages from the cache. It is notoriously hard to get IE to flush it's cache, and so on many occasions, even after you log out, typing the url of one of the "secured" pages would show the cached content from before.

(I've seen this behaviour even when you log as a different user, and IE shows the "Welcome " bar at the top of your page, with the old user's username. Nowadays, usually a reload will update it, but if it's persistant, it could still be a caching issue.)

Stobor
  • 44,246
  • 6
  • 66
  • 69
0

Doing Session.abandon() and destroying the cookie works pretty good. I'm using mvc3 and it looks like the problem occurs if you go to a protected page, log out, and go via your browser history. Not a big deal but still kinda of annoying.

Trying to go through links on my web app works the right way though.

Setting it to not do browser caching may be the way to go.

James
  • 194
  • 3
  • 10
-2

Be aware that WIF refuses to tell the browser to cleanup the cookies if the wsignoutcleanup message from STS doesn't match the url with the name of the application from IIS, and I mean CASE SENSITIVE. WIF responds with the green OK check, but will not send the command to delete cookies to browser.

So, you need to pay attention to the case sensitivity of your url's.

For example, ThinkTecture Identity Server saves the urls of the visiting RPs in one cookie, but it makes all of them lower case. WIF will receive the wsignoutcleanup message in lower case and will compare it with the application name in IIS. If it doesn't match, it deletes no cookies, but will report OK to the browser. So, for this Identity Server I needed to write all urls in web.config and all application names in IIS in lower case, in order to avoid such problems.

Also don't forget to allow third party cookies in the browser if you have the applications outside of the subdomain of STS, otherwise the browser will not delete the cookies even if WIF tells him so.

Stefan
  • 35
  • 5
  • 1
    WIF? STS? ThinkTecture Identity Server? What are all these things, and how to they relate to this question? – Oskar Berggren Apr 25 '16 at 03:01
  • yeah, it's old. But I had to comment. Looks like it was autogenerated and posted by a bot :) – Jag Jan 29 '21 at 16:41
  • These were my experiences at that time with those systems, all of them behaving differently when the user requests a SignOut, whether is based on the MS bug or not. My answer doesn't say something against the chosen answer. It only clarifies what other systems, that happen to use this technology, do. So, there was no bot... – Stefan Apr 12 '21 at 15:15