Skip to content

nJupiter.DataAccess.Ldap

Martin Odhelius edited this page Mar 16, 2015 · 2 revisions

Component containing a generic MembershipProvider and RoleProvider for LDAP . These providers has been successfully tested with Microsoft Active Directory, Novell eDirectory, IBM Tivoli Directory Server, OpenLDAP and Lotus Domino but shall probably work with any type of LDAP-server such as 389 Directory Server, OpenDS / OpenDJ, Apache DS, Sun One / iPlanet / Netscape DS and Oracle Internet Directory (if you are using these providers with any of the server not listed as tested, please contact with status regarding if they successfully work or not).

The component also contain facades and abstractions for System.DirectoryServices namespace and a parser for distinguished names.

History and Purpose

A few years ago I started to work in a project that had the goal to use a CMS that were using .NET's MembershipProvider and RoleProvider architecture for authentication and user management. The customer that would use the CMS solution used an IBM Tivoli LDAP Directory Server and first I thought that would not be any problems since I was pretty sure that it would exist a LDAP MembershipProvider and RoleProvider in the .NET framework, but to my surprise it didn't! Then I started to look around the web for alternatives but I didn't find any LDAP MembershipProvider and RoleProvider anywhere so I decided to write my own. My aims was to write a generic LDAP provider that should work both with my internal Active Directory test server and together with the customer's Tivoli server or with any other LDAP server out there, the result was nJupiter.DataAccess.Ldap.

How to set up nJupiter.DataAccess.Ldap

You can install nJupiter.DataAccess.Ldap via NuGet. The NuGet package do not include any config though so you will have to activate the providers in your web.config and also add configuration for your LDAP-server before it will work.

nJupiter.DataAccess.Ldap is using nJupiter.Configuration which will install together with nJupiter.DataAccess.Ldap if you install it via NuGet. For more information about how to configure and set up nJupiter.Configuration please refer to this page. The config file containing the LDAP server config shall be named nJupiter.DataAccess.Ldap.config and placed anywhere where nJupiter.Configuration can read it. You can find an example file with configurations for different LDAP servers here. Read more how to configure the component here

When you have added configuration for you LDAP server you are ready to turn the providers on in web.config. You do this in the membership and roleManager elements beneath system.web. Here is an example:

<system.web>
   <membership defaultProvider="LdapMembershipProvider">
      <providers>
         <clear/>
         <add name="LdapMembershipProvider"
              ldapServer="Tivoli"
              type="nJupiter.DataAccess.Ldap.LdapMembershipProvider,nJupiter.DataAccess.Ldap" />
      </providers>
   </membership>
   <roleManager enabled="true" defaultProvider="LdapRoleProvider" cacheRolesInCookie="true">
      <providers>
         <clear/>
         <add name="LdapRoleProvider"
              ldapServer="Tivoli"
              type="nJupiter.DataAccess.Ldap.LdapRoleProvider,nJupiter.DataAccess.Ldap" />
      </providers>
   </roleManager>
</system.web>

The ldapServer attribute tells which LDAP configuration in nJupiter.DataAccess.Ldap.config to use, if this attribute is left out the configuration with the default=“true” attribute will be used. If you need more information how to configure MembershipProviders and RoleProviders you can find it here:

How to configure nJupiter.DataAccess.Ldap

You do the configuration for the LDAP servers in the nJupiter.DataAccess.Ldap.config file that you can read more about above. Notice that all config values have to be XML encoded in the config file.

The nJupiter.DataAccess.Ldap.config config file has to contain an ldapServers-element that contain one or more ldapServer-elements. The ldapServer-element has to have a value-attribute that is the name of the server. If you do not specify a name in the ldapServer-attribute for your provider in web.config you have to add a default attribute set to true for any of the ldapServer-elements. Here is an example:

<ldapServer value="MyLdapServer" default="true">
   <!--... server configuration goes here -->
</ldapServer>

Beneath the ldapServer-element you add the configuration for the current server. The first thing you have to do is to define the URL to the LDAP-server. You do this in the url-element by setting the value-attribute to the URL for the server. This value has to be a fully qualified uri as defined in the RFC 2396. Here is an example:

<url value="LDAP://ldap.example.org:389/" />

If you would like to use serverless binding, you can set this value to just “LDAP://”. Notice though that serverless binding is a feature specific for Microsoft Active Directory. Here is an example how to activate this mode:

<url value="LDAP://" />

