IMPLEMENTING .NET ROLE-BASED SECURITY
WITHOUT COM+

By Peter A. Bromberg, Ph.D.
Printer-Friendly Version

Peter Bromberg

Security is important. Most developers don't like security. It requires a lot of thought. It requires study. Most developers would rather just "write code", and leave security to "somebody else". Unfortunately, if you are a developer and your job is to produce an application, then guess who that "somebody else" usually is? It's YOU. Even behemoth Microsoft got the message loud and clear. They've made security the single most important thing, above everything else. Wanna know why .NET Server, which was supposed to be released back in March, is delayed until the third quarter? Security! They weren't satisfied, and so they went back to the drawing board, so to speak. And its for real, too. The Microsoft insiders with whom I am privileged to speak with from time to time are totally focused on security.



I'm focused on security, only partly because I'm interested in it, but mostly because I earn my living developing software for the banking and financial industry, and often regulators such as FDIC require that certain security measures be incorporated into applications. Certainly clients such as banks and insurance companies do. And, you can't just tell them "it's secure" -- they want proof.

There are a few fallacies about security that I think are important for both developers and our managers to be aware of. One is that usually because of lack of knowledge, projects often get hit with what I refer to as "Security Overkill". The result can often be a slower application that is also more expensive to install and maintain. It is certainly possible to have "too much security", especially when the payoff for it is not commensurate.

Another fallacy is that often a huge effort is put into defending applications from attackers that aren't supposed to be able to gain access, when in fact the statistics show that most successful attacks are made by insiders such as disgruntled employees who already had access!

As COM / Windows DNA developers, one of the features we've learned to use is COM+ role-based security. The typical idea is that you might have a database component that's called by users from an ASP web application, and the database component runs under a different ROLE than the code that calls it. Typically this is a Database User role. In COM+, this is extremely easy to do. Just bring up the COM+ Explorer, add a Role and user(s), and check "Enforce Access Checks", and under "This application will run under the following account", you enter the account. Presto! Instant role-based security. Er, sort - of. . . You see, an experienced programmer (read that: "hacker") can easily iterate the COM+ catalog programmatically and discover all your settings!

In .NET, a whole new world of security is opened up for us. But I want to make clear the distinctions that can be made here. Microsoft's "marketing" dictates that COM+ is available in .NET through the ServicedComponent class, and that COM+ is all "optimized" (and other glowing descriptive terms) to work with .NET, etc. And that you should use COM+ for all your role-based and Identity based security needs. Well, let me share with you a quote from Tim McCarthy and Paul Sherriff's recent article in MSDN about COM+(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/comservnet.asp):

"Tip If you do not need your .NET code to work with COM+ services, that is, you are only going to be working in the .NET Framework, do not use System.EnterpriseServices, because there is a performance penalty. "

"If you are building a new application from scratch, it is highly recommended that you build it entirely in the .NET Framework, as it will be much more efficient."

The italics and bold type are of course, mine. Note that they didn't just say there's a performance penalty. They said apps built entirely in the .NET Framework "will be much more efficient". Hey, I was all ready to start running some tests with serviced components, but after reading what the gurus have to say, why should I bother? I always suspected it, anyway. The fact is, almost everybody I've talked to (including published authors whose books I've reviewed) say that the only reason they were ever using COM+ was so they wouldn't have to restart IIS when debugging COM components! I don't know anyone who uses component - enlisted transaction services because its almost always possible to have the transactional business logic code 100 percent in SQL Server stored procedures where it willlikely be more efficient anyway.. Now please understand, I'm not "trashing" COM+ and specifically not COM+ in the .NET platform - they've done a great job of fully integrating it with .NET and making it very easy to use. I'm merely suggesting that my experience has shown that it may not always be the best way to go, especially when your needs are as simple as a role-based security context and there is no need for transactions. For blazing speed, I'll show you an alternative that's not too difficult to implement.

Regarding object pooling and Just-In-Time Activation- well, the jury is out. Of course, we could never do that with VB 6.0 anyway 'cause it's "STA all the way". In .NET, all your objects are free-threaded by default, so I wonder if it would really be worth the trouble deriving from ServicedComponent in .NET? According to Tim Ewald in his excellent MSDN article, "COM+ Integration: How .NET Enterprise Services Can Help You Build Distributed Applications", using JIT activation should be avoided in NET:

"At this point you may be wondering about JIT activation and deactivating an object at the end of every method call. Doesn't that resolve these resource management issues? The general answer is no. Deactivating an object at the end of every call forces an object to release any resource it holds in data members, but it also forces a client that wants to use the same object more than once to reacquire an object for each method call. You can achieve the benefit of this model without the overhead simply by not storing any resources in data members. If you do choose to use JIT activation and deactivate your object at the end of every call, be aware that a client's call to Dispose will actually force a reactivation of your object simply to deactivate and finalize it again. In general, the shift to the CLR doesn't change the rules about using JIT activation. You should avoid it unless it is required by some other runtime services (such as the declarative transaction service) because it is never more efficient, and is often less efficient than simply managing precious resources carefully yourself."

The good news is that you can now employ declarative and imperative role and / or identity - based security directly in your .NET Code, and avoid the potentail performance drag of COM+ as indicated by quotes from the authors above. The security model in .NET is extensive, robust, and extensible. NET Security is a very big subject; and it's too bad - because I haven't yet seen any good comprehensive books out about it yet. Maybe the people at Wrox or aPress or O'Reilly will do one? I figure its good for at least a 1,000 - pager - I'd sure buy a copy!

Managed code can discover the identity or the role of a principal through a Principal object, which contains a reference to an Identity object. .NET Framework identity objects represent users, while roles represent memberships and security contexts. In the .NET Framework, the principal object encapsulates both an identity object and a role. .NET Framework applications grant rights to the principal based on its identity or, more commonly, its role membership.

Identity Objects

The identity object encapsulates information about the user or entity being validated. Identity objects contain a name and an authentication type. The name can either be a user's name or the name of a Windows account, while the authentication type can be either a supported logon protocol, such as Kerberos V5, or a custom value. The .NET Framework defines a GenericIdentity object that can be used for most custom logon scenarios and a more specialized WindowsIdentity object that can be used when you want your application to rely on Windows authentication. Additionally, you can define your own identity class that encapsulates custom user information.

The IIdentity interface defines properties for accessing a name and an authentication type, such as Kerberos V5 or NTLM. All Identity classes implement the IIdentity interface. If the Identity object is a WindowsIdentity object, the identity is assumed to represent a Windows NT security token.

Principal Objects

The principal object represents the security context under which code is running. Applications that implement role-based security grant rights based on the role associated with a principal object. Similar to identity objects, the .NET Framework provides a GenericPrincipal object and a WindowsPrincipal object. You can also define your own custom principal classes.

The IPrincipal interface defines a property for accessing an associated Identity object as well as a method for determining whether the user identified by the Principal object is a member of a given role. All Principal classes implement the IPrincipal interface as well as any additional properties and methods that are necessary. For example, the common language runtime provides the WindowsPrincipal class, which implements additional functionality for mapping Windows NT or Windows 2000 group membership to roles.

A Principal object is bound to a call context (CallContext) object within an application domain (AppDomain). A default call context is always created with each new AppDomain, so there is always a call context available to accept the Principal object. When a new thread is created, a CallContext object is also created for the thread. The Principal object reference is automatically copied from the creating thread to the new thread's CallContext. If the runtime cannot determine which Principal object belongs to the creator of the thread, it follows the default policy for Principal and Identity object creation.

Once you have defined identity and principal objects, you can perform security checks against them in one of the following ways:

  • Using imperative security checks.
  • Using declarative security checks.
  • Directly accessing the Principal object.

Managed code can use imperative or declarative security checks to determine whether a particular principal object is a member of a known role, has a known identity, or represents a known identity acting in a role. Additionally, you can access the values of the principal object directly and perform checks without a PrincipalPermission object. In this case, you simply read the values of the current thread's principal or use the IsInRole method perform authorization.

A link demand causes a security check during just-in-time compilation and only checks the immediate caller of your code. Linking occurs when your code is bound to a type reference, including function pointer references and method calls. If the caller does not have sufficient permission to link to your code, the link is not allowed and a runtime exception is thrown when the code is loaded and run. Link demands can be overridden in classes that inherit from your code.

Here are some code examples:

