-
Notifications
You must be signed in to change notification settings - Fork 0
Forms Authentication
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 classical ASP and ASP.NET applications store user information such as username as session variables. Since http communication is stateless, classical 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 classical 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/.
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 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. If the user clicks the button on the warning popup to indicate he wants to continue working on the application, 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 when the ticket is created or updated, then have a global filter to add it 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 the client sends the ticket cookie to server in every request, but the server does not send back that cookie in every response. It only sends the ticket cookie initially when creating it or when modifying it to extend its expiration time. 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. We will check that initially when the page is loaded, and based on the checking result decide when to check again:
- if the ticket already expired, take user to the login page if user login from this application, or simply close the window otherwise
- 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 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.
- Question: 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?
- Answer: 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"/>- 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.