Skip to content

Forms Authentication

txgz999 edited this page Apr 4, 2019 · 83 revisions

Forms authentication is a feature added in ASP.NET 1.1, and get enhanced with the introducing of membership and security controls in ASP.NET 2.0. Before that many classical ASP and ASP.NET applications store user information such as username as session variables. Since http communication is stateless, classical ASP applications and ASP.NET applications always maintain a session for each browser. A session always exists no matter if the user logs in or not. Such session is often called browser session since there exists one session per browser at any time. A session consists of a session Id and session state. A session Id is generated by server and is stored as cookie at client side and sends with each request to server, in this way at the server side we can recognize requests from the same browser. Session state is an array of data associated with a session id and lives at server side only. At server side, we can find the user data in session state using the session Id sent from the client side. Session state can be stored in different places on server, such as in database, or in a separate state service, but most common in memory. If we store user data in memory session, then restarting application pool would kick users out of the application.

Forms authentication stores username, and other user data if necessary, as an encrypted cookie, thus can survive from application pool restart. When we store user identity in session state, for each request, we need to check session state to decide if user is authenticated in application code. ASP.NET framework provides extra support to the forms authentication which makes it easy to use, for example since the server has knowledge of if user is authenticated, we can use settings in web.config, outside of application code, to prevent unauthorized users from access the whole site or certain pages. We can use User.IsAuthenticated to check if user is authenticated, and from User.Identity we can find username as well as the ticket content.

Forms authentication does not restrict the way to authenticate users. Often once we get the username and password user provided, we verify them against user information stored in a database, then call the FormsAuthentication.SetAuthCookie with the username to generate the forms authentication ticket cookie, then the user is authenticated.

Session timeout value can be set in web.config and the default value is 20 minutes. Session timeout can be either fixed, which means no matter what you do, after that many minutes the session ends; or sliding, which means bore session expires when you send request to server the session expiration time is extended to expire that many minutes from the current time therefore session ends only when we have not sent any request to server for that many minutes. Forms authentication ticket also has a timeout value can also be set in web.config and the default is 30 minutes, and the expiration can also be fixed or sliding. The meaning of sliding is a little different, the expiration time gets extended only if a request is sent to server when the ticket is going to expire within half of the timeout value.

Session and ticket can timeout separately and independently. If we want the session always timeout first, then we just need to make the ticket timeout value to be at least double of the session timeout value. Say session time is N minutes and ticket timeout is 2N minutes. In order for the ticket timeouts, there cannot be any request to server in the last N minutes before the expiration. But if there is no request in N minutes, the session would already timeout, thus it timeouts before the ticket expires. One advantage of doing so is we can have an opportunity to expire the ticket when session timeout, thus make them expire at the same time. See https://itworksonmymachine.wordpress.com/2008/07/17/forms-authentication-timeout-vs-session-timeout/.

In general a http module needs to be registered in web.config for it to be used in the web application, but then how come I do not find FormsAuthenticationModule mentioned in my web.config? It is registered in a root web.config (C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config/web.config) that every web applications uses, for more details, see https://stackoverflow.com/questions/20163911/where-is-formsauthenticationmodule-registered.

<httpModules>
    <add name="OutputCache" type="System.Web.Caching.OutputCacheModule"/>
    <add name="Session" type="System.Web.SessionState.SessionStateModule"/>
    <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule"/>
    <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule"/>

We know the forms authentication ticket gets extended only when user sends a request to server after half way through the expiration time. Exactly where does this logic implemented? It is implemented in the RenewTicketIfOld method of the FormsAuthentication class, which is called by the FormsAuthenticationModule class in the handler (OnEnter->OnAuthentication) to the AuthenticateRequest event.

public static FormsAuthenticationTicket RenewTicketIfOld(FormsAuthenticationTicket tOld) {
    if (tOld == null) return null;

    DateTime utcNow = DateTime.UtcNow;
    TimeSpan ticketAge = utcNow - tOld.IssueDateUtc;
    TimeSpan ticketRemainingLifetime = tOld.ExpirationUtc - utcNow;

    if (ticketRemainingLifetime > ticketAge) return tOld; // no need to renew

    // The original ticket may have had a custom-specified lifetime separate from
    // the default timeout specified in config. We should honor that original
    // lifetime when renewing the ticket.
    TimeSpan originalTicketTotalLifetime = tOld.ExpirationUtc - tOld.IssueDateUtc;
    DateTime newExpirationUtc = utcNow + originalTicketTotalLifetime;

    FormsAuthenticationTicket ticket = FormsAuthenticationTicket.FromUtc(
        tOld.Version /* version */,
        tOld.Name /* name */,
        utcNow /* issueDateUtc */,
        newExpirationUtc /* expirationUtc */,
        tOld.IsPersistent /* isPersistent */,
        tOld.UserData /* userData */,
        tOld.CookiePath /* cookiePath */);
    return ticket;
}

