Click here to Skip to main content
Click here to Skip to main content
Go to top

Active Directory Roles Provider

, 3 Oct 2008
Rate this:
Please Sign up or sign in to vote.
An active directory roles provider

Contents

Introduction

I've written this code in order to fill a percieved gap in Microsoft's current ASP.NET role management offerings. (If you are not familiar with ASP.NET Membership and Role Management, you may want to check out the current framework documentation on Securing ASP.NET Web Sites before continuing this article. The Membership and Role summary pages should give you a good start.)

Currently, if you are using forms-based authentication with Microsoft's ActiveDirectoryMembershipProvider, you are a bit limited in your options for a role provider. There is nothing built into the framework that is as relatively simple to configure and maintain ActiveDirectoryMembershipProvider. The best solution you are left with is setting up AzMan. However, there's a significant learning curve and a lot of conflicting information out there regarding it, and it also requires setup and configuration outside of the web site's web.config.

That's where ADRoleProvider comes into play. Simply stated, ADRoleProvider is a role provider class that allows you to use your existing Active Directory groups as ASP.NET Roles, and attempts to provide that framework as easily as ActiveDirectoryMembershipProvider provides your membership framework. Since it inherits from Microsoft's base RoleProvider class, it should function seamlessly in your site and work with all the standard .NET controls (LoginView, etc) and Web.config authorization settings.

Please make certain that you read through this documentation and understand the security and performance concerns mentioned. Also, look through the code to gain an understanding of what exactly how it operates.

Requirements

In order to make use of this RoleProvider class, there are only two requirements

  1. You must be running an ASP.NET website using version 3.5 of the framework. This is because the code uses the System.DirectoryServices.AccountManagement namespace to enumerate group membership.
  2. Your IIS web server must be a member of the domain you wish to use for group/role maintenance. You can use an off-domain server with a little code tweaking, and I will be supporting this option down the road.
  3. For the time being, attributeMapUsername="sAMAccountName" should be set in your ActiveDirectoryMembershipProvider. This is explained further below in the Technical Summary.

Technical Summary

Using the source code that Microsoft has released for SqlRoleProvider as a jumping-off point, I've tried to make this class adhere as close as possible to the behavior of the "built-in" role providers. The only significant difference to note is that this is a read-only provider. Since management should be done only through Active Directory, any role operations that would require write access will throw a NotSupportedException.

IMPORTANT - All of the Active Directory queries in the class are currently made against the LDAP sAMAccountName attribute. Consequently, any time you are referring to a group or user name in the web.config, please be certain you are using the sAMAccountName. However, ActiveDirectoryMembershipProvider can be configured to use either sAMAccountName OR userPrincipalName. To avoid any confusion, the safe bet is to set attributeMapUsername="sAMAccountName" in the membership configuration section of the web.config. The one critical change I am currently planning for this code is the option to use UPN to avoid any confusion. This change should complete and tested within a week. If you are working in a live environment in which users are accustomed to logging in as username@domain.com rather than username, please hold off using this until then.

Certain built-in or common Active Directory groups are specifically excluded in the source code. For example, Exchange Enterprise Servers would never, ever be used as a role, so that is in an internal exclusion list. These system groups cannot be queried against, and will never show up in any results. You can also specify your own groups to ignore in the Web.config (see configuration below).

Querying Active Directory

All of the heavy lifting done by this class involves querying against Active Directory, and there are as many ways to query Active Directory as there are to shoot yourself in the foot. Below is the complete code for retrieving a user's role (i.e. AD Group) membership. The SQL Caching code is ellipsed out for explanation purposes.

