Forms authentication and role-based security: improving performance

Note: this entry has moved.

The usual approach to custom role-based security using forms authentication (i.e. roles and users fetched from a database) on web apps is the following:

protected void Application_AuthenticateRequest(Object sender, EventArgs e) { // Only replace the context if it has already been handled // by forms authentication module (user is authenticated) if (Context.Request.IsAuthenticated) { string roles[]; // Fetch roles from the database somehow. // Reuse the identity created by Forms authentication. GenericPrincipal ppal = new GenericPrincipal( Context.User.Identity, roles); Context.User = ppal; } }

There's one *huge* drawback to this approach, and it's that it will hit the database on every single request! So, the proposed improved solution is:

protected void Application_AuthenticateRequest(Object sender, EventArgs e) { if (Context.Request.IsAuthenticated) { // Retrieve user's identity from context user FormsIdentity ident = (FormsIdentity) Context.User.Identity; // Retrieve roles from the authentication ticket userdata field string[] roles = ident.Ticket.UserData.Split('|'); // If we didn't load the roles before, go to the DB if (roles[0].Length == 0) { // Fetch roles from the database somehow. // Store roles inside the Forms ticket. FormsAuthenticationTicket newticket = new FormsAuthenticationTicket( ident.Ticket.Version, ident.Ticket.Name, ident.Ticket.IssueDate, ident.Ticket.Expiration, ident.Ticket.IsPersistent, String.Join("|", roles), ident.Ticket.CookiePath); // Create the cookie. HttpCookie authCookie = new HttpCookie( FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(newticket)); authCookie.Path = FormsAuthentication.FormsCookiePath + "; HttpOnly; noScriptAccess"; authCookie.Secure = FormsAuthentication.RequireSSL; if (newticket.IsPersistent) authCookie.Expires = newticket.Expiration; Context.Response.Cookies.Add(authCookie); } // Create principal and attach to user Context.User = new System.Security.Principal.GenericPrincipal(ident, roles); } }

The new version only goes to the DB once. It also uses the same encryption features of forms authentication, as well as its ticket and cookie. A huge performance boost, that's for sure.

Thanks to Hernan for pointing this out while reviewing the security chapter of my last book.

11 Comments

  • I believe that is the way MSDN shows it (storing the roles in the auth cookie).

  • hmmmm, what about the max length of a cookie? - if the app happens to have a lot of different groups and the user in associated with a lot of these groups, you are in trouble :) - at the very least you should check the max length before deploying the cookie.

  • Well, if you're using ASCII role names (most probably), then in 4k (the default maximum cookie size) I'm pretty sure you can put quite a lot roles, don't you?

    The number of groups your app has is irrelevant. What matters is how many belong to a single user. In my experience, each user rarely has more than a couple roles assigned.

  • If your roles' size exceeds cookie size. Neither IE nor VS.NET gives you any error. It simply does not store ANY Cookie. Therefore you end up with no authentication. Your users can not enter your application. Watch out for this error.

  • This is how its done in the Time Tracker sample app at www.asp.net

  • How many times can the FormsAuthenticationTicket be rewritten in a single request?

  • Hey,

    After implementing a custom principal I am questioning the need for doing so. I am wondering if there was any true advantage over the way I was doing it before. Essentially after a successful login I would bring back a hashtable of common user info and an arraylist of roles I had defined in the db... I cached these in the session(sqlstate) and was planning on referencing them there. The way I saw it that was 2 hits, one for the initial call and one for the sqlstate storage (or is the db hit on every request?). With the principal it gets hit every time a request is made or I have to implement some kind of ugly cookie storage.


    Isn't Principal object getting recreated on every request vs. my session info which stays resident for the timeout period?


    Someone please tell me an advantage to this...Why do we really have to go for ROle based security, as it can be accomplished using Sessions ...

  • Well, what you're missing is the role based security is not about just retrieving the principal. it allows you to do security checks via attributes anywhere in your code. The .net runtime will ensure those without any code on your part.

    also, you can use role-based restrictions on navigation using the element in the web.config, and asp.net will ensure only authorized users (via their role) get to them. If you don't use roles and principals, you will have to have custom code in your pages to check if the user is allowed to see it (remember, they can always type the URL or get it from an authorized user).

    If you cache the principal object in Session state, to avoid hitting the DB, that's fine too. It's an optimization, but it has nothing to do with the benefits of role-based security.

  • thanks dcazzulino for the detail explanation.

    It's so obvious that Nikhil is a stupid jerk and should be fouled or terminated from his/her job of being a programmer or whatever things related to programming.

  • This Article is very nice

  • That is exactly why I used Bitmasks to store the Roles a user is assigned instead of creating an "Xref" table that stores the Roles for each user. And if I ever (God forbid) need more than 64 Roles, I can easily switch to storing them as a VarBinary (in SQL) or a string of 1's and 0's like SQL Server does in the SysUsers table which can potentially give me 8000 Roles!

    This of course requires a custom Principal and Identity to be written (which took all of 5 minutes). The only additional code needed then is in the "IsInRole" function which needs to retrieve the ID of the Role (preferably an Int if your using Bitmasks) and the compare that to the roles bitmask of the user.

Comments have been disabled for this content.