[C#]
[CustomPermissionAttribute(SecurityAction.LinkDemand)]
public static string ReadData()
{
//Access a custom resource.
}
[Visual Basic]
<CustomPermissionAttribute(SecurityAction.LinkDemand)> Public Shared Function ReadData() As String
'Access a custom resource.
End Function

PrincipalPermissionAttribute:

[Visual Basic]
<PrincipalPermissionAttribute(SecurityAction.Demand, _
Name := "Bob", Role := "Supervisor")> Public Class SampleClass
[C#]
[PrincipalPermissionAttribute(SecurityAction.Demand, Name="Bob",
Role="Supervisor")]

Public Shared Sub _
<PrincipalPermissionAttribute(SecurityAction.Demand, Name := "MyUser", Role := "User")> _
PrivateInfo()

'Print secret data.
Console.WriteLine(ControlChars.CrLf + "You have access to the private data!")
End Sub

When the security check is performed, both the specified identity and role must match for the check to succeed. However, when you create the PrincipalPermission object, you can pass a null identity string to indicate that the identity of the principal can be anything. Similarly, passing a null role string indicates that the principal can be a member of any role (or no roles at all). For declarative security, the same result can be achieved by omitting either property. For example, the following code uses the PrincipalPermissionAttribute to declaratively indicate that a principal can have any name, but must have the role of teller.

Enum: SecurityAction

The following table describes the time that each of the security actions takes place and the targets that each supports.

Declaration of security action Time of action Targets supported
LinkDemand Just-in-time compilation Class, Method
InheritanceDemand Load time Class, Method
Demand Run time Class, Method
Assert Run time Class, Method
Deny Run time Class, Method
PermitOnly Run time Class, Method
RequestMinimum Grant time Assembly
RequestOptional Grant time Assembly
RequestRefuse Grant time Assembly

 

Members

Member name Description
Assert The calling code can access the resource identified by the current permission object, even if callers higher in the stack have not been granted permission to access the resource (see Assert).
Demand All callers higher in the call stack are required to have been granted the permission specified by the current permission object (see Security Demands).
Deny The ability to access the resource specified by the current permission object is denied to callers, even if they have been granted permission to access it (see Deny).
InheritanceDemand The derived class inheriting the class or overriding a method is required to have been granted the specified permission.
LinkDemand The immediate caller is required to have been granted the specified permission.
PermitOnly Only the resources specified by this permission object can be accessed, even if the code has been granted permission to access other resources (see PermitOnly).
RequestMinimum The request for the minimum permissions required for code to run. This action can only be used within the scope of the assembly.
RequestOptional The request for additional permissions that are optional (not required to run). This action can only be used within the scope of the assembly.
RequestRefuse The request that permissions that might be misused will not be granted to the calling code. This action can only be used within the scope of the assembly.

 

Now that we've endured a highly abbreviated introduction to the native .NET Security Principal and Identity objects, lets get down to some code that puts them into practice. Remember above where I indicated most "role - based" COM+ security simply involves running a component under some separate privileged Windows Account in a specific group (role)?

Let's use some code that does a P/Invoke call into the Win32 API LogonUser to "switch" the identity of the currently executing thread which will allow it to call into .NET managed code that will use either declarative or imperative Identity role checking to prevent unauthorized callers from executing privileged code. When we are done with our "database" call under the new privileged user account, we'll call RevertToSelf() to switch back to the regular identity under which the code was originally running before the database call. Best of all, this call runs in approximately 2 milliseconds on my PIII 667 at home. I'd be willing to BET that's faster than role - based security under COM+, and I doubt there is any question that deployment and maintenance are much simpler! Not only will we be able to switch the identity and prevent any user / role that's not authorized from running our "database" code, we will also be able to check the identity that is asking to perform the required impersonation - and refuse to do so if the correct calling user does not match!

First, we need a handy Class Library to take care of our call to the APIs:

Imports System
Imports System.Runtime.InteropServices
Imports System.Security.Principal
Imports System.Security.Permissions
Imports PAB
Public Class Impersonation
<DllImport("C:\\WINNT\\System32\\advapi32.dll")> _
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 Integer) As Boolean
End Function
<DllImport("C:\\WINNT\\System32\\Kernel32.dll")> _
Public Shared Function GetLastError() As Integer
End Function
Public Function ImpersonateNewUser(ByVal strMachineName As String, ByVal strCurrentUserName As String, ByVal strNewUserName As String, ByVal strNewPassword As String) As Integer

' if a DOMAIN account, strMachineName should be the DOMAIN
'The Windows NT user token.
Dim token1 As Integer
'Get the user token for the specified user, machine, and password using the unmanaged LogonUser method.
'The parameters for LogonUser are the user name, computer name, password,
'Logon type (LOGON32_LOGON_NETWORK_CLEARTEXT), Logon provider (LOGON32_PROVIDER_DEFAULT),
'and user token.
Dim loggedOn As Boolean = LogonUser(strNewUserName, strMachineName, strNewPassword, 3, 0, token1)
Debug.WriteLine("LogonUser called")
Debug.WriteLine("LogonUser Success? " + loggedOn.ToString())
Debug.WriteLine("NT Token Value: " + token1.ToString())
'First lets get the credentials of the actual user that's already in context:
Debug.WriteLine("Before impersonation:")
Dim mWI1 As WindowsIdentity = WindowsIdentity.GetCurrent()
Debug.WriteLine(mWI1.Name)
Debug.WriteLine(mWI1.Token)
If mWI1.Name <> strCurrentUserName Then Return 1
' Now set our IntPtr (token) to the new user whose token we want to impersonate:
Dim token2 As IntPtr = New IntPtr(token1)
Debug.WriteLine("New identity created:")
' now instantiate the new WindowsIdentity object:
Dim mWI2 As WindowsIdentity = New WindowsIdentity(token2)
Debug.WriteLine(mWI2.Name)
Debug.WriteLine(mWI2.Token)
'Now go ahead and Impersonate the new user.
Dim mWIC As WindowsImpersonationContext = mWI2.Impersonate()
Dim sCurrentImp As String = mWI2.Token.ToString()
System.AppDomain.CurrentDomain.SetData(sCurrentImp, mWIC)
Debug.WriteLine("After impersonation:")
Return mWI2.Token.ToInt32()
End Function

Public Function Revert(ByVal sToken As String) As Integer
Dim mWI3 As WindowsImpersonationContext = System.AppDomain.CurrentDomain.GetData(sToken)
mWI3.Undo()
Debug.WriteLine("After impersonation is reverted:")
Dim mWIR As WindowsIdentity
mWIR = WindowsIdentity.GetCurrent()
Debug.WriteLine(mWIR.Name)
Debug.WriteLine(mWIR.Token)
Dim retval As Integer = mWIR.Token.ToInt32()
Return retval
mWIR = Nothing
End Function
End Class

This class has one major function: to accept a machine name, an "authorized user" name to check against, and a new username and password to impersonate assuming the passed-in username is correct. In the Test Harness that accompanies the app, you will set the authorized Machine\UserName or DOMAIN\UserName for it to check against in the line, "If mWI1.Name <> strCurrentUserName Then Return 1". Since this function returns a Windows Identity token, we'll just send back a "1" to show that the call failed on the authorized user check so we can handle it in the calling code. Note also that for convenience, I am storing the ImpersonationContext object in the AppDomain cache by the token integer (converted to a string) so that we can gain access to this at any time:

Dim sCurrentImp As String = mWI2.Token.ToString()
System.AppDomain.CurrentDomain.SetData(sCurrentImp, mWIC)

IMPORTANT: If the user account you want to impersonate is a DOMAIN account (e.g., not native to the local machine the code is running on) then you'll need to pass in the DOMAIN name instead of the MACHINE name, and the Domain account as "UserName" instead of "DOMAIN\UserName".

Now we need a class library that will act as a surrogate or sample of our "Database Component". To keep things simple, I've set one up with only one function, "hello", which if successfully accessed, will just return the name of the Identity under which it was called:

Imports System.Security.Principal
Imports System.Security.Permissions
Imports System.Threading
Public Class Testme
<PrincipalPermissionAttribute(SecurityAction.Demand, Role:="PETER\DBUser")> _
Public Function Hello(ByVal strText As String)
Dim mWI1 As WindowsIdentity = WindowsIdentity.GetCurrent()
Debug.Write(mWI1.Name)
Return strText + " was run under permitted user:" & mWI1.Name & vbCrLf
End Function
Public Sub New()
'Get the current principal and put it into a principal object.
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
Dim MyPrincipal As WindowsPrincipal = CType(Thread.CurrentPrincipal, WindowsPrincipal)
' NOTE: uncomment following lines and Comment out the
' <PrincipalPermissionAttribute(SecurityAction.Demand, Name:="PETER\tester", Role:="PETER\DBUser")> _
' before the "Hello" function to do checks imperatively
'If Not (MyPrincipal.IsInRole("PETER\DBUser")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If
'If Not (MyPrincipal.Identity.Name.Equals("PETER\tester")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If

End Sub
End Class

Notice that I've put in two ways to handle our testing:

1) Declarative, using the <PrincipalPermissionAttribute(SecurityAction.Demand, Role:="PETER\DBUser")> attribute to decorate the Hello method, and
2) In the constructor (which code is commented out) doing it imperatively with code:

