How to use ASP.Net Membership and Role providers with ADLDS / ADAM, with Code

There are scenarios as a developer where you need to connect to and use Active Directory from your ASP.Net or Silverlight application. For whatever reason, being able to immediately connect to your corporate domain may be difficult. Rather than let this hold you up, there is another possibility – you can install ADAM and configure it as a membership and role repository for your applications. ADAM (otherwise known as Active Directory Lightweight Directory Services), is a cut down version of Active Directory, meant specifically for Applications. It can be installed on your local machine or on a server somewhere, and enable you to get up and running quickly.

The first thing you need to do is install ADAM. On a Windows XP SP1, or Windows 2003 Server, you need to download and install ADAM from here: http://www.microsoft.com/downloads/details.aspx?familyid=9688f8b9-1034-4ef6-a3e5-2a2a57b5c8e4&displaylang=en

Then you follow the instructions from here to get the Membership Provider working:

http://erlend.oftedal.no/blog/?blogid=6

From Windows 2003 R2 and later, you need to go install the Active Directory Lightweight Directory Services role from the Server Manager. On later versions of windows (Windows 2008 R2) you can run the ADLSD Setup Wizard to enable you to install instances. Try to follow the instructions at Erlend’s blog above.

After following Erlend’s instructions, you should be able to click on the ASP.Net Configuration menu item in Visual Studio and use the Web Administration Tool to add users to ADAM.

Next you will want to configure ADAM to be used as a Role Provider. All the documentation talks about this separately from being used as a Membership provider, and you wouldn’t be wrong in forming the conclusion that it should just work, however, the reality is that to have both the users and the roles in the one ADAM instance actually requires you to write a custom role provider. We’ll get to that in a moment. Firstly, you need to follow the instructions to install the Authorization Manager. Those instructions are here:
How To: Use ADAM for Roles in ASP.NET 2.0
Make sure you install the Authorization Manager (AzMan) in the same instance as your users.

Now the best way to obtain a custom role provider is to find one that has already been written. I tried this, but didn’t succeed. The closest I could come to was an Active Directory Role Provider that I found on CodePlex. Everything else just wasn’t good enough or didn’t work. But I downloaded the Active Directory Provider so that I can switch over to Active Directory when I am finished my development and ADAM is no longer desired.

The Active Directory Role Provider may be found here: http://adroleprovider.codeplex.com/releases/view/34407

I found instructions on Erlend’s site on how to construct an ADAM role provider, but no code. So I have written some code to do this, which you can find here. I downloaded the code from codeplex and added it to a separate project, then I added my own code to that project. Basically, this implements pretty much everything you need to install an Authorization Provider for ADAM that works with a user store also in the same ADAM instance. You cannot get this working otherwise.

Here is the section of my web config containing references to these providers:

<membership defaultProvider="AspNetActiveDirectoryMembershipProvider">
      <providers>
 <clear/>
        <add name="AspNetActiveDirectoryMembershipProvider" 
type="CT.ActiveDirectoryMembershipProvider" connectionStringName="ADService" 
connectionUsername="CN=MyAppAdmin,OU=Users,OU=Auth,O=App,DC=MEGACORP,DC=COM" 
connectionPassword="123456." connectionProtection="None" enableSearchMethods="true"/>
      </providers>
</membership>
<profile>
      <providers>
        <clear/>
        <add name="AspNetSqlProfileProvider" connectionStringName="DEVELOPMENT" 
applicationName="/MegaCorp.App.Web" type="System.Web.Profile.SqlProfileProvider, 
System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
      </providers>
      <properties>
        <add name="FriendlyName"/>
        <add name="Firstname"/>
        <add name="Surname"/>
        <add name="Company"/>
        <add name="Phone"/>
        <add name="MobilePhone"/>
        <add name="Address"/>
      </properties>