The next thing you have to do is to define a username and password to use when the providers authenticating themselves towards the LDAP server. This user have to have read access in the directory. For more information please read more here and here. Here is an example:

<username value="uid=user,ou=people,dc=example,dc=org" />
<password value="password" />

You can also define the authentication types to use inside the authenticationTypes-element. The possible authentication types are None, Secure, Encryption, SecureSocketsLayer, ReadonlyServer, Anonymous, FastBind, Signing, Sealing, Delegation and ServerBind. One or more of these values can be defined beneath the authenticationTypes-element in one authenticationType-element per type. If you do not define any authenticationTypes-element the default authentication type will be None. Read more about authentication types here. Here is an example:

<authenticationTypes>
   <authenticationType value="SecureSocketsLayer" />
   <authenticationType value="ReadonlyServer" />
</authenticationTypes>

You can also set a time limit in seconds which will be the maximum amount of time the server spends when searching for entries in the LDAP server. You do this in the timeLimit-element. If you leave this element out the default value will be set to 30 seconds. Here is an example where the time limit is set to 5 seconds:

<timeLimit value="5" />

If your server supports Virtual List View (VLV) it is highly recommended that you enables it by setting the 'virtualListViewSupport'-element to true. If you leave this element out the default will be set to false. Without this feature the LdapMembershipProvider will not be able to perform fully and performance wise paging when the GetAllUsers-, FindUsersByName- and FindUsersByEmail-methods is called. Most modern LDAP servers support VLV servers, some of them needs that you actively enable the feature or install an add on for it to work though. Here is a list of known server that supports VLV:

  • IBM Tivoli Directory Server (since 6.2)
  • Microsoft Active Directory (since Windows Server 2003)
  • Sun One
  • Novell eDirectory (since 8.8 with limitations)
  • OpenLDAP (since 2.4, via overlay)
  • 389 Directory Server
  • OpenDS and OpenDJ

Here is an example how to turn this feature on:

<virtualListViewSupport value="true" />

If your server does not support Virtual List View but Server Side Sort you can turn this on by setting the 'propertySortingSupport'-element to true. If you leave this element out the default will be false. If you are using VLV you can leave this element out though because Server Side Sort is required for VLV to function so in that case the sorting will be performed regardless the setting of this flag. Here is an example how to turn this feature on:

<propertySortingSupport value="true" />

You can also define a size limit which will be the maximum number of objects that the server returns in one single search operation. If you leave this element out the default value will be set to 1000. If you set size limit to a value that is larger than the server-determined default of 1000 entries, the server-determined default is used. For better performance when for example a whole set of users, it can be a good idea to keep this value pretty low if your server does not support Virtual List View. If you are using a server that supports VLV it is recommended to leave this element out or to set it to the default value. Here is an example here the size limit is set to a maximum of 100 objects per search:

<sizeLimit value="100" />

If your LDAP server does not support Virtual List View it can be a good reason to set the page size that will be used when the client preforms Simple Paged Results which will be the standard paging function if VLV is not used. Please set this value to a lower value than the size limit. The default value is 0, which means that no simple paging will be performed on searches. If you are using a server that supports VLV it is recommended to leave this element out or to set it to the default value. Here is an example where the page size is set to 50 objects:

<pageSize value="50" />

If your server supports fetching properties by range retrieval you can turn this feature on by setting the rangeRetrievalSupport-element to true. If you leave this element out the default will be false. It is a good idea to turn this on if your directory contains extremely large membership list (>1000 members), without it the provider can only fetch max 1500 per role when calling the GetUsersInRole-method in the LdapRoleProvider. Here is an example how to turn this feature on:

<rangeRetrievalSupport value="true" />

If you want to be able to do wild card search when you for example using the FindUsersByName- and FindUsersByEmail-methods on the LdapMemebershipProvider you have to have wild card search enabled (for example FindUsersByName("john*", 0, 10, out totalRecords) will return a list of users with a name that starts with john). This feature is enabled by default, but if you of some reason want to disable it you can set the allowWildcardSearch-element to false. Here is an example how to turn this feature off:

<allowWildcardSearch value="false" />

Configure user settings

In an LDAP server membership users are typically called people, persons or users, from now on we call them users. To map thous users to a MembershipUser you have to tell the LdapMembershipProvider how to find the user-entries in the LDAP-server and which attributes in the LDAP that conform to the properties in the MembershipUser class. You will do all user specific configuration beneath the users-element which shall be placed inside the ldapServer-element. Here is an example:

Here is an example:

