2 // +----------------------------------------------------------------------+
 
   3 // | Anuko Time Tracker
 
   4 // +----------------------------------------------------------------------+
 
   5 // | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
 
   6 // +----------------------------------------------------------------------+
 
   7 // | LIBERAL FREEWARE LICENSE: This source code document may be used
 
   8 // | by anyone for any purpose, and freely redistributed alone or in
 
   9 // | combination with other software, provided that the license is obeyed.
 
  11 // | There are only two ways to violate the license:
 
  13 // | 1. To redistribute this code in source form, with the copyright
 
  14 // |    notice or license removed or altered. (Distributing in compiled
 
  15 // |    forms without embedded copyright notices is permitted).
 
  17 // | 2. To redistribute modified versions of this code in *any* form
 
  18 // |    that bears insufficient indications that the modifications are
 
  19 // |    not the work of the original author(s).
 
  21 // | This license applies to this document only, not any other software
 
  22 // | that it may be combined with.
 
  24 // +----------------------------------------------------------------------+
 
  26 // | https://www.anuko.com/content/time_tracker/open_source/credits.htm
 
  27 // +----------------------------------------------------------------------+
 
  31 // Auth_ldap.class.php was originally written for LDAP authentication with Windows Active Directory.
 
  32 // It June 2011, it was extended to include support for OpenLDAP. The difference in the code is in the format
 
  33 // of user identification that we pass to ldap_bind().
 
  35 // Windows AD accepts username@domain.com while OpenLDAP needs something like "uid=username,ou=people,dc=domain,dc=com".
 
  36 // Therefore, some branching in the code.
 
  38 // In April 2012, a previously mandatory search for group membership was put in a conditional block (if ($member_of) -
 
  39 // when mandatory membership in groups is actually defined in config.php).
 
  40 // This made the module work with Sun Directory Server when NO GROUP MEMBERSHIP is specified.
 
  41 // Note 1: search is likely to fail with Sun DS if 'member_of' => array()); is used in config.php.
 
  42 // Note 2: search is likely to not work properly with OpenLDAP as well because of Windows specific filtering code in there
 
  43 // (we are looking for matches for Windows-specific samaccountname property). Search needs to be redone during the next
 
  44 // refactoring effort.
 
  48 * Auth_ldap class to authenticate users against an LDAP server (Windows AD, OpenLDAP, and others).
 
  49 * @package TimeTracker
 
  51 class Auth_ldap extends Auth {
 
  54   function __construct($params)
 
  56     $this->params = $params;
 
  57     if (isset($GLOBALS['smarty'])) {
 
  58       $GLOBALS['smarty']->assign('Auth_ldap_params', $this->params);
 
  62   function ldap_escape($str){
 
  63     $illegal = array("(", ")", "#");
 
  65     foreach ($illegal as $id => $char) {
 
  66       $legal[$id] = "\\".$char;
 
  68     $str = str_replace($illegal, $legal,$str); //replace them
 
  73    * Authenticate user against LDAP server.
 
  75    * @param string $login
 
  76    * @param string $password
 
  79   function authenticate($login, $password)
 
  81     // Special handling for admin@localhost - authenticate against db, not ldap.
 
  82     // It is a fallback mechanism when admin account in LDAP directory does not exist or is misconfigured.
 
  83     if ($login == 'admin@localhost') {
 
  84         import('auth.Auth_db');
 
  85         return Auth_db::authenticate($login, $password);
 
  88     if (!function_exists('ldap_bind')) {
 
  89       die ('php_ldap extension not loaded!');
 
  92     if (empty($this->params['server']) || empty($this->params['base_dn'])) {
 
  93       die('You must set server and base_dn in AUTH_MODULE_PARAMS in config.php');
 
  96     $member_of = @$this->params['member_of'];
 
  98     $lc = ldap_connect($this->params['server']);
 
 100     if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 102       echo '$lc='; var_dump($lc); echo '<br />';
 
 103       echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 106     if (!$lc) return false;
 
 108     ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, 3);
 
 109     ldap_set_option($lc, LDAP_OPT_REFERRALS, 0);
 
 110     if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 111       ldap_set_option($lc, LDAP_OPT_DEBUG_LEVEL, 7);
 
 114     // We need to handle Windows AD and OpenLDAP differently.
 
 115     if ($this->params['type'] != 'openldap') {
 
 117       // check if the user specified full login
 
 118       if (strpos($login, '@') === false) {
 
 119         // append default domain
 
 120         $login .= '@' . $this->params['default_domain'];
 
 124       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 125         echo '$login='; var_dump($login); echo '<br />';
 
 128       $lb = @ldap_bind($lc, $login, $password);
 
 130       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 131         echo '$lb='; var_dump($lb); echo '<br />';
 
 132         echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 143         $filter = 'samaccountname='.Auth_ldap::ldap_escape($login);
 
 144         $fields = array('samaccountname', 'mail', 'memberof', 'department', 'displayname', 'telephonenumber', 'primarygroupid');
 
 145         $sr = @ldap_search($lc, $this->params['base_dn'], $filter, $fields);
 
 147         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 148           echo '$sr='; var_dump($sr); echo '<br />';
 
 149           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 152         // if search failed it's likely that account is disabled
 
 158         $entries = @ldap_get_entries($lc, $sr);
 
 160         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 161           echo '$entries='; var_dump($entries); echo '<br />';
 
 162           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 165         if ($entries === false) {
 
 172         // extract group names from
 
 173         // assuming the groups are in format: CN=<group_name>,...
 
 174         for ($i = 0; $i < @$entries[0]['memberof']['count']; $i++) {
 
 175           $grp = $entries[0]['memberof'][$i];
 
 176           $grp_fields = explode(',', $grp);
 
 177           $groups[] = substr($grp_fields[0], 3);
 
 180         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 181           echo '$member_of'; var_dump($member_of); echo '<br />';
 
 184         // check for group membership
 
 185             foreach ($member_of as $check_grp) {
 
 186           if (!in_array($check_grp, $groups)) {
 
 195       return array('login' => $login, 'data' => $entries, 'member_of' => $groups);
 
 198       // Assuming OpenLDAP server.
 
 199       $login_oldap = 'uid='.$login.','.$this->params['base_dn'];
 
 201       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 202         echo '$login_oldap='; var_dump($login_oldap); echo '<br />';
 
 205       // check if the user specified full login
 
 206       if (strpos($login, '@') === false) {
 
 207         // append default domain
 
 208         $login .= '@' . $this->params['default_domain'];
 
 211       $lb = @ldap_bind($lc, $login_oldap, $password);
 
 213       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 214         echo '$lb='; var_dump($lb); echo '<br />';
 
 215         echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 226         $filter = 'samaccountname='.Auth_ldap::ldap_escape($login_oldap);
 
 227         $fields = array('samaccountname', 'mail', 'memberof', 'department', 'displayname', 'telephonenumber', 'primarygroupid');
 
 228         $sr = @ldap_search($lc, $this->params['base_dn'], $filter, $fields);
 
 230         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 231           echo '$sr='; var_dump($sr); echo '<br />';
 
 232           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 235         // if search failed it's likely that account is disabled
 
 241         $entries = @ldap_get_entries($lc, $sr);
 
 243         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 244           echo '$entries='; var_dump($entries); echo '<br />';
 
 245           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
 
 248         if ($entries === false) {
 
 255         // extract group names from
 
 256         // assuming the groups are in format: CN=<group_name>,...
 
 257         for ($i = 0; $i < @$entries[0]['memberof']['count']; $i++) {
 
 258           $grp = $entries[0]['memberof'][$i];
 
 259           $grp_fields = explode(',', $grp);
 
 260           $groups[] = substr($grp_fields[0], 3);
 
 263         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
 
 264           echo '$member_of'; var_dump($member_of); echo '<br />';
 
 267         // check for group membership
 
 268         foreach ($member_of as $check_grp) {
 
 269           if (!in_array($check_grp, $groups)) {
 
 278       return array('login' => $login, 'data' => $entries, 'member_of' => $groups);
 
 282   function isPasswordExternal() {