</profile>
<roleManager enabled="true" cacheRolesInCookie="true" 
defaultProvider="RoleManagerAzManADAMProvider">
       <providers>
          <add connectionStringName="AzManADAMServer" applicationName="MegaCorp.App"
             name="RoleManagerAzManADAMProvider" type="CT.AzManRoleProvider" />
       </providers>
</roleManager>

<connectionStrings>
    <add name="ADService" connectionString="LDAP://localhost:389/OU=Users,OU=Auth,O=App,DC=MEGACORP,DC=COM"/>
    <add name="AzManADAMServer" connectionString="msldap://localhost:389/CN=AzManADAMStore,OU=Auth,O=App,DC=MEGACORP,DC=COM"/>
</connectionStrings>

Before I continue, I should mention that I slipped in the Sql Profile Provider. This is because I am developing a Silverlight 3 application, and the Silverlight Authentication service requires some of these fields to be supplied. So I have installed the aspnet database just so I can get access to the provider table. I also had to change the User Id in the Silverlight Authentication service from a Guid to a string so it could handle the entire user SID.

Now, I was able to run the ASP.Net Web Administration Tool and successfully add roles, add users, and then add users to roles. I did discover one bug in the Web Administration Tool that caused an exception when I clicked the IsActive check box. This exception is caused when the resultant user object is deserialised to be sent over the wire to the application. The good news is, by the time this has happened, all the work has been done, and the error appears to have no other consequences.

Therefore, I decided to swallow up the exception from within the Web Administration Tool. Hey, most of you don’t use the tool anyway, prefering to re-invent the entire application administration.

To do this, you need to go to manageUsers.aspx in C:\Windows\Microsoft.NET\Framework\v2.0.50727\ASP.NETWebAdminFiles\Security\Users
or for 64-bit machines,
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\ASP.NETWebAdminFiles\Security\Users
and wrap a try/catch block around the code causing the exception.

public void EnabledChanged(object sender, EventArgs e) 
{
	CheckBox checkBox = (CheckBox) sender;
	GridViewRow item = (GridViewRow)checkBox.Parent.Parent;
	Label label = (Label) item.FindControl("UserNameLink");
	string userID = label.Text;
	MembershipUser user = (MembershipUser) CallWebAdminHelperMethod(true, "GetUser", new 
	object[] {userID, false /* isOnline */}, new Type[] {typeof(string),typeof(bool)});
	user.IsApproved = checkBox.Checked;

	string typeFullName = "System.Web.Security.MembershipUser, " + typeof
	(HttpContext).Assembly.GetName().ToString();;
	Type tempType = Type.GetType(typeFullName);

	try
	{
		CallWebAdminHelperMethod(true, "UpdateUser", new object[] {(MembershipUser) user}, new 
		Type[] {tempType});
	}
	catch
	{
		//catch any exception occuring here. In most cases this is pure evil.
	}
}

If you want to see the actual source code of the AzMan Role Provider I created, I have added it in below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Interop.Security.AzRoles;
using System.Web.Security;
using System.Diagnostics;
using System.Collections;

