November 25, 2007

ASP.NET 2.0 Forms authentication - Keeping it customized yet simple

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:

  • Make your own implementation of IIdentity if needed (usually GenericIdentity or FormsIdentity are sufficient)
  • Make your own implementation of  IPrincipal. Example:

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> 

  • Your code: A succesful login should establish the encrypted cookie:
FormsAuthentication.SetAuthCookie(userId, false);


  • Your code: Global.asax should enrich each request with the needed extra data and cache it:
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.

8 comments:

  1. Clever construction, Peter!
    Good work.

    ReplyDelete
  2. Nice example. However there is a small error in your principal. The field someCustomProperty should have been someCustomProperty1.

    ReplyDelete
  3. Is there an easy way to make each of these properties available to every single view rendered by WebForms, or say, Razor templates in ASP.NET?

    ReplyDelete
  4. What is MyBusinessLayerSecurityClass???
    I try to implement this Principal in my asp.net MVC 2 application

    ReplyDelete
  5. @bkreeger
    Using the example in the post, you will always be able to access (MyPrincipal)HttpContext.Current.User, or in MVC more prefferably (MyPrincipal) ControllerContext.HttpContext.User.
    @stefan
    It's your custom class that creates your custom principal with, say, some application-specific properties, associations to related domain model objects, etc.

    ReplyDelete
  6. hi @peter, i got an question. This info that you put in cache is really necessary? I can't see where do you can use it.
    thx

    ReplyDelete
  7. okay, now i understand. The Application_AuthenticateRequest executes more than once.

    ReplyDelete
  8. Hello Peter,
    Thanks for this very well explained post. I'm trying to get this to work with a MVC3 application but the httpcontext.current.user gets reset to the default System.Web.Security.RolePrincipal inmediately when I hit the controller so it fails with an unable to cast exception. I checked that the code in the global.asax is run but it still fails. I get it to work by placing the code from the application_authenticaterequest in the constructor of the controller. While it works doesn't seem like an ideal solution, plus I have several controllers. Any idea what I may be missing? thanks a lot.

    ReplyDelete