How to connect Symfony2 with IPBrick LDAP using KnpUGuard

After posting on Twitter my frustrations on trying  to connect Symfony2 to a LDAP server, I got a tweet back from Ryan Weaver pointing me at https://knpuniversity.com/screencast/guard. After a few hours of tinkering i had managed to achieve integration with a LDAP server based on a IPBrick communications server. IPBrick has a extremely easy interface to manage a ton of LDAP users and  groups. No importing of LDIF files and weird queries etc.

I can now actually get the LDAP server to do its job and authenticate people.

Until  KnpuGuard, symfony was actually getting in the way  (Yes I know it is a perceived complexity, it is actually very simple ??) insisting that it authenticate users directly , even if i could figure out how to get it to pull the users out of the LDAP server.

I’m going to show you how *I* did it.

Please note:
I’m not a developer by trade, I’m a Infrastructure/Entrepreneur/Hacker type guy. Don’t start fitting with rage if I don’t use the correct Dependency Injections methods, and Symfony services and factories, classfull form objects and nonce’s and blah blah and other associated computer science “best practices” which don’t actually help you make any money.

All this “blah” is one of the key reasons why LDAP integration in symfony2 is so hard. KnpuGuard makes it a lot easier. I don’t need any local entities in symfony2, all the adds moves and changes will be performed by the IPBrick server LDAP GUI.

This just shows you how to get over this first hurdle. The rest is up to you.

I created a bundle called Enginebundle and created a Security directory and put the following files in it. These I got from the instructions on the KnpuGuard documentation. This is a very high level overview. But I’m sure it will give you enough clues to help you do the same.

The real power is used in the LdapUserprovider.php. This tries to login to the LDAP server using the credentials supplied by the user. If the details are correct, it pulls roles (AKA Groups), email address, SIP address and even photograph from LDAP server and populates the User class.

I now can successfully integrate LDAP by focusing on 4 key files.

  1. The form Authentication class
  2. The User class to hold user details taken from LDAP
  3. A LDAP Class that has all the methods to do LDAP stuff
  4. The User provider which use’s my LDAP class to popular the actual symfony class

The rest of the application development is business as usual for symfony2

User Login Form against LDAP

User Login Form against LDAP

 

Successful Login, Photo and other details pulled from LDAP

Successful Login, Photo and other details pulled from LDAP

LdapUser.php the new User class to be used by Symfony2

// LdapUser.php, to hold the user details and filled by LDAP
namespace Trensys\EngineBundle\Security;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class LdapUser implements UserInterface, EquatableInterface {

    private $username;
    private $email;
    private $photo;
    private $sipaccount;
    private $uid;
    private $password;
    private $salt; // Not really used by LDAP, would be nice !
    private $roles;
    private $displayname;
    private $description;
    private $status;
    private $mailhost;

    public function __construct($username, $password, $salt, array $roles) {
        $this->username = $username;
        $this->password = $password;
        $this->salt = $salt;
        $this->roles = $roles;
    }

    public function setMailhost($mailhost){
        $this->mailhost = $mailhost;
    }
    
    public function getMailhost(){
        return $this->mailhost;
    }
    
    public function setStatus($status) {
        $this->status = $status;
    }

    public function getStatus() {
        return $this->status;
    }

    public function setUid($uid) {
        $this->uid = $uid;
    }

    public function getUid() {
        return $this->uid;
    }

    public function setSipaccount($sipaccount) {
        $this->sipaccount = $sipaccount;
    }

    public function getSipaccount() {

        return $this->sipaccount;
    }

    public function setPassword() {
   // Hack to remove LDAP password once user has been found.
   // is also used to find the user and attempt Login
    }

    public function setUsername($username) {
        $this->username = $username;
    }

    function setDisplayname($displayname) {
        $this->displayname = $displayname;
    }

    function getDisplayname() {
        return $this->displayname;
    }

    function setDescription($description) {
        $this->description = $description;
    }

    function getDescription() {
        return $this->description;
    }

    function setPhoto($photo) {
        $this->photo = $photo;
    }

    function getPhoto() {
        return $this->photo;
    }

    public function setEmail($email) {
        $this->email = $email;
    }

    function getEmail() {
        return $this->email;
    }

