MySQL LDAP Authentication Plugin

As a continuation of previous post, now, I will show how to make a mysql plugin for ldap authentication.

Get the mysql-server source code at http://dev.mysql.com/downloads/mysql/ (http://dev.mysql.com/get/Downloads/MySQL-5.5/mysql-5.5.27.tar.gz/from/http://cdn.mysql.com/)

Installing necessary packages

yum groupinstall 'Development Tools'
yum install cmake ncurses-devel

Download source code, build and start MySQL Server

wget http://dev.mysql.com/get/Downloads/MySQL-5.5/mysql-5.5.27.tar.gz/from/http://cdn.mysql.com/
tar -xzf mysql-5.5.27.tar.gz
cd mysql-5.5.25

# Preconfiguration setup
groupadd mysql
useradd -r -g mysql mysql

# Beginning of source-build specific instructions
cmake .
make
make install

# Postinstallation setup
chown -R mysql .
chgrp -R mysql .
./scripts/mysql_install_db --user=mysql
chown -R root .
chown -R mysql data

cp support-files/mysql.server /etc/init.d/mysql.server

# Start mysql server
/etc/init.d/mysql.server start

Goal 1) The first version of the plugin must should allow user authentication with password.

Create auth_ldap folder within mysql-5.5.25/plugin

mkdir plugin/auth_ldap

Create a new file in plugin/auth_ldap/auth_ldap.c

/*
Author: Ignacio Ocampo <nafiux@gmail.com>
Website: http://www.nafiux.com/blog
Version: 1.0.0
Description:
  Simple auth plugin
*/

#include <mysql/plugin_auth.h>
#include <mysql/client_plugin.h>
#include <mysql.h>
#include <stdio.h>
#include <unistd.h>
#include <syslog.h>
#include <string.h>

static void auth_ldap_log(int priority, char *msg)
{
  openlog("auth_ldap", LOG_PID|LOG_CONS, LOG_USER);
  syslog(LOG_INFO, msg);
  closelog();
}

/* principal function */
static int auth_ldap_server (MYSQL_PLUGIN_VIO *vio,
                               MYSQL_SERVER_AUTH_INFO *info)
{
  unsigned char *pkt;
  int pkt_len;

  /* read the password as null-terminated string, fail on error */
  if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)
    return CR_ERROR;

  /* fail on empty password */
  if (!pkt_len || *pkt == '\0')
  {
    auth_ldap_log(LOG_INFO, "fail empty password");
    info->password_used= PASSWORD_USED_NO;
    return CR_ERROR;
  }

  /* accept any nonempty password */
  info->password_used= PASSWORD_USED_YES;
  auth_ldap_log(LOG_INFO, "accept any nonempty password");

  return CR_OK;
}

static struct st_mysql_auth auth_ldap_handler =
{
  MYSQL_AUTHENTICATION_INTERFACE_VERSION,
  "auth_ldap",           /* required client-side plugin name */
  auth_ldap_server       /* server-side plugin main function */
};

mysql_declare_plugin(auth_ldap)
{
  MYSQL_AUTHENTICATION_PLUGIN,
  &auth_ldap_handler,                 /* type-specific descriptor */
  "auth_ldap",                        /* plugin name */
  "Ignacio Ocampo",                        /* author */
  "LDAP authentication plugin", /* description */
  PLUGIN_LICENSE_GPL,                   /* license type */
  NULL,                                 /* no init function */
  NULL,                                 /* no deinit function */
  0x0100,                               /* version = 1.0 */
  NULL,                                 /* no status variables */
  NULL,                                 /* no system variables */
  NULL,                                 /* no reserved information */
  0                                     /* no flags */
}
mysql_declare_plugin_end;

static int auth_ldap_client (MYSQL_PLUGIN_VIO *vio, MYSQL *mysql)
{
  int res;

  /* send password as null-terminated string in clear text */
  res= vio->write_packet(vio, (const unsigned char *) mysql->passwd, 
                         strlen(mysql->passwd) + 1);

  return res ? CR_ERROR : CR_OK;
}

mysql_declare_client_plugin(AUTHENTICATION)
  "auth_ldap",                        /* plugin name */
  "Ignacio Ocampo",                        /* author */
  "LDAP authentication plugin", /* description */
  {1,0,0},                              /* version = 1.0.0 */
  "GPL",                                /* license type */
  NULL,                                 /* for internal use */
  NULL,                                 /* no init function */
  NULL,                                 /* no deinit function */
  NULL,                                 /* no option-handling function */
  auth_ldap_client                    /* main function */
mysql_end_client_plugin;

