LDAP authentication with Laravel and Adldap2

This post provides a short tutorial on integrating LDAP authentication with the Laravel framework. One of my recent projects has involved adding LDAP authentication to a Laravel web app using Steve Bauman’s excellent Adlap2 package.

Lightweight Directory Access Protocol (LDAP) needs no introduction and any developer who has spent time working on internal ‘enterprise’ software project will have encountered it. Simple integration might just focus on using an LDAP directory to authenticate users using a common username and password across a suite of enterprise applications. More advanced integration work might including binding to a driectory for interactive retrieval of corporate contact data or delegated management of groups and roles.

If you’re planning to use this tutorial to add LDAP support to an existing project some of the set-up and config may vary. It is assumed that you’re working Laravel’s built-in authentication scaffolding as it will be easier to demonstrate how Adldap2 can be layered onto an existing. I’m also assuming that you’re developing with Homestead but that doesn’t matter too much.

  1. First up, we need to satisfy a dependency on the php-ldap extension. This might seem obvious but version of the ldap extension must exactly match the version of PHP installed in your Homestead environment. At the time of writing, this is v7.2 but you might need to alter depending on your set-up.
    
    $sudo apt-get update
    $sudo apt-get install -y php7.2-ldap
    $php -v | grep ldap
    

    Once installed you will want to restart the Nginx web server to ensure the ldap extension is loaded. The easiest way to accomplish this is by running `vagrant provision` from your host’s Homestead directory.

  2. Next, use Artisan to create Laravel’s built-in authentication scaffolding. The Laravel auth scaffold is practically bullet-proof for most out-of-box app requirements and serves as an excellent starting point for LDAP integration with Adldap2.
    
    $php artisan make:auth
    $php artisan migrate
    

     

  3. Access to password recovery functions probably won’t make much sense for common LDAP authentication scenarios, so unless you are planning to support a mix of local Laravel authentication and LDAP, you might want to consider limiting the built-in routes. The following routing should work for most cases, but you might want to customise to suit your needs. Note that I’ve applied a middleware authentication restriction in these routes to protect the ‘/home’ route.
    
    $ php artisan route:list
    +--------+----------+----------------------+---------------+---------------------------------------------------------+--------------+
    | Domain | Method   | URI                  | Name          | Action                                                  | Middleware   |
    +--------+----------+----------------------+---------------+---------------------------------------------------------+--------------+
    |        | GET|HEAD | /                    |               | Closure                                                 | web          |
    |        | GET|HEAD | home                 | home          | App\Http\Controllers\HomeController@index               | web,auth     |
    |        | GET|HEAD | login                | login         | App\Http\Controllers\Auth\LoginController@showLoginForm | web,guest    |
    |        | POST     | login                |               | App\Http\Controllers\Auth\LoginController@login         | web,guest    |
    |        | POST     | logout               | logout        | App\Http\Controllers\Auth\LoginController@logout        | web          |
    +--------+----------+----------------------+---------------+---------------------------------------------------------+--------------+
    

     

  4. Next, alter your User model and corresponding migration. Introduce an ldap username column so you can keep track of which users have been previous authenticated to use the application. After making changes to your migration, don’t forget to re-run your user table migration:
    
    $php artisan migrate:refresh
    

    Here’s a sample from my migration file – as you can see the changes required are minimal. My ldap identifier is ’ladap_username’:

    
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::create('users', function (Blueprint $table) {
                $table->increments('id');
                $table->string('name');
                $table->string('ldap_username')->unique( );
                $table->string('email')->unique();
                $table->string('password');
                $table->datetime('deleted_at')->nullable();
                $table->rememberToken();
                $table->timestamps();
            });
        }
    

     

  5. Use composer to install Adldap2. Edit the “require” block of your composer.json file:
    
        "require": {
            "php": ">=7.0.0",
            "fideloper/proxy": "~3.3",
            "laravel/framework": "5.5.*",
            "laravel/tinker": "~1.0",
            "adldap2/adldap2-laravel": "^3.0",
        },
    

    Then update composer and refresh the autoloader:

    
    $composer update
    $composer dump-autoload
    

     

  6. Configure your LDAP settings. Following is a list of the available config options with examples and short description of each:
    
    ADLDAP_ACCOUNT_PREFIX="ldap_username="
    #LDAP user account query string fragment that precedes your 'username' value
    
    ADLDAP_ACCOUNT_SUFFIX=",ou=staff,dc=example,dc=com"
    #LDAP user account query string fragment that follows your 'username' value
    
    ADLDAP_CONTROLLERS="ldap.example.com"
    #LDAP directory hostname(s)
    
    ADLDAP_BASEDN="ou=staff,dc=example,dc=com"
    #LDAP distinguished name used as the basis for your user account queries
    
    ADLDAP_ADMIN_USERNAME="admin"
    ADLDAP_ADMIN_PASSWORD="password"
    #If your LDAP instance requires your app to authenticate, provide the
    #relevant username and password in these fields
    

    There are a few other options that can be configured but these are the typical minimum. These values are stored in ./config/adldap.php and ./config/adldap_auth.php, but you will want to set them in your .env file rather than editing directly.

  7. Customise your LoginController’s username to correspond with your Adldap2 settings. The LoginController uses an ‘email’ attribute by default but you might want to change this value to match your LDAP instance. This method is provided by the Illuminate\Foundation\Auth\AuthenticatesUsers trait.

    
        /**
         * We're doing LDAP Auth in this application
         * so use the ldap username instead of email address to login
         * This property corresponds with ‘ldap_username’ column in the user table
         */
        public function username( ) {
            return ‘ldap_username’;
        }
    

     

  8. Customise the login view field name. By default, the Laravel auth scaffold provides an email address username field. If your LDAP instance is set up to. use non-email usernames, you can avoid triggering the email field validator by changing the type from “email” to “username”.
    
    <input id="uid" type="username" class="form-control" name="uid" value="{{ old('uid') }}" autofocus>
    