    public function getRoles() {
        return $this->roles;
    }

    public function getPassword() {
        return $this->password;
    }

    public function getSalt() {
        return $this->salt;
    }

    public function getUsername() {
        return $this->username;
    }

    public function eraseCredentials() {
        
    }

    public function isEqualTo(UserInterface $user) {
        if (!$user instanceof WebserviceUser) {
            return false;
        }

        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->salt !== $user->getSalt()) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }

}

Now we have the user we need to create the LdapUserProvider.php which is called by Guard, and we also need to create class (ipbrickLDAP.php) that the provider can use to do all the LDAP query stuff, all in one place.

LdapUserprovider.php, which is a service, and referenced in security.yml as a provider

namespace Trensys\EngineBundle\Security;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

class LdapUserProvider implements UserProviderInterface 
{ 
    private $user_ldap_password;
    private $ipbrickLdap;
    
    public function setLdapPassword($password){
        $this->user_ldap_password = $password;
    }
    
    public function loadUserByUsername($username)
    {
        // Will import this into parameters.yml at one stage
        
        $password = $this->user_ldap_password;
        $ldaphost = "ipbrick.ftw.co.uk";
        $ldapport = 389;
        $basedsn = "dc=ftw,dc=co,dc=uk"; 
        $this->ipbrickLdap = new ipbrickLdap($ldaphost, $ldapport, $basedsn);
        $ldapsearch = $this->ipbrickLdap->Login($username, $password);
  
$defaultPhoto="paste base64 encode of default jpeg here";
        if ($ldapsearch) {
            
            $user = new LdapUser($username, $password, "notused", $ldapsearch['roles']);
            // Map the fields from the LDAP use to the symfony user

            $user->setPassword($password); // Dont forget to hash this somehow
            $user->setEmail($ldapsearch['email']);
            $user->setUsername($username);
            $user->setDisplayname($ldapsearch['displayname']);
            if( isset($ldapsearch['otherdata'][0]['jpegphoto'][0])){
                $user->setPhoto($ldapsearch['otherdata'][0]['jpegphoto'][0]);
            }else{
                $user->setPhoto($defaultPhoto);
            }
             if( isset($ldapsearch['otherdata'][0]['description'][0]) ){
                $user->setDescription($ldapsearch['otherdata'][0]['description'][0]);
            }else{
                $user->setDescription("You have a number not a name: ".$ldapsearch['otherdata'][0]['uidnumber'][0]);
            }
            
            $user->setStatus($ldapsearch['otherdata'][0]['accountstatus'][0]);
            $user->setUid($ldapsearch['otherdata'][0]['uidnumber'][0]);
            $user->setSipaccount($ldapsearch['otherdata'][0]['mail'][0]);
            $user->setMailhost($ldapsearch['otherdata'][0]['mailhost'][0]);

            return $user;
        }
        // Throw an Exception, Either the user can not be found in the LDAP OR
        // The user could not login, SO lets just say that the user can not be 
        // found until he supplies the right username and password :-)
        throw new UsernameNotFoundException(
            sprintf('Sorry these details are not correct.', $username)
        );
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof LdapUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }
       // return $this->loadUserByUsername($user->getUsername());
        return $user;
    }

    public function supportsClass($class)
    {
        return $class === 'Trensys\EngineBundle\Security\LdapUser';
    }
}

ipbrickLDAP.php Does all the LDAP Stuff, used by the provider

namespace Trensys\EngineBundle\Security;

class ipbrickLdap {

    var $ldaphost;
    var $ldapport;
    var $ldapconn;
    var $basedn;
    var $domain;
    var $errormessage;
    var $username;

    public function __construct( $ldaphost,$ldapport, $basedsn) {
     

        $this->ldaphost = $ldaphost;
        $this->ldapport = $ldapport;
        $this->basedn   = $basedsn;
    }

    /*****************************************************************
     * FetchAllUsers()
     *****************************************************************/