/// <span class="code-SummaryComment"><summary></span>
/// Retrieve listing of all roles to which a specified user belongs.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="username"></param></span>
/// <span class="code-SummaryComment"><returns>String array of roles</returns></span>
public override String[] GetRolesForUser(String username)
{
	...
	//Create an ArrayList to store our resultant list of groups.
	ArrayList results = new ArrayList();
	//PrincipalContext encapsulates the server or domain against which all
         //operations are performed.
	using (PrincipalContext context = new PrincipalContext(ContextType.Domain,
             null, _DomainDN))
	{
		try
		{
			//Create a referance to the user account we are querying
                           //against.
			UserPrincipal p = UserPrincipal.FindByIdentity(context,
                               IdentityType.SamAccountName, username);
			//Get the user's security groups.  This is necessary to
                           //return nested groups, but will NOT return distribution groups.
			var groups = p.GetAuthorizationGroups();
			foreach (GroupPrincipal group in groups)
			{
				if (!_GroupsToIgnore.Contains(group.SamAccountName))
				{
					if (_IsAdditiveGroupMode)
					{
						if (
                                                           _GroupsToUse.Contains(
                                                           group.SamAccountName))
						{
							results.Add(
                                                                    group.SamAccountName);
						}
					}
					else
					{
						results.Add(group.SamAccountName);
					}
				}
			}
		}
		catch (Exception ex)
		{
			throw new ProviderException(
                               "Unable to query Active Directory.", ex);
		}
	}
	...
	return results.ToArray(typeof(String)) as String[];
}

Do not let the nested if statements throw you off; they are there only to make certain that the proper groups are ignored/used, depending on your configuration, explained below. As the comments explain, programming against System.DirectoryServices.AccountManagement is actually quite simple.

Caching to SQL Server

Querying against Active Directory can be pretty slow. To alleviate this, ADRoleProvider includes options to enable caching the results from its Active Directory queries to a Microsoft SQL Server. It seems counterintuitive, but caching querying against SQL Server is generally faster than executing AD queries.

The manner of caching is extremely simple. I vacillated between using a more fully normalized series of tables and the simplified method I finally settled upon. Data is cached to a single table as specified below.

  • An ID field, used to speed up duplicate lookups in one of the stored procedures
  • Application Name for the item, allowing a single table to be used for multiple applications
  • The type of object being cached (L=Roles List, U=User membership, R=Role membership)
  • User or role name
  • Comma-separated list of membership
  • Expiration datetime
CacheId    ApplicationId    CacheType    CacheKey    CacheValue    ExpireDT
49    MyApp    L    AllRoles    Customer Service,IT    8/18/2008 2:49:50 PM
50    MyApp    U    asmith    IT    8/18/2008 2:49:50 PM
51    MyApp    R    IT    dsmith,msmith,asmith,jsmith,ssmith,bsmith    8/18/2008 2:49:50 PM

There are certain situations in which this simplified method of caching could present minor annoyances. For example: The cached results for IT is set to expire at 2:50, the results for asmith are set to expire at 3:50, and asmith is removed from the IT Active Directory group. In this situation, there could be an hour period where enumerating IT shows that asmith is not a member, but asmith still thinks he is. This could be avoided by having a group table, a user table, and a join table. However, I decided that the added complexity and overhead was not worth the trade-off, since every time a cache item is set, the integrity of every item involved would have to be verified.

Included in the code zip file is a SQL file that will create the necessary table and stored procedures to implement caching. This has been tested under SQL Server 2005 Standard and Developer editions, and SQL Server 2008 Standard.

SQL Caching Performance

Honestly, the impact of enabling SQL caching is much larger than I had thought it would be. My test environment consists of only two groups and about a half dozen users. With such a small set of data to work with, I figured the performance impact would be minimal. The SQL instance I am connecting to is on a separate server from the test site. The code below basically what I used to test performance, though I actually set it to run a few hundred times.

DateTime dtStart;
DateTime dtEnd;
TimeSpan executionTime;
string[] results;

dtStart = DateTime.Now;
results = Roles.GetAllRoles();
results = Roles.GetRolesForUser("dsoref");
results = Roles.GetUsersInRole("IT");
dtEnd = DateTime.Now;
executionTime = dtEnd - dtStart;
tests.InnerHtml += "Execution Time: " + executionTime.TotalMilliseconds + "<br /><br />";

Without caching enabled, this chunk of code would take from 78 to 154ms to execute, with the average hovering at about 98ms.

With caching enabled, the initial request would take from 81 to 204ms to execute, with the average hovering around 102ms since the data needed to be cached to the SQL database. However, subsequent requests within the expiration time took only 14 to 16ms. I guess SQL is faster than AD after all.

