Eric Day: Writing Authentication Plugins for Drizzle
In this post I’m going to describe how to write an authentication plugin for Drizzle. The plugin I’ll be demonstrating is a simple file-based plugin that takes a file containing a list of ‘username:password’ entries (one per line like a .htpasswd file for Apache). The first step is to setup a proper build environment and create a branch, see the Drizzle wiki page to get going. From here I’ll assume you have Drizzle checked out from bzr and are able to compile it.
Setup a development branch and plugin directory
Change to your shared-repository directory for Drizzle and run (assuming you branched ‘lp:drizzle’ to ‘drizzle’):
shell$ bzr branch drizzle auth-file Branched 1432 revision(s). shell$ cd auth-file
Next, we’ll want to create the plugin directory and create plugin.ini and auth_file.cc.
shell$ mkdir plugin/auth_file
plugin/auth_file/plugin.ini:
[plugin] title=File-based Authentication description=A simple plugin to authenticate against a list of username:password entries in a plain text file. version=0.1 author=Eric Day <[email protected]> license=PLUGIN_LICENSE_GPL
plugin/auth_file/auth_file.cc:
/* -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- * vim:expandtab:shiftwidth=2:tabstop=2:smarttab: * * Copyright (C) 2010 Eric Day * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
#include "config.h"
#include <string>
#include "drizzled/plugin/authentication.h"
#include "drizzled/security_context.h"
using namespace std;
using namespace drizzled;
namespace auth_file
{
class AuthFile: public plugin::Authentication
{
public:
AuthFile(string name_arg):
plugin::Authentication(name_arg)
{ }
bool authenticate(const SecurityContext &sctx, const string &password)
{
/* Let "root" user always succeed for now because of test suite. */
if (sctx.getUser() == "root" && password.empty())
return true;
/* Only allow hard coded username for now. */
if (sctx.getUser() == "auth_file")
return true;
return false;
}
};
static int init(plugin::Context &context)
{
context.add(new AuthFile("auth_file"))
return 0;
}
} /* namespace auth_file */
DRIZZLE_PLUGIN(auth_file::init, NULL);
All authentication plugins need to inherit from the ‘plugin::Authentication’ class and implement an ‘authenticate’ method. This takes the user context and a password as its arguments and simply returns true if the user is allowed or false otherwise. As you can see, this plugin will verify all sessions for the ‘auth_file’ user with any password and deny everything else. It also allows ‘root’ with no password for the test suite, we’ll fix this later so it’s not required. The init method is called when the plugin is loaded, and here we want to register an instance of the plugin class with the kernel. The DRIZZLE_PLUGIN definition is required so the Drizzle kernel can load the module and grab some basic information about it (like the name of the init method).
Create tests cases to verify our plugin works
We’ll want to add some test cases so we can check our plugin as we make progress. This is done by creating test case and result files inside the plugin directory. You’ll want to create the following directories and files:
shell$ mkdir plugin/auth_file/tests shell$ mkdir plugin/auth_file/tests/t shell$ mkdir plugin/auth_file/tests/r
plugin/auth_file/tests/t/basic-master.opt
--plugin-add=auth_file
plugin/auth_file/tests/t/basic.test
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT --replace_regex /@'.*?'/@'LOCALHOST'/ --error ER_ACCESS_DENIED_ERROR connect (bad_user,localhost,bad_user,,,); --replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT connect (auth_file,localhost,auth_file,,,); connection auth_file; SELECT 1;
plugin/auth_file/tests/r/basic.result
connect(localhost,bad_user,,test,MASTER_PORT,); ERROR 28000: Access denied for user 'bad_user'@'LOCALHOST' (using password: NO) SELECT 1; 1 1
The files in the ‘tests/t’ directory drive the test system, and the file in ‘tests/r’ are the results that should match the output. This test tries two connections, one with ‘bad_user’ which should fail, and another with ‘auth_file’ user which should pass. Before writing the code to check against a list of users in a file, we’ll compile what we have so far and check the test cases to make sure things are working properly.
shell$ ./config/autorun.sh ... shell$ ./configure --with-debug ... shell$ make -j 3 ... shell$ make check ... auth_file.basic [ pass ] 6 ...
It works! To save some time while developing, you can also test just the auth_file plugin without everything else by running:
( cd tests && ./dtr --suite=auth_file )
Add options
We’re going to want users to be able to specify a location for the file to load, so we’ll need to tell the kernel about the option through the plugin interface. This is done by adding:
#include "drizzled/configmake.h"
...
static char* users_file= NULL;
static const char DEFAULT_USERS_FILE[]= SYSCONFDIR "/drizzle.users";
...
static DRIZZLE_SYSVAR_STR(users,
users_file,
PLUGIN_VAR_READONLY,
N_("File to load for usernames and passwords"),
NULL, /* check func */
NULL, /* update func*/
DEFAULT_USERS_FILE /* default */);
static drizzle_sys_var* sys_variables[]=
{
DRIZZLE_SYSVAR(users),
NULL
};
...
DRIZZLE_PLUGIN(auth_file::init, auth_file::sys_variables);
The first include is there so we can have access to the SYSCONFDIR macro, which maps to the ‘etc’ directory of our install path. That path plus the file ‘drizzle.users’ is our default. We also define a variable to either this default path or a custom path the user specifies. Next, we define a system variable with the macro DRIZZLE_SYSVAR_STR and provide some information like where to store it, the help string, and the default value. We also need to define a system variables list. The new variable is the only entry in the list right now, but you could define more system variables and add them to this list (just make sure it is NULL terminated). Last, we modify our DRIZZLE_PLUGIN call to give a second argument instead of NULL. This tells the kernel to look for variables in the provided list when loading. With this option, we’ll now be able to specify: –auth-file-users=/some/path/to/drizzles.users
Write the plugin
With the plugin compiling, tests setup, and options specified, we can start to write some real code. First up is adding a couple class methods, the new AuthFile class looks like:
class AuthFile: public plugin::Authentication
{
public:
AuthFile(string name_arg);
/**
* Retrieve the last error encountered in the class.
*/
string& getError(void);
/**
* Load the users file into a local map.
*
* @return True on success, false on error. If false is returned an error
* is set and can be retrieved with getError().
*/
bool loadFile(void);
private:
bool authenticate(const SecurityContext &sctx, const string &password);
string error;
map<string, string> users;
};
We’ve moved the method definitions out of the class (for Drizzle coding standards) and now have two new declarations: loadFile() to load the specified users file into a std::map, and getError() to return errors, if any. The getError() method simply returns the ‘error’ data member, but loadFile() is a bit more interesting:
bool AuthFile::loadFile(void)
{
ifstream file(users_file);
if (!file.is_open())
{
error = "Could not open users file: ";
error += users_file;
return false;
}
while (!file.eof())
{
string line;
getline(file, line);
if (line == "" || line[line.find_first_not_of(" \t")] == '#')
continue;
string username;
string password;
size_t password_offset = line.find(":");
if (password_offset == string::npos)
username = line;
else
{
username = string(line, 0, password_offset);
password = string(line, password_offset + 1);
}
pair<map<string, string>::iterator, bool> result;
result = users.insert(pair<string, string>(username, password));
if (result.second == false)
{
error = "Duplicate entry found in users file: ";
error += username;
file.close();
return false;
}
}
file.close();
return true;
}
This method opens the users file, and for each line, either ignores it because of blank lines/comments or parses out the username:password pair. Note that you don’t need to specify a password option.
Next up, we change the authenticate() method to use the map instead of the hard coded values:
bool AuthFile::authenticate(const SecurityContext &sctx, const string &password)
{
map<string, string>::const_iterator user = users.find(sctx.getUser());
if (user == users.end())
return false;
if (password == user->second)
return true;
return false;
}
This method now looks up users in the map and, if found with a password match, lets the user in. Now lets update our test case to use this. First we need to create a users file to allow the ‘root’ and ‘auth_file’ user we put in our test cases:
plugin/auth_file/tests/t/basic.users
# Always allow root user with no password for drizzletest program root auth_file
plugin/auth_file/tests/t/basic-master.opt
--plugin-add=auth_file --auth-file-users=$DRIZZLE_TEST_DIR/../plugin/auth_file/tests/t/basic.users
Now it’s time to recompile and check our new code:
shell$ make -j 3 ... shell$ ( cd tests && ./dtr --suite=auth_file ) ... auth_file.basic [ pass ] 6 ...
It still works! I’d like to say we’re done here, but notice we’ve not actually tested any passwords. Before trying that, a little explanation about how password authentication is required.
Verifying passwords in Drizzle
Because Drizzle has a pluggable protocol, the usernames and passwords can be coming from any source. They could be coming from the embedded console plugin which passes the password through as plain text, or from the MySQL protocol plugin that uses the custom MySQL hashing algorithm. This means a simple string equality does not suffice for all password sources. The code above handles the plain text case, but since the default connection method is the MySQL protocol, including for the test suite, we need to also handle the case when the user supplied password is hashed.
Verify MySQL Hashed Passwords
To accomplish this we add in an extra check in the authenticate() method. This now looks like:
bool AuthFile::authenticate(const SecurityContext &sctx, const string &password)
{
map<string, string>::const_iterator user = users.find(sctx.getUser());
if (user == users.end())
return false;
if (sctx.getPasswordType() == SecurityContext::MYSQL_HASH)
return verifyMySQLHash(user->second, sctx.getPasswordContext(), password);
if (password == user->second)
return true;
return false;
}
This extra check calls the verifyMySQLHash() method to verify the local password with the client-scrambled password, using the random bytes the server sent during the handshake (password context). This method is:
#include "drizzled/util/convert.h"
#include "drizzled/algorithm/sha1.h"
...
/**
* Verify the local and remote scrambled password match using the MySQL
* hashing algorithm.
*
* @param[in] password Plain text password that is stored locally.
* @param[in] scramble_bytes The random bytes the server sent to client
* to use for scrambling the password.
* @param[in] scrambled_password The result of the client scrambling the
* password remotely.
* @return True if the password matched, false if not.
*/
bool verifyMySQLHash(const string &password,
const string &scramble_bytes,
const string &scrambled_password);
...
bool AuthFile::verifyMySQLHash(const string &password,
const string &scramble_bytes,
const string &scrambled_password)
{
if (scramble_bytes.size() != SHA1_DIGEST_LENGTH ||
scrambled_password.size() != SHA1_DIGEST_LENGTH)
{
return false;
}
SHA1_CTX ctx;
uint8_t local_scrambled_password[SHA1_DIGEST_LENGTH];
uint8_t temp_hash[SHA1_DIGEST_LENGTH];
uint8_t scrambled_password_check[SHA1_DIGEST_LENGTH];
/* Generate the double SHA1 hash for the password stored locally first. */
SHA1Init(&ctx);
SHA1Update(&ctx, reinterpret_cast<const uint8_t *>(password.c_str()),
password.size());
SHA1Final(temp_hash, &ctx);
SHA1Init(&ctx);
SHA1Update(&ctx, temp_hash, SHA1_DIGEST_LENGTH);
SHA1Final(local_scrambled_password, &ctx);
/* Hash the scramble that was sent to client with the local password. */
SHA1Init(&ctx);
SHA1Update(&ctx, reinterpret_cast<const uint8_t*>(scramble_bytes.c_str()),
SHA1_DIGEST_LENGTH);
SHA1Update(&ctx, local_scrambled_password, SHA1_DIGEST_LENGTH);
SHA1Final(temp_hash, &ctx);
/* Next, XOR the result with what the client sent to get the original
single-hashed password. */
for (int x= 0; x SHA1_DIGEST_LENGTH; x++)
temp_hash[x]= temp_hash[x] ^ scrambled_password[x];
/* Hash this result once more to get the double-hashed password again. */
SHA1Init(&ctx);
SHA1Update(&ctx, temp_hash, SHA1_DIGEST_LENGTH);
SHA1Final(scrambled_password_check, &ctx);
/* These should match for a successful auth. */
return memcmp(local_scrambled_password, scrambled_password_check, SHA1_DIGEST_LENGTH) == 0;
}
I won't get into the details of what this method does, this is left as an exercise for the reader. :) The one thing we do care about is if this works, so back to adding to our test cases:
plugin/auth_file/tests/t/basic.users
# Always allow root user with no password for drizzletest program root auth_file auth_file_password:test_password
plugin/auth_file/tests/t/basic.test
--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT --replace_regex /@'.*?'/@'LOCALHOST'/ --error ER_ACCESS_DENIED_ERROR connect (bad_user,localhost,bad_user,,,); --replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT connect (auth_file,localhost,auth_file,,,); connection auth_file; SELECT 1; --replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT connect (auth_file_password,localhost,auth_file_password,test_password,,); connection auth_file_password; SELECT 1; --replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT --replace_regex /@'.*?'/@'LOCALHOST'/ --error ER_ACCESS_DENIED_ERROR connect (bad_user_password,localhost,auth_file_password,bad_password,,);
plugin/auth_file/tests/r/basic.result
connect(localhost,bad_user,,test,MASTER_PORT,); ERROR 28000: Access denied for user 'bad_user'@'LOCALHOST' (using password: NO) SELECT 1; 1 1 SELECT 1; 1 1 connect(localhost,auth_file_password,bad_password,test,MASTER_PORT,); ERROR 28000: Access denied for user 'auth_file_password'@'LOCALHOST' (using password: YES)
With the test files updated, lets compile and run the tests:
shell$ make -j 3 ... shell$ ( cd tests && ./dtr --suite=auth_file ) ... auth_file.basic [ pass ] 11 ...
It works! At this point we have a fully functional plugin. To finish up the plugin, we'll want to commit the changes, push the branch to Launchpad, and propose the plugin for review so it can be merged into the trunk. You can see the full source code in lp:~eday/drizzle/auth-file (it should also appear in the Drizzle trunk in the next couple of days). There are improvements that can be made such as checking if the file changed to reload while running (being conscious of the possibility of multiple concurrent readers) or being able to store passwords in a format other than plain text. Patches are welcome!
I hope this gives you enough information to get started writing your own authentication plugins. I'm going to be working on a direct LDAP authentication plugin next, supporting both plain text and MySQL hashed passwords. If you need any help getting started with your own, come ask your questions on IRC or on the Drizzle mailing list. We'll also be hosting a Drizzle Developer Day after the MySQL Conference where you can get started in person.
URL: http://oddments.org/?p=349
_______________________________________________ Mailing list: https://launchpad.net/~drizzle-discuss Post to : [email protected] Unsubscribe : https://launchpad.net/~drizzle-discuss More help : https://help.launchpad.net/ListHelp

