Enable LDAP Authentication in Blesta

E

Eric Hansen

Guest
For my business I'm using Blesta as my billing/support software. Its pretty nicely done and overall am very pleased with the results. There is one thing missing (which they have some scattered support for): LDAP authentication.

By default, Blesta authenticates via MySQL (the only supported database due to PHP's PDO and their use of MySQL-specific queries). It took me a bit of digging around but I was able to get LDAP authentication to work. However, for now it requires editing a core file so you might want to back up said file and be wary of updating such file in the future. I'll turn this into a plugin once the ability to hook into the login process itself exists, but for now this is the only way.

All work on this will be in <install path>/app/models/users.php and we will be adding some helper functions just to make the overall code easier to handle.

Assumptions
  • This guide also assumes you already have LDAP set up (I'll go into how to do this in another article)
  • Clients and staff have two different OUs. If not then you'll have to make the changes to appropriate DNs.
  • The structure of my LDAP is cn=<CN/username>,ou=<Client|Staff>,dc=example,dc=com; this is important in some parts of here.
  • Its pretty important (read: mandatory) to have PHP-LDAP installed (typically can install it via package manager).
  • This doesn't support multiple companies (a feature that lets you use a single Blesta instance for multiple companies) out of the box. That is out of the scope for this as I don't have a way to test that.
Creating LDAP Users
Our first function we'll add to the Users class is our method to create/add users to LDAP. I did this so that if a user doesn't already exist in the LDAP server, we can add them in.

Code:
        public function ldap_create($cn, $pass, $type){
                if($type == "staff"){
                        $type = "Staff";
                } else{
                        $type = "Clients";
                }

                $ds = ldap_connect("ldap.example.com");

                ldap_start_tls($ds);

                if($ds){
                        $p = explode("@". $cn);
                        $cn1 = $p[0];

                        $info = array();
                        $info['cn'] = $cn1;
                        $info['sn'] = $cn1;
                        $info['description'] = $cn;
                        $info['userPassword'] = $pass;
                        $info['objectclass'] = "organizationalPerson";

                        $rdn = "cn=".$cn1.",ou=".$type.",dc=example,dc=com";

                        $r = ldap_add($ds, $rdn, $info);

                        ldap_close($ds);
                }
        }

We'll go over this:

Code:
                if($type == "staff"){
                        $type = "Staff";
                } else{
                        $type = "Clients";
                }
You'll see how this is used later, but if we're trying to process a log in from /admin/login then $type will be "staff", /client/login will be "client".

Code:
                $ds = ldap_connect("ldap.example.com");

                ldap_start_tls($ds);

Try to connect to the LDAP server. If your LDAP server isn't set up with SSL/TLS support then you can remove the ldap_start_tls() line. However, its strongly advisable to set up SSL/TLS regardless of how you have LDAP configured/connected. Otherwise everything will be sent plain text.

Code:
                        $p = explode("@". $cn);
                        $cn1 = $p[0];

Its possible that the user is logging in with an email. Not all (if any?) LDAP servers are friendly with "@" in the CN, so we get the username from the email and make that their CN. Is it the smartest method? Nope. But, given how many possible combinations there are for usernames, its a very small possibility that you'll run into a collision.

Code:
                        $info = array();
                        $info['cn'] = $cn1;
                        $info['sn'] = $cn1;
                        $info['description'] = $cn;
                        $info['userPassword'] = $pass;
                        $info['objectclass'] = "organizationalPerson";

We create a basic user structure so we can add them to our LDAP OU. We make CN and SN the same because SN is a pointless field in my case, but is required with the organizationalPerson objectClass. We don't worry about hashing the password because LDAP will do that for us.

Code:
                        $rdn = "cn=".$cn1.",ou=".$type.",dc=example,dc=com";

                        $r = ldap_add($ds, $rdn, $info);

                        ldap_close($ds);
                }

Now we construct our RDN, specifying the CN and OU for our new user, then attempt to add it via ldap_add(). On success $r will be true, otherwise its false.

Updating An Account's Password
Next method will be changing the password. Its a much smaller method and the code is similar to above, so I'll only comment on the stuff that's different.

Code:
        public function ldap_passwd($cn, $pass, $type){
                if($type == "staff"){
                        $type = "Staff";
                } else{
                        $type = "Clients";
                }

                $ds = ldap_connect("ldap.example.com");

                ldap_start_tls($ds);

                if($ds){
                        $info = array(
                                        'userPassword' => $pass
                        );

                        $p = explode("@", $cn);
                        $cn1 = $p[0];
                        return ldap_modify($ds, "cn=".$cn1.",ou=".$type.",dc=example,dc=com", $info);
                }

                return false;
        }

We pass the CN (username) of the client, their new password and what type they are in the system to the method and return either true or false.

Code:
                        $info = array(
                                        'userPassword' => $pass
                        );

                        $p = explode("@", $cn);
                        $cn1 = $p[0];
                        return ldap_modify($ds, "cn=".$cn1.",ou=".$type.",dc=example,dc=com", $info);

$info holds the new information of our user. We essentially tell LDAP (find user "cn=...." with our connection $ds and replace their old password with $pass) what needs to be done. Again, LDAP will automatically hash the user's password so we don't have to concern ourselves with it.

Authenticating Against LDAP
Now we get to the fun function, authenticating the user. This one took me a little while to figure out due to the fact that search filter needed to be crafted a little interestingly. Here's the code:

Code:
        public function ldap_auth($cn, $pass, $type){
                $valid = false;

                if($type == "staff"){
                        $type = "Staff";
                } else{
                        $type = "Clients";
                }

                $ds = ldap_connect("ldap.example.com");

                ldap_start_tls($ds);

                if($ds){
                        $ldapbind = ldap_bind($ds, "cn=admin,dc=example,dc=com", "ldap_admin_pw");

                        if($ldapbind){
                                $sr = ldap_search($ds, "ou=".$type.",dc=example,dc=com", "(&(userPassword=".$pass.")(|(cn=".$cn.")(description=".$cn.")))");

                                $info = ldap_get_entries($ds, $sr);

                                if($info['count'] > 0)
                                        return true;
                        }
                }

                return false;
        }

Again a lot of this is similar to the others so won't get into a lot of detail. This one will take a little bit of discussion though. Also, there is the option of of instead binding on the admin account to bind on the user you want to log in as, but I had mixed results when trying that.

Code:
$ldapbind = ldap_bind($ds, "cn=admin,dc=example,dc=com", "ldap_admin_pw");
Here we authenticate ourselves to the LDAP server with an administrative account. Replace "admin" in the "cn=..." line with your own administrative username and also change the "ldap_admin_pw" field. Note that this is NOT a Blesta/MySQL administrative username and password. This needs to be for the LDAP service itself.

Code:
$sr = ldap_search($ds, "ou=".$type.",dc=example,dc=com", "(&(userPassword=".$pass.")(|(cn=".$cn.")(description=".$cn.")))");
We want to get search results for our user. Basically what we're searching for is the userPassword field being what the user provided (again, LDAP hashes for us so no concern on our end) and either the CN or the description field being the CN (username/email).

If you are familiar with SQL than you can think of it as this:
Code:
SELECT * FROM $type WHERE userPassword = $pass AND (cn = $cn OR description = $cn);
LDAP's search filtering is a bit weird.

Code:
                                $info = ldap_get_entries($ds, $sr);

                                if($info['count'] > 0)
                                        return true;

Here we get the search results into a easy to handle PHP array. The results will also return a field "count" that will let us know how many results it found. Really the check should be if($info['count'] == 1) but I mainly did this for testing.

Combining It All
This part took me a good bit to figure out since each method in this class could be the actual point of interest for authenticating a user. However, after some trial and error, I found the method: public function auth(...)

I'm not giving the line # because it'll vary depending on where you put your code from above and such.

In there you'll see a simple if block:

Code:
                if ($user) {
                    ....
                }

Its inside that block we're interested in. Right below the "if..." you'll want to add these lines:

Code:
                        $ldap_res = $this->ldap_auth($vars['username'], $vars['password'], $type);

                        if(!$ldap_res){
                                $this->ldap_create($vars['username'], $vars['password'], $type);
                        }

This is basically saying "$ldap_res = true if the username and password can authenticate against LDAP, else if not then create the record".

Again, not logically the best but it fits my needs.

$vars['username'] will have the submitted username when they logged in and same with $vars['password'] with their password. $type can be anything but that's why before we only cared if it was staff, otherwise its tied to the client in some way.

So basically the if block should now look like this:

Code:
                if ($user) {
                        $ldap_res = $this->ldap_auth($vars['username'], $vars['password'], $type);

                        if(!$ldap_res){
                                $this->ldap_create($vars['username'], $vars['password'], $type);
                        }

                        if ($this->checkPassword($vars['password'], $user->password))
                                $authorized = true;
                        elseif (Configure::get("Blesta.auth_legacy_passwords") && $user->password == md5($vars['password'])) {
                                $authorized = true;

                                // Upgrade user password
                                $this->edit($user->id, array('new_password' => $vars['password'], 'confirm_password' => $vars['password']), false);
                        }
                }

Some better error handling should be done here, but that is quite trivial compared to what we had to do to get LDAP authentication to work.
 

Attachments

  • slide.jpg
    slide.jpg
    40.9 KB · Views: 118,399


Good article!

I think you should define/explain the abbreviations for newbies that want to learn and understand - OU, DN, PDO, ....
 
Good article!

I think you should define/explain the abbreviations for newbies that want to learn and understand - OU, DN, PDO, ....
I mostly wrote this article for the Blesta community but thought it'd be helpful here too. My next series will be installing and configuring OpenLDAP (the LDAP server I use) which will also go into detail about the terms (OU, DN, etc...).
 

Members online


Latest posts

Top