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

February 26, 2010

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.


Registry file for registering ResXFileCodeGeneratorEx

February 22, 2010

I have provided a registry file for registering Dmytro Kryvko’s ResXFileCodeGeneratorEx in Visual Studio 2010.

When you are using a 64-bit O/S, use this one:
ResXFileCodeGeneratorEx.reg

When you are using a 32-bit Operating System, use this one:
ResXFileCodeGeneratorEx32bit.reg

Make sure you run Dmytro’s installer first, as it installs the assemblies in the correct place. All this registry hack does is make it also work in VS2010.

This can be used to get my original code working under Visual Studio 2010 from the article:
How to Enumerate Strings using Resource Files in C#


How to Enumerate Strings using Resource Files in C#

February 14, 2010

Strings are not valid types within enumerations. So the coder usually has to create a whole bunch of constant string values and simply have them grouped together within their classes. It’s not very desirable for a few reasons. For starters, there is no enforcement that a string value has to belong to a particular class, and also, they are much harder to read in from resource files.

Enumerations are an excellent way to make code more reliable, readable and modifiable. They are a special case, where every value of a variable is already known, and you want to be able to describe them in a more readable way. They allow each value to be grouped together so that there is a reduced likelihood that an incorrect value is specified in code.

For example,

public enum Flowers
{
   Rose,
   Tulip,
   Daisy,
   Daffodil
}

If you declare a variable of type Flowers, then you know that only Rose, Tulip, Daisy, Daffodil can be selected.

To do the equivalent with strings would require, perhaps, a class

public class Vehicles
{
    public const string AlfaRomeo = "Alfa Romeo";
    public const string MercedesBenz = "Mercedes Benz";
    public const string Ford = "Ford";
    public const string Porche = "Porche";
    public const string AstonMartin = "Aston Martin";
}

But there is nothing to prevent you from using a string from the wrong class in your code. If you declare a variable for the car

string myCar;

There is nothing to prevent setting the value to something other than what exists in the class Cars.

myCar = "Honda";

Another major benefit of enumeration is that non-domain errors are caught at compile time, not runtime. Finding errors at compile time is significantly cheaper to resolve than finding the error after it has gone live. So it is highly desirable to find these sorts of errors as early as possible in the process.

And it can be done!

I will demonstrate how to better group your strings, and also to treat those strings as enumerations. Here, I will use attributes and reflection to retrieve the values from resource files.

So to start with, create a new string resources file. I have called mine MessageResources. Add a few strings to the resource file. I have added mine as follows:

Message Resources File containing Vehicle strings

Note that I have deliberately chosen names that include spaces. When you use the ToString() method on a standard enumeration, expecting to see their values, it is significantly harder to use those values in, say, a combo box, if they don’t have spaces in them.

Now let me skip to the enumeration itself.

Start with a typical enumeration:

public enum Vehicles
{
    AlfaRomeo,
    MercedesBenz,
    Ford,
    Porche,
    AstonMartin
}

Of course, you need to link this to the resource file, so we’ll do that via attributes. So I will create an attribute for the Enumeration, as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EnumHelperSample
{
    public class EnumAttribute : Attribute
    {
        public Type ResourceType { get; private set; }
        public string ResourceName { get; private set; }
        public int SortOrder { get; private set; }

        public EnumAttribute(Type ResourceType,
                             string ResourceName,
                             int SortOrder)
        {
            this.ResourceType = ResourceType;
            this.ResourceName = ResourceName;
            this.SortOrder = SortOrder;
        }
    }
}

I have decided to include the type of the resource file that contains the strings, the name of the string resource, and the order that I would like the enumerations to be sorted if returned as a list of strings.

However, this won’t work. If you have a look in the MessageResources.Designer.cs file, this generates the following code:


/// <summary>
///   Looks up a localized string similar to Alfa Romeo.
/// </summary>
internal static string AlfaRomeo {
   get {
       return ResourceManager
              .GetString("AlfaRomeo", resourceCulture);
   }
}

For a value to be passed via an attribute, it needs to be a constant, and the MessageResources generated code does not provide a constant string.

