In my continuous quest to migrate a large ASP site to ASP.NET, one central step was to implement forms authentication that comformed to all the existing schemas and business logic around users and user rights.
I think a scenario like this with a lot of predefined conditions is not uncommon. And here the Membership and RoleProvider features of 2.0 usually don't fit (these are great features, but not always aplicable). It usually (like in our case) comes down to something like this:
Membership: Method X, Y and Z of the Membership model are not needed by the preconditions and method Q and R needs to be modified. Special methods A and B needs to be added.
RoleProvider: Preconditions requires a custom RoleProvider class with specialization requirements like with the Membership system.
One solution would be to go ahead and implement your own implementations, but you will probably end up doing a lot of (unnescessary) code work, and the intended productivity benefits will be lost.
My suggested solution is to specialize at alower level of .NET authentication and authorization - IPrincipal and IIdentity. The steps would be:
public class MyPrincipal : IPrincipal
{
public MyPrincipal(IIdentity ident, List<string> roles, int someCustomProperty1, string someCustomProperty2)
{
this.identity = ident;
this.roles = roles;
this.someCustomProperty1 = someCustomProperty1;
this.someCustomProperty2 = someCustomProperty2;
}
IIdentity identity;
public IIdentity Identity
{
get { return identity; }
}
private List<string> roles;
public bool IsInRole(string role)
{
return roles.Contains(role);
}
private int someCustomProperty1;
public int SomeCustomProperty1
{
get { return someCustomProperty1; }
}
private string someCustomProperty2;
public string SomeCustomProperty2
{
get { return someCustomProperty2; }
}
}
Set up web.config to forms based authentication. Typically like:
<system.web>
<authentication mode="Forms">
<forms loginUrl="Logon.aspx">
</forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
FormsAuthentication.SetAuthCookie(userId, false);
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
// Get Forms Identity From Current User
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
// Create a custom Principal Instance and assign to Current User (with caching)
MyPrincipal principal = (MyPrincipal)HttpContext.Current.Cache.Get(id.Name);
if (principal == null)
{
// Create and populate your Principal object with the needed data and Roles.
principal = MyBusinessLayerSecurityClass.CreatePrincipal(id, id.Name);
HttpContext.Current.Cache.Add(
id.Name,
principal,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(0, 30, 0),
System.Web.Caching.CacheItemPriority.Default,
null);
}
HttpContext.Current.User = principal;
}
}
}
}
Looking from an architechtural view, our specializations will reside in 3 places:
-
IIdentity: Your implementation must contain some sort of unique user identity - nothing else.
-
IPrincipal: Your implementation can contain extra user information, and role checking logic must be present (IsInRole as a minimum).
-
Your Business logic: Here you should place the code that handles your specific security - like checking a login, getting the users roles, etc., as well as all the very-special-method-x methods that you preconditions require.
This way you will end up with a clean, easy-to-test security implementation that satisfies the preconditions of your solution - and nothing else. As a bonus you have avoided dependencies between your security code and ASP.NET - this can make testing easier and makes your security code reusable with other types of GUI.