Asp.net MVC - How to check session expire for Ajax request - asp.net-mvc

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

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.

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'
}
}
});

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) {
}
});
}

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

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)

Related

How to use a custom "__RequestVerificationToken"?

I've made a partial view like this (location: MyController/_Form.cshtml):
<form asp-antiforgery="true">
<input type="button" value="submit" />
</form>
Some actions in Controller:
[HttpPost, ValidateAntiForgeryToken]
public IActionResult Test()
{
return Ok(new { succeeded = true });
}
[HttpPost]
public IActionResult GetTemplate()
{
string template = _viewRender<string>("MyController/_Form", null);
return Ok({ template = template });
}
The _viewRender is a service to convert from partial view to a string.
I've tested with these steps:
Using jquery to make a request from client to server to get the template and append to some div.
let onSuccess = function (data) {
$(data.template).appendTo('.myDiv');
};
$.ajax({
url: '/MyController/GetTemplate',
method: 'POST'
}).done(onSuccess).fail(onError);
And the event to detect submiting form looks like:
$(document).on('click', 'input[type=text]', function () {
let _this = $(this);
let token = _this.parent().find('[name=__RequestVerificationToken]').val();
let onSuccess = function (data) {
console.log(data); // should be: Object:{succeeded:true}
};
$.ajax({
url: '/MyController/Test',
method: 'POST',
data: { __RequestVerificationToken: token },
processData: false,
contentType: false
}).done(onSuccess).fail(onError);
});
When I made the request, I always got error code 404 - not found on Console tab.
I'm sure the path is correct. So, I've tried to remove ValidateAntiForgeryToken attribute from Test action and tried again. It's working fine (request status code 200).
So, I guess the problem (which gave 404 error) came from the token. I've used developer tool to check again and I'm sure that I have a token. But I don't know how to check the token is valid or not.
The token was generated from server. I just made a request to get it and appended to body. Then, re-sent it to server. But server didn't accept it.
Why?
This is how it's done in ASP.NET Core...
In Startup.cs you'll need to setup the anti-forgery header name.
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
You need to do this because by default anti-forgery will only consider form data and we want it to work with ajax too.
In your .cshtml file you'll need to add #Html.AntiForgeryToken() which will render a hidden input with the validation token.
Finally in your ajax code you need to setup the request header before sending.
beforeSend: function(xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
}
So in your case the ajax code will look like this.
$.ajax({
url: '/MyController/Test',
method: 'POST',
beforeSend: function(xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
processData: false,
contentType: false
}).done(onSuccess).fail(onError);
Two things.
First, I use a custom filter instead of ValidateAntiForgeryToken. I don't remember why. Probably ValidateAntiForgeryToken doesn't work with AJAX requests.
Here's the code for the custom filter I use.
[AttributeUsage(AttributeTargets.Class)]
public sealed class ValidateAntiForgeryTokenOnAllPostsAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException(nameof(filterContext));
}
var request = filterContext.HttpContext.Request;
// Only validate POSTs
if (request.HttpMethod == WebRequestMethods.Http.Post)
{
// Ajax POSTs and normal form posts have to be treated differently when it comes
// to validating the AntiForgeryToken
if (request.IsAjaxRequest())
{
var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];
var cookieValue = antiForgeryCookie?.Value;
AntiForgery.Validate(cookieValue, request.Headers["__RequestVerificationToken"]);
}
else
{
new ValidateAntiForgeryTokenAttribute().OnAuthorization(filterContext);
}
}
}
}
Second, the token goes in the request header not the data part. I add it to the header using ajaxSetup in the layout file. That way I don't have to worry about remembering to add it to every AJAX request.
$.ajaxSetup({
cache: false,
headers: { "__RequestVerificationToken": token }
});

how to send authenticated ajax call to web API