Enter Dmytro Kryvko and the ResXFileCodeGeneratorEx Custom Tool.

Dmytro has replaced the custom tool found on the standard Resources file with a new custom tool that he wrote. This tool generates a constant string for every resource name that is generated within the resource file.

You can download it from here:
http://www.codeproject.com/KB/dotnet/ResXFileCodeGeneratorEx.aspx

If you really want to go and improve his code, by all means, go ahead. For our purposes, it will be fine simply to run the installer.

After installing this on your machine, you select properties on the resources file, and change the custom tool to the extended version, ending in Ex.

Changing Message Resources Custom Tool to ResXFileCodeGeneratorEx

Now we can pass the resource names as constants.

The enumeration code then becomes:

namespace EnumHelperSample
{
    public enum Vehicles
    {
        [EnumAttribute(typeof(MessageResources),
                       MessageResources.ResourceNames.AlfaRomeo,
                       1)]
        AlfaRomeo,
        [EnumAttribute(typeof(MessageResources),
                       MessageResources.ResourceNames.MercedesBenz,
                       4)]
        MercedesBenz,
        [EnumAttribute(typeof(MessageResources),
                       MessageResources.ResourceNames.Ford,
                       3)]
        Ford,
        [EnumAttribute(typeof(MessageResources),
                       MessageResources.ResourceNames.Porche,
                       5)]
        Porche,
        [EnumAttribute(typeof(MessageResources),
                       MessageResources.ResourceNames.AstonMartin,
                       2)]
        AstonMartin
    }

}

Ok, that’s all very well, but how do we retrieve the values?

Well, I have written an EnumHelper class as a set of extensions to retrieve those values.

This first one is to retrieve a value from the associated resource. I do this by extending the Enum type. It allows me to write the following syntax:

Debug.WriteLine(Vehicles.AlfaRomeo.GetString());
Debug.WriteLine(Vehicles.MercedesBenz.GetString());

This outputs:

Alfa Romeo
Mercedes Benz

public static string GetString(this Enum value)
{
    EnumAttribute ea =
               (EnumAttribute)value.GetType()
                .GetField(value.ToString())
                .GetCustomAttributes(typeof(EnumAttribute), false)
                .FirstOrDefault();
     if (ea != null)
     {
           PropertyInfo pi = ea.ResourceType
                               .GetProperty("ResourceManager");
            if (pi != null)
            {
                ResourceManager rm = (ResourceManager)pi
                                     .GetValue(null, null);
                return rm.GetString(ea.ResourceName);
            }
     }

     return string.Empty;
}

And finally to retrieve the entire sorted list, I extended Type itself, as follows:

public static IList GetStrings(this Type enumType)
{
    SortedList<int, string> stringList = new SortedList<int, string>();
    FieldInfo[] fiArray = enumType.GetFields();
    foreach (FieldInfo fi in fiArray)
    {
        EnumAttribute ea =
            (EnumAttribute)fi
                 .GetCustomAttributes(typeof(EnumAttribute), false)
                 .FirstOrDefault();
         if (ea != null)
         {
              PropertyInfo pi = ea.ResourceType
                                  .GetProperty("ResourceManager");
              if (pi != null)
              {
                  ResourceManager rm = (ResourceManager)pi
                                        .GetValue(null, null);
                  stringList.Add(ea.SortOrder,
                                 rm.GetString(ea.ResourceName));
              }
        }
    }
    return stringList.ToList();
}

So to display the entire sorted list of values:

IList result = typeof(Vehicles).GetStrings();
foreach (KeyValuePair<int,string> kvp in result)
{
    Debug.WriteLine(kvp.Value);
}

Finally, a note. Allowing the enumerations to be specified via Resource Name constants passed as attributes still requires the use of strings. This means other string constants could be passed in to the attribute types.

I believe that the likelihood of this occurring is significantly less of a risk than if the developer was loading up and passing around string constants themselves. And anyway, you’ll need to know which resource file that the strings are coming from. If you are that worried about it, you could always modify Dmytro’s code!

Meanwhile, my code may be found here:

EnumHelperSample.zip