8

We are using Ajax call across the application- trying to find out a global solution to redirect to login page if session is already expired while trying to execute any Ajax request. I have coded following solution taking help from this post - Handling session timeout in ajax calls

NOT SURE WHY IN MY CARE EVENT "HandleUnauthorizedRequest" DOES NOT GET FIRED.

Custom Attribute:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class CheckSessionExpireAttribute :AuthorizeAttribute
    {
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                var url = new UrlHelper(filterContext.RequestContext);
                var loginUrl = url.Content("/Default.aspx");

                filterContext.HttpContext.Session.RemoveAll();
                filterContext.HttpContext.Response.StatusCode = 403;
                filterContext.HttpContext.Response.Redirect(loginUrl, false);
                filterContext.Result = new EmptyResult();
            }
            else
            {
                base.HandleUnauthorizedRequest(filterContext);
            }

        }

    }

Using Above custom attribute as follow in controller action:

 [NoCache]
 [CheckSessionExpire]
 public ActionResult GetSomething()
 {
  }

AJAX Call(JS part):

function GetSomething()
{
   $.ajax({
        cache: false,
        type: "GET",
        async: true,
        url: "/Customer/GetSomething",
        success: function (data) {

        },
        error: function (xhr, ajaxOptions, thrownError) {

        }
}

Web Config Authentication settings:

  <authentication mode="Forms">
      <forms loginUrl="default.aspx" protection="All" timeout="3000" slidingExpiration="true" />
    </authentication>

I am try to check it by deleting browser cooking before making ajax call but event "CheckSessionExpireAttribute " does not get fired- any idea please.

Thanks,

@Paul

paul sim
  • 433
  • 2
  • 8
  • 23
  • Did you remove the `.ASPXAUTH` cookie before sending the request? – shakib Sep 21 '17 at 09:43
  • Are you wanting to check **Session** expiration or **Logged Out / Login Expired**? I think you are wanting to do something with the AJAX request if the user is logged out but I wanted to clarify since you are using the terms interchangeably. – Tommy Sep 25 '17 at 17:02
  • @Tommy assuming he's talking about login expiration, how would that be any different from session expiration? – Tiramonium Sep 26 '17 at 19:09
  • 1
    @Tiramonium `Session` is used to store variable between request for a specific user in either the web server's memory or some other persistent storage mechanism. The end user is typically given a cookie with a `Session Id`. An authentication cookie (or token) is typically an encrypted cookie that stores your logout expiration time and your user id or user name. The web server uses this to ensure you are an authenticated user and are authorized for the request you made. You can have only session, only authorization, neither or both. They are completely independent of each other. – Tommy Sep 26 '17 at 19:45
  • @Tiramonium - They also have different configuration areas/settings. I've seen devs accidentally let a session timeout be 20 minutes and the authorization logout be an hour. Users would "lose" data after 20 minutes of idle even though they were still logged in for instance. That was a fun one :) – Tommy Sep 26 '17 at 19:48
  • @Tommy if I got this right, the authentication duration in MVC is set in the Web.config solution file, which sets the authentication cookie expiration. But where is the session timeout usually set at? – Tiramonium Sep 26 '17 at 20:12
  • Still in the web.config, just a different section (https://stackoverflow.com/questions/1205828/how-to-set-session-timeout-in-web-config). However, be careful because in the new Identity framework stuff, you set your configurations for authentication in the `startup.cs` file (https://stackoverflow.com/questions/27027748/asp-net-identity-session-timeout). Lastly, here is an SO answer discussion this topic - https://stackoverflow.com/questions/17812994/forms-authentication-timeout-vs-sessionstate-timeout – Tommy Sep 26 '17 at 20:25
  • @Tommy,Tiramonium - What i wanted to achieve is if session is already expired and user try to make any ajax call then event "HandleUnauthorizedRequest" will get fire and from there if it is an ajax call user will be redirected to login page. As per my knowledge as ajax call follow the same request pipe line except rendering part, event "HandleUnauthorizedRequest" should get fired. In my case, as mentioned in my earlier comment, during ajax call if session is expired control directly goes to ERROR part of ajax call, controller action is not get called by ajax. – paul sim Sep 27 '17 at 05:39
  • You already sending status code 403, why don't you use it and redirect at client. something like following `error: function (xhr, ajaxOptions, thrownError) { if(xhr.status === 403){ location.href = '/Default.aspx' } }` remove the `filterContext.HttpContext.Response.Redirect(loginUrl, false);` which wont have any effect – Nilesh Sep 27 '17 at 15:04
  • @Nilesh- As mentioned earlier "HandleUnauthorizedRequest" event does not get fired when session is expired, so in JS i am not getting 403. – paul sim Sep 27 '17 at 17:25

5 Answers5

2

If I got the question right (and even if I didn't, thanks anyway, helped me solve my own situation), what you wanted to avoid was having your login page to load inside an element which was supposed to display a different View via Ajax. That or get an exception/error status code during a Ajax form post.

So, in short, the annotation class will need to override 2 methods, not just HandleUnauthorizedRequest, and it will redirect to a JsonResult Action that will generate the parameters for your Ajax function to know what to do.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class SessionTimeoutAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        IPrincipal user = filterContext.HttpContext.User;
        base.OnAuthorization(filterContext);
        if (!user.Identity.IsAuthenticated) {
            HandleUnauthorizedRequest(filterContext);
        }
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new RedirectToRouteResult(new
            RouteValueDictionary(new { controller = "AccountController", action = "Timeout" }));
        }
    }
}

Then set this annotation in your authentication Action, so every time it gets called, it will know where the request came from, and what kind of return it should give.

[AllowAnonymous]
[SessionTimeout]
public ActionResult Login() { }

Then your redirected Json Action:

[AllowAnonymous]
public JsonResult Timeout()
{
    // For you to display an error message when the login page is loaded, in case you want it
    TempData["hasError"] = true;
    TempData["errorMessage"] = "Your session expired, please log-in again.";

    return Json(new
    {
        @timeout = true,
        url = Url.Content("~/AccountController/Login")
    }, JsonRequestBehavior.AllowGet);
}

Then in your client function (I took the privilege of writing it as $.get() instead of $.ajax():

$(document).ready(function () {
    $("[data-ajax-render-html]").each(function () {
        var partial = $(this).attr("data-ajax-render-html");
        var obj = $(this);

        $.get(partial, function (data) {
            if (data.timeout) {
                window.location.href = data.url;
            } else {
                obj.replaceWith(data);
            }
        }).fail(function () {
            obj.replaceWith("Error: It wasn't possible to load the element");
        });
    });
});

This function replaces the html tag with this data-ajax-render-html attribute, which contains the View address you want to load, but you can set it to be loaded inside the tag by changing replaceWith for the html() property.

Tiramonium
  • 406
  • 4
  • 13
  • @Tiramonium- Thanks for your suggestion- i will try your solution and let you know. In my case, if session is already expired and user click any button which make an ajax request- in that case "OnAuthorization/HandleUnauthorizedRequest" event does not get fired. – paul sim Sep 27 '17 at 17:44
  • Simply if session is already expired and user make any ajax request then user should be redirected to login page- to achieve that i wanted to have a custom attribute which will work for any ajax request in my application. For that i implemented CheckSessionExpireAttribute but dont know why HandleUnauthorizedRequest event does not get fire if session is expired- Hope i am able to communicate my problem. – paul sim Sep 27 '17 at 17:52
  • @paulsim yes, this is exactly what my answer aims at. You place the annotation on your Login Action, then if it detects it got called from a JsonResult Action, it will redirect to a different JsonResult Action which will communicate to the ajax function you want to redirect the whole DOM instead of just the ajax element. Try it and let me know how it works out for you. – Tiramonium Sep 27 '17 at 18:22
1

I think that is only a client-side problem. In web server you can just use the classic Authorize attribute over actions or controllers. That will validate that the request is authenticated (if there's a valid authentication cookie or authorization header) and sets HTTP 401 if not authenticated.

Note: a session will automatically be recreated if you don't send authorization info in the request, but the request will not be authorized

Solution

Then the javascript client you must handle the redirect (browsers do it automatically but with ajax you need to do it manually)

$.ajax({
    type: "GET",
    url: "/Customer/GetSomething",
    statusCode: {
       401: function() {
          // do redirect to your login page
          window.location.href = '/default.aspx'
       }
    }
});
Luca Corradi
  • 1,573
  • 1
  • 13
  • 16
0

I checked and tested the code, looks like clearly.. the problem is that the ajax call is wrong..

I fix Ajax code, try this..

function GetSomething() {
        $.ajax({
            cache: false,
            type: "GET",
            async: true,
            url: "/Customer/GetSomething",
            success: function (data) {

            },
            error: function (xhr, ajaxOptions, thrownError) {

            }
        });
    }
  • In my case HandleUnauthorizedRequest event does not get fired. – paul sim Sep 21 '17 at 10:04
  • https://stackoverflow.com/questions/11088381/handleunauthorizedrequest-not-overriding I hope this might help you @paulsim – Suvethan Nantha Sep 25 '17 at 16:45
  • In my case, if session is expired(tried deleting browser cookies), while debugging ajax method, it is seen control directly goes to error section of ajax method with below error- "Object moved

    Object moved to here.

    " I think that is why mehtod "HandleUnauthorizedRequest" in custom atribute does no get fired. ANY IDEA PLEASE.
    – paul sim Sep 26 '17 at 08:39
0

On HttpContext.Request.IsAjaxRequest()

Please see this related article on why an Ajax request might not be recognized as such.

XMLHttpRequest() not recognized as a IsAjaxRequest?

It looks like there is a dependency on a certain header value (X-Requested-With) being in the request in order for that function to return true.

You might want to capture and review your traffic and headers to the server to see if indeed the browser is properly sending this value.

But, are you even sure it's hitting that line of code? You might also want to debug with a break point and see what values are set.

On Session vs Authentication

Authorization and Session timeout are not always exactly the same. One could actually grant authorization for a period longer than the session, and if the session is missing, rebuild it, as long as they are already authorized. If you find there is something on the session that you'd be loosing that can't be rebuilt, then perhaps you should move it somewhere else, or additionally persist it somewhere else.

Form Authentication cookies default to timeout after 30 minutes. Session timeout default is 20 minutes.

Session timeout in ASP.NET

HandleUnauthorizedRequest not overriding

Greg
  • 1,982
  • 17
  • 20
0

Sorry to say that: The solution you need is impossible. The reason is:

  • To redirect user to login page, we have 2 methods: redirect at server, redirect at client
  • In your case, you're using Ajax so we have only 1 method: redirect at client (reason is, basically, Ajax means send/retrieve data to/from server. So it's impossible to redirect at server)
  • Next, to redirect at client. Ajax need to information from server which say that "redirect user to login page" while global check session method must be return Redirect("url here").
  • clearly, global check session method can not return 2 type (return Redirect(), return Json,Xml,Object,or string)

After all, I suggest that:

  • Solution 1: Don't use ajax
  • Solution 2: You can use ajax, but check session timeout method at server which is not globally. Mean that you must multiple implement (number of ajax call = number of implement)
chibao
  • 23
  • 6