Role-Based Security checks in ASP.net
Post date: Aug 1, 2011 3:59:58 AM
Role-based security is not new to the .NET Framework. If you already have experience developing COM+ components, you surely have come across role-based access security. The concept of role-based security for COM+ applications is the same as for the .NET Framework. The difference lies in the way security is implemented.
When we talk about role-based security, we repeatedly use the same example. This is not because we can’t create our own example, but because it explains role-based security in a way everyone understands. So here it is: You build a financial application that can handle deposit transactions. The rule in most banks is that the teller is authorized to make transactions up to a certain amount—let’s say $5,000. If the transaction goes beyond that amount, the teller’s manager has to step in to perform the transaction. However, because the manager is only authorized to do transactions up to $10,000, the branch manager has to be called to process a deposit transaction that is over that amount.
Therefore, using this analogy, role-based security has to do with limiting the tasks a user can perform, based on the role(s) that user plays or the user’s identity. Within the .NET Framework, this all comes down to the principal that holds the identity and role(s) of the caller. As discussed earlier in this chapter, every thread is provided with a principal object. To have the .NET Framework handle the role-based security in the same manner as it does code access security, we define the permission class PrincipalPermission. To avoid any confusion, PrincipalPermission is not a derived class of CodeAccessPermission. In fact, PrincipalPermission holds only three attributes: User, Role, and the Boolean IsAuthenticated.
Principals
Let’s get back to where it all starts: the principal. From the moment an application domain is initialized, a default call context is created, to which the principal will be bound. If a new thread is activated, the call context and the principal are copied from the parent thread to the new thread. Together with the Principal object, the Identity object is also copied. If the CLR cannot determine the principal of a thread, a default Principal and Identity object is created so that the thread can run at least with a security context with minimum rights. There are three type of principals: WindowsPrincipal, GenericPrincipal, and CustomPrincipal. The latter goes beyond the scope of this appendix and is not discussed any further. Let’s take a look at the first two.
WindowsPrincipal
Because the WindowsPrincipal that references the WindowsIdentity is directly related to a Windows user, this type of identity can be regarded as very strong because an independent source authenticated this user.
To be able to perform role-based validations, you have to create a WindowsPrincipal object. In the case of the WindowsPrincipal, this process is reasonably straightforward, and there are actually two ways of implementing it. The one you choose depends on whether you have to perform just a single validation of the user and role(s), or you have to do this repeatedly. Let’s start with the single validation solution:
Initialize an instance of the WindowsIdentity object using this code:
C#: WindowsIdentity WinIdent = WindowsIdentity.GetCurrent(); VB.NET: Dim WinIdent as WindowsIdentity = WindowsIdentity.GetCurrent()
Create an instance of the WindowsPrincipal object and bind the WindowsIdentity to it:
C#: WindowsPrincipal WinPrinc = new WindowsPrincipal(WinIdent); VB.NET: Dim WinPrinc as New WindowsPrincipal(WindIdent)
Now you can access the attributes of the WindowsIdentity and WindowsPrincipal object:
C#: string PrincName = WinPrinc.Identity.Name; C#: string IdentName = WinIdent.Name; //this is the same as the previous line C#: string IdentType = WinIdent.AuthenticationType; VB.NET: Dim PrincName As String = WinPrinc.Identity.Name VB.NET: Dim IdentName As String = WinIdent.Name 'this is the same as VB.NET: the previous line VB.NET: Dim IdentType As String = WinIdent.AuthenticationType
If you have to perform role-based validation repeatedly, binding the WindowsPrincipal to the thread is more efficient, so that the information is readily available. In the previous example, you did not bind the WindowsPrincipal to the thread because it was intended to be used only once. However, it is good practice to always bind the WindowsPrincipal to the thread; that way, in case a new thread is created, the principal is also copied to the new thread:
Create a principal policy based on the WindowsPrincipal and bind it to the current thread. This initializes an instance of the WindowsIdentity object, creates an instance of the WindowsPrincipal object, binds the WindowsIdentity to it, and then binds the WindowsPrincipal to the current thread. This is all done in a single statement:
C#: AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal); VB.NET: AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy. WindowsPrincipal)
Get a copy of the WindowsPrincipal object that is bound to the thread:
C#: WindowsPrincipal WinPrinc = (WindowsPrincipal) Thread.CurrentPrincipal; VB.NET: Dim WinPrinc As WindowsPrincipal = Ctype(Thread.CurrentPrincipal, _ WindowsPrincipal)
It is possible to bind the WindowsPrincipal in the first method of creation to the thread. However, your code must be granted the SecurityPermission permission to do so. If that is the case, you bind the principal to the thread with the following:
C#: Thread.CurrentPrincipal = WinPrinc; VB.NET: Thread.CurrentPrincipal = WinPrinc
GenericPrincipal
In a situation in which you do not want to rely on the Windows authentication but want the application to take care of it, you can use the GenericPrincipal.
Note
Always use an authentication method before letting a user access your application. Authentication, in any shape or form, is the only way to establish an identity. Without it, you are not able to implement role-based security.
Let’s assume that your application requested a username and password from the user, checked it against the application’s own authentication database, and established the user’s identity. You then have to create the GenericPrincipal to be able to perform role-based verifications in your application:
Create a GenericIdentity object for the User1 you just authenticated:
C#: GenericIdentity GenIdent = new GenericIdentity("User1"); VB.NET: Dim GenIdent As New GenericIdentity("User1")
Create the GenericPrincipal object, bind the GenericIdentity object to it, and add roles to the GenericPrincipal:
C#: string[] UserRoles = {"Role1", "Role2", "Role5"}; C#: GenericPrincipal GenPrinc = new GenericPrincipal(GenIdent, UserRoles); VB.NET: Dim UserRoles as String() = {"Role1", "Role2", "Role5"} VB.NET: Dim GenPrinc As New GenericPrincipal(GenIdent, UserRoles)
Bind the GenericPrincipal to the thread. Again, you need SecurityPermission:
C#: Thread.CurrentPrincipal = GenPrinc; VB.NET: Thread.CurrentPrincipal = GenPrinc
Manipulating Identity
You can manipulate the identity that is held by a principal object in two ways. The first is replacing the principal; the second is by impersonating an identity.
Replacing the principal object on the thread is a typical action you perform in applications that have their own authentication methods. To be able to replace a principal, your code must have been granted the SecurityPermission, or more specifically, the SecurityPermission attribute ControlPrincipal. This will allow your own code to be able to pass on the PrincipalObject to other code. This attribute grants you the permission to manipulate the principal so that the CLR allows you to pass on the principal. You can replace the Principal object by performing these steps:
Create a new identity and Principal object, and initialize it with the proper values.
Bind the new principal to the thread:
C#: Thread.CurrentPrincipal = NewPrincipalObject; VB.NET: Thread.CurrentPrincipal = NewPrincipalObject
Impersonating is also a way of manipulating the principal, with the intent of taking on another user’s identity user to perform some actions on that user’s behalf. You can identify two variations:
The code has to impersonate the WindowsPrincipal that is attached to the thread. This might seem a little odd, but you have to remember that your code is part of an application domain that runs in a process. A user—whether a system account, a service account, or even an interactive user—starts this process on the Windows platform. Although the principal can be used to perform role-based verification within the code, accessing protected resources is still done with the identity of the process user, unless you actively use the user account of principal through impersonation.
The code has to impersonate a user who is not attached to the current thread. The first thing you have to do is obtain the Windows token of the user you want to impersonate. This has to be done with the unmanaged code LogonUser. The obtained token has to be passed to a new WindowIdentity object. Now you have to call the Impersonate method of WindowsIdentity. The old identity—hence, token—has to be saved in a new instance of WindowsImpersonationContext.
At the end of the impersonation, you have to change back to the original user account by calling the Undo method of the WindowsImpersonationContext.
Remember, the Principal object is not changed; rather, the WindowsIdentity token, representing the Windows account, is switched with the current token. At the end of the impersonation, the tokens are switched back again, as shown in the following steps:
Call the LogonUser method, located in the unmanaged code library advapi32.dll. You pass the username, domain, password, logon type, and logon provider to this method, which will return a handle to a token. For the sake of the example, we will call it hImpToken.
Create a new WindowsIdentity object and pass it the token handle:
C#: WindowsIdentity ImpersIdent = new WindowsIdentity(hImpToken); VB.NET: Dim ImpersIdent As New WindowsIdentity(hImpToken)
Create a WindowsImpersonationContext object and call the Impersonate method of ImpersIndent:
C#: WindowsImpersonationContext WinImpersCtxt = ImpersIdent.Impersonate(); VB.NET: Dim WinImpersCtxt As WindowsImpersonationContext = ImpersIdent.Impersonate()
At the end of the call, the original Windows token has to be put back in the Identity object:
C#: WinImpersCtxt.Undo(); VB.NET: WinImpersCtxt.Undo()
You could have done Steps 2 and 3 in one statement that looks like this:
Dim WinImpersCtct As WindowsImpersonationContext = _ WindowsIdentity.Impersonate(hImptoken)
Remember that you cannot impersonate when you use a GenericPrincipal, because it does not reference a Windows identity. For generic principals, you need to replace the principal with one that has a new identity.
Role-Based Security Checks
Now that we’ve discussed the creation and manipulation of PrincipalObject, it’s time to take a look at how it can assist you in performing role-based security checks. Here is where PrincipalPermission, already mentioned in the beginning of the “Role-Based Security” section, comes into play. Using PrincipalPermission, you can make checks on the active Principal object, whether WindowsPrincipal or GenericPrincipal. The active Principal object can be one you created to perform a one-time check, or it can be the principal you bound to the thread. Like the code access permissions, the PrincipalPermission can be used in both the declarative and the imperative way.
To use PrincipalPermission in a declarative manner, you need to use the PrincipalPermissionAttribute object, as shown in Figures A.17 and A.18.
[PrincipalPermissionAttribute(SecurityAction.Demand, Name = "User1", Role = "Role1" )] public int Act2() { return 1; } [assembly:PrincipalPermissionAttribute(SecurityAction.Demand, Role = "Administrator")]
Figure A.17: Using the PrincipalPermissionAttribute: C#
Public Shared Function _ <PrincipalPermissiobAttribute(SecurityAction.Demand, _ Name := "User1", Role := "Role1")> Act2() As Integer ' body of the function End Function <assembly: PrincipalPermissionAttribute(SecurityAction.Demand, Role := 'Administrator')>
Figure A.18: Using the PrincipalPermissionAttribute: VB.NET
To use the imperative manner, you can perform the PrincipalPermission check, as shown in Figures A.19 and A.20.
PrincipalPermission PrincPerm = new PrincipalPermission("User1", "Role1"); PrincPerm.Demand();
Figure A.19: Using PrincipalPermission: C#
Dim PrincPerm As New PrincipalPermission("User1", "Role1") PrincPerm.Demand()
Figure A.20: Using PrincipalPermission: VB.NET
It is also possible to use the imperative to set the PrincipalPermission object in two other ways, as shown in Figures A.21 and A.22.
PrincipalPermission PrincPerm = new PrincipalPermission(PermissionState.Unrestricted);
Figure A.21: C#
Dim PrincState As PermissionState = Unrestricted Dim PrincPerm As New PrincipalPermission(PrincState)
Figure A.22: VB.NET
The permission state (PrincState) can be None or Unrestricted, where None means the principal is not authenticated. Therefore, the username is Nothing, the role is Nothing, and Authenticated is false. Unrestricted matches all other principals.
bool PrincAuthenticated = true; PrincipalPermission PrincPerm = new PrincipalPermission("User1", "Role1", PrincAuthenticated);
Figure A.23: C#
Dim PrincAuthenticated As Boolean = True Dim PrincPerm As New PrincipalPermission("User1", "Role1", PrincAuthenticated)
Figure A.24: VB.NET
The IsAuthenticated field (Princauthenticated) can be true or false.
In a situation in which you want PrincipalPermission.Demand() to allow more than one user/role combination, you can perform a union of two PrincipalPermission objects. However, this is possible only if the objects are of the same type. Thus, if one PrincipalPermission object has set a user/role, and the other object uses PermissionState, the CLR throws an exception. The union looks like Figures A.25 and A.26.
PrincipalPermission PrincPerm1 = new PrincipalPermission("User1", "Role1"); PrincipalPermission PrincPerm2 = new PrincipalPermission("User2", "Role2"); PrincPerm1.Union(PrincPerm2).Demand();
Figure A.25: C#
Dim PrincPerm1 As New PrincipalPermission("User1", "Role1") Dim PrincPerm2 As New PrincipalPermission("User2", "Role2") PrincPerm1.Union(PrincPerm2).Demand()
Figure A.26: VB.NET
The Demand will succeed only if the principal object has the user User1 in the role Role1 or User2 in the role Role2. Any other combination fails.
As we mentioned before, you can also directly access the Principal and Identity objects, thereby enabling you to perform your own security checks without using PrincipalPermission. Besides the fact that you can examine a little more information, this solution also prevents you from handling exceptions that can occur using PrincipalPermission. You can query the WindowsPrincipal in the same way the PrincipalPermission does this:
The name of the user by checking the value of WindowsPrincipal.Identity.Name:
[ C#: if (WinPrinc.Identity.Name == "User1" || C#: WinPrinc.Identity.Name.Equals("DOMAIN1\\User1")) C#: { C#: } VB.NET: If (WinPrinc.Identity.Name = "User1") or _ VB.NET: WinPrinc.Identity.Name.Equals("DOMAIN1\User1") Then VB.NET: End If
An available role by calling the IsInRole method:
C#: if (WinPrinc.IsInRole("Role1")) C#: { C#: } VB.NET: If (WinPrinc.IsInRole("Role1")) Then VB.NET: End If
Determining if the principal is authenticated, by checking the value of WindowsPrincipal.Identity.IsAuthenticated:
C#: if (WinPrinc.Identity.IsAuthenticated) C#: { C#: } VB.NET: If (WinPrinc.Identity.IsAuthenticated) Then VB.NET: End If
Additionally for PrincipalPermission, you can check the following WindowsIdentity properties:
AuthenticationType Determines the type of authentication used. Most common values are NTLM and Kerberos.
IsAnonymous Determines if the user is identified as an anonymous account by the system.
IsGuest Determines if the user is identified as a guest account by the system.
IsSystem Determines if the user is identified as the system account of the system.
Token Returns the Windows account token of the user.