To compile the plugin, we can use the macros defined for cmake in MySQL source code.

Create a new file in plugin/auth_ldap/CMakeLists.txt:

MYSQL_ADD_PLUGIN(auth_ldap auth_ldap.c
  MODULE_ONLY MODULE_OUTPUT_NAME "auth_ldap")

To compile the plugin, run de make command at the root of source code of MySQL (in the same directory where you ran to top):

make

Plugin installation:

cp plugin/auth_ldap/auth_ldap.so /usr/local/mysql/lib/plugin/auth_ldap.so
chgrp mysql /usr/local/mysql/lib/plugin/auth_ldap.so

/usr/local/mysql/bin/mysql -h 127.0.0.1 -u root
mysql> INSTALL PLUGIN auth_ldap SONAME 'auth_ldap.so';

We will implement proxy user support in authentication plugin (see http://dev.mysql.com/doc/refman/5.5/en/writing-authentication-plugins.html#writing-authentication-plugins-proxy-users)

The idea is authenticate any user with LDAP, however, it user must be mapped to a mysql local user, to do it:

Create the Database and Proxy to authenticate any external user through out auth_ldap plugin.

As you can see, the local user “dev” serves as proxy for any user and can access the database “dev_example1″

mysql> CREATE DATABASE dev_example1;
mysql> CREATE USER 'dev'@'localhost';
mysql> GRANT ALL ON dev_example1.* TO 'dev'@'locahost';
mysql> USE mysql;
mysql> DELETE FROM user WHERE User='';
mysql> CREATE USER ''@'' IDENTIFIED WITH auth_ldap;
mysql> GRANT PROXY ON 'dev'@'localhost' TO ''@'';
mysql> SELECT user, host, plugin FROM user;
+------+-----------------------+-----------+
| user | host                  | plugin    |
+------+-----------------------+-----------+
| root | localhost             |           |
| root | localhost.localdomain |           |
| root | 127.0.0.1             |           |
| root | ::1                   |           |
| dev  | %                     |           |
|      |                       | auth_ldap |
+------+-----------------------+-----------+
6 rows in set (0,00 sec)

Test our plugin:

/usr/local/mysql/bin/mysql -h 127.0.0.1 --user=foo
ERROR 1045 (28000): Access denied for user 'foo'@'localhost' (using password: NO)

/usr/local/mysql/bin/mysql -h 127.0.0.1 --user=foo --password=bar
Welcome to the MySQL monitor.  Commands end with ; or \g.

Check the log:

tail /var/log/messages
Aug 11 12:00:50 localhost auth_ldap[3383]: fail empty password
Aug 11 12:00:55 localhost auth_ldap[3383]: accept any nonempty password

Goal 1 completed!

Goal 2) Implement ldap authentication and mapping with local user

/*
Author: Ignacio Ocampo <nafiux@gmail.com>
Website: http://www.nafiux.com/blog
Version: 1.0.2
Description:
  Auth plugin with LDAP
*/

#include <mysql/plugin_auth.h>
#include <mysql/client_plugin.h>
#include <mysql.h>
#include <stdio.h>
#include <unistd.h>
#include <syslog.h>
#include <string.h>
#include <ldap.h>

#define LDAP_SERVER     "ldap://nafiux.com:389"
#define LDAP_PATH       "ou=People,dc=nafiux,dc=com"

static void auth_ldap_log(int priority, char *msg)
{
  openlog("auth_ldap", LOG_PID|LOG_CONS, LOG_USER);
  syslog(LOG_INFO, msg);
  closelog();
}

/* ldap login function */
static int auth_ldap_login(char *username, unsigned char *password)
{
  LDAP *ld;
  int rc;
  char dn[200];
  char buffer[200];

  /* Open LDAP Connection*/
  if( ldap_initialize( &ld, LDAP_SERVER ) )
  {
    auth_ldap_log(LOG_ERR, "ldap_initialize");
    return( 1 );
  }

  /* User authentication (bind) */
  sprintf(dn, "cn=%s,%s", username, LDAP_PATH);
  rc = ldap_simple_bind_s( ld, dn, password );
  if( rc != LDAP_SUCCESS )
  {
    sprintf(buffer, "Authentication failed: %s", dn);
    auth_ldap_log(LOG_INFO, buffer);
    return( 1 );
  }

  sprintf(buffer, "Authentication successful: %s", dn);
  auth_ldap_log(LOG_INFO, buffer);
  return( 0 );
}