<ldapServer value="MyLdapServer" default="true">
   <users>
   <!--... user configuration goes here -->
   </users>
</ldapServer>

In the ´base´-element you define the base DN. The base shall be distinguished name that point to the level in the LDAP under which the users are located. If you need to know more about distinguished names you can read more about it here or here. The search that will be performed will search the whole sub tree so it will be able to find users in several levels, but to make it as performance wise as possible it is a good idea to configure this as close to the users as possible. A base config can for example looks like this:

<base value="ou=people,o=example,dc=org" />

The next thing you have to do is to define a LDAP format filter that shall be used to find the user entries. If you need to know more about search filters you can read more about them here, here or here. Notice that the filter has to be XML encoded in the config file. The default will be (objectClass=*) but performance wise it can be a good idea to make a more specific filter. If you are using Microsoft Active Directory this filter shall typically be (sAMAccountType=805306368), and for an ADAM server it is typically (&amp;(objectClass=user)(objectCategory=person)) while for for example an IBM Tivoli server is is typically (objectClass=person). Here is an example how a configuration for this can look like:

<filter value="(objectClass=person)"/>

The next thing you will have to do is to tell which attribute that contains the unique user name in a user entry, the so called Relative Distinguished Name (RDN). This is the attribute where the user name that for example will be used when you log in is stored. The default value if you leave this element out is CN but this can vary from different LDAP servers, Microsoft Active Directory or ADAM for example typically use the sAMAccountName-name for this while the IBM Tivoli server often use ´uid´. Here is an example how a configuration for this can look like:

<rdnAttribute value="uid" />

You can also define in which formats the user names shall be returned by the component by changing the value in the nameType-element. The possible values for this setting are CN, RDN or DN. The default if you leave this element out i CN and is probably the value the majority of the consumers of this component shall use, that will return the user name as a standard common name. RDN will return the name formatted as a Relative Distinguished Name and the DN will result in that the names are returned as full Distinguished Names. Values other than CN will probably only be interesting for LDAP developers, so leave this element out or set to CN if you do not have any specific reason for anything else. Here is an example where the value is set to default:

<nameType value="cn" />

If you are using an LDAP server that store in which groups a user are a member of, not just in the group, but also in the user itself, you can define the attribute in the user entry that is storing the memberships, you do this by setting the attribute name in the membershipAttribute-element. Most LDAP servers cross store the membership both in the user and in the group, but there are some exceptions, like Oracle Internet Directory and some installations of Lotus Domino, for such servers you leave this setting blank or leave the element out completely. If your LDAP server supports cross storing of membership it is highly suggested to set this value though, even if it will work without it, because it is much more performance wise to just search the user for it's groups rather than to search the groups for user belongings. IBM Tivoli servers typically use ibm-allGroups for this while for example Microsoft Active Directory typically use memberOf. Here is an example:

<membershipAttribute value="memberOf" />

If the users in your LDAP server has e-mail-addresses stored and want them to be mapped to the Email-property on the MembershipUser you can configure the emailAttribute-element to be set to the corresponding attribute in the LDAP. The default value if you leave this element out is will be mail. Here is an example how you configure this setting:

<emailAttribute value="mail" />

You can map the Comment-property on the MembershipUser by setting the descriptionAttribute-element to a corresponding attribute in the LDAP. It can for example be a good idea to set this to an attribute that contains the full name for the user, for example the CN attribute, here is an example:

<descriptionAttribute value="cn" />

You can also map the LastPasswordChangedDate-, CreationDate- and LastLoginDate-property on the MembershipUser by setting the lastPasswordChangedDateAttribute-, creationDateAttribute- and lastLoginDateAttribute-elements to a corresponding attributes in the LDAP. Notice that some LDAP-servers store these values in a format that is not generically supported to load via the System.DirectoryServices.DirectorySearcher though, and will therefor result in an “COMException : Exception from HRESULT: 0x8000500C”. If you get this error, please clear these settings. Here is an example how to configure these values:

<creationDateAttribute value="whenCreated" />
<lastLoginDateAttribute value="lastLogon" />
<lastPasswordChangedDateAttribute value="pwdLastSet" />

