Overview
I a previous article, I complained about the lack of a true 403 Forbidden error in ASP.NET and the clumsy way that authentication is handled by FormsAuthenticationModule
.
The solution I came up with was a good first attempt but, in my opinion, came up short. Particularly when handling authentication of requests that ScriptModule
touches. So I decided to take another swing at it, this time with some ammunition.
I took the list of HttpModules
from the root Web.config
and the ScriptModule
that is added to ASP.NET 3.5 web apps and dove in with Reflector. You can read about my findings in this article.
I then merged each modules' event handlers into the Application/Request lifecycle chart in the order in which they are handled.
This presented a very clear picture of how a request cycles through the modules and clearly pointed out where I had misplaced code and overcomplicated the task. I recommend exercises like this to anyone interested in writing authentication or security related code, especially provider based features.
After a better informed redesign and rewrite, I ended up with a module that fulfills the previous requirements and added another for good measure.
What It Does
- Enable a true 403 for requests that are under privileged (e.g. an authenticated user attempting to access admin content).
Standard ASP.NET behaviour is to forever blithely cycle the login page with no way of detecting a 403 short of brittle URL munging kludges in login.aspx that I and many others have resorted to over the years. - Enable custom error page processing for 403 errors.
This has not been possible since the introduction of ASP.NET and it begs the question why Microsoft keeps 403 as an example in the template Web.config code. Am I the only one who has ever noticed this? - Enable, by means of a request header, the ability to completely circumvent the Login redirect behaviour of
FormsAuthentication
in favor of hard 401 and 403 HTTP status codes.
This functionality is designed for use by AJAX requests in which a login redirect does nothing but add reams of kludgey code on every request to determine if the 200 I just got was a login page or not.
The ScriptModule
introduced in 3.5 partially handles this task but only for requests that are of content-type application/json and only if they are targeted to a 'rest' endpoint such as a webHttp
binding or a ScriptService
. This leaves all other requests at the mercy of FormsAuthentication
and its WebForm
UI centric implementation. - And, finally, I added what will likely cause some to cry foul: the ability to pass a plain text username/password in the request headers to authenticate a request before the
AuthenticateRequest
event.
This enables background authentication of headless async requests in such scenarios that require it. It is loudly proclaimed in the source and now here that passing unencrypted credentials over an unsecure connection is begging for a security breach. I implore you to only use this functionality over an SSL connection. Enough said about that. Not my job to placate security snobs or protect people from thyself.
How It Does It
Configuration of the module is accomplished via a section in Web.Config. The module may be disabled completely in WebConfig
or for an individual request by specifying mode:disabled
in the request header.
The default behaviour is to generate hard 403 Forbidden status for underprivileged requests that do not have a request header or in which that header specifies mode:none
and to generate hard 401 and 403 status for requests that possess the request header with mode:script
.
If the header contains a credential pair and the request does not already contain a ASPXAUTH ticket, those credentials are validated against the current MembershipProvider
. The result is returned in a response header. You may also specify logout:true in the request header to perform a logout. The logout happens before the login and both may be specified in the same request, in effect switching identities.
BeginRequest
- If
FormsAuth
is enabled and a logout:true
is found in the request header - perform logout. - If
FormsAuth
is enabled, the current request has no Forms ticket and credentials are supplied attempt a login.
AuthenticateRequest
FormsAuthentication
parses any ticket present into a FormsIdentity
and associates it with the current Request.
PostAuthenticateRequest
- The current principal is wrapped in a proxy principal upon which the
IsAuthenticated
property can be manipulated. This proxy is then passed to the UrlAuthorizationModule
in both authenticated and unauthenticated states to get a full understanding of the authentication status of the request. - A response header is created and set containing relevant information about the request and user. This is intended for use by client script and can be disabled in the configuration file if desired.
- If the user is authenticated and the requested resource is Forbidden due to role or username exclusions, a 403 is generated. If it is not a script request, the custom 403 error page is rendered, if present.
- If the user is not authenticated and the requested resource is protected AND it is a script request, a 401 is generated and written to the response but then the status code of the request is changed to 499.
Why? To work around some really obtuse behaviour by UrlAuthorizationModule
and FormsAuthenticationModule
which do not honor SkipAuthorization
or Application.CompleteRequest
. We flag it as a 499 to sneak it by these pesky varmints and pick it back up again in PreSendRequestHeaders
after all other modules have finished and change it back to a 401 before sending it out the door.
PreSendRequestHeaders
- All the heavy lifting is already done. Here we just watch for a 499 and change it back to a 401.
AccessControlModule In Action
The source download contains a demo app and extensive integration tests. You can find the link at the top of this article.
The default document of the demo contains a list of links to resources of varying degrees of security; anonymous, user and admin roles. I will enumerate the illustrative workflows, each begins in an unauthenticated state.
Standard Login redirect Behaviour:
- Logout
- USERS
Restored 403 and Custom Error Page Behaviour:
- Logout
- ADMIN
- Login as 'user'
Also included are an exception generation and 404 link to demonstrate standard Custom Error behaviour.
The integration test page takes all of the examples from this article and uses an XMLHttpRequest
object to access virtually every type of exposable ASP.NET endpoint and resource in a comprehensive permutation of security levels and authentication levels.
Tested endpoints:
- Static resources; HTML fragments and JavaScript files
- ASPX
GET
and POST
- ASPX
Static
Page Methods - simple and complex JSON parameters HTTPHandler
(ASHX) Form GET
, POST
and JSON POST
- ASMX
WebService
XML GET
and POST
with simple form-encoded parameters - ASMX
ScriptService POST
with simple and complex JSON parameters - WCF Service
webHttp
endpoint with simple and complex JSON parameters
Conclusion
AccessControlModule
can impart a greater consistency and usability upon the default behaviour of FormsAuthentication
and allow any client script code to leverage FormsAuthentication
in a straight forward manner.
Related Topics and Resources
IE HTTP 1.1 Implementation Foibles:
In the process of implementing this module, I ran into yet another IE condition that required a workaround. 1203x (INTERNET_CONNECTION_RESET
, ABORT
, RETRY
, etc.) HTTP errors in IE are infamous and 'the Google' is filled with people struggling with these errors. I found that I ran into these problems mostly when running in the VS dev server (webdevserver.exe) which uses port numbers in the URL. Running on port 80 in IIS produced virtually no errors.
With this clue, I stumbled across a fellow by the nick 'perkiset' and his most excellent message board and got some confirmation of this as well as some tips on bottle-feeding IE to reduce the frequency of these errors and some proven workarounds. The short story is that IE has used the same brittle HTTP 1.1 implementation since V6 and it chokes intermittently on URLs with port numbers and/or SSL connections.
- On the server side: when possible send a '
connection:close
' response header. - On the client side:
- Always explicitly set content-length for
POST
S - Construct your request methods in such a way as to enable a finite number of self-retries upon receipt of a specific set of IE related network errors.
Reflection: the .NET scalpel
After the limited success of the first attempt at this module, I took a deeper look at the stock ASP.NET HTTP modules and the way they interact with each other and with requests. To confirm some assumptions, I started a reflection based wrapper/proxy library that allowed me to patch and compile the reflected source code for selected modules, replacing their counterparts in the pipeline, and step through the processing of various types of requests. I am aware of source servers and source stepping, but it did not lend itself to this scenario. In any case, I cannot post the source for the replacement modules as the source, even with my shims, is not covered by SSCI.
The reflection library that makes it possible is not similarly restricted. You may find the source for that here on CodePlex. I have used this technique extensively in the past while developing provider based features and the utility has proven its usefulness to me. The posted source is not comprehensive, focusing primarily on the classes related to the HttpModule
and FormsAuthentication
pipeline, and the wrappers are not API complete. In most cases, I implemented only the internal
/private
members and types that were required to reuse the source for selected modules.
It is my intention to build upon this wrapper library as needs arise.