Using the Code

First, you will want to compile the provider and copy the resulting .dll into your website's bin folder. You could instead copy the .cs file into your App_Code directory, but this is somewhat less secure. Granted, you should be able to trust all of your developers, but better safe than sorry.

Web.Config

Next, you will want to enter the proper configuration settings into your web.config, as below. I will run through each of these settings.

...
<connectionStrings>
    ...
    <add name="ActiveDirCS"
        connectionString="LDAP://DC=YourDomain,DC=com"/>

</connectionStrings>
...
<roleManager enabled="true" defaultProvider="ActiveDirRP">
    <providers>
        <clear/>
        <add applicationName="MyApp"

            name="ActiveDirRP" 
            type="DanielPS.Roles.ADRoleProvider" 
            activeDirectoryConnectionString="ActiveDirCS" 
            groupMode="Additive" 
            groupsToUse="IT, Customer Service" 
            groupsToIgnore="Senior Management" 
            usersToIgnore="asmith, ksose"

            enableSqlCache="True"
            sqlConnectionString="SQLCacheCS"
            cacheTimeInMinutes="30" />
    </providers>

</roleManager>
...
  • Name should be specified as with any other role provider for reference in the web.config.
  • Type will refer to our new roleprovider class, DanielPS.Roles.ADRoleProvider, or something else if you don't like the namespace.
  • ApplicationName should be the same as used in your membershipprovider section.
  • activeDirectoryConnectionString should be LDAP-formatted and serverless, i.e. LDAP://DC=YourDomain,DC=com. If you specify a server, an error will be thrown.
  • The most important setting here is groupMode. It has two options:
    • Additive - All Active Directory groups are essentially invisible and useless unless the are specified in the groupsToUse section. This is the safest method, as you are assured that no secure Active Directory groups are exposed to the web site, and will not be listed even on a GetAllRoles() call. All groups you wish to use as roles must be specified in groupsToUse
    • Subtractive - All Active Directory groups are exposed as roles unless they are listed in the groupsToIgnore section. This is somewhat less secure, but requires less maintenance when groups are added or removed. As mentioned, there is a list in the source code of common AD groups that will be ignored whether or not they are in the groupsToIgnore list.
  • groupsToUse is a comma-separated list of groups that should be used as roles. This is only used if groupMode is set to Additive.
  • groupsToIgnore is a comma-seperated list of groups that should be ignore for roles purposes. They will not should in the results for any Roles functions. In the example above, Roles.GetRolesForUser() will never include "Senior Management" in the results, and Roles.GetUsersInRole("Senior Management") will throw a ProviderException.
  • enableSqlCache should be set to True to enable SQL caching. I highly recommend it.
  • sqlConnectionString is the name of the connection string to use for SQL connections if SQL caching is enabled.
  • cacheTimeInMinutes is the length of time, in minutes, that items should be cached if SQL caching is enabled.

Note: If a groupMode is additive, and a group is specified in both groupsToUse and groupsToIgnore, groupsToIgnore will take precedence.

Note: If a group is specified in groupsToUse, but does not exist in Active Directory, it will be ignored.

Groups and Users Excluded in the Source

The following groups are excluded in the source code. Even specifying them in groupsToUse will not make them functional.

Domain Guests, Domain Computers, Group Policy Creator Owners, Guests, Users, Domain Users, Pre-Windows 2000 Compatible Access, Exchange Domain Servers, Schema Admins, Enterprise Admins, Domain Admins, Cert Publishers, Backup Operators, Account Operators, Server Operators, Print Operators, Replicator, Domain Controllers, WINS Users, DnsAdmins, DnsUpdateProxy, DHCP Users, DHCP Administrators, Exchange Services, Exchange Enterprise Servers, Remote Desktop Users, Network Configuration Operators, Incoming Forest Trust Builders, Performance Monitor Users, Performance Log Users, Windows Authorization Access Group, Terminal Server License Servers, Distributed COM Users, Administrators, Everybody, RAS and IAS Servers, MTS Trusted Impersonators, MTS Impersonators

The following users are excluded in the source code. They will not show up in any group membership enumeration.

