- Custom Authentication
- Hybrid Authentication Scheme
- Operation Based Security
- Conclusion
Hybrid Authentication Scheme
When developing intranet enterprise solutions, requirements usually enforce you to use Windows identity-based authentication (honestly, this makes you quite happy because you don't have to roll your own authentication stuff). The pain comes when you want to enforce role based authentication in your application using Windows domain's groups. It's quite likely that you will face some resistance, to say the least, coming from system administrators who won't be quite happy to add a new bunch of Windows groups into the domain to reflect the application roles required by the application. To circumvent this, MTS was the first infrastructure providing a hybrid approach: Roles (that is, groups) are defined at the application level (specifically in the COM+ application), but such roles are filled up with the Windows domain user. In this way, you get the best of two worlds: robust authentication for free and flexible role configuration management.
This hybrid approach can be easily implemented in .NET using WindowsIdentity and CustomPrincipal objects.
The first thing we need is a custom store in which the mapping among users and roles is defined. The simplest, still very flexible, approach is to use an XML file that can be as simple as this:
<root> <role name="ApplicationAdmins"> <user name="PCSABBA\Administrator"></user> </role> <role name="ApplicationUsers"> <user name="PCSABBA\Administrator"></user> <user name="PCSABBA\enricos"></user> </role> </root>
For the sake of simplicity, I'll just quickly mention that in a real world application, this resource should be encrypted or at least strongly ACL-protected.
Now it's time to proceed in the development of our custom authentication class. This class exposes a single static method called login. The login method picks up the current WindowsIdentity and passes it to the nested MycustomPrincipal class constructor. If the constructor returns successfully, the MycustomPrincipal object must be put in the thread's current principal (as explained in the first article of this series). Additionally, the SetThreadPrincipal method on the current application domain is called to set this new Principal as the default one for any new thread created in the Application Domain.
public class AuthenticationModule { public static void Login() { MycustomPrincipal l_MycustomPrincipal = new MycustomPrincipal (WindowsIdentity.GetCurrent()); Thread.CurrentPrincipal = l_MycustomPrincipal ; AppDomain.CurrentDomain.SetThreadPrincipal(l_MycustomPrincipal); } }
The MycustomPrincipal constructor is responsible for discovering the proper roles the user belongs to and then store this information in an ArrayList. Picking up the correct roles is really easy because we just need to apply a simple XPath query against the XML document containing the userroles mapping.
public class MycustomPrincipal : IPrincipal { private static XmlDocument i_xmlroles= new XmlDocument (); private IIdentity i_Identity =null; private ArrayList i_roles=new ArrayList() ; //The XML file is loaded, once for all, in a static variable in the class //static constructor static MycustomPrincipal() { i_xmlroles.Load (Path.GetDirectoryName (Assembly.GetExecutingAssembly().Location) + @"\Roles.xml" ); } //code outside this assembly can't create this principal object internal MycustomPrincipal(IIdentity p_IIdentity) { lock (i_xmlroles) { XmlNodeList l_roles = i_xmlroles.SelectNodes ("//role[user/@name='" + p_IIdentity.Name + "']"); foreach (XmlNode l_rl in l_roles ) i_roles.Add (l_rl.Attributes.GetNamedItem ("name").Value ); } } public IIdentity Identity { get { return i_Identity; } } public bool IsInRole(string role) { return i_roles.Contains (role); } }
After the authentication phase has been completed successfully, access control can proceed as usual by using security demands or explicit calls to the IsInRole method.
if (Thread.CurrentPrincipal.IsInRole ("ApplicationAdmins")==true) { //do work ... } else throw new Exception ("Access Denied");