Friday, March 30, 2007

LDAP, SASL/DIGEST-MD5 authentication on Linux

In my previous post I discussed how to make an LDAP authentication against Active Directory, on Windows, using "native" Windows LDAP library.
In this post, I will show how to achieve the same on Linux, using OpenLDAP and DIGEST-MD5 authentication.

The problem with OpenLDAP programming is that the documentation for SASL binds has only been completed in the latest 2.4 ("alpha") versions. If your version of Linux still uses some older version (such as 2.3.27 present in "current stable" Fedora Core 6) all you find in the man page (concerning SASL binds) is "description still under construction"...
It is, however, wise to go to the OpenLDAP webpage, and get the man page (for 2.4 versions) from there: ldap_sasl_bind(3)

A few notes:
  • to authenticate to ActiveDirectory, you need to use SASL/DIGEST-MD5 authentication in LDPA
  • to get it working, you need to use ldap_sasl_interactive_bind() function, which is probably the most "expanded" form of all the bind functions
  • the function actually requires that you write a "callback" function, which will return all required authentication information when it is asked, then pass this function as a parameter to ldap_sasl_interactive_bind_s.
  • As described in the man page, this callback function accepts 4 parameters: the first is the LDAP* structure, the second contains flags, the third may contain the custom parameters (we will actually make use of this!) and the fourth is the sasl_interact_t* pointer indicating what type of infromation SASL requests from us.
  • We will need to use OpenLDAP and Cyrus SASL - i.e. you need to include the ldap.h and sasl.h header files, then link your program against libldap and libsasl2 (note "2" at the end!) libraries.
In the example you'll find below, I declare a structure type my_authdata, which will be used to pass the authentication data to the callback function.

Then, I define the callback function called my_sasl_interact . As you will see from the code: depending on what is requested (the case statement), I return the appropriate information. Note that if you actually refered to the SASL documentation, you'll find a few more "requests" that might happened. In our case we provide just the minimal set, that seems to be sufficient. The function is called by SASL routines, and it is given a list (array) of requests in the interact variable. Each request is of type sasl_interact_t, and the function should assign requested value to the .result and .len elements of the structure.
Note also that the function will be called with the parameter *defaults , in which we will pass the authentication credentials.

Let's turn to the main function now. Before discussing the contents in details of implementation, one more notice... In my "real" application I wanted to be able to authenticate many users (i.e. one user logs in, then logs out, another logs in, logs out, etc...) without restarting the application. This turned out to be again a non-trivial puzzle and needed special care (I'll discuss it in a moment, while speaking about unbinding). That is why in the example below I perform the authentication in a loop - actually three times - with the same credentials. This is only to demonstrate that multiple subsequent authentications are possible.

OK, let's discuss the code now.
First of all, you see that I create the my_authdata structure called auth . Of course it could have been created "on the flight", somewhere in the main function - it is just more convenient to keep all the settings at the beginning of the function. I specify the SASL authentication mechanism to be "DIGEST-MD5", and specify SASL flags to be LDAP_SASL_QUIET. Read the SASL documentation if you want to learn more about flags, or simply assume that this one works.
Then I call ldap_initialize to get my LDAP *ld handle and sett protocol version to LDAP_VERSION3. No magic here.
Then I call the ldap_sasl_interactive_bind_s function. Note that for SASL authentication the bindDN paramerer is not needed, and NULL is passed. Note also that I pass my callback function: my_sasl_interact , and the pointer to the authentication data - this data will be passed to the callback function. That's all.

Now another thing comes: we want to unbind (i.e. "logout") the previous user, to be able to bind as another. The OpenLDAP man pages tell you to use ldap_unbind (or one of the variants: I use ldap_unbind_ext_s , but it's practically the same). If you ended your loop here, however, your next authentication WOULD FAIL. This is because SASL needs additional cleanup steps! If you looked at the code of OpenLDAP's examples, you would have noticed that they call sasl_done() function. Indeed, this one cleans-up SASL. It cleans it up so much, that the next authentication would fail again... Therefore just after that, you need to re-initialize SASL by calling sasl_client_init(NULL); .

That's the end of the story.
The only missing part now is the code. Here it is:


#include <stdio.h>
#include <stdlib.h>
#include <ldap.h>
#include <sasl.h>

typedef struct {
char* username;
char* password;
} my_authdata;

int my_sasl_interact(
LDAP *ld,
unsigned flags,
void *defaults,
void *in )
{
my_authdata *auth=(my_authdata*)defaults;

sasl_interact_t *interact = (sasl_interact_t*)in;
if( ld == NULL ) return LDAP_PARAM_ERROR;

while( interact->id != SASL_CB_LIST_END ) {

char *dflt = (char*)interact->defresult;

switch( interact->id ) {
case SASL_CB_GETREALM:
dflt=NULL;
break;
case SASL_CB_AUTHNAME:
dflt=auth->username;
break;
case SASL_CB_PASS:
dflt=auth->password;
break;
default:
printf("my_sasl_interact asked for unknown %i\n",interact->id);
}
interact->result = (dflt && *dflt) ? dflt : (char*)"";
interact->len = strlen( (char*)interact->result );

interact++;
}
return LDAP_SUCCESS;
}

int main( int argc, char **argv )
{

int rc, i;

for (i=1;i<=3;i++) {


LDAP *ld = NULL;

static my_authdata auth;
auth.username="myUserName"; /* adopt this */
auth.password="myPassword"; /* adopt this */

int authmethod = LDAP_AUTH_SASL;
char *sasl_mech = ber_strdup( "DIGEST-MD5" );
char *ldapuri = ber_strdup( "ldap://myLdapServer.myDomain" ); /* adopt this */


int protocol = LDAP_VERSION3;
unsigned sasl_flags = LDAP_SASL_QUIET;
char *binddn=NULL;

rc = ldap_initialize( &ld, ldapuri );
if( rc != LDAP_SUCCESS ) {
fprintf( stderr, "Could not create LDAP session handle (%d): %s\n", rc, ldap_err2string(rc) );
exit( EXIT_FAILURE );
}


if( ldap_set_option( ld, LDAP_OPT_PROTOCOL_VERSION, &protocol ) != LDAP_OPT_SUCCESS ) {
fprintf( stderr, "Could not set LDAP_OPT_PROTOCOL_VERSION %d\n", protocol );
exit( EXIT_FAILURE );
}

rc = ldap_sasl_interactive_bind_s( ld, binddn,
sasl_mech, NULL, NULL,
sasl_flags, my_sasl_interact, &auth );

if( rc != LDAP_SUCCESS ) {
ldap_perror( ld, "ldap_sasl_interactive_bind_s" );
ldap_unbind_ext_s( ld,NULL,NULL);
exit( EXIT_FAILURE );
}

printf("BIND WAS OK!\n");

rc=ldap_unbind_ext_s( ld,NULL,NULL);
sasl_done();
sasl_client_init( NULL );

}
return rc ;
}


(c) Piotr Golonka 2007

No comments: