Z Code
Secure Your Directory-Enabled Apps
Find out how Active Directory Application Mode and Windows Authorization Manager can simplify security, personalization, and configuration for your directory-enabled applications.
Technology Toolbox: VB.NET, ASP.NET, Windows Authorization Manager, Active Directory Application Mode, Active Directory
Implementing a robust security infrastructure around an application is one of the toughest tasks you can face in the design and development process. The job is even harder when your requirements call for the security infrastructure to have zero or low impact on the existing IT infrastructure. This requirement often leads to custom and ad-hoc security solutions that are difficult to implement correctly unless you're a highly experienced developer. A new capability in Active Directory (AD)Active Directory Application Mode (ADAM)and Windows Authorization Manager give you the tools to implement a robust solution that handles authentication, operation-based authorization, personalization, and application configuration for directory-enabled applications without resorting to a Windows Identity-based infrastructure. In this article, I'll introduce you to ADAM and Authorization Manager and show you how to use them to develop such a solution easily.
ADAM shares a storage engine (Directory System Agent [DSA]) and the protocol to access it (Lightweight Directory Access Protocol [LDAP]) with AD (see Figure 1). Unlike AD, ADAM doesn't run as an operating-system service. You can even install multiple ADAM instances on the same computer as long as they listen on different ports. Each ADAM instance manages an AD store that's divided into partitions. Every ADAM instance includes two system partitionsthe schema partition and the configuration partitionand one or more application partitions. All partitions contain data in the form of objects in a hierarchical tree structure.
The configuration partition keeps track of the store's configuration information, including the existing partitions' names and the configuration set's properties, such as its replica scheduling (more about configuration sets later). The schema partition holds the definition of all the object types (and their attributes) you can create and store in that specific ADAM instance. In other words, each object in the directory is an instance of one or more classes defined in the schema partition. Each class comprises a set of attributes. Class and attribute definitions are actually object instances of the classSchema and attributeSchema classes, respectively. These classes then serve as the building blocks of the schema meta model. Note that no particular class owns attributes; you define them separately and then attach them to one or more classes as required.
Classes and attributes both come with a reach-metadata set of information that constrains and drives object instantiation. You can specify which attributes are mandatory and which are optional for each class, and you can specify which object types can be the parent of an instance of the class in the tree structure. Each attribute also has plenty of associated information, the most interesting of which is the attribute syntax (the attribute's data type), the attribute multiplicity (you can define single-value or multivalue attributes), and whether the attribute is indexed (for fastest queries).
Classes come in three types: structural, abstract, and auxiliary. You can instantiate only structural classes. One class can inherit from another one, thereby acquiring all the superclass's and possible superior classes' attributes. A class can also inherit from auxiliary classes. All classes must inherit directly or indirectly from the class named "top," which plays the same role in AD that System.Object does in .NET (see Additional Resources for more information on ADAM).
Replicate Application Partitions
You can replicate an application partition and keep it in sync across multiple ADAM instances distributed over the network. Doing so requires you to pull all ADAM instances participating in the replica into a configuration set. ADAM instances belonging to the same configuration set share a common configuration partition and a common schema partition that aren't replicated; a specific ADAM instance in the groupcalled the operation masterowns them. The operation master is the only instance that can modify the configuration and schema partitions. ADAM replicates modifications to a mirrored application partition's directory object automatically to the other mirrored application partitions in the configuration set. The replication occurs according to a schedule an administrator configures.
You can use Windows Authentication to bind to an ADAM store, or you can provide the username/password pair of a user defined in the ADAM store. The default ADAM installation doesn't include any built-in security principle, but you can add a user class by running the ms-user.ldf script (at installation time or later). You can create your own user classes as well: Any class is a valid ADAM identity as long as its definition includes the msDS-bindableobject auxiliary class and the unicodePwd attribute. Newly created ADAM users and Windows users have no default rights. You must add ADAM or Windows users to one of the roles (Administrator, Instances, Readers, or Users) defined in the configuration Partition/CN=Roles folder to let them browse and/or edit application partitions' objects.
The .NET Framework provides access to directory stores through a set of classes in the System.DirectoryServices namespace. The DirectoryEntry class is the most relevant, because it represents an object instance within the store's hierarchical structure. No sort of connection object is required when you bind to an AD store. You use the DirectoryEntry constructor to specify the server name and the port identifying the ADAM instance, authentication options, the required AD object's distinguished name and, eventually, a username/password pair. The constructor doesn't throw an exception if you provide invalid credentials, because authentication doesn't take place until the first time you try accessing a directory object. (You'll get an exception at that point, eventually.)
The AuthenticationOption parameter in the DirectoryEntry constructor is a flag-based enum with a long list of possible values. Three of them are the most relevant to ADAM. You use AuthenticationOption.Secure if you want to bind using a Windows identity. Pass null to the Username and Password constructor parameters if you want to bind using the running thread's identity. Use AuthenticationOption.None if you want to bind using an internal ADAM user. Note that in this case, the username/password pair is sent in clear text, so you should OR AuthenticationOption.None with AuthenticationOption.Encrypt or set up an IPSec channel between the client and the server where the ADAM instance runs:
Dim entr As DirectoryEntry = new _
DirectoryEntry( _
"LDAP://localhost:389/" _
+ "DC=MyAppXX,DC=SABBASOFT," & _
"DC=COM", username, pwd, _
AuthenticationTypes.None)
Once you acquire a handle to an AD object, you can query and modify it, navigate to its children, and use a simple query language to search among its descendants for objects whose attributes match specified conditions (see Additional Resources). You use the DirectorySearcher object in .NET to post a query to the store, using the FindOne or the FindAll methods to obtain the results:
Dim mySearcher As New _
DirectorySearcher(ADentryPoint)
mySearcher.Filter = _
("(&(objectClass=user)(name=Joe))")
Dim res As SearchResult = _
mySearcher.FindOne
Implement an Authorization Infrastructure
You've seen that ADAM is a good candidate for managing a user store and providing authentication services. The next step is to choose an authorization infrastructure that grants or denies access to resources and operations according to the active user's identity. The Windows Authorization Manager fits the bill. This component is available on Windows 2003 and Windows 2000 SP4 as a set of COM objects and a .NET interop wrapper around them. You use an MMC snap-in to manage Authorization Manager stores. Authorization Manager introduces a new operation-based approach that's much more flexible than classic .NET role-based security.
An Authorization Manager store can be an XML file or an AD partition. Each store contains one or more applications. You register a list of operations, tasks, and roles for each application (see Figure 2). Operations are low-level operations the application can perform. Each task contains one or more operations and other tasks. You can associate script blocks to tasks. Scripts are triggered during the runtime authorization phase (which I'll describe shortly). Each role contains operations, tasks, and other roles. Each application also contains one or more application groups, each of which contains Windows users and groups and eventually other application groups.
You pull operations, tasks, and roles together in the role assignments section by associating them with application groups. That is, you map application concepts to users or groups of users. You can even bypass the definition of application groups and go straight to role-assignment definition by mapping operations, tasks, and roles directly to Windows users and groups.
As you can see in Figure 2, a high degree of indirection provides more flexibility than you might ever ask for. (Avoid using more than you need.) At run time, application code uses the AccessCheck method to ask Authorization Manager for permission to perform a list of operations (the lower-level, finer-grained entities). If you define an authorization script, you must pass parameter names and values as the method call's fourth and fifth parameters. Authorization Manager analyzes the operation, task, and role configuration against the active user and returns an array of integers (as many items as the number of the requested operations). Authorization Manager allows the nth requested operation if the corresponding nth entry of the returned array is 0 (see Listing 1).
From what you've seen so far, it appears that Authorization Manager can work only against Windows identities. However, if this were the case, you wouldn't be able to integrate ADAM and Authorization Manager to form a complete security solution where Windows identities are out of the picture. It's true that the administration MMC snap-in lets you insert only Windows users and groups. Fortunately, though, Authorization Manager memorizes only the user and group security identifiers (SIDs) in its store. This means that you can use the Authorization Manager API to feed the store with arbitrary SIDs.
Similarly, you're not required to set up the client context with a valid Windows user SID at run time. How you set the SID is up to you. If you want to use a Windows token to set up a context, simply call InitializeClientContextFromToken and pass the token as the first parameter. Note that if you want to use the process or calling thread token (if you're impersonating a remote client, as in ASP.NET applications), pass 0 as the input parameter. However, what you need for a complete security solution is another API named InitializeClientContextFromStringSid. This method accepts any SID to set up a client context. In this case, the SID doesn't need to belong to a Windows user. The Authorization Manager will accept it and try to match it against the SIDs present in the application store when AccessCheck is called.
ADAM users have a SID attribute that's assigned a random value when the user object is created. The whole authentication process consists of these steps: Bind to ADAM, extract the ADAM user's SID, then initialize the Authorization Manager client context with the SID (see Listing 2). (Note you must resort to P/Invoke to convert the SID from a byte array to its string representation.) As you might have noticed already, the only thing that's missing is an administrative console that extends the Authorization Manager snap-in to ease assignment of ADAM user SIDs to Authorization Manager application groups and/or role assignments. You'll find a WinForms-based application in this article's sample code that provides this functionality.
Extend the ADAM Schema
You'll underuse ADAM if you employ it only for authentication purposes. A directory store's main goal isn't to authenticate users, but to act as a repository for application-specific and enterprise-wide settings for users and software systems. Management of enterprise-wide settings is typically the AD infrastructure's responsibility. In contrast, ADAM aims to provide directory services for specific applications. Each application typically extends the ADAM schema, introducing new object types and attributes according to its particular needs.
This article's sample code provides a simple example that extends the schema with two classes (SS-Appsettings and SS-Personalization) that manage application and user settings, respectively. The LDF file in the sample code imports these schema extensions and some sample data to your ADAM instance. Each class holds a simple custom, string-based attribute so you can use a name/value-pair approach to query for user and application information. The SS-Personalization class accepts only a user class as its possible superior, because its task is to store user-specific preferences. The SS-Appsettings class's possible superiors are the container and the dnsDomain classes. (A more strongly typed approach is more suitable for real-world applications.)
The sample code follows .NET programming guidelines by wrapping the authentication and authorization concepts I've discussed into custom .NET Principal and Identity objects to hide the security infrastructure's inner implementation details. The sample code also includes an ASP.NET application that uses ASP.NET forms authentication to show how to interact with the security infrastructure. The ASP.NET app also takes advantage of personalization to customize part of the Web-page rendering according to user settings. Once a user logs in, a simple Web form lets you test the outcome of authorization checks against different operations according to the active user's identity. Remember that you must add the ASP.NET application pool's identity to the application partition's Readers role in order to be granted access to configuration settings.
I hope you've gained an appreciation of how you can use ADAM and Authorization Manager to develop an authentication solution that has little or no impact on your IT infrastructurewithout resorting to custom security-algorithm implementations. You can extend the simple framework I've presented here easily to rely on Windows authentication, still preserving all the authorization, personalization, and configuration features that directory services and Authorization Manager provide.