Skip to content

Forms Authentication

txgz999 edited this page Apr 13, 2019 · 83 revisions

Forms authentication is a feature Microsoft added to ASP.NET in version 1.1, and enhanced with the introducing of membership and security controls in version 2.0. Before that many classic ASP and ASP.NET applications store user information such as username as session variables. Since http communication is stateless, classic ASP and ASP.NET runtime always maintain a session for each browser in each application to help maintenance user state. 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 sent to client as a cookie. Thus this session Id cookie is sent back to server with every subsequent request, 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 various 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. In summary, session allows an application to store data for each user, but classic ASP and ASP.NET runtime do not provide extra support for using session to control user authentication.

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 or not, we can use settings in web.config, outside of application code, to prevent unauthorized users from access the whole site or certain pages. In application code, 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.

The logic to authenticate user is beyond the scope of the forms authentication and can be still controlled by each application. Forms authentication provides a way to maintain such information and establish authentication to general features provided by ASP.NET such as security controls. Often once we get the username and password from user, 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 for that user, in this way that 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 expires; or sliding, which means before session expires everytime you send request to server the session expiration time is extended to be that many minutes from the current time therefore session ends only when you have not sent any request to server for that many minutes. Forms authentication ticket also has a timeout value that 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 to 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 times out before the ticket expires. One advantage of doing so is we can have an opportunity to expire the ticket when session times out by handling the session ends event, thus make them expire at the same time. See https://itworksonmymachine.wordpress.com/2008/07/17/forms-authentication-timeout-vs-session-timeout/.

Timeout Warning

We want to design a strategy to handle forms authentication expiration. The goal is to display a warning popup window to user before forms authentication expires to give user a chance to extend the ticket. We need to take into consideration that there are multiple applications sharing the same ticket and a user may work on them 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. If the user clicks the button on a warning popup to indicate he wants to continue to work, the forms authentication expiration time should be extended and all warning popups should disappear. If we reach the ticket expiration time, all application should either display the login page or simple disappear depending on if it is the application user logged in 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 find it from server side and pass to the client side as a separate cookie (in the following we will call it expiration-time cookie for convenience). We can find that value from server side only when the ticket is created or updated. We can have a filter to add that value as a cookie in the OnActionExecuted method, see here, or better do that in a http application event handler since then the code can be shared by both MVC and WebForm applications:

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);
        }
    }
}

Notice in classic ASP and ASP.NET, The Request and Response both have a property called Cookies, which makes cookie looks like symmetric in request and response. But actually there is a difference which can be seen better in the http level: in http request, there is a header named Cookie, but in http response, the header that contain cookies is named Set-Cookie, which indicates the fact that the client sends every cookie to server in every request, but the server does not send back every cookie in every response. It only sends the cookie initially when creating it or when modifying it. This is true to any cookie including ticket cookie. Thus we should only set the expiration-time cookie when the response contains the ticket cookie. Notice the way we check if ticket cookie exists in response, we cannot check directly if Context.Response.Cookies[FormsAuthentication.FormsCookieName] is null since that would create the cookie if it does not exist.

Our approach is to check the ticket remaining time repeatedly when needed and act based on it. The time of the next check a decision made at the current check. We will check that initially when the page is loaded, and every time based on the checking result to decide when to check again:

  • if the ticket already expired, take user to the login page or simply close the window
  • if the ticket is going to expire in 2 minutes, display the warning popup if it is not there yet, and check again in 1 second
  • if the ticket’s remaining time is more than 2 minutes, close the popup if it is opened, and wait till 2 minutes from current expiration time to check again

The warning popup window may contain the following elements:

  • a remaining seconds counter that will be updated by every checking
  • an OK button for user to indicate he wants to continue working. Once clicked, send an Ajax call to server to extend the ticket expiration time
  • an Close button for user to indicate he wants to stop working. Once clicked, close the browser window; or
  • a Log Off button for user to indicate he wants to stop working. Once clicked, post back to server to sign off authentication and redirect user to the login page

That are all an application needs to do if it does not use session, which is typical situation in a MVC application. If an application uses session, we need some extra work to make sure session not expired before ticket expires. Here we assume the session timeout value is equal to or greater than the ticket timeout value. We want to renew session before it expires as long as the ticket is not expired. We will modify the last step of the checking routine a little as

  • if the ticket’s remaining time is more than 2 minutes, close the popup if it is opened, send an Ajax call to server to extend the session, and wait till 2 minutes from current expiration time to check again

