Skip to content

Commit

Permalink
Implemented LdapLoginModule
Browse files Browse the repository at this point in the history
    - 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
AW3i committed May 9, 2017
1 parent 227c7a3 commit d5cf66d
Show file tree
Hide file tree
Showing 2 changed files with 464 additions and 0 deletions.
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));
}
}
Loading

0 comments on commit d5cf66d

Please sign in to comment.