In this post series, we will study the Lightweight Directory Access Protocol (LDAP): a protocol developed in the 90s to be an open, simpler alternative to other directory protocols. We will also talk about Active Directory (Microsoft's LDAP implementation with extra features) and how to use it as an authentication mechanism. For the purposes of this post, we will focus on the generic LdapConnection API. In the next post, we will take a look at the Active Directory specific PrincipalContext API. Get the full code and read on!
What is LDAP?
LDAP is a protocol that defines a series of operations through which you can access information that is part of a directory. A directory is a tree containing a set of attributes associated with a unique identifier (or primary key). If you are familiar with document-based databases, this may sound familiar. The primary key is usually a name. This means that LDAP is perfectly suited to be a user information database. Even though most of the time it is used as a user directory, LDAP can also work as a generic information sharing service.
One common use of LDAP is as part of single-sign-on (SSO) systems. If you are not familiar with SSO, read our introduction to SSO.
The following diagram shows how a simple SSO system can work using LDAP. The diagram shows a simplified Microsoft Active Directory configuration using LDAP. Active Directory stores user information in an LDAP server. When users attempt to login to their Windows PC, Windows validates the login information against the LDAP/Active Directory server. Whenever a user tries to do something that requires authentication, an application can use information from the Active Directory server to validate the user's identity. Of course, if SSO is not required, Active Directory can also be used as a simple authentication mechanism.
Protocol overview
The best way to understand a protocol is to get your hands a bit dirty and learn its inner workings. Fortunately, barring binary encoding details and other low-level stuff, LDAP is a fairly simple protocol. LDAP defines a series of operations that are available to clients. Clients can connect to two types of servers:
- Directory System Agent (DSA): a server which allows LDAP operations
- Global Catalog: a special type of server that stores reduced sets of replicated information from DSAs to speed up searches.
“The best way to understand a protocol is to get your hands a bit dirty and learn its inner workings.”
Tweet This
Clients send requests to the server. In turn, the server answers those requests. Most requests are asynchronous; others are necessarily synchronous (such as the connection handshake). Additionally, the server may send special messages to clients even when there are no pending requests that require a response (for example, the server may send a message to notify clients that it is shutting down). All information is encoded using ASN.1 (see below for more details). TLS and/or SASL may be used to ensure privacy and perform authentication.
Supported operations
The following operations have .NET samples. A full working example can be found in the example section below.
Common operations supported by LDAP are:
- Search for specific entries
var request = new SearchRequest("ou=users,dc=example,dc=com", "(objectClass=simpleSecurityObject)", SearchScope.Subtree, null); var response = (SearchResponse)connection.SendRequest(request); foreach(SearchResultEntry entry in response.Entries) { //Process the entries }
- Test to determine whether a given attribute is present and has the specified value.
var request = new CompareRequest("uid=test,ou=users,dc=example,dc=com", "userPassword", "{SSHA}dFyxYbqyPKlQ7Py1T14XupyVfz7UFIz+"); var response = (CompareResponse)connection.SendRequest(request); if(response.ResultCode == ResultCode.CompareTrue) { //Attribute present and has the right value }
- Add/modify/delete entries.
var request = new AddRequest("uid=test,ou=users,dc=example,dc=com", new DirectoryAttribute[] { new DirectoryAttribute("uid", "test"), new DirectoryAttribute("ou", "users"), new DirectoryAttribute("userPassword", "badplaintextpw"), new DirectoryAttribute("objectClass", new string[] { "top", "account", "simpleSecurityObject" }) }); connection.SendRequest(request);
- Move an entry to a different path.
var request = new ModifyDNRequest("uid=test,ou=users,dc=example,dc=com", "ou=administrators,dc=example,dc=com", "uid=test"); connection.SendRequest(request);
Furthermore, additional protocol management operations are defined (connect, disconnect, negotiate protocol version, etc.).
ASN.1 and BER encoding
All operations are performed using messages encoded in Abstract Syntax Notation One (ASN.1) format using Basic Encoding Rules (BER). ASN.1 is defined in ITU standard X.680, while BER and other encodings are part of ITU standard X.690.
ASN.1 defines a series of datatypes (such as integer, string, etc.), a textual format description (schema), and a textual representation of values. BER, on the other hand, defines a binary encoding for ASN.1. BER is a traditional tag-length-value encoding. If you are interested in the gritty details, you can read a nice summary of BER encoding at Wikipedia.
Here is a schema taken directly from LDAP's RFC that shows the message format for LDAP:
LDAPMessage ::= SEQUENCE { messageID MessageID, protocolOp CHOICE { bindRequest BindRequest, bindResponse BindResponse, unbindRequest UnbindRequest, searchRequest SearchRequest, searchResEntry SearchResultEntry, searchResDone SearchResultDone, searchResRef SearchResultReference, modifyRequest ModifyRequest, modifyResponse ModifyResponse, addRequest AddRequest, addResponse AddResponse, delRequest DelRequest, delResponse DelResponse, modDNRequest ModifyDNRequest, modDNResponse ModifyDNResponse, compareRequest CompareRequest, compareResponse CompareResponse, abandonRequest AbandonRequest, extendedReq ExtendedRequest, extendedResp ExtendedResponse, ..., intermediateResponse IntermediateResponse }, controls [0] Controls OPTIONAL } MessageID ::= INTEGER (0 .. maxInt)
In the example above, we can see that an LDAP message carries a message id (an integer going from 0 to
maxInt
), an operation object (each object is defined elsewhere), and an extra field called control
(which is defined somewhere else in the schema under Control
). LDAP is defined using the same notation as the data format it uses internally. Behold the power of ASN.1!A simpler example with actual data:
World-Schema DEFINITIONS AUTOMATIC TAGS ::= BEGIN Human ::= SEQUENCE { name UTF8String, first-words UTF8String DEFAULT "Hello World", age CHOICE { biblical INTEGER (1..1000), modern INTEGER (1..100) } OPTIONAL } END first-man Human ::= { name "Adam", -- use default for first-words -- age biblical: 930 }
In this example we first see a schema for a
human
. A human
has two required fields (name
and first-words
) and an optional field (age
). The first-words
field has a default value of "Hello World" in case it is missing in a model. The age
field in turn can be one of two options: biblical
(any integer from one to 1000) or modern
(any integer from one to 100). What follows after the schema is a human
model conforming to the above schema (a human named "Adam," using the default value for first-words
, with a biblical age of 930).LDAP Data Interchange Format (LDIF)
Even though LDAP uses ASN.1 internally, and ASN.1 can be represented as text, there is a different textual representation for LDAP information called LDAP Data Interchange Format (LDIF). Here's a sample:
dn: cn=Barbara J Jensen,dc=example,dc=com cn: Barbara J Jensen cn: Babs Jensen objectclass: person description: file:///tmp/babs sn: Jensen
The two-letter attributes in the example above are:
- dn: distinguished name
- dc: domain component
- cn: common name
- sn: surname
LDIF can also be used as a means to perform operations:
dn: cn=Babs Jensen,dc=example,dc=com changetype: modify add: givenName givenName: Barbara givenName: babs
The examples above make it clear that the distinguished name (DN) uniquely identifies an entry.
When it comes to LDAP, LDIF is much more common than the alternatives. In fact, tools such as OpenLDAP use LDIF as input/output.
Example: using LDAP from a C# client
.NET provides a convenient set of classes to access LDAP and Active Directory servers. Here are the relevant .NET docs. The following example has been tested against OpenLDAP 2.4. Get the full code.
The user model for our example includes fields for:
- uid: user id (name)
- ou: organizational unit
- userPassword: hashed user password
- objectClass: typical classes for user accounts
Note this is not the model for an Active Directory user. Active Directory users can be validated using the bind operation (see below).
Validating user credentials using bind
In practice, credentials stored in an LDAP directory are validated using the bind operation. The bind operation means "log-in to an LDAP server using a specific set of credentials." If the bind operation succeeds, the credentials are valid. The mapping of a user to an actual entry in the LDAP directory is set up in the server configuration (Active Directory has specific rules for this; other LDAP servers leave this detail to the administrator).
/// <summary> /// Another way of validating a user is to perform a bind. In this case, the server /// queries its own database to validate the credentials. The server defines /// how a user is mapped to its directory. /// </summary> /// <param name="username">Username</param> /// <param name="password">Password</param> /// <returns>true if the credentials are valid, false otherwise</returns> public bool validateUserByBind(string username, string password) { bool result = true; var credentials = new NetworkCredential(username, password); var serverId = new LdapDirectoryIdentifier(connection.SessionOptions.HostName); var conn = new LdapConnection(serverId, credentials); try { conn.Bind(); } catch (Exception) { result = false; } conn.Dispose(); return result; }
Validating user credentials manually
If you have full access to the credentials stored in the directory, you can compare the hashed passwords of your users to validate credentials. Note that this is NOT how Active Directory stores credentials. Users in an Active Directory server must be validated using the "bind" operation (using either this API or PrincipalContext, which we will discuss in the next post). See the previous example for information on how to perform a bind operation using this API.
/// <summary> /// Searches for a user and compares the password. /// We assume all users are at base DN ou=users,dc=example,dc=com and that passwords are /// hashed using SHA1 (no salt) in OpenLDAP format. /// </summary> /// <param name="username">Username</param> /// <param name="password">Password</param> /// <returns>true if the credentials are valid, false otherwise</returns> public bool validateUser(string username, string password) { var sha1 = new SHA1Managed(); var digest = Convert.ToBase64String(sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password))); var request = new CompareRequest(string.Format("uid={0},ou=users,dc=example,dc=com", username), "userPassword", "{SHA}" + digest); var response = (CompareResponse)connection.SendRequest(request); return response.ResultCode == ResultCode.CompareTrue; }
Establishing a connection
public Client(string username, string domain, string password, string url) { var credentials = new NetworkCredential(username, password, domain); var serverId = new LdapDirectoryIdentifier(url); connection = new LdapConnection(serverId, credentials); }
Connection parameters used for this example are:
: testusername
: example.comdomain
: testpassword
: localhost:389url
Check your server's configuration to pick the right connection parameters. If you are using LDAP + SASL, do not forget to set the right SASL params in the OpenLDAP config file. For instance, the following line tells OpenLDAP to use the SASL database directly.
sasl-auxprops sasldb
Adding a user to the directory
/// <summary> /// Adds a user to the LDAP server database. This method is intentionally less generic than the search one to /// make it easier to add meaningful information to the database. /// NOTE: this is not an Active Directory user. /// </summary> /// <param name="user">The user to add</param> public void addUser(UserModel user) { var sha1 = new SHA1Managed(); var digest = Convert.ToBase64String(sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(user.UserPassword))); var request = new AddRequest(user.DN, new DirectoryAttribute[] { new DirectoryAttribute("uid", user.UID), new DirectoryAttribute("ou", user.OU), new DirectoryAttribute("userPassword", "{SHA}" + digest), new DirectoryAttribute("objectClass", new string[] { "top", "account", "simpleSecurityObject" }) }); connection.SendRequest(request); }
See the full code for examples on searching, modifying and deleting entries.
Aside: setting up Auth0 for LDAP use
At Auth0 we care about all our clients. If you have an existing LDAP deployment, you can integrate it with Auth0. LDAP deployments are usually installed inside a corporate network. In other words, they are private. Since they are private, there is no access to the LDAP server from the outside. Since our authentication solution works from the cloud, it is necessary to provide a means for the internal network to communicate with our servers. This is what we provide in the form of the Active Directory/LDAP connector. This is a service that is installed in your network to provide a bridge between your LDAP server and our own servers in the cloud. Worry not! The connector uses an outbound connection to our servers so you don't need to set up special rules in your firewall.
To enable LDAP for your Auth0 apps, first go to
Connections
-> Enterprise
-> Active Directory / LDAP
. Follow the steps to setup the LDAP connector (you will need the LDAP server details) and then enable LDAP for your app.The following examples use the LDAP server setup for our C# example above.
Auth0 + LDAP using C#
Once you have enabled LDAP in the dashboard and set up the connector, you can follow the usual steps for our Resource Owner Password flow. Logging in using an email and password just works!
public class RequestBody { [JsonProperty("client_id")] public string ClientId { get; set; } [JsonProperty("audience")] public string Audience { get; set; } [JsonProperty("grant_type")] public string GrantType { get; set; } [JsonProperty("username")] public string Username { get; set; } [JsonProperty("password")] public string Password { get; set; } [JsonProperty("scope")] public string Scope { get; set; } [JsonProperty("realm")] public string Realm { get; set; } } public class ResponseBody { [JsonProperty("access_token")] public string AccessToken; [JsonProperty("id_token")] public string IdToken; [JsonProperty("expires_in")] public string ExpiresIn; [JsonProperty("scope")] public string Scope; [JsonProperty("token_type")] public string TokenType; } class Program { static HttpClient client = new HttpClient(); static void Main(string[] args) { RunAsync().GetAwaiter().GetResult(); } static async Task RunAsync() { client.BaseAddress = new Uri("YOUR-AUTH0-DOMAIN"); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); try { RequestBody req = new RequestBody { ClientId = "YOUR-AUTH0-CLIENT-ID", Audience = "YOUR-API-AUDIENCES", // See https://auth0.com/docs/api-auth GrantType = "http://auth0.com/oauth/grant-type/password-realm", // See https://auth0.com/docs/api-auth/tutorials/password-grant#realm-support Realm = "YOUR-LDAP-CONNECTION", Scope = "openid profile email YOUR-ADDITIONAL-SCOPES" Username = "test", Password = "test" }; HttpResponseMessage response = await client.PostAsJsonAsync("oauth/token", req); response.EnsureSuccessStatusCode(); ResponseBody resp = await response.Content.ReadAsAsync<ResponseBody>(); // You can use the ID token to get user information in the frontend. string idToken = resp.IdToken; // You can use this token to interact with server-side APIs. string accessToken = resp.AccessToken; } catch (Exception e) { Console.WriteLine(e.Message); } } }
Auth0 + LDAP using our REST API
You can log in using our RESTful API for database, passwordless and LDAP users.
curl -H 'Content-Type: application/json' -X POST -d '{ "grant_type":"http://auth0.com/oauth/grant-type/password-realm", "realm": "your-ldap-connection", "client_id":"FyFnhDX2kSqtpMZ6pGe6QpQuJmD7s4dj", "username":"test", "password":"test" }' https://speyrott.auth0.com/oauth/token
Conclusion
LDAP was designed as a lightweight protocol that can access directory contents. As it evolved over the years, it gained important features, such as authentication and transport security. As a well defined means to get user information, it has found its way to small and big deployments. Its simplicity and openness have kept LDAP relevant through the years. Nowadays, single sign on systems can also work using LDAP. Fortunately, integrating LDAP to existing or new projects is easy. In our next post, we will focus on Active Directory specifics using the PrincipalContext API. Stay tuned!
Want to learn more about Single Sign-On? Get The Definitive Guide on SSO (74-page free eBook) here.
About the author
Sebastian Peyrott
Senior Engineer