This modification does not change the frequency of our checking, it only adds Attlee extra work to do. We are going to prove our session will not expire before the ticket expires using mathematical induction. For convinence, we denote the session timeout value as ST and expiration time as SE; the ticket timeout value as TT and the ticket expiration time is TE; and current time as CT.

  • First at the beginning when the page is initially loaded, session has ST remaining time, The ticket may have TT remaining time or less depending on how many remaining time the ticket has. Since ST>=TT, we know SE=CT+ST>=CE+TT>=TE at that time
  • Now we assume after Nth checking, SE>=TE, now consider the (N+1)-th time of checking
    • if we find out the ticket is going to expires in 2 minutes, then it means the ticket has noted extended s last checking, therefore SE>=TE is still valid
    • if we find out the ticket has more then 2 minutes from expiration, it means the ticket got extended sometime after the last checking. We will send an Ajax call to extend session. This call should not have side affect to extend ticket since ticket should be extended only as consequence of user working on the application, and that user may have not worked on this application since last checking and the ticket got extended just because he worked on another application. Luckily that would not happen because when the ticket got extended it must have less that half of TT remaining, which means it must got extended at least half of TT time after the ticket expiration time known at the last checking, then it still has more than half of TT time remaining in this checking. After this call again SE>=TE

We should also expire session when ticket expires so no remnant is carried over when the current user or a new user login from the same browser. Unfortunately there is no ticket expiration event we can attach a handler to, but we can still expire session in the following way:

  • in the action that handles user log off, abandon session in addition to sign off forms authentication
  • when user gets to the login page, sign off forms authentication and abandon session

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.

Ticket-Expiration-Time Cookie Revisit

At the beginning, as show above, I store the following value in the cookie:

ticket.Expiration.ToString()

then at the client side, I can easily get the ticket remaining time as follows

var ticketExpirationTime = $.cookie('ticketExpirationTime');
var remainingMilliseconds = new Date(ticketExpirationTime) - new Date();

but soon I realized that that value does not include the millisecond part of ticket.Expiration, which is one reason the ticket expires later than I thought. I tried to include the milliseconds in the cookie as:

ticket.Expiration.ToString("MM/dd/yyyy hh:mm:ss.fff tt")

but my javascript code have problem to understand such format (e.g. 04/09/2019 08:10:10.123 AM) when passing to new Date and returns NaN. I found Date.parse can handle this value in Chrome, but not in IE. In IE we can do the following

Date.pase('04/09/2019 08:10:10.123 AM')

but it simply ignores the millisecond part (e.g. 123). To support all browsers without any surprise, we better pass datetime as milliseconds instead as string. I tried to store the milliseconds after 1/1/1970:

Math.Ceiling((ticket.Expiration.ToUniversalTime() - new DateTime(1970, 1, 1)).TotalMilliseconds).ToString()

This is the total milliseconds from 1/1/1970 to the current UTC time. This value is comparable to the client side getTime call, so the remaining milliseconds can be found from

var millisecondsRemaining = expirationTime - (new Date()).getTime();

It works fine except now it becomes hard to inspect the expiration time in debugging. Therefore I still prefer to store datetime value in cookie. The solution is to round the expiration time up to the next whole second:

var dt = ticket.Expiration;
var roundupdt = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
if (dt.Millisecond > 0) roundupdt = roundupdt.AddSeconds(1);
var cookie = new HttpCookie("ticketExpirationTime", roundupdt.ToString());

Another issue I realized is the ticket expiration time is regardng the server clock, set at server and check by server-side framework code, and thus we cannot simply compare this value with client clock to determine if ticket expires or not. We need to know the clock difference between browser and server. It is not possible to get the exact value of this difference, but we can easily get a upper bound of it: let server expose the server time in a method, then client makes an Ajax call to this method to get the server time back and compare it with the client time at the receiving time. This is not exact the clock difference but an upper bound of the it (the difference of this upper bound and the exact difference is the time between the server executing the method and the client gets response). With this information we can be sure the ticket expires no later than when we think it expires:

public string GetServerTime() {
    return DateTime.Now.ToString();
}
var clockDifference = (function () {
    var serverTime;
    $.ajax({
        url: serverTimeUrl,
        type: 'get',
        dataType: 'text',
        async: false,
        success: function (data) {
            serverTime = new Date(data);
        }
    });
    var clientTime = new Date();
    return clientTime - serverTime;
})();

We only need to call it once when we start the timeout monitoring. Then every time we check timeout, use that value to adjust the remaining time calculation:

var millisecondsRemaining = new Date(expirationTime) - new Date() + clockDifference;

FAQ

  • Question: In general a http module needs to be registered in order to be used in a web application, but then how come I do not find FormsAuthenticationModule mentioned anywhere in the web.config file of my application?
  • Answer: It is registered in a machine-level web.config (C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config/web.config) and thus available to every web application running in the classic pipeline mode, for more details, see https://stackoverflow.com/questions/20163911/where-is-formsauthenticationmodule-registered.
<system.web>
    <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"/>

It is also available to every web application running in the integrated pipeline mode since it is registered in C:\Windows\System32\inetsrv\config\applicationHost.config:

<system.webServer>
    <modules>
         ...
         <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="managedHandler" />
  • Question: We know that 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?
  • Answer: 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;
}
  • Question: Say we have multiple applications share the same forms authentication ticket, the new expiration time when a ticket get renewed is based on the timeout setting of the application that initially creates the ticket, or the timeout setting of the application that renews this ticket?
  • Answer: The code of the RenewTicketIfOld method shows that it is based on the timeout setting of the application that initially creates the ticket.

Additional Readings

Clone this wiki locally