Authentication and Authorization with Windows Accounts in ASP.NET
If you are providing web-based information for a closed
group of users, such as a company or similar organisation with roles and
membership, then Windows authentication make a great deal of sense for
ASP.NET websites or even .NET applications. Why, and how do you
implement it? Matteo explains all.
Probably almost all of you have developed or are
developing ASP.NET applications that allow users to manage their own
data and resources in a multi-user environment. These will require that
each user has his own user name and password, which he uses to log into
the web application, and access his information.
To accomplish this, you may be using, or have used, ASP.NET Forms authentication. The user enters his username and password in the login page and, after they are authenticated against some database tables, he is ready to operate.
In this article I would like to propose a different schema that relies on users’ Windows accounts rather than Forms authentication, and show the benefits that this approach can offer.
We will consider only those ASP.NET applications that are owned by an organization in which all users have their own Windows account, maybe stored in the company’s Active Directory.
Authentication and Authorization
When we create a web application, we want to expose the application’s users to information. This might be text, data, documents, multimedia content, and so on. Sometimes, we also need to manage access to this information, restricting certain users’ access to some of them. This is where authentication and authorization come in.Before presenting this Windows account authentication and authorization proposal, I would like to define what authentication and authorization mean, the difference between the two and how the .NET Framework manages them. If you are already confident with these concepts you can skip to the next section.
Authentication
Generally speaking, Authentication is the ability to identify a particular entity. The need for authentication occurs when we have some resources that we want to make available to different entities. We store these resources in a centralized place and instruct the system that manages them to prevent entities that we don’t recognize from having access. Anonymous authentication refers to a situation in which we grant access to resources to all users, even if we don’t know them.
In web applications, we expose resources to users. We authenticate each user by requesting his credentials, normally a username and password, that we have assigned to him, or that he got during what we call the registration process.
The .NET Framework uses the following authentication terminology:
- Principal: this represents the security context under which code is running. Every executing thread has an associated principal.
- Identity: this represents the identity of the authenticated user. Every Principal has an associated identity.
- GenericPrincipal, WindowsPrincipal
- GenericIdentity, WindowsIdentity
As their names suggest, WindowsPrincipal and WindowsIdentity are related to Principals and Identities associated with a Windows account, while GenericPrincipal and GenericIdentity are related to generic authentication mechanisms. GenericPrincipal and WindowsPrincipal implement the IPrincipal interface, while GenericIdentity and WindowsIdentity implement the IIdentity interface.
Authorization
Authorization is the ability to grant or deny access to resources, according to the rights defined for the different kinds of entities requesting them.When dealing with Windows Operating System, and its underlying NTFS file system, authorizations are managed by assigning to each object (files, registry keys, cryptographic keys and so on) a list of the permissions granted to each user recognized by the system.
This list is commonly called the “Access Control List” or ACL (the correct name is actually “Discretionary Access Control List” or DACL, to distinguish it from the “System Access Control List” or SACL). The ACL is a collection of “Access Control Entries” or ACEs. Each ACE contains the identifier for a specific user (“Security Identifier” or SID) and the permissions granted to it.
As you probably already know, to view the ACL for a specific file, you right-click the file name, select Properties and click on the Security tab. You will see something like this:
The “Group or user names” section lists all the users and groups, by name, which have at least one ACE in the ACL, while the “Permissions” section lists all the permissions associated with a specific group or user (or, rather, with its SID). You can modify the ACL by pressing the Edit button.
To view the ACL of a specific file using the .NET Framework, you can use the FileSecurity class that you can find under the System.Security.AccessControl namespace. The following example shows how to browse the ACL of a file named “C:\resource.txt”:
FileSecurity f = File.GetAccessControl(@"c:\resource.txt");
AuthorizationRuleCollection acl = f.GetAccessRules(true, true, typeof(NTAccount));
foreach (FileSystemAccessRule ace in acl)
{
Console.WriteLine("Identity: " + ace.IdentityReference.ToString());
Console.WriteLine("Access Control Type: " + ace.AccessControlType);
Console.WriteLine("Permissions: " + ace.FileSystemRights.ToString() + "\n");
Authentication in IIS 7 and 7.5
With definitions out the way, we’re ready to see how to setup a Windows account authentication and authorization schema in an ASP.NET application. First, we’ll look at how authentication with Windows accounts works.It’s important to note that this type of authentication doesn’t involve the ASP.NET engine. It works at the Internet Information Server (IIS) level instead, so all that’s required is the correct IIS configuration. The authentication types available in IIS can be viewed by using the IIS Manager:
Anonymous Authentication: this is the most commonly used type of authentication. With it, all users can access the web site.
ASP.NET Impersonation: this is not really an authentication method, but relates to authorizations granted to a web site’s users. We will see later how impersonation works.
Basic Authentication: this is a Windows account authentication, in the sense that the user needs to have a username and password, recognized by the operating system, to use the application. When the user calls a web page, a dialog box asking for his credentials appears. If the user provides valid credentials for a valid Windows account, the authentication succeeds. This type of authentication is not considered secure because authentication data is transmitted to the server as plain text.
Digest Authentication: this is similar to Basic Authentication, but more secure. Authentication data is sent to the server as a hash, rather than plain text. Basic Authentication and Digest Authentication are both standardized authentication methods. They are defined in RFC 2617.
Forms Authentication: this is ASP.NET’s own authentication, based on the login page and the storage of users’ credentials in a database, or similar location.
Windows Authentication: this type of authentication uses the NTLM or Kerberos Windows authentication protocols, the same protocols used to log into Windows machines. As for Basic Authentication and Digest Authentication, the credentials provided by the user must match a valid Windows account.
There are two other authentication methods that I have not mentioned here: Active Directory Client Certificate Mapping Authentication and IIS Client Certificate Mapping Authentication. Both use the X.509 digital certificate installed on the client; how they work is outside the scope of this article.
For the purpose of this article, we can use Basic Authentication, Digest Authentication or Windows Authentication, each of which relies on Windows accounts. When they’re used, the current executing thread is associated with a Principal object that is able to give us information about the authenticated user. I wrote a simple application that shows you how to do that. Its source code is available at the top of this article as a zip file.
The application defines a method, called WritePrincipalAndIdentity(), which give us the following information:
- The name of the authenticated user.
- The user’s role, by checking its role membership.
- The type of authentication performed.
/// <summary>
/// Explore the authentication properties of the current thread.
/// </summary>
public void WritePrincipalAndIdentity()
{
IPrincipal p = Thread.CurrentPrincipal;
IIdentity i = Thread.CurrentPrincipal.Identity;
WriteToPage("Identity Name: " + i.Name);
WriteToPage("Is Administrator: " + p.IsInRole(@"BUILTIN\Administrators"));
WriteToPage("Is Authenticate: " + i.IsAuthenticated);
WriteToPage("Authentication Type: " + i.AuthenticationType);
WriteToPage(" ");
}
Rather than using Thread.CurrentPrincipal, we could use the User property of the Page object to achieve the same result. I prefer to use the Thread.CurrentPrincipal,
to point out that the principal is always associated with the executing
thread. The importance of this will be clearer in the Role-Based
Security Paragraph.
When we run this application, using, for example, digest authentication (remembering to disable the anonymous authentication) the logon window ask us for our credentials.
Suppose that we need to write a web application that associates the user with his own data, for example a list of contacts or some appointments. It easy to see that, at this stage, we have all the information needed to manage all the data (contacts or appointments) related to a single user. If we save all of them in a database using the username (or better a hash of it) provided by the authentication stage as the table key, we are able to fill all the application’s web pages with only the user’s specific content, as we do with Forms authentication. This is possible without having to write any lines of code.
Another important advantage comes from the fact that, by using the Principal object, we are able to check if an authenticated user belongs to a specific security group. With this information, we can develop applications that are “role-enabled”, in the sense that we can allow a specific user to use only the features available for his role. Suppose, for example, that the web application has an admin section and we want to allow only administrators to see it: we can check the role of the authenticated user and hide the links to the admin page if the user is not an administrator. If we use Active Directory as container for users’ credentials, we can take advantage of its ability to generate group structures flexible enough to generate role-based permissions for even very heterogeneous kinds of users.
However, from a security point of view, authentication alone is not enough. If, for example, we hide the link to the admin page for non-administrator users, they can nonetheless reach the admin page using its URL, breaking the security of the site. For this reason, authorization plays a very important role in designing our application. We will now see how to prevent this security issue occurring.
Authorization in ASP.NET Applications
Suppose that we have a file, “resource.txt”, inside the web application root that we want to make available only to administrators. We can prevent users who aren’t administrators from accessing the file by setting up its ACL properly. For simplicity, let’s say we want to prevent “CASSANDRA\matteo” accessing it. Figure 6 shows how to do that:/// <summary>
/// Check if a resource can be loaded.
/// </summary>
public void CanLoadResource()
{
FileStream stream = null;
try
{
stream = File.OpenRead(Server.MapPath("resource.txt"));
WriteToPage("Access to file allowed.");
}
catch (UnauthorizedAccessException)
{
WriteException("Access to file denied.");
}
finally
{
if (stream != null) stream.Dispose();
}
}
The CanLoadResource() method tries to open resource.txt, in order to read its content. If the load succeeds, the “Access to file allowed.” message is written on the page. If an UnauthorizedAccessException exception is thrown, the message “Access to file denied.” is written on the page, as an error. The WriteException() method is a helper method used to write an exception message on the page.
Now we launch our application with authorizations set as in Figure 6 and use “CASSANDRA\matteo” to log into the application. Doing that, we obtain something that should sound strange:
This happens because, in this case, the Application Pool associated with the web application works in Integrated mode, which relates authentication and authorization to different users. Specifically, authentication involves the user identified by the credentials provided, while authorization involves the user account used by the Application Pool associated with the application. In our example, the Application Pool uses the NETWORK SERVICE account, which has permission to access the file.
We’ll try to deny these permissions by modifying the ACL of the resources.txt file:
To use authorization at the authenticated user level, we need to use Impersonation. With impersonation, we are able to allow the Application Pool to run with the permissions associated with the authenticated user. Impersonation only works when the Application Pool runs in Classic Mode (in Integrated mode the web application generates the “500 – Internal Server Error” error). To enable impersonation, we need to enable the ASP.NET Impersonation feature, as noted in Figure 3 and the discussion that followed it.
If we switch our Application Pool to Classic Mode (enabling the ASP.NET 4.0 ISAPI filters, too) and enable ASP.NET impersonation, the demo application output becomes:
To take advantage of Integrated mode without having to abandon impersonation, we can use a different approach: running our application in Integrated mode and enabling impersonation at the code level when we need it. To do so, we use the WindowsImpersonationContext class, defined under the System.Security.Principal namespace. We modify the CanLoadResource() method as follows:
/// <summary>
/// Check if a resource can be loaded.
/// </summary>
public void CanLoadResource()
{
FileStream stream = null;
WindowsImpersonationContext imp = null;
try
{ IIdentity i = Thread.CurrentPrincipal.Identity;
imp = ((WindowsIdentity)i).Impersonate();
stream = File.OpenRead(Server.MapPath("resource.txt"));
WriteToPage("Access to file allowed.");
}
catch (UnauthorizedAccessException)
{
WriteException("Access to file denied.");
}
finally
{
if (imp != null)
{
imp.Undo();
imp.Dispose();
}
if (stream != null) stream.Dispose();
}
}
With the modification added, we can force the application to
impersonate the authenticated user before opening the file. To achieve
this, we have used the Impersonate() method of the WindowsIdentity class (the class to which the Identity property belongs). With it, we have created a WindowsImpersonationContext object. This object has a method, Undo(), that is able to revert the impersonation after the resource has been used./// Check if a resource can be loaded.
/// </summary>
public void CanLoadResource()
{
FileStream stream = null;
WindowsImpersonationContext imp = null;
try
{ IIdentity i = Thread.CurrentPrincipal.Identity;
imp = ((WindowsIdentity)i).Impersonate();
stream = File.OpenRead(Server.MapPath("resource.txt"));
WriteToPage("Access to file allowed.");
}
catch (UnauthorizedAccessException)
{
WriteException("Access to file denied.");
}
finally
{
if (imp != null)
{
imp.Undo();
imp.Dispose();
}
if (stream != null) stream.Dispose();
}
}
If we try to run our application with permissions as in Figure 8, we see that we are able to access resource.txt even if the Application Pool is working in Integrated Mode.
Now we can resolve the security issue presented earlier.
If we want to use Windows accounts to develop a “role-based” application, we can use authentication to identify the user requesting resources and we can use authorization, based on the user’s identity, to prevent access to resources not available for the user’s role. If, for example, the resource we want to protect is a web page (like the admin page), we need to set its ACL with the right ACEs, and use impersonation to force the Application Pool to use the authenticated user’s permissions.
However, as we have seen, when the Application Pool uses Integrated mode, impersonation is available only at code level. So, although it’s easy in this situation to prevent access to resources (like the resource.txt file) needed by a web page, it’s not so easy to prevent access to a web page itself. For this, we need to use another IIS feature available in IIS Manager, .NET Authorization Rules:
I leave you to test how it works.
Role-Based Security
A further advantage of using Windows account authentication is the ability to use a .NET Framework security feature called Role-Based Security.Role-Based Security permits us to protect our resources from unauthorized authenticated users. It relies on checking if an authenticated user belongs to a specific role that has authorization to access a specific resource. We have already seen how to do that: use the IsInRole() method of the thread’s Principal object.
The .NET Framework security team decided to align this type of security check to Code Access Security (which I wrote about in previous articles) by defining a programming model similar to it. Specifically, a class named PrincipalPermission, found under the System.Security.Permissions namespace, has been defined. It permits us to check the role membership of an authenticated user both declaratively (using attributes) and imperatively (using objects), in the same manner as CAS checks.
Suppose that we want resource.txt to be readable only by administrators. We can perform a declarative Role-Based security check in this way:
/// <summary>
/// Load a resource
/// </summary>
[PrincipalPermissionAttribute(SecurityAction.Demand, Name = "myname", Role = "administrators")]
public void LoadResource()
{
…..
where “myname” is the username that we want to check./// Load a resource
/// </summary>
[PrincipalPermissionAttribute(SecurityAction.Demand, Name = "myname", Role = "administrators")]
public void LoadResource()
{
…..
If declarative Role-Based security is not what we need (because, in this case, we need to know the identity of the user first), we can use an imperative Role-Based security check:
/// <summary>
/// Load a Resource
/// </summary>
public void LoadResource()
{
try
{
// Create a PrincipalPermission object.
PrincipalPermission permission =
new PrincipalPermission(Thread.CurrentPrincipal.Identity.Name, "Administrators");
// Demand this permission.
permission.Demand();
…..
}
catch (SecurityException e)
{
…..
}
}
In both cases, if the user does not belong to the Administrators group, a security exception is thrown./// Load a Resource
/// </summary>
public void LoadResource()
{
try
{
// Create a PrincipalPermission object.
PrincipalPermission permission =
new PrincipalPermission(Thread.CurrentPrincipal.Identity.Name, "Administrators");
// Demand this permission.
permission.Demand();
…..
}
catch (SecurityException e)
{
…..
}
}
The PrincipalPermission class doesn’t add anything to our ability to check the permission of an authenticated user. In my opinion, the IsInRole() method gives us all the instruments we need, and is simpler to use. Despite this, I’ve included PrincipalPermission in this discussion for completeness. Maybe this is the same reason that the .NET development team added this type of class to the .NET Framework base classes.
I end this section by mentioning that Role-Based Security can even be implemented in desktop applications. In this case, the authenticated user is a user that logs into the machine.
When a desktop application starts, by default, the identity of the authenticated user is not “attached” to the executing thread. The Principal property of the current thread and the Identity property of the Principal property are set to GenericPrincipal and GenericIdentity respectively, and the Name property of the Identity property is empty.
If we launch the following code in a Console application:
static void Main(string[] args)
{
Console.WriteLine("Type of Identity: " + Thread.CurrentPrincipal.Identity.GetType());
Console.WriteLine("Identity Name: " + Thread.CurrentPrincipal.Identity.Name);
}
{
Console.WriteLine("Type of Identity: " + Thread.CurrentPrincipal.Identity.GetType());
Console.WriteLine("Identity Name: " + Thread.CurrentPrincipal.Identity.Name);
}
We get:
This is, however, a feature we can turn on. We need to modify the previous code as follows:
static void Main(string[] args)
{
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Console.WriteLine("Type of Identity: " + Thread.CurrentPrincipal.Identity.GetType());
Console.WriteLine("Identity Name: " + Thread.CurrentPrincipal.Identity.Name);
}
Launching the application, we now get:{
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Console.WriteLine("Type of Identity: " + Thread.CurrentPrincipal.Identity.GetType());
Console.WriteLine("Identity Name: " + Thread.CurrentPrincipal.Identity.Name);
}
Conclusion
In this article we have seen how Windows accounts can be used to implement authentication and authorization in ASP.NET applications. Even if this type of approach is rarely used, Forms Authentication being the commonly adopted solution, it can have a lot of advantages:- Less code to develop and maintain. Authorization and authentication with Windows accounts does not require the developer to write specific code for the management of user credentials, authorizations, password recovery and so on.
- Centralization of user credentials, access rights, password policies, role-based policies and identity management in general. All the security information related to a specific user is stored in a centralized place, Active Directory. When a new employee arrives at an organization, permissions have to be added only in the Directory structure, not in each web server used by the company, making the authorization process simpler to manage.
- More security. In a decentralized security environment,
sometimes users have to remember more than one username and password.
Sometimes they are forced to write them down to remember them. Security
experts think this is one of the most dangerous security issues.
Moreover, if an employee with, say, ten accounts for ten different
applications, stored in ten different places, leaves an organization,
it’s easy to forget to remove all their credentials, allowing them to
access, or even steal confidential data. Post by..
No comments :
Post a Comment