Administrator, TsInternetUser, Guest, krbtgt, Replicate, SERVICE, SMSService

Future Enhancements

  • Add configuration options to allow a non-domain IIS server to host a web site making use of this code without any tweaking.
  • Add configuration options to use Common-Name rather than sAMAccountName.

History

  • 8/12/2008 - Initial Posting
  • 8/20/2008 - Update to include SQL Caching, rewrite of some sections for clarification, more detailed explanations, and misc. corrections
  • 8/20/2008 - Rewrote group membership enumeration to use System.DirectoryServices.AccountManagement (.NET 3.5 only) and support recursive membership
  • 9/4/2008 - Source updated
  • 9/30/2008 - Article Rewrite

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Daniel_PS

United States United States
No Biography provided

Comments and Discussions

 
BugReading values from the cache are truncated to 256 chars PinmemberRussell-X12-Mar-14 21:24 
QuestionWonderful Code! Pinmemberdanlkb218-Feb-14 10:48 
BugADO.NET errors eg. ExecuteNonQuery requires an open and available Connection. The connection's current state is Connecting Pinmembermathaus40412-Feb-14 18:34 
QuestionAttempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014) [modified] PinprofessionalStevoCavender28-Jun-13 8:16 
GeneralRe: Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014) PinmemberMember 948430418-Sep-13 4:29 
QuestionI made some changes would you want to roll them into this version? PinmemberCorey Fournier3-May-13 5:54 
QuestionactiveDirectoryConnectionString should be LDAP-formatted and serverless, i.e PinmemberTylrwb18-Oct-12 7:09 
SuggestionASP.NET now supports this natively Pinmemberdukekujo3-Oct-11 4:48 
GeneralRe: ASP.NET now supports this natively PinmemberMember 889459815-May-12 3:19 
GeneralThanks PinmemberMember 78975065-May-11 10:23 
GeneralSharePoint 2010 Pinmemberzebra102419-Nov-10 6:43 
I managed to get this provide to work in SharePoint 2010 using the claims based authentication and FBA.
A couple of issues I ran into.
1. I had to install the DLL in the bin directory of the STS web service in addition to CA and the SP site.
2. I had to modify the code to do a case-insensitive search in the RoleExists method because the STS web service passed the group in all lower case but the AD groups are in mixed case
3. I had to set the Trust level to Full so the People Picker would function

 
///
/// Determine if given role exists
///

/// Role to check
/// Boolean indicating existence of role
public override bool RoleExists(string rolename)
{
string searchRole = rolename.ToLower();
foreach (String strRole in GetAllRoles())
{
if (searchRole == strRole.ToLower()) return true;
}
return false;
}
GeneralProviding a username and password to the role provider Pinmembernvierros28-May-10 20:00 
QuestionHow can I use it with loginview component? Pinmemberkarlvonoderberg28-Mar-10 23:19 
GeneralIIS authentication method Pinmembertwan.goosen12-Mar-10 1:48 
GeneralFindUsersInRole doesn't seem right Pinmemberbvillanu5-Mar-10 7:52 
GeneralCentral Admin Pinmemberfounders22-Dec-09 6:19 
GeneralRe: Central Admin Pinmemberfounders22-Dec-09 6:48 
QuestionI'm trying to get this to work with WSS 3.0 PinmembereppNator17-Dec-09 10:00 
GeneralExcellent program! [modified] Pinmembergtrmitch11-Aug-09 10:44 
GeneralRe: Excellent program! PinmemberFredrik sewen5-Jul-10 5:22 
Questionnon domain IIS Pinmemberhkgartner13-Apr-09 22:00 
GeneralA referral was returned from the server. Pinmemberjglaser13-Apr-09 5:51 
GeneralRe: A referral was returned from the server. PinmemberMember 772695930-Jun-11 7:37 
Question"can't load type" DanielPS.Roles.ADRoleProvider PinmemberSDonahue22-Dec-08 18:26 
GeneralRe: "can't load type" DanielPS.Roles.ADRoleProvider PinmemberDonald Neisler22-Jan-09 12:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140926.1 | Last Updated 3 Oct 2008
Article Copyright 2008 by Daniel_PS
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid