-
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 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 within each application to help maintain 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. The 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 application 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 that 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 fully controlled by each application. Forms authentication provides a way to maintain such information and establish authentication to generic 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 session expires only if you idle for than many minutes, before session expires everytime you send request to server the session expiration time is extended to have that many minutes from the current time. 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 server receives a request after half of the timeout value has passed.
Session and ticket can time out separately and independently. If we want the session to always time out 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 times out, 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 time out, thus it times out before the ticket does. 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 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.
The ticket expiration time is part of the forms authentication data, which is encrypted at server side. Thus we cannot get this value at client side using javascript code. We need to find it at server side and pass it to the client side as a separate cookie (in the following we will call it expiration-time cookie for convenience). We can have a filter to add that value as a cookie in the OnActionExecuted method in a MVC application, 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 objects both have a property called Cookies, which makes cookie looks like symmetric in http request and http response. But actually there is a difference which can be seen better in the http level: in a http request, there is a header named Cookie, but in a 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 Periodically and act based on it. When ticket exists and not expired, we check every second to see
- if the ticket already expired or is missing, take user to login page
- if the ticket is going to expire in 2 minutes, display the warning popup if it is not opened yet
- if the ticket’s remaining time is more than 2 minutes, close the popup if it is opened
Originally I thought if there is more than 2 minutes left, we do not need to check every second, and can wait it we thin there are only 2 minutes left to check again. But that approach cannot handle the situation that user may click a logoff button on a page (not the logoff button in the warning popup window) to manually logoff.
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
- a Log Off button for user to indicate he wants to stop working. Once clicked, post back to server to sign off authentication
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 need to 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 if we detect ticket expiration time changes (the reason we need this condition is we should only extend session only every time ticket extends)
We are going to prove our session will not expire before the ticket expires as long as the session timeout value is a little bigger than the ticket timeout value using mathematical induction. For convinence, we denote the session timeout value as ST and the session 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 after 1 second the (N+1)-th time of checking happens: if there is no change to TE, then that inequality is still valid; if TE changes, we send a request to extend session. At then end that inequality is valid again
We should 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.
When ticket expires, every application window is loaded with the login page. I cannot close any of them as we originally planned because browser prevents window to be closed by javascript code.
In the last section, I mentioned the need to make an Ajax call to extend session. How can we achieve that? Notice session is specific to each application, and thus this call has to be a request to a resource within an individual application. Is it possible we can make it to happen without modifying each individual application? The solution is to use a http handler. The handler is implemented in a separate assembly added to GAC, then used by every application through global configuration.
At the beginning, as shown 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 expiration time as milliseconds as follows:
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 when 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 difference since we cannot check clock at server side and at client side at the exact same time, 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 it (the difference of this upper bound and the exact difference is the time between the server executing the method and the client gets the response). Using this information we can make 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;Our timeout monitoring routine is a javascript file stored in a centralized location and included in every page in every web application. How can we make sure client will get the newest version if we update that javascript file? One possible approach is to prevent client side from caching, e.g. add a version based on the current time when adding reference to that script file. But this approach is not favored since it adds two much traffic. Therefore I decide to add a driver file: that is the file included in every page. This file is very simple and its only functionality is to load the monitoring routine. This file has no version since I don’t expect it to change. The version of the monitoring routine file in introduced in the driver script when it includes the monitoring routine. How do we generate that version number directly determines how often the client side can cache the monitoring routine. I have thought of the following approaches:
- Every time user creates a new browser session, we use a new version number. I expect we can achieve that using a session cookie. The driver checks if that cookie exists. If not exist, it means this is a browser user just opened, so derive a version number of the current time then put it in the session cookie. Then as long as that cookie exists, use that cookie value as the version number. In that way we only get the monitoring routine once in every session. I like this approach but I do not have a reliable simple way to create cookie in javascript without using jquery.cookie.js. So I did not use it
- Due to the difficulty of creating cookie, I prefer approach that is based on existing cookies. We cannot use the asp.net session id cookie because it is http-only cookie and thus not available to javascript code. I end up using the ticket expiration-time cookie, which is still OK since the client side only get the monitoring routine every time the ticket extends
One thing deserves mentioning is the way the driver adds the monitoring routine script. At first I use jquery way to add that script line to the body. Then I notice in some application, jquery add an extra version number, which causes the script not cachable. I believe it is caused by a global no-caching Ajax call setting in those applications. Therefore I switch to the javascript way of creating a script element set its src property then add to the body.
In summary, our solution of timeout warning feature consists of the following 6 pieces:
- an assembly in GAC containing (both requires configuration)
- a http module to setup ticket expiration time cookie
- a http handler to extend session
- an independent web application
- a js file to establish client-side timeout monitoring checking, and to support the popup warning window. This file should be included by every web page in every web application (normally through layout page or master file)
- a css file chaining popup warning window styles, it is included to web pages by the js file above
- a generic handler to extend ticket
- a generic handler to logoff
- 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.
-
Question: How to create a http module used for all applications on a server?
-
Answer:
- create a project of type class library and add reference to System.Web.dll, then create a class to inherit IHttpModule and add the application event handler code there. Clicking Signing tab of the Property Page window, and check 'Sign the Assembly' where we can choose a string key name file (the extension is .snk), and we can ask VS to generate the content (i.e. VS can generate a key for us)
- add it to GAC on my machine: run Developer Command Prompt for Visual Studio 2017 as Administrator, and execute
gacutil -i C:\dev\MyTestModule.dll - find this dll in GAC: C:\Windows\Microsoft.NET\assembly\GAC_MSIL\MyTestModule\v4.0_1.0.0.0__012345aaaaaaaaaa, where 4.0 is .NET runtime version, 1.0.0.0 is the dll version, and 0012345aaaaaaaaaa is public key token
- add it to the system.web/httpModules section in the global level web.config (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\web.config) to support applications running in the classic pipeline mode, and to the system.webServer/modules section in C:\Windows\System32\inetsrv\config\applicationHost.config to support application running in integrated pipeline mode. As an alternative, we can add it to the web.config in the web site root folder (normally C:\inetpub\wwwroot) instead in both the system.web/httpModules and system.webServer/modules sections.
-
Question: I added handlers/modules to C:\Windows\System32\inetsrv\config\applicationHost.config, but it seems they are not running?
-
Answer: We can verify if a handler or module is running by inspecting the Handler Mappings and Modules panes to see if the handler or the module is listed there. If they are not there, probably the reason is IIS uses a different applicationHost.config file. C:\Windows\System32\inetsrv\config is the default path of this file, but its location can be changed: in IIS Manager, select the machine node, then in the Features View pane, select Management->Shared Configuration, there we can set Configuration Location->Physical Path; or we can modify the redirection.config file in C:\Windows\System32\inetsrv\config, and enter the new path there.
-
Question: The FormsAuthenticationTicket class has a property named IsPersistent that determines if the forms authentication ticket cookie should be persistent or not. But how come there is no corresponding attribute in the system.web/authentication/forms node in web.config that allows application owner to set this value?
-
Answer: Microsoft expects this is not an application-level setting, instead it is decision made by each user. Take a look at the Login control (https://github.com/microsoft/referencesource/blob/master/System.Web/UI/WebControls/login.cs) . There is a "Remember Me" checkbox in the login screen. If a user select that checkbox, then the Login control would create a persistent cookie for the forms authentication ticket:
private void AttemptLogin() {
...
AuthenticateEventArgs authenticateEventArgs = new AuthenticateEventArgs();
OnAuthenticate(authenticateEventArgs);
if (authenticateEventArgs.Authenticated) {
System.Web.Security.FormsAuthentication.SetAuthCookie(UserNameInternal, RememberMeSet);
...