By Michele Leroux Bustamante
For any application development cycle, one of the first critical steps to take before you code is to envision and document the entire system’s component architecture. With this information on hand, it is possible to evaluate security requirements for each facet and workflow through that component architecture. Evaluating security means that a team should model possible threats to the system, but to support this effort developers must have a firm grasp on security features of .NET including code access security (CAS) and role-based security. They must also understand how the runtime security engine interacts with these security features.
Now that .NET has been around for a while, many developers have at least a basic understanding of role-based security and how to implement this for Windows and Web-based applications. Still, things can get confusing given so many design choices, particularly when you begin discussing multithreading and distributed architecture. In this article I’ll focus on features related to authentication, authorization and role-based security. I’ll summarize examples of best practices for implementing role-based security in some typical .NET application scenarios including rich clients, Web applications, and Web services, to give you a birds-eye view of your options. For each of these application architectures, I’ll also comment on the applicability of Windows-authentication versus custom or forms-based authentication models.
Let’s begin with a quick review some of the basic .NET authentication concepts.
Identity and Principal Objects: Just Who Are You Anyways?
Role-based security is built on the premise that users are authenticated, which is the process of identifying the user. Once identified, the user can be authorized or, assigned roles and permissions. Credentials like a username and password are usually provided to authenticate users, and this information is used to create a security principal representing this user’s identity at runtime. The .NET Framework object model includes built-in support to work with Windows, custom and even Microsoft Passport credentials.
Let’s assume that credentials have been collected from the user, and that a security principal has been created. To understand how this security principal is used by the runtime it is important to consider the relationship between the running process, the application domain, and the assemblies loaded within that application domain. In addition, we must consider how one or more logical threads of execution are handled. Figure 1, shows a single process with a Windows executable (WinClient.exe) hosted within an application domain. The main thread (t1) may start another thread (t2) to invoke a method in another DLL assembly (ServerLib.dll).
Figure 1
By default the process runs under the logged in user’s Windows identity, and this governs what resources can be accessed by any thread of execution within that process. If a method invoked by the execution context of t1 or t2 attempts to write to the file system, the process identity governs its privileges. Yet, each thread of execution can also be assigned an identity which governs how role-based security checks are evaluated at runtime. For example, ServerLib.dll can demand that the executing thread have an authenticated security principal within the BUILTIN\Administrators role for any methods that access the file system. This additional check can complement resource protection afforded by the Windows operating system’s checks against the process identity.
The following is an example of a method that declaratively (using .NET attributes) demands the identity attached to the call context is within the BUILTIN\Administrators role:
<PrincipalPermission(SecurityAction.Demand, Role:="BUILTIN\Administrators")> _ Public Sub AdminOnlyMethod() 'perform restricted actions End Sub
For this demand to work the user must be authenticated, and its security principal attached to the executing thread. Later I’ll review how this authentication step is handled by different application architectures. For now, just know that each thread has access to its security principal through the CurrentPrincipal property. WindowsPrincipal and GenericPrincipal types, and any custom principals also implementing the IPrincipal interface, can be attached to a thread by setting this property. A WindowsPrincipal is used for Windows authentication schemes, and a GenericPrincipal for custom authentication schemes that don’t rely on the Windows domain. The IPrincipal interface provides a consistent way for security principals to expose information about a user’s identity and roles. This includes access to an identity object, represented at runtime by a valid IIdentity type for example a WindowsIdentity or GenericIdentity.
The point here is that the user is identified by an identity abstraction at runtime, and this identity must be somehow attached to each executing thread to be applicable during role-based security checks. Assuming the main thread has been assigned a valid security principal, any new thread will be initialized by the runtime with the same principal. That means you shouldn’t need to manually propagate this information to new threads within the same application domain.
NOTE: Crossing application domain, process and machine boundaries using .NET Remoting or Enterprise Services requires additional discussion and is covered in several resources recommended at the end of this article.
Throughout this article I’ll focus on how user’s are authenticated and subsequently authorized, how the process identity is assigned its Windows identity, how (and why) to override this with impersonation, and how to apply role-based security with thread identities that are propagated throughout the execution context.
Authentication and the Smart Client
Whether you’re building smart clients (a new generation of Windows application with offline functionality and Web services on the back-end) or traditional rich clients (read: non-Web highly functional user interface) the process of authentication must still begin with collection of credentials. I mentioned that a Windows executable runs within its own process, the assembly loaded into an application domain, the process identity that of the logged on user to the machine. By default, however, no authentication takes place, thus the execution thread is not yet associated with a security principal. Access to system resources from this process are still governed by the process identity, but role-based permission demands will fail without an authenticated principal tied to the execution context.
Running as the Logged On User
There are a number of ways to authenticate a user for a Windows Forms application. If the intention is to use the security principal attached to the running process, then you can assign it to the current thread:
Thread.CurrentPrincipal = New WindowsPrincipal(WindowsIdentity.GetCurrent())
The WindowsIdentity object exposes the Windows identity of the running process, which is used here to create a new WindowsPrincipal for the thread. The identity has already been authenticated (during login, presumably) and the WindowsPrincipal object will have access to the same underlying roles associated with this identity. Until this statement is encountered the thread does not have a security principal, however, if you set the PrincipalPolicy of the application domain before the main thread is created, new threads will be assigned this WindowsPrincipal automatically:
AppDomain.CurrentDomain.SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal)
The same results are achieved if the ThreadPrincipal is set before the main thread is created:
AppDomain.CurrentDomain.SetThreadPrincipal(New WindowsPrincipal(WindowsIdentity.GetCurrent())
In the code sample, this is done in Main() before the main form is loaded. The point of this is to save the trouble of setting the primary thread’s CurrentPrincipal. Once this is set, a permission demand or link demand can evaluate the properties of the security principal for the execution context. For example a component can restrict method access to authenticated callers using a declarative permission demand:
<PrincipalPermission(SecurityAction.Demand, Authenticated:=True)> _ Public Shared Sub ShowIdentitiesToAuthenticatedUsers()
The permission demand can also verify a particular user or role (although the former is not practical):
<PrincipalPermission(SecurityAction.Demand, _ Role:="BUILTIN\Administrators")> _ Public Shared Sub ShowIdentitiesToAdministrators()
The PrincipalPermissionAttribute, like other code access security attributes, can invoke security demands, link demands and inheritance demands for particular methods, or for all member access when applied to the class definition as shown here:
<PrincipalPermission(SecurityAction.Demand, _ Role:="BUILTIN\Administrators")> _ Public Class ProtectedOperations // all operations are protected End Class
Attributes make it possible to declaratively enforce role-based security, but if more flexibility is required permissions can be checked at runtime at a more granular level. Declarative demands protect the entire class or method call, whereas programmatic demands can be performed just prior to executing functionality that should be restricted to a particular role or user:
Dim p As New PrincipalPermission("MLBLAPTOP\Administrator", _ "BUILTIN\Administrators", True) p.Demand() // do something restricted to the Administrator account
Part of baking role-based security into your applications includes strategically placing these types of permission demands to encapsulate the protection of restricted functionality. This can further protect resources if the authenticated user has fewer privileges than the identity of the executing process, but it can also be a means to provide additional information to users about those restrictions. Furthermore, the roles assigned to the authenticated user can be used to control what features of the user interface are enabled or disabled.
If Not (Thread.CurrentPrincipal.IsInRole("Administrators")) Then Me.labInfo.Text = "Only administrators can alter images." Me.txtDescrip.Enabled = False Me.txtDate.Enabled = False End If
It is important to note that the logged on user means the user credentials that were used to run the process. By default, this is the user that logged on to the machine, however any process can be "Run As…" another identity, therefore that identity becomes the logged on user for this process. Regardless, using the same identity for the security principal assigned to executing threads is an acceptable design for many Windows applications. However, we must address the need for applications to access resources that may not be accessible to the logged on user. Next I’ll explain how to request user credentials and create a Windows token for the security principal, which will also lead to discussions about runtime impersonation which can be used to silently elevate privileges.
Gathering Windows Credentials
An application can collect user credentials and use the LogonUser function to create a valid Windows token. From this a WindowsPrincipal can be generated that wraps the token’s identity. The following statement uses the DllImportAttribute to import the LogonUser method from the external advapi32.dll:
<DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _ Public Shared Function _ LogonUser(ByVal lpszUsername As String, _ ByVal lpszDomain As String, _ ByVal lpszPassword As String, ByVal dwLogonType As Integer, _ ByVal dwLogonProvider As Integer, ByRef phToken As IntPtr) As Boolean End Function
First, present a dialog to your users to collect domain, user ID and password information. Then, using Platform Invoke (P-Invoke) to call LogonUser a token is generated that can be used to create a WindowsIdentity:
Dim user As IIdentity Dim principal As WindowsPrincipal Dim refToken As IntPtr Dim loggedIn As Boolean loggedIn = LogonAPI.LogonUser(userName, domain, password, LogonAPI.LOGON32_LOGON_NETWORK_CLEARTEXT, LogonAPI.LOGON32_PROVIDER_DEFAULT, refToken) If loggedIn = True Then user = New WindowsIdentity(refToken, "NTLM", WindowsAccountType.Normal, True) principal = New WindowsPrincipal(user) Thread.CurrentPrincipal = p End If
There are several constructors for the WindowsIdentity type, but you must invoke a constructor that initializes the IsAuthenticated property, otherwise the identity remains unauthenticated as far as the runtime is concerned.
NOTE: In my example I’m assuming the authentication type is NTLM, because I’m not part of an Active Directory domain, but it is also possible to inspect the token to find out what its type is, rather than hard-coding as shown here. Generally, authentication type is an unimportant trait.
This Windows identity is wrapped by a WindowsPrincipal which can be assigned to the current thread’s CurrentPrincipal property to affect how demands are evaluated during execution. The WindowsPrincipal type uses low level APIs to access roles for the associated Windows token, so there is no need to write custom code to gather roles.
Collecting credentials when the application starts makes it possible to force the user to log in to the application, regardless of the logged on user attached to the process. This also means that the security principal assigned to the thread may not match that of the process identity that governs access to system resources. That’s ok if your role-based security checks are not tied to the same resources that the application may require access to, however this inconsistency can lead to a disconnected security model. A better model would be to choose one of a) using the process identity for runtime role-based security checks or b) use the supplied user credentials to create a security token to attach as the security principal for the main (and subsequent) thread(s), and impersonate that token so that the process identity matches that of the security principal.
Runtime Impersonation
For a consistent approach to security the process identity should match the runtime security principal. This way access to system resources is governed by the user who logged in to the application. To achieve this, we tell the process to impersonate a particular identity.
Using the same token retrieved from a call to LogonUser above, you can call the Impersonate method on the WindowsIdentity object:
loggedIn = LogonAPI.LogonUser(userName, domain, password, LogonAPI.LOGON32_LOGON_NETWORK_CLEARTEXT, LogonAPI.LOGON32_PROVIDER_DEFAULT, refToken) If loggedIn = True Then Dim impContext As WindowsImpersonationContext impContext = WindowsIdentity.Impersonate(refToken) user = New WindowsIdentity(refToken, "NTLM", WindowsAccountType.Normal, True) principal = New WindowsPrincipal(user) Thread.CurrentPrincipal = principal End If
Impersonate() actually invokes the low level API function, ImpersonateLoggedOnUser, which sets the process identity to a special state where access to system resources is now governed by the impersonated identity. The current thread will not reflect this new identity unless you also attach the WindowsPrincipal to it and other already executing threads. That leads to a good point about when you should impersonate. If the intent is to collect credentials and log in a user for the duration of the application’s execution, then before any new threads are started, the process of setting the primary thread’s security principal must be completed. This means that new threads will receive this identity, without the need for manual assignment (there are a few exceptions).
Impersonation is reversed by calling Undo() from the WindowsImpersonationContext originally returned by the impersonation event:
m_impContext.Undo()
Elevating Privileges with Impersonation
It is reasonable to think that a particular component may want to access resources based on a privileged account. For example, a component that accesses restricted file system directories may require administrative privileges, while the application is running under a lower privilege user account.
The same impersonation process described above can be used to impersonate a specific account for this purpose, however, rather than asking the user to supply credentials they don’t have access to credentials would have to be supplied by some other means such as hard-coded into the component or gathered from application configuration. Of course, due to the sensitive nature of these credentials this information must be encrypted and protected from access. External configuration files are too easily accessible, even if data is encrypted within, so it would be better to hide encrypted credentials in the Windows Registry, embed encrypted values in the component that impersonates, or use a component that runs in a separate process such as a serviced component that is configured to run with the higher privilege account, thus removing the need to impersonate or come up with complicated encryption and configuration schemes.
Of course, when taking the impersonation route, the component that performs this impersonation should at least verify that it is being called by a trusted source. Using a code access security demand, for example, the component can require that a specific assembly or any assembly carrying a specific strong name signature, are making the request. This excludes malicious callers from writing clients to consume this component that is able to access sensitive resources with elevated account privileges.
In the code sample supplied with this article, the method that performs the impersonation can only be invoked if the StrongNamePermission link demand is satisfied:
<StrongNameIdentityPermission(SecurityAction.LinkDemand, _ PublicKey:="002400000…A8")> _ Public Shared Function Save(ByVal stm As Stream, ByVal filename As String, ByVal descrip As String, ByVal dt As String) As Stream ' method code End Function
Within the method, the Finally block reverses impersonation to insure that code will not continue to run with a higher privilege, even if an error occurs:
Dim imp As WindowsImpersonationContext Try imp = WindowsLogon.ImpersonateUser("MLBLAPTOP", _ "mlbadmin", "mlbadmin") bmp.Save(originalPhoto) newBmp.Save(newPhoto) Finally imp.Undo() End Try
Impersonation credentials should never be hard-coded in the component, and certainly wherever they are stored should be encrypted. Impersonation at runtime is a complicated matter since the account with file system permissions may change, yet must be safely stored while configurable. To reduce configuration complexity, it may make more sense to deploy a serviced component (Enterprise Services) that is configured to run under the appropriate identity, so that the component is not coupled to the process of setting runtime identities. (See resources for references on this subject.)
The Case for Custom Authentication
At this point, you have learned how processes and threads are assigned identities, how Windows identities are authenticated, and how role-based security demands can interact with the authenticated user. You also can see how impersonation may alter the identity under which resources are accessed by a particular thread. Some Windows applications may supply their own database to store credentials, thus the need for custom authentication schemes. The process identity still takes on the logged on user by default, and impersonation can still be used by creating an alternate token at runtime, however a GenericPrincipal will be attached to each thread instead of a WindowsPrincipal.
After collecting user credentials you create a GenericIdentity by supplying the username and authentication type (which you provide):
Dim user As IIdentity user = New GenericIdentity("Admin", "CustomAuthentication")
Assuming you’ve accessed the credential store to authenticate the user, you can collect the appropriate list of roles and create a new GenericPrincipal for the identity:
Dim principal As GenericPrincipal Dim roles() As String = GetUserRoles(user) principal = New GenericPrincipal(user, roles) Thread.CurrentPrincipal = principal
NOTE: You can create custom identity and principal objects that encapsulate behaviors, so any IIdentity and IPrincipal type can be applied here.
Once the CurrentPrincipal is set for the main thread, new threads will also inherit it from the call context, so there is no need to manually set this principal on each thread you create. The exception to this is when ThreadPool.QueueUserWorkItem is used to create the thread. Permission demands for user credentials work in the same manner as with Windows credentials, except that role demands must be named according to valid roles in the database:
Dim p As PrincipalPermission p = New PrincipalPermission(Nothing, "ReadWrite", True) p.Demand()
Role checks can also be used to enable/disable application functionality. A custom authentication model can also be useful also for invoking Web services from a smart client application. These credentials can be assigned to the username token passed to the service, using the WS-Security standard supported by Web Services Enhancements (WSE) 2.0.
In this case, users are likely to log in at application startup, the GenericPrincipal assigned to the main thread to wrap the user’s identity and applicable roles. This security principal will not govern access to resources directly, but can be used to limit access to functionality, and to propagate the user through calls to Web services.
In the following sections I’ll discuss how these authentication and authorization concepts apply to Web applications and services.
Authentication and Impersonation with ASP.NET
The same distribution of process identity and thread identity exists for Web applications and services. By default, the ASP.NET process identity is identified by the <processModel> section of the machine.config. Unless the worker process is asked to impersonate another account, this is the identity that governs your Web application’s access to system resources such as the file system, the Windows registry, and the database if integrated Windows accounts are used. There remains the same distinction between the process identity and the security principal attached to the thread handling each request.
This becomes important for Web applications since we need to carefully consider the resources that processes and components have access to. By default the ASP.NET worker process runs under a lower privilege account however it is often necessary for business components supporting the application to access resources that require a particular identity is impersonated.
NOTE: You should avoid elevating the privileges of the ASP.NET worker process identity, rather, impersonate as needed or configure serviced components with the correct privilege to access resources.
IIS and Integrated Windows Authentication
If the application is configured in IIS to use integrated Windows authentication, and anonymous access is turned off, then IIS is responsible for gathering user credentials and creating a valid Windows token. Basic authentication means that credentials are passed in clear text, so this is not recommended unless the application is hosted on a secure server using SSL certificates. Digest authentication relies on an Active Directory store and is the most likely scenario for an Intranet application. In my example, since I’m running from my local machine, I’m using NTLM.
Regardless of the mechanism, with integrated Windows authentication a token is generated by IIS and ASP.NET attaches a security principal to the thread processing the request. Unlike with the rich client scenario, for ASP.NET applications we do not have to mess with the creation of the security principal for Windows authentication. Instead the following web.config setting triggers the right behavior:
<authentication mode="Windows" /> <authorization> <allow users="*" /> </authorization>
Role-based security checks from business components invoked during the round trip automatically validate against the generated Windows security principal. The <authorization> section above allows all users access to the site, once authenticated. You can also restrict the site to specific users and roles, or use this section to protect subdirectories and specific files specified in a <location> tag of the web.config.
Impersonation on the Web
In the case of Intranet applications it is often a good idea to impersonate the user that logged in to the site, to insure that resources are restricted based on the user’s rights. To configure this type of impersonation for a Web application you would add this entry to the web.config:
<identity impersonate="true" />
With this configuration, the security principal for the user authenticated by IIS is automatically impersonated for the request. If IIS is configured for anonymous access, the default token specified for anonymous access is impersonated, although this is not generally useful. The point here is that you can restrict access to resources by user, for each request.
Even for non-Intranet applications where users are not authenticated via integrated windows security, it may be useful for the application process to impersonate a user other than that configured in <processModel>. This can be done in the web.config by specifying the identity as shown here:
<identity impersonate="true" userName="dbwrite" password="dbwrite" />
This approach is not recommended for many reasons, but primarily you don’t want the entire application to run with higher privileges granted to access the database or other protected resources. Instead, any component invoked during a round trip that requires a higher privilege account to operate can impersonate the appropriate account at runtime for the duration of the invocation, which reduces the ability for a hacker to exploit the system under that account. An even better model would be to configure a serviced component (Enterprise Services) that executes under the privilege necessary to access the resource. In general, it is best to run the ASP.NET process under a low privilege account, and isolate components that require additional privilege, to remove them from the main process where hackers may have opportunity to run malicious code.
Forms Authentication
For Internet applications, managing an Active Directory store of credentials for every user that hits the site is just not a manageable proposition. So, rather than IIS authenticating the user, anonymous access is usually enabled and ASP.NET is left to determine the appropriate authentication. Setting the authentication model to Forms authentication will automatically redirect rejected requests to login.aspx (this form name is configurable). To deny access to anonymous users deny ? within the <authorization> section:
<authentication mode="Forms" /> <authorization> <deny users="?" /> <allow users="*" /> </authorization>
As with the Windows client example, a GenericPrincipal is created and attached to the execution context, but in this case the identity it references is a FormsIdentity rather than a GenericIdentity. The FormsIdentity has additional information about the FormsAuthenticationTicket created during the login process. You can use the RedirectFromLoginPage method of the FormsAuthentication utility object, or manually create the FormsAuthenticationTicket, and associated cookie this way:
Dim redirectUrl As String = FormsAuthentication.GetRedirectUrl( Me.TextBox1.Text, False) Dim authCookie As HttpCookie = FormsAuthentication.GetAuthCookie( Me.TextBox1.Text, True) authCookie.Expires = DateTime.Now.AddMinutes(20) Response.Cookies.Add(authCookie) Response.Redirect(redirectUrl)
The benefits of managing this process yourself are somewhat interesting. In fact the authentication cookie by default is set with an expiry date of DateTime.Now.AddYears(50). That means that unless you actually sign out through the Web site, or delete your cookies, you will likely never have to log in again on the same machine. Of course you’ll replace the machine before the cookie runs out.
For role-based permission demands to work, you still need to provide information about the user’s roles. With the stateless model of ASP.NET, the assignment of roles is disconnected from the creation of the authentication ticket. Once authenticated, each request from the user will pass an HTTP header with the cookie:
Cookie: .ASPXAUTH=838DC6…07
The ASP.NET runtime uses this header to create the GenericPrincipal and FormsIdentity for the request context, which is how we access the thread’s identity:
System.Web.HttpContext.Current.User.Identity
Before the request is processed, you can pick up the AuthenticateRequest application event in the Global.asax and create a new GenericPrincipal for the user’s identity that assigns roles as shown in the rich client example:
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs) If System.Web.HttpContext.Current.Request.IsAuthenticated Then Dim user As IPrincipal = System.Web.HttpContext.Current.User ' look up the user's roles, possibly cache them Dim principal As GenericPrincipal = _ New GenericPrincipal(user.Identity, _ GetUserRoles(user.Identity.Name)) System.Web.HttpContext.Current.User = principal End If End Sub
This ensures that each round trip has a security principal with information necessary to support role-based permission demands. In fact, this is what assigns the appropriate GenericPrincipal to the thread processing the request.
With this authentication model, you leverage a custom data store with user credentials, assign those credentials using FormsAuthentication utility functions, and from there your permission demands to restrict component access are handled by the runtime.
Web Services Authentication and WSE 2.0
At this point we’re winding down the options for discussing new authentication techniques, and the variation exists mostly with how we pass credentials around to make things happen. Web services that are hosted as part of an Intranet solution can leverage Windows authorization schemes through IIS in the same manner as for Web applications discussed earlier. The same IIS settings and web.config sections are set to use Windows authentication and impersonation is handled with the same techniques and best practices discussed earlier.
The main distinction with Web services is on the client side. Browsers handle the presentation of the dialog to collect user credentials at runtime, but Web service proxy classes are not equipped to handle this by default. Assuming the client application has already collected Windows credentials for the logged on user, or through dialog, it is possible to tell the Web service proxy what credentials to use for the Web service invocation. Here is an example:
Dim svc As New localhost.PhotoSaver svc.Credentials = System.Net.CredentialCache.DefaultCredentials svc.SaveImage(bytes, Me.labFilename.Text, Me.txtDescrip.Text, Me.txtDate.Text)
More commonly, services are invoked using more complex security technologies as specified by the OASIS specification for WS-Security. This is beyond the scope of this article, however sample code is provided for you to review a simple implementation of WS-Security where a username token is passed to the service, and the service in turn creates a GenericPrincipal after verifying the token. The issues around process identity versus thread processing the request are identical to those issues discussed for ASP.NET applications above.
Summary
In this article I reviewed the basics of authorization and authentication for .NET applications, but the main message here is that it is important to understand how system resources are protected and how role-based security is enforced and the distinction between them. The common theme across all application architectures is that every process has an identity that restricts access to system resources, yet the execution thread (which means the round trip thread for Web applications and services) can be assigned a Windows or custom security principal to enforce runtime role-based decisions. Understanding your component architecture prior to designing a security model is critical, in order to make decisions about the need for impersonation, or the need to configure components to run under elevated privileges. The resources I have listed for this article will also lead you to additional information about crossing process boundaries with .NET Remoting and Enterprise Services, the latter of which is a key component of most scalable .NET solutions.
Resources
Michele’s Security Resource Page (code samples for this article can be found here)
http://www.dotnetdashboard.net/sessions/securitysummit.aspx
Michele’ Blog
http://www.dasblonde.net
IDesign Downloads
http://www.idesign.net