Friday, August 10, 2007

Authorization with multiple role providers

Provider-based authorization and authentication is definitely a huge step forward from the primitive form authentication we've got in ASP.NET 1.0. Unfortunately Microsoft again didn't do a great job unifying the approaches. It seems that Membership, Role and Profile projects were handled by three geographically and developmentally separated teams who had no desire to communicate with each other. The Membership provider architecture and implementation is easily the best from the three while Profile - the worst (it is noteworthy that even a quite thorough book on security from the Microsoft itself ignores Profile features). Despite the similarities in implementation all three provider models are frustratingly different. Like in the case with Team System, the problem seems to be a feeble BA job.

Out-of-the-box implementations work good like Membership or fair, like others for a standard project. But what if we need an admin-type application which can service few client apps? Client membership databases can either be shared or separate, while admin application will be able to access them all. It can give us great flexibility and cut amount of code tremendously. Anyway, even for the other cause it is still nice to handle multiple providers.

Membership works out of the box without a glitch(surprise, surprise) - it is no brainier to authenticate against arbitrary provider using Membership.Providers collection. Profile sucks as usual - from my point of view it doesn't make any sense to use multiple providers if we can not inherit profile from different base classes. The Role provider imposes some challenge though. Wouldn't it be nice to change default provider programmaticaly once for all application?

Static Roles class will serve you a RolePrincipal associated with the default Role Provider, thus Context.User.IsInRole() will not give us what we want. There are plenty ways around but it is good idea to let developers use this customary method. The following code is based on a snippet from a "Professional ASP.NET 2.0 Security, Membership, and Role Management" - an excellent (but quite heavy, literally :) book by Stefan Schackow. The idea is to replace IPrincipal Context.User class with RolePrincipal one, which constructor accepts Role Provider name - that's exactly what we need. Stefan proposes to hook the method to the GetRoles event in the RoleManagerModule. In this case you should consider tricky business of passing along the desired provider name. The Session object is not accessible this early in the pipeline and other possible meanings, - query string and cookie - still may not be the weapon of choice. Query string will add an extra headache if we use URL rewriting or serve extensive amount of dynamic pages from Content Management System. Cookie should be guaranteed to stay untouched for the whole user session or something nasty could happen. The code can be placed in the application page controller class. If you use back-door for a seamless login, the gateway and front login page should run exactly the same logic.

public static void SetRoleProviderForCurrentUser(string applicationName, HttpContext context)
{
if (!context.User.Identity.IsAuthenticated) return;
if (string.IsNullOrEmpty(applicationName)) return;
RolePrincipal newPrincipal=null;
if (Roles.CacheRolesInCookie)
{
if ((!Roles.CookieRequireSSL || context.Request.IsSecureConnection))
{
try
{
HttpCookie cookie = context.Request.Cookies[Roles.CookieName];
if (cookie != null)
{
string cookieValue = cookie.Value;
if (cookieValue != null && cookieValue.Length > 4096)
Roles.DeleteCookie();
else
{
//ensure proper casing
if (!String.IsNullOrEmpty(Roles.CookiePath) && Roles.CookiePath != "/")
cookie.Path = Roles.CookiePath;
cookie.Domain = Roles.Domain;
//create a new principal
newPrincipal = new RolePrincipal(
GetRoleProviderName(applicationName),
context.User.Identity,
cookieValue);
}
}
}
catch { /*no cookie? no problem, ignore the error*/ }
}
else
{
if (context.Request.Cookies[Roles.CookieName] != null) Roles.DeleteCookie();
}
}
if (newPrincipal==null)
{
newPrincipal = new RolePrincipal(
GetRoleProviderName(applicationName), context.User.Identity);
}
context.User = newPrincipal;
Thread.CurrentPrincipal = context.User;
}
Note that we manually synchronize the Thread.CurrentPrincipal with Context.User - the job normally done by DefaultAuthenticationModule when we login into the application. Dominick Baier had a great article about differences and similarities of these two objects quite a while ago.

6 comments:

Steve Wong said...

Thank you for your idea and I understand it is not a good idea that to pass query string and cookie. In Stefan example, he pass to RoleManager_GetRoles in global.asax. But I cannot understand your meaning of adding the code to page controller class. Can you show an example of how to invoke the function. As I am strugling how to change the role provider in my application.

Thanks a lot.

Steve Wong said...

Please ignore my previous message and I can call your funtion directly on .cs page.

Thank you.

Michael Goldobin said...

glad to help :)

Anonymous said...

What would be the implementation of GetRoleProviderName(applicationName) method in your example?

Anonymous said...

What would the implementation be for your GetRoleProviderName(applicationName) method?

Michael Goldobin said...

In my post I am talking about the application which service multiple applications, each having it's own security harness. Like in the case of Content Management System it would be file or user administration application.
Thus the GetRoleProviderName(applicationName) is just selecting relevant provider by particular application name.
Another usage is if your application is split on two parts: e.g. Internal and External, where membership for Internal is AD and External - Oracle providers.
As you see the concrete implementation of the GetRoleProviderName(applicationName) method is not important. It can be something simple as a static or configurable list or something more sophisticated, like parsing a <membership> section from web.config.


© 2008-2013 Michael Goldobin. All rights reserved