/* principal function */
static int auth_ldap_server (MYSQL_PLUGIN_VIO *vio,
                               MYSQL_SERVER_AUTH_INFO *info)
{
  unsigned char *pkt;
  int pkt_len;

  /* read the password as null-terminated string, fail on error */
  if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)
    return CR_ERROR;

  /* fail on empty password */
  if (!pkt_len || *pkt == '\0')
  {
    auth_ldap_log(LOG_INFO, "fail empty password");
    info->password_used= PASSWORD_USED_NO;
    return CR_ERROR;
  }

  /* accept any nonempty password */
  info->password_used= PASSWORD_USED_YES;
  //auth_ldap_log(LOG_INFO, "accept any nonempty password");

  if(auth_ldap_login(info->user_name, pkt))
    return CR_ERROR;

  strcpy(info->authenticated_as, "dev"); // local user
  strcpy(info->external_user, info->user_name);

  return CR_OK;
}

static struct st_mysql_auth auth_ldap_handler =
{
  MYSQL_AUTHENTICATION_INTERFACE_VERSION,
  "auth_ldap",           /* required client-side plugin name */
  auth_ldap_server       /* server-side plugin main function */
};

mysql_declare_plugin(auth_ldap)
{
  MYSQL_AUTHENTICATION_PLUGIN,
  &auth_ldap_handler,                 /* type-specific descriptor */
  "auth_ldap",                        /* plugin name */
  "Ignacio Ocampo",                        /* author */
  "LDAP authentication plugin", /* description */
  PLUGIN_LICENSE_GPL,                   /* license type */
  NULL,                                 /* no init function */
  NULL,                                 /* no deinit function */
  0x0100,                               /* version = 1.0 */
  NULL,                                 /* no status variables */
  NULL,                                 /* no system variables */
  NULL,                                 /* no reserved information */
  0                                     /* no flags */
}
mysql_declare_plugin_end;

static int auth_ldap_client (MYSQL_PLUGIN_VIO *vio, MYSQL *mysql)
{
  int res;

  /* send password as null-terminated string in clear text */
  res= vio->write_packet(vio, (const unsigned char *) mysql->passwd, 
                         strlen(mysql->passwd) + 1);

  return res ? CR_ERROR : CR_OK;
}

mysql_declare_client_plugin(AUTHENTICATION)
  "auth_ldap",                        /* plugin name */
  "Ignacio Ocampo",                        /* author */
  "LDAP authentication plugin", /* description */
  {1,0,0},                              /* version = 1.0.0 */
  "GPL",                                /* license type */
  NULL,                                 /* for internal use */
  NULL,                                 /* no init function */
  NULL,                                 /* no deinit function */
  NULL,                                 /* no option-handling function */
  auth_ldap_client                    /* main function */
mysql_end_client_plugin;

Befor compile the plugin, you need to update the plugin/auth_ldap/CMakeLists.txt (add the LINK_LIBRARIES “ldap” option)

MYSQL_ADD_PLUGIN(auth_ldap auth_ldap.c
  MODULE_ONLY MODULE_OUTPUT_NAME "auth_ldap"
  LINK_LIBRARIES "ldap")

Install openldap-devel package

yum install openldap-devel

Make, copy plugin and restart server (see the previous steps).

Finally, I try the plugin completely.

/usr/local/mysql/bin/mysql -h 127.0.0.1 -u foo --password=bar
ERROR 1045 (28000): Access denied for user 'foo'@'localhost' (using password: YES)

/usr/local/mysql/bin/mysql -h 127.0.0.1 -u nafiux --password=REAL_LDAP_PASSWORD
Welcome to the MySQL monitor.  Commands end with ; or \g.

Check the log:

tail /var/log/messages
Aug 11 14:46:43 localhost auth_ldap[28666]: Authentication failed: cn=foo,ou=People,dc=nafiux,dc=com
Aug 11 14:47:04 localhost auth_ldap[28666]: Authentication successful: cn=nafiux,ou=People,dc=nafiux,dc=com

Check the available databases and user session information:

/usr/local/mysql/bin/mysql -h 127.0.0.1 -u nafiux --password=REAL_LDAP_PASSWORD
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 20
Server version: 5.5.27 Source distribution

Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| dev_example1       |
| test               |
+--------------------+
3 rows in set (0,00 sec)

mysql> SELECT USER(), CURRENT_USER(), @@proxy_user, @@external_user;
+------------------+----------------+--------------+-----------------+
| USER()           | CURRENT_USER() | @@proxy_user | @@external_user |
+------------------+----------------+--------------+-----------------+
| nafiux@localhost | dev@localhost  | ''@''        | ''@''           |
+------------------+----------------+--------------+-----------------+
1 row in set (0,00 sec)

Goal 2 completed!