From 2570aa4d76a0cfeb64d6a8cda1ac9eafbedc8d83 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Thu, 5 Mar 2009 09:14:20 +0000
Subject: [PATCH] Add LDAP & LDAPMulti authentication module.

This adds two authentication modules which are meant to replace auth/login.php
and auth/login-ldapmulti.php.


git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@1372 44740490-163a-0410-bde0-09ae8108e29a
---
 config-templates/authsources.php           |  88 +++++++++++
 modules/ldap/default-enable                |   0
 modules/ldap/docs/ldap.txt                 | 170 +++++++++++++++++++++
 modules/ldap/lib/Auth/Source/LDAP.php      |  57 +++++++
 modules/ldap/lib/Auth/Source/LDAPMulti.php |  93 +++++++++++
 modules/ldap/lib/ConfigHelper.php          | 158 +++++++++++++++++++
 6 files changed, 566 insertions(+)
 create mode 100644 modules/ldap/default-enable
 create mode 100644 modules/ldap/docs/ldap.txt
 create mode 100644 modules/ldap/lib/Auth/Source/LDAP.php
 create mode 100644 modules/ldap/lib/Auth/Source/LDAPMulti.php
 create mode 100644 modules/ldap/lib/ConfigHelper.php

diff --git a/config-templates/authsources.php b/config-templates/authsources.php
index 2fb5aec93..7ca97b9cd 100644
--- a/config-templates/authsources.php
+++ b/config-templates/authsources.php
@@ -62,6 +62,94 @@ $config = array(
 		'secret' => 'xxxxxxxxxxxxxxxx',
 	),
         
+	/* Example of a LDAP authentication source. */
+	'example-ldap' => array(
+		'ldap:LDAP',
+
+		/* The hostname of the LDAP server. */
+		'hostname' => 'ldap.example.org',
+
+		/* Whether SSL/TLS should be used when contacting the LDAP server. */
+		'enable_tls' => FALSE,
+
+		/*
+		 * Which attributes should be retrieved from the LDAP server.
+		 * This can be an array of attribute names, or NULL, in which case
+		 * all attributes are fetched.
+		 */
+		'attributes' => NULL,
+
+		/*
+		 * The pattern which should be used to create the users DN given the username.
+		 * %username% in this pattern will be replaced with the users username.
+		 *
+		 * This option is not used if the search.enable option is set to TRUE.
+		 */
+		'dnpattern' => 'uid=%username%,ou=people,dc=example,dc=org',
+
+		/*
+		 * As an alternative to specifying a pattern for the users DN, it is possible to
+		 * search for the username in a set of attributes. This is enabled by this option.
+		 */
+		'search.enable' => FALSE,
+
+		/*
+		 * The DN which will be used as a base for the search.
+		 * This can be a single string, in which case only that DN is searched, or an
+		 * array of strings, in which case they will be searched in the order given.
+		 */
+		'search.base' => 'ou=people,dc=example,dc=org',
+
+		/*
+		 * The attribute(s) the username should match against.
+		 *
+		 * This is an array with one or more attribute names. Any of the attributes in
+		 * the array may match the value the username.
+		 */
+		'search.attributes' => array('uid', 'mail'),
+
+		/*
+		 * The username & password the simpleSAMLphp should bind to before searching. If
+		 * this is left as NULL, no bind will be performed before searching.
+		 */
+		'search.username' => NULL,
+		'search.password' => NULL,
+	),
+
+	/* Example of an LDAPMulti authentication source. */
+	'example-ldapmulti' => array(
+		'ldap:LDAPMulti',
+
+		/*
+		 * A list of available LDAP servers / user groups. The value of each element is
+		 * an array in the same format as an LDAP authentication source.
+		 */
+		'employees' => array(
+			/*
+			 * A short name/description for this group. Will be shown in a dropdown list
+			 * when the user logs on.
+			 *
+			 * This option can be a string or an array with language => text mappings.
+			 */
+			'description' => 'Employees',
+
+			/*
+			 * The rest of the options are the same as those available for
+			 * the LDAP authentication source.
+			 */
+			'hostname' => 'ldap.employees.example.org',
+			'dnpattern' => 'uid=%username%,ou=employees,dc=example,dc=org',
+		),
+
+		'students' => array(
+			'description' => 'Students',
+
+			'hostname' => 'ldap.students.example.org',
+			'dnpattern' => 'uid=%username%,ou=students,dc=example,dc=org',
+		),
+
+	),
+
 );
 
 ?>
