Apr 19th 2008 05:33 pm

Detecting authentication cookie expiration on asyncronous requests

As functionality moves from postbacks and server side processing to the client via update panels and ajax behaviors, a common situation can occur when the user’s authentication cookie has expired and the user executes an asynchronous request. 

Normally with a synchronous request, the web server will return a response with HTTP code 301 (redirect) and the browser will redirect the user to the login page.  However, for asynchronous requests, however, the server simply responds with an HTTP code of 500 (error).
The ms ajax framework renders asynchronous requests as JSON rather than the standard SOAP.  This makes consuming server resources via javascript much easier.  The problem is that the ms ajax framework doesn’t distinguish between unhandled errors and authentication timeouts when processing requests as JSON.

The code that controls this is the class System.Web.Script.Services.RestHandler of the system.web.extensions assembly.

internal static void ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData) {
	try
	{
		if (!s_permissionSetChecked) 	{
			s_permissionSet = (NamedPermissionSet) typeof(HttpRuntime).GetProperty("NamedPermissionSet", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null, null);
			s_permissionSetChecked = true;
		}
		if (s_permissionSet != null) 	{
			s_permissionSet.PermitOnly();
		}
		IDictionary rawParams = GetRawParams(methodData, context);
		InvokeMethod(context, methodData, rawParams);
	} catch (Exception exception) {
		//THIS IS WHERE .NET CATCHES ALL EXCEPTIONS AND RETURNS THE 500 error
		WriteExceptionJsonString(context, exception);
	}
}                  

internal static void WriteExceptionJsonString(HttpContext context, Exception ex) {
	context.Response.ClearHeaders();
	context.Response.ClearContent();
	context.Response.Clear();
	context.Response.StatusCode = 500;
	context.Response.StatusDescription = HttpWorkerRequest.GetStatusDescription(500);
	context.Response.ContentType = “application/json”;
	context.Response.AddHeader(”jsonerror”, “true”);
	using (StreamWriter writer = new StreamWriter(context.Response.OutputStream, new UTF8Encoding(false)))
	{
		if (ex is TargetInvocationException) {
			ex = ex.InnerException;
		}
		if (context.IsCustomErrorEnabled) {
			writer.Write(JavaScriptSerializer.SerializeInternal(new WebServiceError(AtlasWeb.WebService_Error, string.Empty, string.Empty)));
		} else {
			writer.Write(JavaScriptSerializer.SerializeInternal(new WebServiceError(ex.Message, ex.StackTrace, ex.GetType().FullName)));
		}
		writer.Flush();
	}
}

To get around this issue we need to detect JSON responses with HTTP code 500 and no authentication cookie in order to return our own slightly modified response. This is done with an http module.

Imports System                  

	Public Class SessionModule
		Implements IHttpModule                  

#Region " IHttpModule Implementation "                  

		Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
			If context Is Nothing Then
				Throw New System.ArgumentNullException("context")
			End If                  

			AddHandler context.EndRequest, AddressOf OnEndRequest                  

		End Sub                  

#End Region                  

#Region " Private Methods "                  

		Private Sub OnEndRequest(ByVal sender As Object, ByVal e As EventArgs)
			'if this is a web service call with no auth cookie, send a different response than the generic error so that we can handle it appropriately
			'not the "customErrors" section defines (if not "off") that generic messages will be sent (including web services)
			'since it's a scriptHandler for asmx (for json/atlas) we can't get around that constraint with web service extensions (they never get hit)
			'so we do it here instead.
			'NOTE: we're returning a matching json object that asp.net is already configured to return (copied from reflector):
			'System.Web.Script.Services.RestHandler->WriteExceptionJsonString
			If System.IO.Path.GetExtension(System.Web.HttpContext.Current.Request.FilePath).ToLowerInvariant = ".asmx" AndAlso HttpContext.Current.Request.Cookies.Get(System.Web.Security.FormsAuthentication.FormsCookieName) Is Nothing Then
				WriteAccessDeniedExceptionJsonString(HttpContext.Current)
			End If
		End Sub                  

		Private Shared Sub WriteAccessDeniedExceptionJsonString(ByVal context As HttpContext)
			context.Response.ClearHeaders()
			context.Response.ClearContent()
			context.Response.Clear()
			context.Response.StatusCode = 500
			context.Response.StatusDescription = HttpWorkerRequest.GetStatusDescription(500)
			context.Response.ContentType = "application/json"
			context.Response.AddHeader("jsonerror", "true")
			Using writer As New System.IO.StreamWriter(context.Response.OutputStream, New UTF8Encoding(False))
				writer.Write(New WebServiceError("Authentication failed.", String.Empty, String.Empty).ToJson)
				writer.Flush()
			End Using
		End Sub                  

#End Region                  

#Region " Auth Cookie missing on Web Service request response "                  

		'this class is used to replicate the response from the .net framework when an json web service call
		'is denied because of missing auth ticket
		Private Class WebServiceError                  

#Region " Declarations "                  

			Private Const JsonTemplate As String = "{{""Message"":""{0}"",""StackTrace"":""{1}"",""ExceptionType"":""{2}""}}"
			Private _exceptionType As String
			Private _message As String
			Private _stackTrace As String                  

#End Region                  

#Region " Constructors "                  

			Public Sub New(ByVal msg As String, ByVal stack As String, ByVal type As String)
				_message = msg
				_stackTrace = stack
				_exceptionType = type
			End Sub                  

#End Region                  

#Region " Public Methods "                  

			Public Function ToJson() As String
				Return String.Format(System.Globalization.CultureInfo.InvariantCulture, JsonTemplate, _message, _stackTrace, _exceptionType)
			End Function                  

#End Region                  

		End Class                  

#End Region                  

	End Class

Don’t forget to wire the http module into the asp.net pipeline using the httpModules section of the web.config.

Now, on the client side we need to hook a method into the asp.net ajax client side framework to listen for any http request that returns with our extra information (HTTP code=500 and Message=”Authentication failed”) and redirect the user accordingly. We can do this using the ms ajax application framework completedRequested event:

// attaches a handler that is fired first after web service is returned, before specific handler is called
// specifically, if the web service errored because of authentication failure (auth cookie expired)
// then redirect the page to the login page.
// note the login page is hard coded is assumed to be in the path site/app path/login
Sys.Net.WebRequestManager.add_completedRequest(On_WebRequestCompleted);                  

function On_WebRequestCompleted(sender, eventArgs)
{
  if ( sender.get_statusCode() === 500 ) {
   if (sender.get_object().Message === "Authentication failed.") {
		window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname.substring(0,window.location.pathname.indexOf('/',1)) + '/login.aspx?redirectUrl=' + encodeURIComponent(window.location.pathname + window.location.search);
    }
  }
}

This will redirect the user to page “login.aspx” and contain the current url as the redirectUrl querystring parameter.
That’s it. Now when the user’s authentication expires he will be redirected to the login page as expected.

1 Comment »

One Response to “Detecting authentication cookie expiration on asyncronous requests”

  1. Rui on 11 Jul 2008 at 5:22 am #

    Thanks so much!

Trackback URI | Comments RSS

Leave a Reply

« ScrollTo using the ajax control toolkit animation framework | Detecting and avoiding “A potentially dangerous Request.Form value was detected from the client” in asp.net »