Say we have multiple applications share the same form authentication ticket, the new expiration time when a ticket get renewed is based on the timeout setting of the application that initially create the ticket, or the timeout setting of the application that renews this ticket? Based on the code of the RenewTicketIfOld method, it is based on the timeout setting of the application that initially create the ticket.

We want to display a warning to user before form authentication expires and give user a chance to extend the ticket. We need to consider there are multiple applications sharing the same ticket and running at the same time. Each may display their warning because it is not possible to display a warning on top of every window on certain machines. Then if user click the button on the warning, form authentication expiration time should be extended and all warnings should disappear. If we reach the ticket expiration time, all application should either go to the login screen or simple disappear depends if it the the application user login from.

Notice that in general, we cannot get cookie expiration time through javascript (from here,

The browser is responsible for managing cookies, and the cookie's expiration time and date help the browser manage its store of cookies. Therefore, although you can read the name and value of a cookie, you cannot read the cookie's expiration date and time

thus we need to store the form authentication expiration time as a separate cookie (say we call it expiration-time cookie). We can find that value from server side, then have a global filter that set that cookie in the OnActionExecuted method, see [https://stackoverflow.com/questions/6227222/how-do-i-add-a-cookie-for-every-visitor-to-my-asp-net-mvc-site|here].

protected void Application_EndRequest(object sender, EventArgs e) {
    if (this.Context.Response.Cookies.AllKeys.Contains(FormsAuthentication.FormsCookieName)) {
        HttpCookie tcookie = this.Context.Response.Cookies[FormsAuthentication.FormsCookieName];
        if (string.IsNullOrEmpty(tcookie.Value)) {
            var cookie = new HttpCookie("ticketExpirationTime") {
                Expires = DateTime.Now.AddDays(-1),
            };
            this.Context.Response.Cookies.Add(cookie);
        }
        else {
            var ticket = FormsAuthentication.Decrypt(tcookie.Value);
            var cookie = new HttpCookie("ticketExpirationTime", ticket.Expiration.ToString());
            cookie.Path = ticket.CookiePath;
            if (ticket.IsPersistent) cookie.Expires = ticket.Expiration;
            this.Context.Response.Cookies.Add(cookie);
        }
    }
}

When a page is (re-)loaded, we set up a timeout callback to say 2 minutes before the expiration time. Then when the callback is called, check if there are still more than 2 minutes before the expiration. If yes, create a new timeout callback that would be called 2 minutes before the expiration. If not, display a popup message, and create an interval callback that called say 10 times a second. If user clicks the message, makes an ajax call to server to extend the expiration time, and due to the filter, the expiration-time cookie would be updated as well. When the interval callback is called, it checks the current time with expiration time

  • if there are more than 2 minutes away, close the warning, and stop the interval callback, and create a timeout callback to be called at 2 minutes before expiration time
  • if there are no more than 2 minutes away, do nothing
  • if the current time passes the expiration time, close warning and application

That is all an application need to do if it does not use session, which is typical in for a MVC application. If an application uses session, we need extra work to extend session when form authentication ticket expiration time get extended. In the following we assume that the session timeout value is no less than the ticket timeout value. We can store the session expiration time as a local variable, and when ever page is (re-)loaded and when ever we sends a request to server, it is set to current time plus timeout value. Then use a timer to run periodically to make sure this this value is greater than the expiration-time cookie value. If we find that is not true, send a request to server to extend session. We need to choose the right frequency of this checking to make sure the request we send to the server will not alter the ticket expiration time, that is possible if we send request before half of the ticket timeout time has been passed. We can do the check every N/2-1 minute where N is the ticket timeout value. Let's consider a concrete sample, say the session timeout is 21 minutes and the ticket timeout is 20 minutes, then we do the checking every 9 minutes, let us verify our request would not alter ticket expiration time: say after one checking SE>TE is verified (SE: session expiration time; TE: ticket expiration time). Then in the next checking, there are the following situations:

  • SE>TE: do nothing
  • SE<=TE: then we need to send request to server. This situation implies that TE got extended within this 9 minutes. Since there are no more than 9 minutes passed since the ticket got extended, the ticket has at least 11 minutes left. Therefore when we send a request to server to extend the session at this moment, the ticket would not get extended There is no harm to check more often, but it is not necessary.

If the session timeout value is less than the ticket timeout value, I believe there is no proper way to extend session when needed without affecting the ticket expiration time.

Clone this wiki locally