You can also tell the component to load additional LDAP attributes together with the LdapMembershipUsers, you do this by adding attribute-elements beneath the attributes-elements. Thous attributes can then be accessed via ((LdapMembershipUser)membershipUser).Attributes["attributeName"]. If you do not set the excludeFromNameSearch-attribute to true on the attribute-element the attribute will also be searchable via the FindUsersByName-method on the LdapMembershipProvider, which means that if you for example have a user with the username “jdoe” and this user also have an attribute with his given name “John” with the attribute ´givenName´ that is not excluded from the search, this user can be found by using FindUsersByName("John*", 0, 10, out totalRecords). Here is an example where you tell the component to also load the given name (givenName), the sure name (sn) and the title for all users, but title shall not be searchable:

<attributes>
   <attribute value="givenName" />
   <attribute value="sn" />
   <attribute value="title" excludeFromNameSearch="true" />
</attributes>

If you of some reason do not want that the LdapMembershipProvider shall wrap the MembershipUser in a custom LdapMembershipUser you can turn this feature off by setting the membershipUserWrappingEnabled-element to false. This can for example be a good idea if you are planning to use the ASP.NET Web Site Administration Tool to access users and roles, but do not want to put the nJupiter.DataAccess.Ldap.dll file in the GAC (read more about this in the Troubleshooting. If you leave this element out the default will be true. Here is an example how to disable wrapping of the MembershipUser:

<membershipUserWrappingEnabled value="false" />

Notice though that if you disable this feature you will not be able to access the specific LdapMembershipUser functionality like accessing the Attributes-property that contains LDAP-properties loaded together with the user.

Configure group settings

In an LDAP server membership roles are typically called groups. To map thous groups to a be a role in the LdapRoleProvider you have to tell the provider how to find the group-entries in the LDAP-server and which attribute in the group entries that contain a reference to the users that are members of the group. You will do all user specific configuration beneath the groups-element which shall be placed inside the ldapServer-element. Here is an example:

Here is an example:

<ldapServer value="MyLdapServer" default="true">
   <groups>
   <!--... group configuration goes here -->
   </groups>
</ldapServer>

In the ´base´-element you define the base DN. The base shall be distinguished name that point to the level in the LDAP under which the groups are located. If you need to know more about distinguished names you can read more about it here or here. The search that will be performed will search the whole sub tree so it will be able to find groups in several levels, but to make it as performance wise as possible it is a good idea to configure this as close to the groups as possible. A base config can for example looks like this:

<base value="ou=groups,o=example,dc=org" />

The next thing you have to do is to define a LDAP format filter that shall be used to find the group entries. If you need to know more about search filters you can read more about them here, here or here. Notice that the filter has to be XML encoded in the config file. The default will be (objectClass=*) but performance wise it can be a good idea to make a more specific filter. If you are using Microsoft Active Directory or ADAM this filter shall typically be (objectClass=group), while for for example an IBM Tivoli server is is typically (objectClass=groupOfNames). Here is an example how a configuration for this can look like:

<filter value="(objectClass=groupOfNames)"/>

The next thing you will have to do is to tell which attribute that contains the unique group name in a group entry, the so called Relative Distinguished Name (RDN). The default value if you leave this element out is CN and is also the most common value for most LDAP server. Here is an example how a configuration for this can look like:

<rdnAttribute value="cn" />

You can also define in which formats the groups names shall be returned by the component by changing the value in the nameType-element. The possible values for this setting are CN, RDN or DN. The default if you leave this element out i CN and is probably the value the majority of the consumers of this component shall use, that will return the group name as a standard common name. RDN will return the name formatted as a Relative Distinguished Name and the DN will result in that the names are returned as full Distinguished Names. Values other than CN will probably only be interesting for LDAP developers, so leave this element out or set to CN if you do not have any specific reason for anything else. Here is an example where the value is set to default:

<nameType value="cn" />

You will also have to define the attribute that contain a reference to the users that are members of the group. You do this by setting the attribute name in the membershipAttribute-element. The typically value for this attribute is member for most LDAP servers. Notice that this attribute is mandatory and if you leave it out it will be set to the default value member. Here is an example how this config can look like:

<membershipAttribute value="member" />

Summary

Here is an example config where all possible element is defined:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <ldapServers>
      <ldapServer value="MyLdapServer" default="true">
         <url value="LDAP://ldap.example.org:389/" />
         <username value="uid=user,ou=people,dc=example,dc=org"/>
         <password value="password"/>
         <authenticationTypes>
            <authenticationType value="None" />
         </authenticationTypes>
         <timeLimit value="5" />
         <sizeLimit value="100" />
         <pageSize value="0" />
         <virtualListViewSupport value="true" />
         <propertySortingSupport value="true" />
         <rangeRetrievalSupport value="true" />
         <allowWildcardSearch value="true" />
         <users>
            <base value="ou=people,dc=example,dc=org" />
            <filter value="(objectClass=person)"/>
            <rdnAttribute value="uid" />
            <nameType value="cn" />
            <membershipAttribute value="memberOf" />
            <emailAttribute value="mail" />
            <descriptionAttribute value="cn" />
            <creationDateAttribute value="whenCreated" />
            <lastLoginDateAttribute value="lastLogon" />
            <lastPasswordChangedDateAttribute value="pwdLastSet" />
            <attributes>
               <attribute value="givenName" />
               <attribute value="sn" />
               <attribute value="title" excludeFromNameSearch="true" />
            </attributes>
            <membershipUserWrappingEnabled value="true" />
         </users>
         <groups>
            <base value="ou=groups,dc=example,dc=org" />
            <filter value="(objectClass=groupOfNames)"/>
            <rdnAttribute value="cn" />
            <membershipAttribute value="member" />
            <nameType value="cn" />
         </groups>         
      </ldapServer>
   </ldapServers>
</configuration>

How to use

The LdapMembershipProvider and LdapRoleProvider are working like standard MembershipProviders and RoleProviders, the providers are read only though and does not implement any functionality that writes information to the LDAP server, besides that you shall be able to use without doing anything more that explained in the previous sections.

A bonus feature beside the standard provider interface is that the LdapMembershipProvider is able to load LDAP attributes for users besides the standard MembershipUser attributes (defined in the attributes-element in the users config, look above for more info). Those attributes can then be accessible by casting the MembershipUser to an nJupiter.DataAccess.Ldap.LdapMembershipUser and read the Attributes property. Here is an example:

var ldapMembershipUser = System.Web.Security.Membership.GetUser("username") as LdapMembershipUser;
if(ldapMembershipUser != null){
    var givenName = ldapMembershipUser.Attributes["givenName"];
}

The LdapMembershipUser also has a property called ´Path´ that will contain the full LDAP-path which can be used to track back the MembershipUser to a specific LDAP entry. Notice that you can not access the MembershipUser as a LdapMembershipUser if you have set the membershipUserWrappingEnabled-element to false in the users config (please read above for further info).

Troubleshooting

If you get “System.InvalidOperationException: The value for the property VirtualListView cannot be set”, try do disabling property sorting by setting the virtualListViewSupport to false in nJupiter.DataAccess.Ldap.config

If you get “System.InvalidOperationException: The value for the property Sort cannot be set”, try do disabling property sorting by setting the propertySortingSupport to false in nJupiter.DataAccess.Ldap.config

If you get “System.InvalidOperationException: The value for the property PageSize cannot be set.”, try to disable paging by either removing the pageSize element entirely or by setting its value to 0 in nJupiter.DataAccess.Ldap.config

If you get similar exceptions you can also try to turn of the rangeRetrievalSupport by setting its flag to false.

If you get “COMException (0x80005000): Unknown error (0x80005000)” or similar it is probably caused by difference between your authenticationTypes-setting and how your LDAP server is configured.

If you get “COMException : Exception from HRESULT: 0x8000500C” your LDAP server probably stores some of the attributes in a format that is not generically supported to load via the System.DirectoryServices.DirectorySearcher, this generally happens when the value is stored as a non string format, for example time stamps. Try to clear the settings for creationDateAttribute, lastLoginDateAttribute and lastPasswordChangedDateAttribute and remove any additional attribute that is not in a string format that you have added under the attributes-element.

If you get “COMException (0x8007203a): The server is not operational” or similar it is typically returned when server is not of some reason reachable in the network, most certainly cause by an incorrect connection string, port or authentication type or by network related issues not associated with the component itself.

For other COMExceptions please refer to this page

If you are using the ASP.NET Web Site Administration and get an error similar to “Type is not resolved for member 'nJupiter.DataAccess.Ldap.LdapMembershipUser,nJupiter.DataAccess.Ldap'” that is casued of that the ASP.NET Web Site Administration Tool needs to serialize the MembershipUser object for an user, but since the ASP.NET Web Site Administration Tool does not run in the same application domain as your web it will not be able to access the assembly that contains this LdapMembershipUser class. To solve this you have two choices: either you can register nJupiter.DataAccess.Ldap in the GAC so it can be accessed globally by any application, or you can turn of wrapping of the MembershipUser by setting the membershipUserWrappingEnabled-element to false in users config (please see above).

You can’t perform that action at this time.