    function FetchAllUsers() {
        $this->errormessage = null;
        $this->ldapconn = ldap_connect($this->ldaphost, $this->ldapport);

        if (!$this->ldapconn) {
            $this->errormessage = "Error: FetchAllUsers() could not get a connection to the LDAP Server";
            return FALSE;
        }
        
        $usersBase = "ou=Users,".$this->basedsn;
        $result = ldap_search($this->ldapconn, $usersBase, "(objectClass=*)");
        $userdata = ldap_get_entries($this->ldapconn, $result);

        ldap_close($this->ldapconn);
        return $userdata;
    }

    /*****************************************************************
     * FetchAllGroups()
     *****************************************************************/

    function FetchAllGroups() {
        $this->errormessage = null;
        $this->ldapconn = ldap_connect($this->ldaphost, $this->ldapport);

        if (!$this->ldapconn) {
            $this->errormessage = "Error: FetchAllGroups() could not get a connection to the LDAP Server";
            return FALSE;
        }
        
        $GroupsBase = "ou=Groups,".$this->basedn;
        $result = ldap_search($this->ldapconn, $GroupsBase, "(objectClass=*)");
        $data = ldap_get_entries($this->ldapconn, $result);
        ldap_close($this->ldapconn);
        
        return $data;
    }

    /*****************************************************************
     * FetchUserMembershipGroups()
     *****************************************************************/

    function FetchUserMembershipGroups($username) {
        $this->errormessage = null;
        // Calling user must already be logged in $this->ldapconn

        $GroupsBase = "ou=Groups,".$this->basedn;
        $result = ldap_search($this->ldapconn, $GroupsBase, "memberUid=$username");

        if(! $result){
            $this->errormessage = ldap_error($this->ldapconn);
            return FALSE;
        }

        $groupdata = ldap_get_entries($this->ldapconn, $result);
        return $groupdata;
    }
    /*****************************************************************
     * getRoles()
     *****************************************************************/
    function getRoles() {
        $groupdata = $this->FetchUserMembershipGroups($this->username);
        $groups = array();
         foreach($groupdata as $group){
             if ($group['cn'][0]){
             $groups[] = strtoupper ("ROLE_".$group['cn'][0]);
             }
         }
         return $groups;
    }

    /*****************************************************************
     * CheckGroupMembership()
     *****************************************************************/

    function CheckGroupMembership() {
        $this->errormessage = null;
        $this->ldapconn = ldap_connect($this->ldaphost, $this->ldapport);

        if (!$this->ldapconn) {
            $this->errormessage = ldap_error($this->ldapconn);
            return FALSE;
        }
       
        $GroupsBase = "ou=Groups,".$this->basedn;
        $result = ldap_search($this->ldapconn, $GroupsBase, "cn=Contractors");
        $data = ldap_get_entries($this->ldapconn, $result);
        ldap_close($this->ldapconn);
        return $data;
    }

    /*****************************************************************
     * FetchEntities()
     *****************************************************************/

    function FetchEntities($username, $password) {
        $this->errormessage = null;
        $dnusername = "uid=" . $username . ",ou=Users," . $this->basedn;
        $this->ldapconn = ldap_connect($this->ldaphost, $this->ldapport);

        if (!$this->ldapconn) {
            $this->errormessage = ldap_error($this->ldapconn);
            return FALSE;
        }

        $bindp = ldap_bind($this->ldapconn, $dnusername, $password);

        if ($bindp == false) {
           $this->errormessage = ldap_error($this->ldapconn);
            return FALSE;
        }
        $GroupsBase = "ou=Contacts,".$this->basedn;
        $result = ldap_search($this->ldapconn, $GroupsBase, "uid=*.0");
        $data = ldap_get_entries($this->ldapconn, $result);
        ldap_close($this->ldapconn);
        return $data;
    }

    /****************************************************************
     * CheckuserLogin()
     *****************************************************************/

    function CheckUserLogin($username, $password) {
        $this->errormessage = null;
        $dnusername = "uid=" . $username . ",ou=Users," . $this->basedn;

        $this->ldapconn = ldap_connect($this->ldaphost, $this->ldapport);
        if (!$this->ldapconn) {
            $this->errormessage = ldap_error($this->ldapconn);
            return FALSE;
        }
        $bindp = ldap_bind($this->ldapconn, $dnusername, $password);
        if ($bindp == false) {
            $this->errormessage = ldap_error($this->ldapconn);
            return FALSE;
        }
        return $bindp;
    }

