forked from appserver-io/appserver
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- This module is able to bind and authenticate to an OpenLdap server, either anonymously or with a user bind - The module can find the roles of the user authenticating and pass them on to the appserver authentication manager - Implmentation is loosely based on LdapExtLoginModule from picketbox
- Loading branch information
Showing
2 changed files
with
464 additions
and
0 deletions.
There are no files selected for viewing
394 changes: 394 additions & 0 deletions
394
src/AppserverIo/Appserver/ServletEngine/Security/Auth/Spi/LdapLoginModule.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,394 @@ | ||
<?php | ||
|
||
/** | ||
* AppserverIo\Appserver\ServletEngine\Security\Auth\Spi\LdapLoginModule.php | ||
* | ||
* NOTICE OF LICENSE | ||
* | ||
* This source file is subject to the Open Software License (OSL 3.0) | ||
* that is available through the world-wide-web at this URL: | ||
* http://opensource.org/licenses/osl-3.0.php | ||
} | ||
* | ||
* PHP version 5 | ||
* | ||
* @author Alexandros Weigl <a.weigl@techdivision.com> | ||
* @copyright 2017 TechDivision GmbH <info@appserver.io> | ||
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) | ||
* @link https://github.com/appserver-io/appserver | ||
* @link http://www.appserver.io | ||
*/ | ||
|
||
namespace AppserverIo\Appserver\ServletEngine\Security\Auth\Spi; | ||
|
||
use AppserverIo\Lang\String; | ||
use AppserverIo\Lang\Boolean; | ||
use AppserverIo\Collections\HashMap; | ||
use AppserverIo\Collections\MapInterface; | ||
use AppserverIo\Psr\Security\Auth\Subject; | ||
use AppserverIo\Psr\Security\Auth\Login\LoginException; | ||
use AppserverIo\Psr\Security\Auth\Login\FailedLoginException; | ||
use AppserverIo\Psr\Security\Auth\Callback\CallbackHandlerInterface; | ||
use AppserverIo\Appserver\ServletEngine\Security\SecurityException; | ||
use AppserverIo\Appserver\ServletEngine\Security\Utils\Util; | ||
use AppserverIo\Appserver\ServletEngine\Security\Utils\ParamKeys; | ||
use AppserverIo\Appserver\ServletEngine\Security\Utils\SharedStateKeys; | ||
use AppserverIo\Appserver\ServletEngine\RequestHandler; | ||
use AppserverIo\Appserver\ServletEngine\Security\SimpleGroup; | ||
|
||
/** | ||
* This class provides LDAP login functionality to an openldap server. | ||
* | ||
* @author Alexandros Weigl <a.weigl@techdivision.com> | ||
* @copyright 2017 TechDivision GmbH <info@appserver.io> | ||
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) | ||
* @link https://github.com/appserver-io/appserver | ||
* @link http://www.appserver.io | ||
*/ | ||
class LdapLoginmodule extends UsernamePasswordLoginModule | ||
{ | ||
|
||
/** | ||
* The LDAP url of the LDAP server | ||
* | ||
* @var string | ||
*/ | ||
protected $ldapUrl = null; | ||
|
||
/** | ||
* The LDAP port of the LDAP server | ||
* | ||
* @var string | ||
*/ | ||
protected $ldapPort = 389; | ||
|
||
/** | ||
* The LDAP start tls flag. Enables/disables tls requests to the LDAP server | ||
* | ||
* @var boolean | ||
*/ | ||
protected $ldapStartTls = null; | ||
|
||
/** | ||
* The LDAP servers base distinguished name | ||
* | ||
* @var string | ||
*/ | ||
protected $baseDN = null; | ||
|
||
/** | ||
* The administrator user DN with the permissions to search the LDAP directory. | ||
* | ||
* @var string | ||
*/ | ||
protected $bindDN = null; | ||
|
||
/** | ||
* The credential of the administrator user | ||
* | ||
* @var string | ||
*/ | ||
protected $bindCredential = null; | ||
|
||
/** | ||
* A search filter used to locate the context of the user to authenticate | ||
* The input username/userDN as obtained from the login module | ||
* callback will be substituted into the filter anywhere a "{0}" expression is seen. | ||
* A common example search filter is "(uid={0})". | ||
* | ||
* @var string | ||
*/ | ||
protected $baseFilter = null; | ||
|
||
/** | ||
* The fixed DN of the context to search for user roles. | ||
* | ||
* @var string | ||
*/ | ||
protected $rolesDN = null; | ||
|
||
/** | ||
* A search filter used to locate the roles associated with the authenticated user. | ||
* The input username/userDN as obtained from the login module callback | ||
* will be substituted into the filter anywhere a "{0}" expression is | ||
* seen. The authenticated userDN will be substituted into the filter anywhere a | ||
* "{1}" is seen. An example search filter that matches on the input username is: | ||
* "(memberUid={0})". An alternative that matches on the authenticated userDN is: | ||
* "(member={1})". | ||
* | ||
* @var string | ||
*/ | ||
protected $roleFilter = null; | ||
|
||
/** | ||
* Allow Anonymous Logins to OpenLDAP | ||
* | ||
* @var boolean | ||
*/ | ||
protected $allowEmptyPasswords = null; | ||
|
||
/** | ||
* A Hashmap storing the authenticating users roles | ||
* | ||
* @var mixed | ||
*/ | ||
protected $setsMap = null; | ||
|
||
/** | ||
* Initialize the login module. This stores the subject, callbackHandler and sharedState and options | ||
* for the login session. Subclasses should override if they need to process their own options. A call | ||
* to parent::initialize() must be made in the case of an override. | ||
* | ||
* The following parameters can by default be passed from the configuration. | ||
* | ||
* ldapUrl: The LDAP server to connect | ||
* ldapPort: The port which the LDAP server is running on | ||
* baseDN: The LDAP servers base distinguished name | ||
* bindDN: The administrator user DN with the permissions to search the LDAP directory. | ||
* bindCredential: The credential of the administrator user | ||
* baseFilter: A search filter used to locate the context of the user to authenticate | ||
* rolesDN: The fixed DN of the context to search for user roles. | ||
* rolFilter: A search filter used to locate the roles associated with the authenticated user. | ||
* ldapStartTls: The LDAP start tls flag. Enables/disables tls requests to the LDAP server | ||
* allowEmptyPasswords: Allow/disallow anonymous Logins to OpenLDAP | ||
* | ||
* | ||
* @param \AppserverIo\Psr\Security\Auth\Subject $subject The Subject to update after a successful login | ||
* @param \AppserverIo\Psr\Security\Auth\Callback\CallbackHandlerInterface $callbackHandler The callback handler that will be used to obtain the user identity and credentials | ||
* @param \AppserverIo\Collections\MapInterface $sharedState A map shared between all configured login module instances | ||
* @param \AppserverIo\Collections\MapInterface $params The parameters passed to the login module | ||
* | ||
* @return void | ||
*/ | ||
public function initialize(Subject $subject, CallbackHandlerInterface $callbackHandler, MapInterface $sharedState, MapInterface $params) | ||
{ | ||
|
||
// call the parent method | ||
parent::initialize($subject, $callbackHandler, $sharedState, $params); | ||
|
||
// initialize the hash encoding to use | ||
if ($params->exists(ParamKeys::URL)) { | ||
$this->ldapUrl = $params->get(ParamKeys::URL); | ||
} | ||
if ($params->exists(ParamKeys::PORT)) { | ||
$this->ldapPort = $params->get(ParamKeys::PORT); | ||
} | ||
if ($params->exists(ParamKeys::BASE_DN)) { | ||
$this->baseDN = $params->get(ParamKeys::BASE_DN); | ||
} | ||
if ($params->exists(ParamKeys::BIND_DN)) { | ||
$this->bindDN= $params->get(ParamKeys::BIND_DN); | ||
} | ||
if ($params->exists(ParamKeys::BIND_CREDENTIAL)) { | ||
$this->bindCredential = $params->get(ParamKeys::BIND_CREDENTIAL); | ||
} | ||
if ($params->exists(ParamKeys::BASE_FILTER)) { | ||
$this->baseFilter = $params->get(ParamKeys::BASE_FILTER); | ||
} | ||
if ($params->exists(ParamKeys::ROLES_DN)) { | ||
$this->rolesDN = $params->get(ParamKeys::ROLES_DN); | ||
} | ||
if ($params->exists(ParamKeys::ROLE_FILTER)) { | ||
$this->roleFilter = $params->get(ParamKeys::ROLE_FILTER); | ||
} | ||
if ($params->exists(ParamKeys::START_TLS)) { | ||
$this->ldapStartTls = $params->get(ParamKeys::START_TLS); | ||
} | ||
if ($params->exists(ParamKeys::ALLOW_EMPTY_PASSWORDS)) { | ||
$this->allowEmptyPasswords = $params->get(ParamKeys::ALLOW_EMPTY_PASSWORDS); | ||
} | ||
$this->setsMap = new HashMap(); | ||
} | ||
|
||
/** | ||
* Perform the authentication of username and password through LDAP. | ||
* | ||
* @return boolean TRUE when login has been successfull, else FALSE | ||
* @throws \AppserverIo\Psr\Security\Auth\Login\LoginException Is thrown if an error during login occured | ||
*/ | ||
public function login() | ||
{ | ||
$this->loginOk = false; | ||
|
||
// array containing the username and password from the user's input | ||
list ($name, $password) = $this->getUsernameAndPassword(); | ||
|
||
if ($name === null && $password === null) { | ||
$this->identity = $this->unauthenticatedIdentity; | ||
} | ||
|
||
if ($this->identity === null) { | ||
try { | ||
$this->identity = $this->createIdentity($name); | ||
} catch (\Exception $e) { | ||
throw new LoginException(sprintf('Failed to create principal: %s', $e->getMessage())); | ||
} | ||
} | ||
$ldap_connection = $this->ldapConnect(); | ||
if ($ldap_connection) { | ||
// Replace the placeholder with the actual username of the user | ||
$this->baseFilter = preg_replace('/\{0\}/', "$name", $this->baseFilter); | ||
|
||
$search = ldap_search($ldap_connection, $this->baseDN, $this->baseFilter); | ||
$entry = ldap_first_entry($ldap_connection, $search); | ||
$userDN = ldap_get_dn($ldap_connection, $entry); | ||
|
||
if (!(isset($userDN))) { | ||
throw new LoginException(sprintf('User not found in LDAP directory')); | ||
} | ||
} else { | ||
throw new LoginException(sprintf('Couldn\'t connect to LDAP server')); | ||
} | ||
|
||
//Bind the authenticating user to the LDAP directory | ||
$bind = ldap_bind($ldap_connection, $userDN, $password); | ||
if ($bind === false) { | ||
throw new LoginException(sprintf('Username or password wrong')); | ||
} | ||
|
||
// query whether or not password stacking has been activated | ||
if ($this->getUseFirstPass()) { | ||
// add the username and password to the shared state map | ||
$this->sharedState->add(SharedStateKeys::LOGIN_NAME, $name); | ||
$this->sharedState->add(SharedStateKeys::LOGIN_PASSWORD, $this->credential); | ||
} | ||
$this->rolesSearch($name); | ||
|
||
$this->loginOk = true; | ||
return true; | ||
} | ||
|
||
/** | ||
* Returns the password for the user from the sharedMap data. | ||
* | ||
* @return void | ||
*/ | ||
public function getUsersPassword() | ||
{ | ||
return null; | ||
} | ||
|
||
/** | ||
* Overridden by subclasses to return the Groups that correspond to the to the | ||
* role sets assigned to the user. Subclasses should create at least a Group | ||
* named "Roles" that contains the roles assigned to the user. | ||
* | ||
* @return array Array containing the sets of roles | ||
* @throws \AppserverIo\Psr\Security\Auth\Login\LoginException Is thrown if password can't be loaded | ||
*/ | ||
protected function getRoleSets() | ||
{ | ||
return $this->setsMap->toArray(); | ||
} | ||
|
||
/** | ||
* Adds a role to the setsMap | ||
* | ||
* @param string $groupName The name of the group | ||
* @param string $name The name of the role to be added to the group | ||
* @return void | ||
*/ | ||
protected function addRole($groupName, $name) | ||
{ | ||
if ($this->setsMap->exists($groupName) === false) { | ||
$group = new SimpleGroup(new String($groupName)); | ||
$this->setsMap->add($groupName, $group); | ||
} else { | ||
$group = $this->setsMap->get($groupName); | ||
} | ||
try { | ||
$group->addMember($this->createIdentity(new String($name))); | ||
} catch (\Exception $e) { | ||
} | ||
} | ||
|
||
/** | ||
* Extracts the common name from a Distinguished name | ||
* | ||
* @param string $dn The distinguished name of the authenticating user | ||
* @return array | ||
* | ||
*/ | ||
protected function extractCNFromDN($dn) | ||
{ | ||
$splitArray = explode(',', $dn); | ||
$keyValue = array(); | ||
foreach ($splitArray as $value) { | ||
$tempArray = explode('=', $value); | ||
$keyValue[$tempArray[0]] = array(); | ||
$keyValue[$tempArray[0]][] = $tempArray[1]; | ||
} | ||
|
||
return $keyValue['cn']; | ||
} | ||
|
||
/** | ||
* return's the authenticated user identity. | ||
* | ||
* @return \appserverio\psr\security\principalinterface the user identity | ||
*/ | ||
protected function getIdentity() | ||
{ | ||
return $this->identity; | ||
} | ||
|
||
/** | ||
* Creates a new connection to the ldap server, binds to the ldap server and returns the connection | ||
* | ||
* @return resource|false | ||
*/ | ||
protected function ldapConnect() | ||
{ | ||
|
||
$ldap_connection = ldap_connect($this->ldapUrl, $this->ldapPort); | ||
|
||
if ($ldap_connection) { | ||
if ($this->ldapStartTls === 'true') { | ||
ldap_start_tls($ldap_connection); | ||
} | ||
ldap_set_option($ldap_connection, LDAP_OPT_PROTOCOL_VERSION, 3); | ||
|
||
//anonymous login | ||
if ($this->allowEmptyPasswords === 'true') { | ||
$bind = ldap_bind($ldap_connection); | ||
} else { | ||
$bind = ldap_bind($ldap_connection, $this->bindDN, $this->bindCredential); | ||
} | ||
if (!$bind) { | ||
throw new LoginException('Bind to server failed'); | ||
} | ||
} else { | ||
return false; | ||
} | ||
return $ldap_connection; | ||
} | ||
|
||
/** | ||
* Search the authenticated user for his user groups/roles | ||
* The found roles are then added to the setsMap hashmap | ||
* | ||
* @param string $user the authenticated user | ||
* @param string $userDN the DN of the authenticated user | ||
* @return void | ||
*/ | ||
protected function rolesSearch($user, $userDN) | ||
{ | ||
if ($this->rolesDN === null || $this->roleFilter === null) { | ||
return; | ||
} | ||
|
||
$groupName = Util::DEFAULT_GROUP_NAME; | ||
$ldap_connection = $this->ldapConnect(); | ||
$this->roleFilter = preg_replace("/\{0\}/", "$user", $this->roleFilter); | ||
$this->roleFilter = preg_replace("/\{1\}/", "$userDN", $this->roleFilter); | ||
$search = ldap_search($ldap_connection, $this->rolesDN, $this->roleFilter); | ||
$entry = ldap_first_entry($ldap_connection, $search); | ||
do { | ||
$dn = ldap_get_dn($ldap_connection, $entry); | ||
$roleArray = $this->extractCNFromDN($dn); | ||
foreach ($roleArray as $role) { | ||
$this->addRole($groupName, $role); | ||
} | ||
} while ($entry = ldap_next_entry($ldap_connection, $entry)); | ||
} | ||
} |
Oops, something went wrong.