I have a Web Api Application which has the following question.
[HttpGet]
[Route("Account/userName{userName}/password={password}/rememberMe/{rememberMe}")]
public HttpResponseMessage LogIn(string userName, string password, bool rememberMe)
{
if (User.Identity.IsAuthenticated)
{
return Request.CreateResponse(HttpStatusCode.Conflict, "already logged in.");
}
var dbPerson = dbContext.Persons.Where(x => x.UserName.Equals(userName) && x.EncryptedPassword.Equals(password)).FirstOrDefault();
if (dbPerson != null)
{
FormsAuthentication.SetAuthCookie(userName, rememberMe);
return Request.CreateResponse(HttpStatusCode.OK, "logged in successfully");
}
else
{
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
}
I am calling from another MVC project. I Got the authentication but very next page where I am calling the ajax method
var uri = 'http://localhost:44297/api/XXXX';
$(document).ready(function () {
// Send an AJAX request
$.getJSON(uri)
.done(function (data) {
// On success, 'data' contains a list of products.
for (var i = 0; i < data.$values.length; i++)
{
}
})
.fail(function() {
console.log( "error" )});
});
I am getting GET http://localhost:44297/api/StudyFocus 401 (Unauthorized). how I can solve this issue. I know I need to pass some cookie/session value with this ajax call. but I don't know how. can anyone explain me with example.
My application relies on web Api project including authentication. I need to make web api application secure using form authentication. Any help is highly appreciable. Thanks
You can't authenticate web api by the use of cookies or session. You need access token to do that.
Follow this tutorial for the implementation http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api

Having problems with Membership Login expiring and use of Ajax.ActionLink, login page appears in Ajax return area

I am using MVC3, ASP.NET4.5, C#, Razor, Ajax
I have implemented an Ajax.Actionlink. All works well.
However, since I am also using Membership Services, I will get logged out of the system if I do not use it for say 20 mins. If one is on the Ajax page, and one clicks an Ajax link, the Ajax returns, within the TargetDiv, the Login page and not the correct partial view.
I suspect that if the app has expired then the response needs to override the ajax response, and return the full login page.
How can I solve this please.
You can use a custom authorize attribute to handle the situation.
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var request = filterContext.HttpContext.Request;
var response = filterContext.HttpContext.Response;
if (request.IsAjaxRequest())
{
response.StatusCode = 590; //custom status code, might as well be 401, dont know if that would violate any proncipal
filterContext.Result = new EmptyResult();
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
add some js codes in master page to handle the custom status code.
$(function () {
return $(document).ajaxError(function (e, xhr, opts) {
if (590 == xhr.status) {
return document.location = document.location;
}
});
});
use it as any [Authorize] attribute for the ajax actions you like to be authorized
public class HomeController : Controller
{
[AjaxAuthorize]
public JsonResult AjaxMethod()
{
return Json(new {message = "Hello"}, JsonRequestBehavior.AllowGet);
}
}
The custom Authorize filter will handle the unauthorized request and issue an empty
result with status code 590
The response will be caught as ajaxerror since the statuscode is 5**
The document will be reloaded (assuming the page is an authenticated one), as such will be redirected to login page.
Hope this helps

Redirect from Unathorized request to a new view

I want to redirect to a view, but the view is loading in the partial view which the [Authorize] attribute is on.
is there something else than response.redirect ?
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
string authUrl = this.redirectUrl; //passed from attribute NotifyUrl Property
//if null, get it from config
if (String.IsNullOrEmpty(authUrl))
{
authUrl = System.Web.Configuration.WebConfigurationManager.AppSettings["RolesAuthRedirectUrl"];
}
if (!String.IsNullOrEmpty(authUrl))
{
filterContext.HttpContext.Response.Redirect(authUrl);
}
}
base.HandleUnauthorizedRequest(filterContext);
}
[AuthorizeUsers(Roles = "Administrator", NotifyUrl = "/CustomErrors/Error404")]
public ActionResult addToCart(int ProductID, string Quantity)
{
...
}
It appears that you are invoking the controller action that is decorated with this Authorize attribute using an AJAX call. And you want to fully reload the page if the user is not authorized.
Phil Haack wrote an very nice blog post illustrating how you could achieve that: http://haacked.com/archive/2011/10/04/prevent-forms-authentication-login-page-redirect-when-you-donrsquot-want.aspx/
The idea is simple. Since you made an AJAX request to this controller action, the only way to move out from the partial is to make the redirect using javascript. So for example your AJAX code could detect if the contoller action returned 401 HTTP status code and then use the window.location.href to redirect away to the login page:
$.ajax({
url: '/protected',
type: 'POST',
statusCode: {
200: function (data) {
// Success
},
401: function (data) {
// Handle the 401 error here.
window.location.href = '/login'
}
}
});
The only difficulty here is how to make the standard Authorize attribute return 401 instead of attempting to redirect to the login page. And Phil Haack illustrated the AspNetHaack NuGet which achieves this.

Different response to non-authenticated users and AJAX calls

My ASP MVC (1.0) website has a default login page (based on OpenId - but that shouldn't make a different). It works fine when AuthorizedAttribute is on the Action/Controller.
However, I have AJAX requests coming in as well. Here is what I do with them:
if (Request.IsAjaxRequest())
{
if (Request.IsAuthenticated)
{
// Authenticated Ajax request
}
else
{
// Non-authenticated Ajax request.
Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Json(new { response = "AUTHENTICATION_FAILED" });
}
}
The problem is if I set the Response.StatusCode to Unauthorized, the request is redirected to my login page which is not good for Ajax requests.
Any suggestions for this issue is appreciated.
This is a common problem.
The Authorize attribute returns a Http 401 Unauthorized response. Unfortunately, however if you have FormsAuthentication enabled, the 401 is intercepted by the FormsAuthenticationModule which then performs a redirect to the login page - which then returns a Http 200 (and the login page) back to your ajax request.
The best alternative is to modify your authorization code to return a different Http status code - say 403 - which is not caught by the formsAuthenticationModule and you can catch in your Ajax method.
You can make your own authorize filter that inherent from the framework one, and override the function that write to the response when the user is not authorized, not setting the status code if the request is from ajax. Something like this:
public class MyAutorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
filterContext.Result = new JsonResult() { Data = new { response = "AUTHENTICATION_FAILED" } };
else
filterContext.Result = new HttpUnauthorizedResult();
}
}
And now on your action use the new attribute
[MyAutorize]
public ActionResult myAction()
{
if (Request.IsAuthenticated) // You should not need to ask this here
{
// Authenticated Ajax request
}
else
{
// Non-authenticated Ajax request.
Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Json(new { response = "AUTHENTICATION_FAILED" });
}
}
One way of solving this is to add a text to uniquely identify the login page and use it in AJAX call back to redirect to a login page again. Here is a sample code using jQuery global callbacks....
$(document).bind("ajaxComplete", function(event, response, ajaxOptions) {
if (response.status == 200 && response.responseText.match(/LOGIN_PAGE_UNIQUE_KEY/)) {
self.location = "/web/login?timeout=1";
return false;
}
});

Resources