    /*****************************************************************
     * Login()
     *****************************************************************/

    function Login($username, $password) {
        $this->errormessage = null;
        $dnusername = "uid=" . $username . ",ou=Users," . $this->basedn;

        $this->ldapconn = @ldap_connect($this->ldaphost, $this->ldapport);
        if (!$this->ldapconn) {
            $this->errormessage = ldap_error($this->ldapconn);
            return false;
        }

        $bindp = @ldap_bind($this->ldapconn, $dnusername, $password);

        if ($bindp == false) {
            $this->errormessage = ldap_error($this->ldapconn);
            return false;
        }
        
        $this->username = $username;
        $usersBase = "ou=Users,".$this->basedn;
        $result = ldap_search($this->ldapconn, $usersBase, "uid=$username");
        $userdata = ldap_get_entries($this->ldapconn, $result);
        $displayname = $userdata[0]['displayname'][0];
        $mail = $userdata[0]['mail'][0];
        $roles = $this->getRoles();

       // $user = new IpbrickldapUser($username, $password, NULL, $roles, $displayname,$mail );

        ldap_close($this->ldapconn);
        return array('username'=> $username,
                     'displayname' => $displayname,
                     'email'=> $mail,
                     'roles'=> $roles,
                     'otherdata'=> $userdata);
    }

}

Ok Now we need the FormLoginAuthenticator.php which will process the login form when the users attempts to login.

FormLoginAuthenticator.php which is a service, and referenced in security.yml as a knpuGuard Authenticator

namespace Trensys\EngineBundle\Security;

use KnpU\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class FormLoginAuthenticator extends AbstractFormLoginAuthenticator {

    // The symfony Police will flame you and send you to a skinny jeaned, mini driving
    // lumber jacked shirt, Starbucks drinking bearded hell.
    // Injecting the whole container causes tears in the space time continuum, You have been warned ! 

    private $container;

    public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container) {
        $this->container = $container;
    }

    // -------------------------------------------------------------------------

    public function getCredentials(Request $request) {
        //Fetch the submitted credentials from the login form
        //We could also include a nonce validation here to be extra secure

        if ($request->getPathInfo() != '/login_check') {
            return;
        }

        $username = $request->request->get('_username');
        $session = $request->getSession();
        $session->set(\Symfony\Component\Security\Core\Security::LAST_USERNAME, $username);

        $password = $request->request->get('_password');

        return array(
            'username' => $username,
            'password' => $password
        );
    }

    public function getUser($credentials, UserProviderInterface $userProvider) {
        //If we are here then we have some credentials. Here we are supposed to fetch
        // a user by the username supplied in the login form, However we do not want
        // a local user datbase in symfony, So what we do
        // (1) generate a fake non persistent one
        // (2) read the  ldap database using a LDAP Query and generate the user if it exits
        // and is enabled.
        //
        // Here we either handback a user or we don't (UserInterface Object)

        /*
         * A) If you return some User object (using whatever method you want) - then you'll continue on to 
         * checkCredentials().
         * B) If you return null or throw any Symfony\Component\Security\Core\Exception\AuthenticationException, 
         * authentication will fail and the user will be redirected back to the login page: see getLoginUrl().
         */

        $username = $credentials['username'];
        $password = $credentials['password'];
        $userProvider->setLdapPassword($password);
        return $userProvider->loadUserByUsername($username);
    }

    public function checkCredentials($credentials, UserInterface $user) {

        if(!$user->getStatus() == 'active'){
            throw new BadCredentialsException();
        }
        
    }

    protected function getLoginUrl() {
        return $this->container->get('router')->generate('trensys_engine_security_login');
    }

    protected function getDefaultSuccessRedirectUrl() {
        return $this->container->get('router')->generate('trensys_engine_security_welcome');
    }

}

Now we have all the required files, we need to do some plumbing to get all this stuff hooked up in Symfony.

Configure services.xml

services:
#    service_name:
#        class: AppBundle\Directory\ClassName
#        arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
     app.form_login_authenticator:
        class: Trensys\EngineBundle\Security\FormLoginAuthenticator
        arguments: ["@service_container"]
     
     ldap_user_provider:
         class: Trensys\EngineBundle\Security\LdapUserProvider