'If Not (MyPrincipal.IsInRole("PETER\DBUser")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If
'If Not (MyPrincipal.Identity.Name.Equals("PETER\tester")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If

Note also that we are setting the Principal Policy of the Current AppDomain to Windows Principal in the constructor.

Finally, we need a "Test Harness" to check all this stuff out (I've left out the IDE -Generated form designer code for brevity):

Imports System.Security.Principal
Imports System.Security.Permissions
Imports System.Threading
Public Class Form1
Inherits System.Windows.Forms.Form

Dim retval2 As Integer
Dim retval As Integer
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim mWI2 As WindowsIdentity = WindowsIdentity.GetCurrent()
' show original identity:
TextBox1.Text += "Original Identity: " & mWI2.Name & vbCrLf
' Impersonate the required user:
Dim ip As PAB.Impersonation = New PAB.Impersonation()
retval=ip.ImpersonateNewUser(System.Environment.MachineName, TextBox2.Text, TextBox3.Text, TextBox4.Text)

If retval = 1 Then
MessageBox.Show("UH-OH, Wrong user running app!")
Exit Sub
End If
'Show stored impersonation Token
TextBox1.Text += "Impersonation Token: " & retval.ToString()
' Make the privileged call...
Dim tD As Testme = New Testme()
TextBox1.Text += vbCrLf & tD.Hello("Security")
tD = Nothing
' Revert to original user
Dim retval2 As Integer = ip.Revert(retval.ToString())
TextBox1.Text += vbCrLf & "Reverted: " & retval2.ToString() & vbCrLf
' Show current identity
TextBox1.Text += "Final Identity: " & mWI2.Name
ip = Nothing
End Sub

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
TextBox1.Clear()
Dim mWI1 As WindowsIdentity = WindowsIdentity.GetCurrent()
TextBox1.Text += "Current Identity: " & mWI1.Name
Dim tD As Testme = New Testme()
TextBox1.Text += vbCrLf & tD.Hello("Security")
tD = Nothing
mWI1 = WindowsIdentity.GetCurrent()
TextBox1.Text += "Identity: " & mWI1.Name
End Sub
End Class

 

What we are doing here when you run the test harness form is:

1) If you first click Button2 (No Security) the code attempts to instantiate the TestMe class and call its Hello method. This method call will fail, because the security checks will return a Security Exception:

 

2) If you fill in an Authorized user, a DBUser and password, and click Button1 ("Security") then the code calls the impersonation API:

ip.ImpersonateNewUser(System.Environment.MachineName, TextBox2.Text, TextBox3.Text, TextBox4.Text)

Since the call to the Hello Method is now being run under the required user / role identity, the call will succeed:

 

Of course, the user information you see above is native to the machine I ran this code on; you'll need to revise the checks and users to correspond to your specific machine. You can see above that the Hello method was indeed run under the required "PETER\tester" identity / role, and that the final Identity after calling the Revert Method is the original user once again.

Role - based security allows developers to provide access permissions to specified users / groups at a very fine-grained level. This article only touches on one area; this is a big subject and a full understanding of code access security in the .NET Framework requires significant study. However the power and exceptional speed of being able to control access to code through these various techniques is well worth the effort. As has been seen, COM+ is superflous, overkill, and completely unnecessary to perform these actions!

 

Download the code that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.