Click here to Skip to main content
Click here to Skip to main content

Salient.Web.Security.AccessControlModule

By , 2 Jan 2010
Rate this:
Please Sign up or sign in to vote.

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

  1. 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.
  2. 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?
  3. 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.
  4. 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.

  1. BeginRequest
    1. If FormsAuth is enabled and a logout:true is found in the request header - perform logout.
    2. If FormsAuth is enabled, the current request has no Forms ticket and credentials are supplied attempt a login.
  2. AuthenticateRequest
    1. FormsAuthentication parses any ticket present into a FormsIdentity and associates it with the current Request.
  3. PostAuthenticateRequest
    1. 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.
    2. 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.
    3. 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.
    4. 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.
  4. PreSendRequestHeaders
    1. 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:

  1. Logout
  2. USERS

Restored 403 and Custom Error Page Behaviour:

  1. Logout
  2. ADMIN
  3. 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:

  1. Static resources; HTML fragments and JavaScript files
  2. ASPX GET and POST
  3. ASPX Static Page Methods - simple and complex JSON parameters
  4. HTTPHandler (ASHX) Form GET, POST and JSON POST
  5. ASMX WebService XML GET and POST with simple form-encoded parameters
  6. ASMX ScriptService POST with simple and complex JSON parameters
  7. 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.

  1. On the server side: when possible send a 'connection:close' response header.
  2. On the client side:
    1. Always explicitly set content-length for POSTS
    2. 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.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

Sky Sanders
Software Developer (Senior) Salient Solutions
United States United States
My name is Sky Sanders and I am an end-to-end, front-to-back software solutions architect with more than 20 years experience in IT infrastructure and software development, the last 10 years being focused primarily on the Microsoft .NET platform.
 
My motto is 'I solve problems.' and I am currently available for hire.
 
I can be contacted at sky.sanders@gmail.com

Comments and Discussions

 
GeneralExcellent solution to an annoying problem PinmemberCarl Reid4-Jan-10 0:32 
GeneralLongin is incomplete Pinmembercarinarlong29-Sep-09 10:12 
GeneralRe: Longin is incomplete PinmemberSky Sanders2-Jan-10 1:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140415.2 | Last Updated 2 Jan 2010
Article Copyright 2009 by Sky Sanders
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid