Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concerning the sliding expiration of ASP.NET's forms authentication and session

We have a ASP.NET 4.5 WebForms application using the native forms authentication and session functionality. Both have a timeout of 20 minutes with sliding expiration.

Imagine the following scenario. A user has worked in our application for a while and then proceeds to do some other things, leaving our application idle for 20 minutes. The user then returns to our application to write a report. However, when the user tries to save, he/she is treated with the login screen, and the report is lost.

Obviously, this is unwanted. Instead of this scenario, we want the browser to be redirected to the login page the moment either authentication or session has expired. To realize this, we have build a Web Api service that can be called to check whether this is the case.

public class SessionIsActiveController : ApiController
{
    /// <summary>
    /// Gets a value defining whether the session that belongs with the current HTTP request is still active or not.
    /// </summary>
    /// <returns>True if the session, that belongs with the current HTTP request, is still active; false, otherwise./returns>
    public bool GetSessionIsActive()
    {
        CookieHeaderValue cookies = Request.Headers.GetCookies().FirstOrDefault();
        if (cookies != null && cookies["authTicket"] != null && !string.IsNullOrEmpty(cookies["authTicket"].Value) && cookies["sessionId"] != null && !string.IsNullOrEmpty(cookies["sessionId"].Value))
        {
            var authenticationTicket = FormsAuthentication.Decrypt(cookies["authTicket"].Value);
            if (authenticationTicket.Expired) return false;
            using (var asdc = new ASPStateDataContext()) // LINQ2SQL connection to the database where our session objects are stored
            {
                var expirationDate = SessionManager.FetchSessionExpirationDate(cookies["sessionId"].Value + ApplicationIdInHex, asdc);
                if (expirationDate == null || DateTime.Now.ToUniversalTime() > expirationDate.Value) return false;
            }
            return true;
        }
        return false;
    }
}

This Web Api service is called every 10 seconds by the client to check if either authentication or session has expired. If so, the script redirects the browser to the login page. This works like a charm.

However, calling this service triggers the sliding expiration of both authentication and session. Thus, essentially, creating never ending authentication and session. I have set a breakpoint at the start of the service to check if it is one of our own functions that triggers this. But this is not the case, it seems to occur somewhere deeper in ASP.NET, before the execution of the service.

  1. Is there a way to disable the triggering of ASP.NET's authentication and session sliding expirations for a specific request?
  2. If not, what is best practice to tackle a scenario like this?
like image 883
maartendekker Avatar asked Jan 31 '13 15:01

maartendekker


2 Answers

  1. This seems to be impossible. Once sliding expiration is enabled, it is always triggered. If there is a way to access the session without extending it, we have not been able to find it.

  2. So how to tackle this scenario? We came up with the following alternative solution to the one originally proposed in the question. This one is actually more efficient because it doesn't use a web service to phone home every x seconds.

So we want to have a way to know when either ASP.NET's forms authentication or session has expired, so we can pro-actively logout the user. A simple javascript timer on every page (as proposed by Khalid Abuhakmeh) would not suffice because the user could be working with the application in multiple browser windows/tabs at the same time.

The first decision we made to make this problem simpler is to make the expiration time of the session a few minutes longer than the expiration time of the forms authentication. This way, the session will never expire before the forms authentication. If there is a lingering old session the next time the user tries to log in, we abandon it to force a fresh new one.

All right, so now we only have to take the forms authentication expiration into account.

Next, we decided to disable the forms authentication's automatic sliding expiration (as set in the web.config) and create our own version of it.

public static void RenewAuthenticationTicket(HttpContext currentContext)
{
    var authenticationTicketCookie = currentContext.Request.Cookies["AuthTicketNameHere"];
    var oldAuthTicket = FormsAuthentication.Decrypt(authenticationTicketCookie.Value);
    var newAuthTicket = oldAuthTicket;
    newAuthTicket = FormsAuthentication.RenewTicketIfOld(oldAuthTicket); //This triggers the regular sliding expiration functionality.
    if (newAuthTicket != oldAuthTicket)
    {
        //Add the renewed authentication ticket cookie to the response.
        authenticationTicketCookie.Value = FormsAuthentication.Encrypt(newAuthTicket);
        authenticationTicketCookie.Domain = FormsAuthentication.CookieDomain;
        authenticationTicketCookie.Path = FormsAuthentication.FormsCookiePath;
        authenticationTicketCookie.HttpOnly = true;
        authenticationTicketCookie.Secure = FormsAuthentication.RequireSSL;
        currentContext.Response.Cookies.Add(authenticationTicketCookie);
        //Here we have the opportunity to do some extra stuff.
        SetAuthenticationExpirationTicket(currentContext);
    }
}

We call this method from the OnPreRenderComplete event in our application's BasePage class, from which every other page inherits. It does exactly the same thing as the normal sliding expiration functionality, but we get the opportunity to do some extra stuff; like call our SetAuthenticationExpirationTicket method.

public static void SetAuthenticationExpirationTicket(HttpContext currentContext)
{
    //Take the current time, in UTC, and add the forms authentication timeout (plus one second for some elbow room ;-)
    var expirationDateTimeInUtc = DateTime.UtcNow.AddMinutes(FormsAuthentication.Timeout.TotalMinutes).AddSeconds(1);
    var authenticationExpirationTicketCookie = new HttpCookie("AuthenticationExpirationTicket");
    //The value of the cookie will be the expiration date formatted as milliseconds since 01.01.1970.
    authenticationExpirationTicketCookie.Value = expirationDateTimeInUtc.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds.ToString("F0");
    authenticationExpirationTicketCookie.HttpOnly = false; //This is important, otherwise we cannot retrieve this cookie in javascript.
    authenticationExpirationTicketCookie.Secure = FormsAuthentication.RequireSSL;
    currentContext.Response.Cookies.Add(authenticationExpirationTicketCookie);
}

Now we have an extra cookie at our disposal that always represents the correct forms authentication expiration time, even if the user works in different browser windows/tabs. After all, cookies have a browser wide scope. Now the only thing left is a javascript function to verify the cookie's value.

function CheckAuthenticationExpiration() {
    var c = $.cookie("AuthenticationExpirationTicket");
    if (c != null && c != "" && !isNaN(c)) {
        var now = new Date();
        var ms = parseInt(c, 10);
        var expiration = new Date().setTime(ms);
        if (now > expiration) location.reload(true);
    }
}

(Note that we use jQuery Cookie Plugin to retrieve the cookie.)

Put this function in an interval, and users will be logged out the moment his or her forms authentication has expired. Voilà :-) An extra perk of this implementation is that you now have control over when the forms authentication's expiration gets extended. If you want a bunch of web services that don't extend the expiration, just don't call the RenewAuthenticationTicket method for them.

Please drop a comment if you have anything to add!

like image 147
maartendekker Avatar answered Oct 07 '22 18:10

maartendekker


Your website functionality should work without JavaScript or you just replace one problem with another. I have tackled this problem also and here is how it was solved:

When you authenticate yourself then session cookie is created with default lifetime on 20 min. When this expires user will be logged out.

cookie image

When user selects "remember me" in the sign in form then additional persistence cookie [AuthCookie] is created in client side and in the database. This cookie has a lifetime of 1 month. Whenever page is loaded, session and persistence cookie data is recreated with a new lifetime (normally you want to decrypt/crypt the ticket).

Imagine the following scenario. A user has worked in our application for a while and then proceeds to do some other things, leaving our application idle for 20 minutes. The user then returns to our application to write a report. When the user tries to save, his session is restored before the request.

One way to do this is to extend global.aspx to handle prerequest. Something in the lines of:

    void application_PreRequestHandlerExecute(object sender, EventArgs e){
        ...
        if (HttpContext.Current.Handler is IRequiresSessionState) {
            if (!context.User.Identity.IsAuthenticated)
                AuthService.DefaultProvider.AuthenticateUserFromExternalSource();

AuthenticateUserFromExternalSource should check if cookie data matches with the database one, because anything stored in client side can be changed. If you have paid services with access rights then you need to check if user still has those rights and then you can recreate the session.

like image 29
Margus Avatar answered Oct 07 '22 20:10

Margus