Session Cookies expiration handling in ASP.NET MVC

2019-02-18 02:38发布

问题:

I my project I'm using WIF (but this is not really important for the context of this question. You can use alternative framework which handles your authentication. Question is about dealing with authentication failures while performing ajax requests). Nevertheless, in my case I've written custom server logic which inherits from ClaimsAuthenticationManager, and handles authentication:

public override IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal)
{
    if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated)
    {
        // add some custom claims
    }
    return incomingPrincipal;
}

Now, after I delete all Session Cookies, end then enter any page again, I'm redirected to the login page served by WIF, and I'm requested to log again. Everything works as expected.

But if I make an ajax request instead, I've got an error, which is intercepted by this:

$(document).ready(function () {
    $.ajaxSetup({
        error: function (XMLHttpRequest, textStatus, errorThrown) {            
            // do something
        }
    });
});

Unfortunately XMLHttpRequest object does not return any meaningful message, based on which I could handle this kind of error in any other way as others. In this particular case I just want application to redirect to the login page - as the normal request does.

While the ajax call is executing, the method Authenticate from ClaimsAuthenticationManager is invoked. Identity.IsAuthenticated returns false, method ends and all is done. Even the OnAuthorization method from BaseController is not invoked, so I cannot pass any status to the ajax result object.

protected override void OnAuthorization(AuthorizationContext filterContext)
{
    if (filterContext.HttpContext.Request.IsAjaxRequest() && !User.Identity.IsAuthenticated)
    {
        //do something, for example pass custom result to filterContext
    }
    base.OnAuthorization(filterContext);
}

How to resolve the puzzle ?

回答1:

I've found some resources about this (see bottom of the answer), and mixed up with following solution:

while performing ajax request, I specified that I want json back:

$.ajax({
    url: action,
    type: 'POST',
    dataType: 'json',
    data: jsonString,
    contentType: 'application/json; charset=utf-8',
    success:
        function (result, textStatus, xhr) {
        }
});

Because my framework handles authentication, while token expires, it puts http status 302 to the response. Because I don't want my browser to handle 302 response transparently, I catch it in Global.asax, and changed status to 200 OK. Aditionally, I've added header, which instructs me to process such response in special way:

protected void Application_EndRequest()
{
    if (Context.Response.StatusCode == 302
        && (new HttpContextWrapper(Context)).Request.IsAjaxRequest())
    {                
        Context.Response.StatusCode = 200;
        Context.Response.AddHeader("REQUIRES_AUTH", "1");
    }
}

Response content is not properly serialized to json, which results in parsing error. Error event is invoked, inside which the redirection is performed:

$(document).ready(function () {
    $.ajaxSetup({
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            if (XMLHttpRequest.getResponseHeader('REQUIRES_AUTH') === '1') {
                // redirect to logon page
                window.location = XMLHttpRequest.getResponseHeader('location');
            }
            // do sth else
        }
    });
});

See How to manage a redirect request after a jQuery Ajax call and here How do you deal with AJAX requests when user session expires, or where the request terminates in a 302 for more explanation.

UPDATE:

Meantime, I figured out new solution, in my opinion much better, because can be applied to all ajax requests out of the box (if they do not redefine beforeSend event obviously):

$.ajaxSetup({
    beforeSend: checkPulse,
    error: function (XMLHttpRequest, textStatus, errorThrown) {
        document.open();
        document.write(XMLHttpRequest.responseText);
        document.close();
    }
});

function checkPulse(XMLHttpRequest) {
    var location = window.location.href;
    $.ajax({
        url: "/Controller/CheckPulse",
        type: 'GET',
        async: false,
        beforeSend: null,
        success:
            function (result, textStatus, xhr) {
                if (xhr.getResponseHeader('REQUIRES_AUTH') === '1') {
                    XMLHttpRequest.abort(); // terminate further ajax execution
                    window.location = location;
                }
            }
    });
}

The controller method can be anything simplest:

[Authorize]
public virtual void CheckPulse() {}

The Application_EndRequest() stays the same as before.