\ No newline at end of file
diff --git a/modules/ldap/default-enable b/modules/ldap/default-enable
new file mode 100644
index 000000000..e69de29bb
diff --git a/modules/ldap/docs/ldap.txt b/modules/ldap/docs/ldap.txt
new file mode 100644
index 000000000..9ed6e6f8d
--- /dev/null
+++ b/modules/ldap/docs/ldap.txt
@@ -0,0 +1,170 @@
+LDAP module
+===========
+
+The LDAP module provides a method for authenticating users against a
+LDAP server. There are two seperate authentication modules:
+
+
+`ldap:LDAP`
+: Authenticate the user against a single LDAP server.
+
+`ldap:LDAPMulti`
+: Allow the user to chose one LDAP server to authenticate against.
+
+
+`ldap:LDAP`
+-----------
+
+This module is used when you have an organization with a single LDAP
+server with all the users. To create a LDAP authentication source, open
+`config/authsources.php` in a text editor, and add an entry for the
+authentication source:
+
+	'example-ldap' => array(
+		'ldap:LDAP',
+
+		/* The hostname of the LDAP server. */
+		'hostname' => 'ldap.example.org',
+
+		/* Whether SSL/TLS should be used when contacting the LDAP server. */
+		'enable_tls' => FALSE,
+
+		/*
+		 * Which attributes should be retrieved from the LDAP server.
+		 * This can be an array of attribute names, or NULL, in which case
+		 * all attributes are fetched.
+		 */
+		'attributes' => NULL,
+
+		/*
+		 * The pattern which should be used to create the users DN given the username.
+		 * %username% in this pattern will be replaced with the users username.
+		 *
+		 * This option is not used if the search.enable option is set to TRUE.
+		 */
+		'dnpattern' => 'uid=%username%,ou=people,dc=example,dc=org',
+
+		/*
+		 * As an alternative to specifying a pattern for the users DN, it is possible to
+		 * search for the username in a set of attributes. This is enabled by this option.
+		 */
+		'search.enable' => FALSE,
+
+		/*
+		 * The DN which will be used as a base for the search.
+		 * This can be a single string, in which case only that DN is searched, or an
+		 * array of strings, in which case they will be searched in the order given.
+		 */
+		'search.base' => 'ou=people,dc=example,dc=org',
+
+		/*
+		 * The attribute(s) the username should match against.
+		 *
+		 * This is an array with one or more attribute names. Any of the attributes in
+		 * the array may match the value the username.
+		 */
+		'search.attributes' => array('uid', 'mail'),
+
+		/*
+		 * The username & password the simpleSAMLphp should bind to before searching. If
+		 * this is left as NULL, no bind will be performed before searching.
+		 */
+		'search.username' => NULL,
+		'search.password' => NULL,
+	),
+
+
+You should update the name of this authentication source
+(`example-ldap`) to have a name which makes sense to your organization.
+You also need to update the `hostname` and `dnpattern` options. The
+`hostname` should be the hostname of your LDAP server, and the
+`dnpattern` should be a pattern which can be used to generate the `dn`
+of a user with a given username.
+
+All other options have default values, and are not required.
+
+### Searching for a user ###
+
+Sometimes you cannot generate the users `dn` from the username, or you
+may want to allow the user to authenticate with for example their email
+address as the username. In this case, you can configure the LDAP
+module to search for the users `dn` by searching for the username in
+one or more attributes.
+
+To enable searching, you must set the `search.enable` option to `TRUE`.
+You must then configure the `search.base` and the `search.attributes`
+options. The `search.base`-option must be the `dn` which should be used
+as the base/root of the search. The `search.attributes`-option is an
+array with attributes the username should be matched against.
+
+The `dnpattern` option will not be used if searching is enabled.
+
+Some LDAP servers may require authentication before a search can be
+performed. In this case, you should configure the `search.username`
+and `search.password` options. The `search.username` option is a `dn`
+which can be used to perform a search, and the `search.password` option
+is the password for that `dn`.
+
+
+
+`ldap:LDAPMulti`
+----------------
+
+This module can be used if your organization has seperate groups with
+seperate LDAP servers or seperate LDAP configurations. To use this
+authentication module, open `config/authsources.php` in a text editor,
+and add an entry which uses this module:
+
+	'example-ldapmulti' => array(
+		'ldap:LDAPMulti',
+
+		/*
+		 * A list of available LDAP servers. The index is only an identifier,
+		 * and can be any string. The value of each element is an array in the
+		 * same format as an LDAP authentication source.
+		 */
+		'employees' => array(
+			/*
+			 * A short name/description for this group. Will be shown in a dropdown list
+			 * when the user logs on.
+			 *
+			 * This option can be a string or an array with language => text mappings.
+			 */
+			'description' => 'Employees',
+
+			/*
+			 * The rest of the options are the same as those available for
+			 * the LDAP authentication source.
+			 */
+			'hostname' => 'ldap.employees.example.org',
+			'dnpattern' => 'uid=%username%,ou=employees,dc=example,dc=org',
+		),
+
+		'students' => array(
+			'description' => 'Students',
+
+			'hostname' => 'ldap.students.example.org',
+			'dnpattern' => 'uid=%username%,ou=students,dc=example,dc=org',
+		),
+
+	),
+
+The name of the authentication source (`example-ldapmulti`) should be
+changed to something that makes sense for your organization. Each entry
+in the configuration represents the configuration for one group of
+users. The `description`-option in each group is the name of the group,
+and will be shown to the user in a dropdown list on the login page.
+
+The `description`-option can also be an array with descriptions in
+different languages:
+
+	'description' => array(
+		'en' => 'Employees',
+		'no' => 'Ansatte',
+	),
+
+All options from the `ldap:LDAP` configuration can be used in each
+group, and you should refer to the documentation for that module for
+more information about available options.
+
+
diff --git a/modules/ldap/lib/Auth/Source/LDAP.php b/modules/ldap/lib/Auth/Source/LDAP.php
new file mode 100644
index 000000000..f49ff333c
--- /dev/null
+++ b/modules/ldap/lib/Auth/Source/LDAP.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * LDAP authentication source.
+ *
+ * See the ldap-entry in config-templates/authsources.php for information about
+ * configuration of this authentication source.
+ *
+ * This class is based on www/auth/login.php.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_ldap_Auth_Source_LDAP extends sspmod_core_Auth_UserPassBase {
+
+	/**
+	 * A LDAP configuration object.
+	 */
+	private $ldapConfig;
+
+
+	/**
+	 * Constructor for this authentication source.
+	 *
+	 * @param array $info  Information about this authentication source.
+	 * @param array $config  Configuration.
+	 */
+	public function __construct($info, $config) {
+		assert('is_array($info)');
+		assert('is_array($config)');
+
+		/* Call the parent constructor first, as required by the interface. */
+		parent::__construct($info, $config);
+
+		$this->ldapConfig = new sspmod_ldap_ConfigHelper($config,
+			'Authentication source ' . var_export($this->authId, TRUE));
+	}
+
+
+	/**
+	 * Attempt to log in using the given username and password.
+	 *
+	 * @param string $username  The username the user wrote.
+	 * @param string $password  The password the user wrote.
+	 * @return array  Associative array with the users attributes.
+	 */
+	protected function login($username, $password) {
+		assert('is_string($username)');
+		assert('is_string($password)');
+
+		return $this->ldapConfig->login($username, $password);
+	}
+
+}
+
+
+?>
\ No newline at end of file
diff --git a/modules/ldap/lib/Auth/Source/LDAPMulti.php b/modules/ldap/lib/Auth/Source/LDAPMulti.php
new file mode 100644
index 000000000..34f3fb425
--- /dev/null
+++ b/modules/ldap/lib/Auth/Source/LDAPMulti.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * LDAP authentication source.
+ *
+ * See the ldap-entry in config-templates/authsources.php for information about
+ * configuration of this authentication source.
+ *
+ * This class is based on www/auth/login.php.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_ldap_Auth_Source_LDAPMulti extends sspmod_core_Auth_UserPassOrgBase {
+
+	/**
+	 * An array with descriptions for organizations.
+	 */
+	private $orgs;
+
+	/**
+	 * An array of organization IDs to LDAP configuration objects.
+	 */
+	private $ldapOrgs;
+
+
+	/**
+	 * Constructor for this authentication source.
+	 *
+	 * @param array $info  Information about this authentication source.
+	 * @param array $config  Configuration.
+	 */
+	public function __construct($info, $config) {
+		assert('is_array($info)');
+		assert('is_array($config)');
+
+		/* Call the parent constructor first, as required by the interface. */
+		parent::__construct($info, $config);
+
+		$this->orgs = array();
+		$this->ldapOrgs = array();
+		foreach ($config as $orgId => $orgCfg) {
+			if (array_key_exists('description', $orgCfg)) {
+				$this->orgs[$orgId] = $orgCfg['description'];
+			} else {
+				$this->orgs[$orgId] = $orgId;
+			}
+
+			$orgCfg = new sspmod_ldap_ConfigHelper($orgCfg,
+				'Authentication source ' . var_export($this->authId, TRUE) .
+				', organization ' . var_export($orgId, TRUE));
+			$this->ldapOrgs[$orgId] = $orgCfg;
+		}
+	}
+
+
+	/**
+	 * Attempt to log in using the given username and password.
+	 *
+	 * @param string $username  The username the user wrote.
+	 * @param string $password  The password the user wrote.
+	 * @param string $org  The organization the user chose.
+	 * @return array  Associative array with the users attributes.
+	 */
+	protected function login($username, $password, $org) {
+		assert('is_string($username)');
+		assert('is_string($password)');
+
+		if (!array_key_exists($org, $this->ldapOrgs)) {
+			/* The user has selected an organization which doesn't exist anymore. */
+			SimpleSAML_Logger::warning('Authentication source ' . var_export($this->authId, TRUE) .
+				': Organization seems to have disappeared while the user logged in.' .
+				' Organization was ' . var_export($org, TRUE));
+			throw new SimpleSAML_Error_Error('WRONGUSERPASS');
+		}
+
+		return $this->ldapOrgs[$org]->login($username, $password);
+	}
+
+
+	/**
+	 * Retrieve list of organizations.
+	 *
+	 * @return array  Associative array with the organizations.
+	 */
+	protected function getOrganizations() {
+		return $this->orgs;
+	}
+
+}
+
+
+?>
\ No newline at end of file
diff --git a/modules/ldap/lib/ConfigHelper.php b/modules/ldap/lib/ConfigHelper.php
new file mode 100644
index 000000000..59e43f455
--- /dev/null
+++ b/modules/ldap/lib/ConfigHelper.php
@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * LDAP authentication source configuration parser.
+ *
+ * See the ldap-entry in config-templates/authsources.php for information about
+ * configuration of these options.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_ldap_ConfigHelper {
+
+
+	/**
+	 * String with the location of this configuration.
+	 * Used for error reporting.
+	 */
+	private $location;
+
+
+	/**
+	 * The hostname of the LDAP server.
+	 */
+	private $hostname;
+
+
+	/**
+	 * Whether we should use TLS/SSL when contacting the LDAP server.
+	 */
+	private $enableTLS;
+
+
+	/**
+	 * Whether we need to search for the users DN.
+	 */
+	private $searchEnable;
+
+
+	/**
+	 * The username we should bind with before we can search for the user.
+	 */
+	private $searchUsername;
+
+
+	/**
+	 * The password we should bind with before we can search for the user.
+	 */
+	private $searchPassword;
+
+
+	/**
+	 * Array with the base DN(s) for the search.
+	 */
+	private $searchBase;
+
+
+	/**
+	 * The attributes which should match the username.
+	 */
+	private $searchAttributes;
+
+
+	/**
+	 * The DN pattern we should use to create the DN from the username.
+	 */
+	private $dnPattern;
+
+
+	/**
+	 * The attributes we should fetch. Can be NULL in which case we will fetch all attributes.
+	 */
+	private $attributes;
+
+
+
+	/**
+	 * Constructor for this configuration parser.
+	 *
+	 * @param array $config  Configuration.
+	 * @param string $location  The location of this configuration. Used for error reporting.
+	 */
+	public function __construct($config, $location) {
+		assert('is_array($config)');
+		assert('is_string($location)');
+
+		$this->location = $location;
+
+		/* Parse configuration. */
+		$config = SimpleSAML_Configuration::loadFromArray($config, $location);
+
+		$this->hostname = $config->getString('hostname');
+		$this->enableTLS = $config->getBoolean('enable_tls', FALSE);
+		$this->searchEnable = $config->getBoolean('search.enable', FALSE);
+
+		if ($this->searchEnable) {
+			$this->searchUsername = $config->getString('search.username', NULL);
+			if ($this->searchUsername !== NULL) {
+				$this->searchPassword = $config->getString('search.password');
+			}
+
+			$this->searchBase = $config->getString('search.base');
+			$this->searchAttributes = $config->getArray('search.attributes');
+
+		} else {
+			$this->dnPattern = $config->getString('dnpattern');
+		}
+
+		$this->attributes = $config->getArray('attributes', NULL);
+	}
+
+
+	/**
+	 * Attempt to log in using the given username and password.
+	 *
+	 * Will throw a SimpleSAML_Error_Error('WRONGUSERPASS') if the username or password is wrong.
+	 * If there is a configuration problem, an Exception will be thrown.
+	 *
+	 * @param string $username  The username the user wrote.
+	 * @param string $password  The password the user wrote.
+	 * @return array  Associative array with the users attributes.
+	 */
+	public function login($username, $password) {
+		assert('is_string($username)');
+		assert('is_string($password)');
+
+		$ldap = new SimpleSAML_Auth_LDAP($this->hostname, $this->enableTLS);
+
+		if (!$this->searchEnable) {
+			$ldapusername = addcslashes($username, ',+"\\<>;*');
+			$dn = str_replace('%username%', $ldapusername, $this->dnPattern);
+		} else {
+			if ($this->searchUsername !== NULL) {
+				if(!$ldap->bind($this->searchUsername, $this->searchPassword)) {
+					throw new Exception('Error authenticating using search username & password.');
+				}
+			}
+
+			$dn = $ldap->searchfordn($this->searchBase, $this->searchAttributes, $username, TRUE);
+			if ($dn === NULL) {
+				/* User not found with search. */
+				SimpleSAML_Logger::info($this->location . ': Unable to find users DN. username=\'' . $username . '\'');
+				throw new SimpleSAML_Error_Error('WRONGUSERPASS');
+			}
+		}
+
+		if (!$ldap->bind($dn, $password)) {
+			SimpleSAML_Logger::info($this->location . ': '. $username . ' failed to authenticate. DN=' . $dn);
+			throw new SimpleSAML_Error_Error('WRONGUSERPASS');
+		}
+
+		return $ldap->getAttributes($dn, $this->attributes);
+	}
+
+}
+
+
+?>
\ No newline at end of file
-- 
GitLab