namespace CT
{
    public sealed class AzManRoleProvider : RoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            var users = GetUsersInRole(roleName);
            foreach (string user in users)
            {
                if (user.Equals(username, StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            return false;
        }

        private string connectionStringName;
        public string ConnectionStringName
        {
            get
            {
                if (this.connectionStringName == null)
                {
                    this.connectionStringName = "msldap://localhost:389/CN=AzManADAMStore,OU=Auth,O=SCP,DC=MEGACORP";
                }
                return this.connectionStringName;
            }
            set
            {
                this.connectionStringName = value;
            }
        }

        public IAzApplication2 ConnectToAzManApplication()
        {
            var _azStore = new AzAuthorizationStoreClass();
            _azStore.Initialize(0, this.ConnectionStringName, null);
            var _azApp = _azStore.OpenApplication2(this.ApplicationName, null);
            return _azApp;
        }

        public override string[] GetRolesForUser(string username)
        {
            var _azApp = ConnectToAzManApplication();
            var user = Membership.GetUser(username);
            var context = _azApp.InitializeClientContextFromStringSid(user.ProviderUserKey.ToString(), 1, null);
            var roles = context.GetRoles("") as object[];
            return Array.ConvertAll(roles, new Converter<object, string>(AzManRoleProvider.GetRoleStringRepresentation));
        }

        public static string GetRoleStringRepresentation(object role)
        {
            return role.ToString();
        }

        public override void CreateRole(string roleName)
        {
            var _azApp = ConnectToAzManApplication();
            var task = _azApp.CreateTask(roleName, null);
            task.IsRoleDefinition = 1;
            task.Submit(0, null);

            var role = _azApp.CreateRole(roleName, null);
            role.AddTask(roleName, null);
            role.Submit(0, null);
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            var _azApp = ConnectToAzManApplication();
            _azApp.DeleteRole(roleName, null);
            _azApp.DeleteTask(roleName, null);
            return true;
        }

        public override bool RoleExists(string roleName)
        {
            var _azApp = ConnectToAzManApplication();
            foreach (IAzRole role in _azApp.Roles)
            {
                if (role.Name == roleName)
                {
                    return true;
                }
            }
            return false;
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            var _azApp = ConnectToAzManApplication();
            foreach (var userName in usernames)
            {
                foreach (var roleName in roleNames)
                {
                    var currentUser = Membership.GetUser(userName);
                    var role = _azApp.OpenRole(roleName, null);
                    role.AddMember(currentUser.ProviderUserKey.ToString(), null);
                    role.Submit(0, null);
                }
            }
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            var _azApp = ConnectToAzManApplication();
            foreach (var userName in usernames)
            {
                foreach (var roleName in roleNames)
                {
                    var currentUser = Membership.GetUser(userName);
                    var role = _azApp.OpenRole(roleName, null);
                    role.DeleteMember(currentUser.ProviderUserKey.ToString(), null);
                    role.Submit(0, null);
                }
            }
        }

        public override string[] GetAllRoles()
        {
            var _azApp = ConnectToAzManApplication();
            List<string> rolesList = new List<string>();
            foreach (IAzRole role in _azApp.Roles)
            {
                rolesList.Add(role.Name);
            }
            return rolesList.ToArray();
        }

        private string applicationName;
        public override string ApplicationName
        {
            get
            {
                if (applicationName == null) applicationName = "MegaCorp.App";
                return applicationName;
            }
            set
            {
                applicationName = value;
            }
        }

        public void CreateApplicationGroup(string groupName)
        {
            var _azApp = ConnectToAzManApplication();
            _azApp.CreateApplicationGroup(groupName, null);
        }


        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();

        }

        Converter<object, string> newConverter = new Converter<object, string>(Convert.ToString);

        public override string[] GetUsersInRole(string roleName)
        {
            List<string> userList = new List<string>();
            var _azApp = ConnectToAzManApplication();
            var role = _azApp.OpenRole(roleName, null);

            string[] members = Array.ConvertAll<object, string>((object[])role.Members, newConverter);
            foreach (string memberProviderKey in members)
            {
                System.Security.Principal.SecurityIdentifier sid = new System.Security.Principal.SecurityIdentifier(memberProviderKey);
                var user = Membership.GetUser(sid);
                userList.Add(user.UserName);
            }
            return userList.ToArray();
        }
    }
}

Note that you’ll need a reference to Microsoft.Interop.Security.AzRoles to be able to compile that code. On earlier systems, such as XP, you’ll need to download the Windows 2000 Authorization Manager runtime from here: http://www.microsoft.com/downloads/details.aspx?FamilyID=7edde11f-bcea-4773-a292-84525f23baf7&DisplayLang=en

This is actually referenced in the article: How To: Use ADAM for Roles in ASP.NET 2.0 found above. You need to install the Authorization Manager runtime, then find the assembly on your system, add it to the GAC, and reference the original assembly from within you web application.

Leave a comment