Configure Security.yml

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:

    encoders:
        # Our user class and the algorithm we'll use to encode passwords
        # http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
        AppBundle\Entity\User: bcrypt
        
    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        ldap_service:
            id: ldap_user_provider
        

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        # Before production set some sensible firewall rules
        main:
            anonymous: ~
            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html
            knpu_guard:
                authenticators:
                    - app.form_login_authenticator

With the plumbing done we now need a controller and a view to display the login form, and present the form again if the user fails to login (Don’t forget to review security.yml to suit your firewall needs)

Security Controller

namespace Trensys\EngineBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class SecurityController extends Controller {

    /**
     * @Route("/login")
     * @Template()
     */
    public function loginAction() {

       $helper = $this->get('security.authentication_utils');

        return array('last_username' => $helper->getLastUsername(),
            'error' => $helper->getLastAuthenticationError() );
    }
    
    /**
     * @Route("/logout")
     * 
     */
    public function logoutAction(){
        $this->get('security.token_storage')->setToken(null);
        $this->get('request')->getSession()->invalidate();
        $url= $this->generateUrl('trensys_engine_security_login');
       return  $this->redirect($url); 
    }
    /**
     * @Route("/login_check")
     * @Template()
     */
    public function loginCheckAction() {
        // will never be executed
        die('I should not have got here !');
        return array(
                // ...
        );
    }
    
    /**
     * @Route("/welcomelogin")
     * @Template()
     */
    public function welcomeAction() {
        
        $user = $this->getUser();
        
        return array('displayname'=> $user->getDisplayname());
    }

}

And here is our form/view. I will leave you to turn it into a form object with nonces etc.

login.html.twig based on a Bootstrap theme

< !DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Login</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/>
<!-- Bootstrap 3.3.4 -->
<link href="{{ asset('bundles/engine/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet" type="text/css" />
<!-- Font Awesome Icons -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<!-- Theme style -->
<link href="{{ asset('bundles/engine/dist/css/AdminLTE.min.css') }}" rel="stylesheet" type="text/css" />
<!-- iCheck -->
<link href="{{ asset('bundles/engine/plugins/iCheck/square/blue.css') }}" rel="stylesheet" type="text/css" />

<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
< ![endif]-->
</head>
<body class="login-page">
<div class="login-box">
<div class="login-logo">
<a href=""><b>Herber</b><br /> Intelligent Business automation</a>
</div><!-- /.login-logo -->
<div class="login-box-body">
<p class="login-box-msg">Sign in to start your session</p>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData) }}
</div>
{% endif %}
<form action="{{ path('trensys_engine_security_logincheck') }}" method="post">
<div class="form-group has-feedback">
<input type="text" id="username" class="form-control" name="_username" value="{{ last_username }}" placeholder="Username" autocomplete="off" />
<span class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" id="password" name="_password" />
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-8">


</div><!-- /.col -->
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">Sign In</button>
</div><!-- /.col -->
</div>
</form>
</div><!-- /.login-box-body -->
</div><!-- /.login-box -->

<!-- jQuery 2.1.4 -->
<script src="{{ asset('bundles/engine/plugins/jQuery/jQuery-2.1.4.min.js') }}" type="text/javascript"></script>
<!-- Bootstrap 3.3.2 JS -->
<script src="{{ asset('bundles/engine/bootstrap/js/bootstrap.min.js')}}" type="text/javascript"></script>
<!-- iCheck -->
<script src="{{ asset('bundles/engine/plugins/iCheck/icheck.min.js') }}" type="text/javascript"></script>
<script>
$(function () {
$('input').iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue',
increaseArea: '20%' // optional
});
});
</script>
</body>
</html>

Ryan, thank you for KnpUguard, perhaps a proper dev can create a nice LDAPBundle 🙂

I will update this post as I improve the integration, form objects, nonces etc.

Have fun !


Comments:

Hi,

great work!
which boostrap temmplate do u use?

Thanks

Comment by Pedro, November 2015 11:22:42 AM

je vous demande si l’extension de fichier services.xml ou services.yml ??????

Comment by zied, March 2016 11:08:39 AM
Leave a comment
You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>



Search

Recent Posts

Recent Comments

Older Posts