By this stage you should hopefully have a successful LDAP integration and managing to authenticate users against it. The use-case covered in this tutorial relatively simple and there a stack of other advanced integrations that ADLDAP can support. If there’s a enough interest, I’ll consider working on a full user binding tutorial in the future.

Parting tips

  • LDAP implementations tend to vary greatly and don’t always follow standard conventions and structure. You should consult with the directory administrator and don’t make assumptions on how data is organised.
  • Spend a bit of time investigating the ldapsearch CLI tool. Testing your ldap base parameter and other queries in ldapsearch will be much faster than trying to work things out on-the-fly while developing your app. This will help you establish a point-of-failure boundary if you run into problems and can also assist in designing appropriate tests for application.
  • Consider in advance which model attributes you intend to read from LDAP and which you’ll store locally within your application. Reading data regularly from the directory will introduce a performance overhead cost, so caching data might be a sensible tactic in this case. Of course, caching can introduce performance and complexity issues of a different nature, so consider your approach carefully.

Installing PHP & Oracle PDO Drivers on Ubuntu

Given that PHP and Oracle databases are fairly mainstream platforms these days, you might expect that there would be a simple, straight-forward way to install and connect PHP with Oracle on Ubuntu.

Nope.

If it’s not something you do regularly, or, unless you’ve got a fool-proof set of recent instructions, it is painful.

I faced the problem last week while configuring a a new virtual server. After a few hours of bumbling around I finally found a decent guide here for an earlier version of the Oracle instant client drivers. On the off-chance that it saves someone else some hassle, I thought I’d post a slight updated version that will work for PHP5.3 and Instant Client 11.2. Chances are that I’ll need to refer to this guide in the future, too.

I started with a brand-spanking new VM, running Ubuntu 12.04. Your mileage may vary if attempting to install over the top if earlier versions etc. You’ll need root privileges to run many of these commands, so you might want start a root shell (sudo su -).

First, fetch instant client and SDK packages from Oracle:
http://www.oracle.com/technology/software/tech/oci/instantclient/index.html
For 64bit Ubuntu, you’ll need to grab both “instantclient-basic-linux.x64-*.zip” and “instantclient-sdk-linux.x64-*.zip”.

