An issue that has been common for me is the way that ASP.Net FormsAuthenticationModule handles login redirection and the opaque nature of the mechanism itself.
Any authentication failure, 401 Unauthorized, e.g. not logged in, and 403 Forbidden, e.g. logged in but no permissions, are both unquestionably redirected to the the login page specified in the forms element.
While this behavior regarding the 401 is expected, I have always taken issue with the 403 redirect. It is confusing and counterproductive. The user is already logged in and it has been determined that they do not have appropriate permissions to access the resource.
What is the point or usefulness of blindly sending them to the login page and leaving the developer with no clean hook to catch a 403 and react to it appropriately?
My general strategy for dealing with this, and apparently other people's as well, was to put kludges in the code-behind of login.aspx. This made me feel dirty and unloved. But that is not something new and I just avoided the sitchiation.
Until now.
Recently I have been writing a lot of client javascript and the login page issue raised it's head again in a big way.
Regexing responseText for fragments of the login page to determine if a headless ajax request that purports to be 200 was unauthorized was just a bit too lame for me so something had to be done.
The only appropriate place to deal with an issue like this, in my opinion, is in an HttpModule after the request has been 'authenticated' and before it is authorized. So PostAuthenticateRequest is a logical place to start.
In my approach I take authorization one step farther and check access for the current request/user combination in both unauthenticated and authenticated state. Doing this I can determine if the current user not only cannot access the resource at _this_ time due to authentication, but if the user will ever be able to access the resource and react accordingly.
digression: think about that... an unauthenticated user attempts to access a protected resource (that they do not have permissions for) and get redirected to login. They login and get ReturnUrl'd back to the resource they have no permission for and get dumped back at login...wash, rinse repeat until the user leaves confused and frustrated or blows up your help desk on a saturday night.. oooohhh AND AND AND leaving the dev no way to tell that it was a 403?!??!
What idiot let that go on for 1.0, 1.1, 2.0, 3.0, 3.5 and 4.0? Somebody needs to be fired in Redmond.
Ok, back to the regularly scheduled program...
In the PostAuthenticateRequest we have a principal, be it logged in or not, that we can feed to the UrlAuthorizationModule to check permissions before FormsAuthenticationModule dumps in on login's doorstep. As I stated before, I check both authenticated and unauthenticated by wrapping the principal and identity so I can set IsAuthenticated any which way before checking.
If the principal will never be able to access the resource I set status to 403, flush and CompleteRequest. Works great - get a flat stop with a 403.
If the principal has permissions but is not authenticated I should be able to set a 401, flush and CompleteRequest. Right? Wrong.
CompleteRequest advertises that it skips directly to EndRequest, do not pass go, do not collect 200 bucks. Well, it sure doesn't act like it. FormsAuthenticationModule takes that request, which by the way has already had headers and content written not to mention being flushed and having CompleteRequest called on it, and attempts to RedirectToLogin causing an http runtime exception. Wonderful. No amount of jiggling could get me around it. Looked like all of the message posts stating that you cannot go around FormsAuthenticationModule without replacing or removing it were right. Or were they?
Cut to the deceptively simple solution: If, in PostAuthenticateRequest it is determined that you want to set a 401 status in addition or instead of 302ing to login, just set a flag, let the pipeline do what it wants and wait until PreSendRequestHeaders. Then clear the response, rewrite the url, set the status codes and let it go. Bingo. works like a charm.
I have partially implemented the processing of 403 custom error page, if specified. I have tried every which way to set the headers and status to get custom errors to take control of 401 and 403 when I do not want to handle them. No joy. So I am just reading the config file to get the url and redirect types. This part is still a little rough but works well enough. A login from a forbidden custom error page, even if the original url is showing redirects back to forbidden. I just need to spend a little time in that method.
UPDATE: Sample source has been removed. In the course of writing another article I found myself reading the source for System.Web.Handlers.ScriptModule and found that by blind luck I am 'almost' exactly on track with this approach. With this newfound knowledge I was able to produce a more robust implementation of the AccessControl module that was posted here. Salient.Web.Security.AccessControlModule
Technorati tags:
FormsAuthentication