- Identity and Principal Objects
- Authentication
- Authorization
- Conclusion
In the pre-.NET era, you didn't have to worry about all the different types of identities and principals. Low-level OS APIs automatically attached a principal token to processes. That token was used along the way to provide authorization; that is, it checked permissions against resources. (Admittedly, this picture is somehow simplifiedfor example, ignoring impersonation and remote procedure calls.)
In .NET, there are no particular requirements for the assembly code doing "authentication." Almost any piece of code is allowed to set (or change) the authentication token. The authentication token is represented by a Principal object, and it must be set into the Thread.CurrentPrincipal static property.
The .NET runtime has the Principal object, placed into the thread's CurrentPrincipal, flow out of band across method calls and uses it to perform its built-in authorization mechanism (which I'll cover shortly).
You'll probably place a Principal object in the thread's CurrentPrincipal, even when the application doesn't use .NET built-in authorization, in order to take advance of the out-of-band Principal object flow. If your application spawns different threads, you might think that calling CurrentPrincipal is required on each thread. Fortunately, that's not the case. Just call the SetThreadPrincipal method on the current Application Domain object. This method accepts any object implementing the IPrincipal interface. Doing so guarantees that the provided Principal object is copied automatically to all threads created or entering the Application Domain.
There is no automatic hook into Windows authentication in .NET. If you want to use the Windows Identity and Principal objects also as .NET Identity and Principal objects, you must explicitly require it by calling the ApplicationDomain's SetPrincipalPolicy static method:
//this call is typically put in the application's main AppDomain.CurrentDomain.SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal);
Other legal values of the PrincipalPolicy enumerator are NoPrincipal and UnAuthenticatedPrincipal. Calling SetPrincipalPolicy using NoPrincipal doesn't set any Principal object into Thread's CurrentPrincipal. If you don't call SetPrincipal Policy or SetThreadPrincipal (that is, if you ignore any security-related stuff at application startup), the .NET Framework calls SetPrincipalPolicy with the UnAuthenticatedPrincipal parameter. This parameter instructs the .NET runtime to put a generic Principal containing a Generic Identity object into the thread's CurrentPrincipal.
For an alternative way to access the WindowsPrincipal and WindowsIdentity objects, you have to call the WindowsIdentity.GetCurrent static method and then pass the obtained WindowsIdentity to the WindowsPrincipal constructor.
WindowsIdentity wi = WindowsIdentity.GetCurrent (); WindowsPrincipal wp = new WindowsPrincipal (wi);
This option is useful if you don't want to attach the WindowsPrincipal to the current thread.
Custom Authentication
At this point, you have all the information you need to develop a custom authentication mechanism, using the following steps:
Check provided credentials against a user store.
Create a pair of Identity and Principal objects.
Set the Principal object into the Thread.CurrentPrincipal property and eventually on the whole Application Domain.
The authorization code in .NET can be as simple as the following code snippet:
private bool authenticate(string p_uid,string p_pwd) { string[] l_roles; if (CheckAgainstUserStore(p_uid,p_pwd, out l_roles)==true) { MyIdentity l_MyIdentity = new MyIdentity (p_uid); MyPrincipal l_MyPrincipal = new MyPrincipal(l_MyIdentity,l_roles ); //for the current thread Thread.CurrentPrincipal = l_MyPrincipal ; //for threads which will be created later on AppDomain.CurrentDomain.SetThreadPrincipal (l_MyPrincipal); return true; } else return false; }
MyPrincipal and MyIdentity are two custom classes; the CheckAgainstUserStore method matches the provided username and passwords against a user account store (file, database, Active Directory, and so on).
NOTE
For better encapsulation, you should move the CheckAgainstUserStore method and the assignment to the CurrentPrincipal right into the MyPrincipal constructor. This call is typically placed in the executable main routine.
Some of you might be thinking: "Hey, wait a minute! Does any piece of code really modify the Thread.CurrentPrincipal? This is not actually what happens when dealing with Windows security. Isn't any particular permission required?"
The answer is yes: You need a particular CAS permission to manipulate the CurrentPrincipal, specifically the SecurityPermission with the SecurityPermissionFlag.ControlPrincipal bit flag set (see Figure 1). Neither the intranet nor the Internet CodeGroups are granted this permission, so in a default configuration, only assemblies loaded from the local hard-drive can modify the thread's Principal.
Figure 1 The ControlPrincipal Security Permission as shown in the Microsoft .NET Framework 1.1 Configuration snap-in.
If you think this doesn't guarantee enough protection, you can toughen the requirements of your authentication and authorization code in the following way:
Authentication: Place the authorization code, custom Principal classes, and custom Identity classes in the same assembly and provide an internal (Friend in VB.NET) constructor only for the latter two. No other assemblies can then create your custom security objects. There is a more sophisticated approach available as well: You can allow only assemblies developed by your company to create custom security objects. To do so, just sign all your company assemblies with the same key and then place the appropriate StrongNameSecurityPermission on the authentication assembly entry points.
Authorization: After you know exactly who can create a specific security object, you can restrict the authorization phase, requiring a specific Principal object type being present in Thread.CurrentPrincipal (more on this later).
Authentication in ASP.NET
Due to the stateless nature of HTTP, authentication in ASP.NET has some peculiarities. IIS provides four authentication mechanisms: Basic, Integrated, Digest, and Certificate Mapping. All these options map an Internet user to a local or domain user on the Web server. To have the user Identity and Principal transparently flow from IIS to the ASP.NET context, all you have to do is set authentication mode to Windows in the web.config file:
<authentication mode="Windows" />
NOTE
In ASP.NET, the WindowsPrincipal is available both through Thread.CurrentPrincipal and the Page.User property (yes, there is a Principal in it, not an Identity).
Most of the time, the complexity of managing a Windows user store for Internet application users requires you to develop other kinds of authentication solutions. ASP.NET offers two built-in authentication mechanisms: PassPort authentication and Form authentication. (Covering PassPort authentication is out of the scope of this article.)
Form authentication is an infrastructure for developing your own ASP.NET-based custom authentication. Instead of reauthenticating the user on each call, an authentication cookie is issued (it can be encrypted and protected against tampering). The Form Authentication infrastructure automatically redirects unauthenticated users to a login page and puts a GenericPrincipal (containing a FormsIdentity object) into both Thread.CurrentPrincipal and Page.User when the logon phase has completed successfully. The Form authentication infrastructure rebuilds and reattaches the FormsIdentity object to the ASP.NET execution context on each page request. However the infrastructure doesn't maintain in the Principal object the roles the user belongs to across HTTP requests. The best approach to work around this problem is the following:
Once and for all, stuff the roles list into the userdata parameter of the FormsAuthenticationTicket constructor while performing Form authentication:
private void Login_Click(object sender, System.EventArgs e) { if(pf_AuthenticateUser(txtUserName.Text)) { string l_roles = pf_getroles(txtUserName.Text); FormsAuthenticationTicket tkt = new FormsAuthenticationTicket(1, txtUserName.Text , DateTime.Now, DateTime.Now.AddMinutes(30), false, l_roles); string cookiestr = FormsAuthentication.Encrypt(tkt); HttpCookie ck = new HttpCookie(FormsAuthentication.FormsCookieName, cookiestr); ck.Path = FormsAuthentication.FormsCookiePath; Response.Cookies.Add(ck); string strRedirect =Request["ReturnUrl"]; if (strRedirect==null) strRedirect = "default.aspx"; Response.Redirect(strRedirect, true); } }
Extract the user data in the AuthenticateRequest Application event (which fires on each Web request); re-create a new, properly configured, GenericPrincipal; and assign it to the Thread.CurrentPrincipal and Page.User methods:
protected void Application_AuthenticateRequest(Object sender, EventArgs e) { if (Context.Request.IsAuthenticated ) { String[] arrRoles ; System.Web.Security.FormsIdentity ident ; ident = (System.Web.Security.FormsIdentity ) Context.User.Identity; arrRoles = ident.Ticket.UserData.Split (','); Context.User = new GenericPrincipal(ident, arrRoles); } }