Put both downloads somewhere convenient and extract:

mkdir -p /opt/oracle/instantclient
cp ~/instantclient-basic-*-*.zip /opt/oracle/instantclient/
cp ~/instantclient-sdk-*-*.zip /opt/oracle/instantclient/
cd /opt/oracle/instantclient
unzip ./instantclient-basic-*-*.zip
unzip ./instantclient-sdk-*-*.zip
mv instantclient*/* ./rmdir instantclient*/

Next, we need to create some symlinks so that the oracle shared libraries appear where they need to:

ln -s libclntsh.so.* libclntsh.soln -s libocci.so.* libocci.so
echo /opt/oracle/instantclient >> /etc/ld.so.conf
ldconfig

The following steps aren’t strictly necessary unless you need a TNS names config: You put the config details into sqlnet.ora file.

mkdir -p network/admin
touch network/admin/sqlnet.ora

Now install apache, php etc:

apt-get install --yes php5 php5-cli php5-dev php-db php-pear
apt-get install --yes build-essential libaio1

Install the pecl oci8 extension:

pecl install oci8

When prompted for the ORACLE_HOME path, enter “shared,instantclient,/opt/oracle/instantclient”

Add the configuration to your php.ini files:

echo "# configuration for php OCI8 module" > /etc/php5/cli/conf.d/oci8.ini
echo "extension=oci8.so" >> /etc/php5/cli/conf.d/oci8.ini

Now we install pdo_oci. It hasn’t been updated in a while so a few bits of fancy linking are in order…

cd /usr/include/
ln -s php5 php

cd /opt/oracle/instantclient

mkdir -p include/oracle/11.2/
cd include/oracle/11.2/
ln -s ../../../sdk/include client
cd -

mkdir -p lib/oracle/11.2/client
cd lib/oracle/11.2/client
ln -s ../../../../ lib
cd -

pecl channel-update pear.php.net
mkdir -p /tmp/pear/download/
cd /tmp/pear/download/
pecl download pdo_oci
tar xvf PDO_OCI*.tgz
cd PDO_OCI*

### copy the lines below into the file "config.m4.patch"
*** config.m4 2005-09-24 17:23:24.000000000 -0600
--- /home/myuser/Desktop/PDO_OCI-1.0/config.m4 2009-07-07 17:32:14.000000000 -0600
***************
*** 7,12 ****
--- 7,14 ----
if test -s "$PDO_OCI_DIR/orainst/unix.rgs"; then
PDO_OCI_VERSION=`grep '"ocommon"' $PDO_OCI_DIR/orainst/unix.rgs | sed 's/[ ][ ]*/:/g' | cut -d: -f 6 | cut -c 2-4`
test -z "$PDO_OCI_VERSION" && PDO_OCI_VERSION=7.3
+ elif test -f $PDO_OCI_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME.11.2; then
+ PDO_OCI_VERSION=11.2
elif test -f $PDO_OCI_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME.10.1; then
PDO_OCI_VERSION=10.1
elif test -f $PDO_OCI_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME.9.0; then
***************
*** 119,124 ****
--- 121,129 ----
10.2)
PHP_ADD_LIBRARY(clntsh, 1, PDO_OCI_SHARED_LIBADD)
;;
+ 11.2)
+ PHP_ADD_LIBRARY(clntsh, 1, PDO_OCI_SHARED_LIBADD)
+ ;;
*)
AC_MSG_ERROR(Unsupported Oracle version! $PDO_OCI_VERSION)
;;
#EOF


patch --dry-run -i config.m4.patch && patch -i config.m4.patch &&
phpize ORACLE_HOME=/opt/oracle/instantclient

./configure --with-pdo-oci=instantclient,/opt/oracle/instantclient,11.2

make && make test && make install && mv modules/pdo_oci.so /usr/lib/php5/20090626/

Finally, add the configuration lines that enable the pdo_oci extension:

cat - < /etc/php5/apache2/conf.d/pdo_oci.ini# configuration for php PDO_OCI module
extension=pdo_oci.so
EOF

Check that everything succeeded:

php --info | grep oci