Hi Scott,
I investigated this further and it looks like you're right. The session state is not available at the time
IRequestAuthorizationHandler's
Authorize method is called. I guess this is to be expected. ELMAH's authorization occurs right before it returns one of its request handlers back to
ASP.NET and as you know,
ASP.NET relies on the handler indicating sessions state requirements via
IRequiresSessionState and
IReadOnlySessionState. It's a bit of a catch 22 scenario. The good news is that I have managed to hack a workaround that doesn't require any changes to ELMAH 1.1 and attached is a working example built on top of the demo supplied with ELMAH. Following is an explanation of the solution that requires 3 classes in all (see the
App_Code directory).
There are really two reasons why session state is not available during authorization. One, and as we discovered, it's just too plain early. Two, none of the ELMAH handlers use session state so they don't implement
IRequiresSessionState or
IReadOnlySessionState. Even if
IRequestAuthorizationHandler.Authorize wasn't being called too early, the session state would have never been made available by
ASP.NET because the handlers don't advertise their need for it. Consequently, the first thing that needed fixing was implementing those interfaces on the handlers. Unfortunately, this would mean forking ELMAH's sources and maintaining a modified version. To avoid that, the solution takes a wrapper approach. The first class is pretty straightforward and generic:
public sealed class SessionRequringHandler :
IHttpHandler,
IRequiresSessionState
{
public IHttpHandler InnerHandler { get; private set; }
public SessionRequringHandler(IHttpHandler handler) {
InnerHandler = handler;
}
public void ProcessRequest(HttpContext context) {
InnerHandler.ProcessRequest(context);
}
public bool IsReusable {
get { return InnerHandler.IsReusable; }
}
}
It takes any IHttpHandler during construction and delegates to it. Because this class advertises session-requirement, it can cause session state to be summoned during a request when the actual handler wouldn't have. Now on to the next class, which is also dead straightforward:
public class ErrorLogHandlerFactory :
Elmah.ErrorLogPageFactory
{
public override IHttpHandler GetHandler(
HttpContext context,
string requestType,
string url, string pathTranslated)
{
var handler = base.GetHandler(context, requestType, url, pathTranslated);
return new SessionRequringHandler(handler);
}
}
This is a handler factory that inherits from ELMAH's handler factory and I've named it the same. It first calls the base implementation and then takes the returned handler and wraps it up in an instance of
SessionRequiringHandler. Then instead of returning the original handler, the latter one is returned and which will cause
ASP.NET to make session state available. To make this work, you'll need to register the new handler factory in lieu of the original one from EMAH; that is, for
elmah.axd (or whatever path you've chosen instead) in the
httpHandlers section in
web.config.
The final piece of the puzzle is the authorization handler itself and here's the implementation:
public class ElmahAuthorizationHandler :
HttpModuleBase,
IRequestAuthorizationHandler
{
protected override void OnInit(HttpApplication application) {
application.PreRequestHandlerExecute += delegate {
var context = application.Context;
if (IsFlaggedRequest(context))
CompleteAuthorization(context);
};
base.OnInit(application);
}
protected override bool SupportDiscoverability {
get { return true; }
}
public virtual bool Authorize(HttpContext context) {
FlagRequest(context);
return true;
}
protected virtual void CompleteAuthorization(HttpContext context) {
// Forbid more than 100 requests within a given session.
if (context.Session == null)
throw new Exception("Session unavailable.");
var count = (int)(context.Session["ElmahRequestCount"] ?? 0) + 1;
if (count > 100)
throw new HttpException(403, "Unauthorized request.");
context.Session["ElmahRequestCount"] = count;
}
protected virtual void FlagRequest(HttpContext context)
context.Items[GetType()] = true;
}
protected virtual bool IsFlaggedRequest(HttpContext context)
return (bool)(context.Items[GetType()] ?? false);
}
}
What the handler basically does is that it defers the decision to authorize. When the
Authorize method is called, it merely flags the request as one requiring an authorization check later in the processing and tells ELMAH to let it go through for now as if it is authorized. When the call returns, ELMAH returns a handler for the request. This handler then gets wrapped by our new
ErrorLogHandlerFactory (the one overriding the original one from ELMAH) into another one requiring session state (
SessionRequiringHandler). When
ASP.NET sees the handler and that it implements
IRequiresSessionState, it goes ahead and makes the session state available to the request context. Later when the handler is just about to be executed,
ASP.NET raises the
PreRequestHandlerExecute event, to which
ElmahAuthorizationHandler subscribes. It is then during this event that the real authorization check is conducted, the one based on session state. I've chosen to implement
IRequiresSessionState on
RequiringSessionState to have a read-write access to session state for demo purposes but you can decide to use
IReadOnlySessionState if it is good enough for your case.
Hope this helps.