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 »
Rui on 11 Jul 2008 at 5:22 am #
Thanks so much!