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

Monday, March 19, 2007

LDAP, SASL/DIGEST-MD5 and Active Directory

It's not obvious to find an information how to authenticate into a Windows Active Directory using LDAP mechanisms, and in particular: SASL/DIGEST-MD5. Needless to say that Windows and Linux have different APIs to do that: Windows provides its own API, whereas on Linux one would most likely use OpenLDAP .

To start with: a few words of warning for Windows developers. If you are deemed to work with MS Visual Studio 6 (as I am, due to some compatibility reasons), you may notice "mysterious" behaviour of your code: it will simply crash with no reason, while executing a call to LDAP functions. This is caused by the fact that the version of wldap32.lib that is shipped with VS6 does not "match" the DLL that you find in your windows xp, etc (all in all: VS6 is an "ancient" stuff). Just look at these article from Microsoft's support: 283199, and this discussion . Apparently, you need an updated version of wldap32.lib file... The problem is that VS6 is not supported anymore, and there's no Microsoft SDK available for it anymore!
What you could do, however, is to get the wldap32.h from a newer version of Visual Studio, and copy it to your VS6's \Lib subdirectory (overwriting the old one!). You could also get this file from one of the newer SDK's, but remember: the SDK will NOT work with your VS6...
To be able to find out whether you have an "old" version of wldap32.lib, or the new one, have a look at the date of this file (usually in C:\Program Files\Microsoft Visual Studio\VC98\Lib ): if it is 13 May 1998, then it is the "buggy" one and needs to be replaced. I have one with date 14 April 2005 (got it from one of SDK's) and it works OK.
A few more words... Don't try to "upgrade" the include file the same way (i.e. the winldap.h)... there are some conflicts and the compiler cannot cope with mismatched versions. Just leave your old one as it is! All in all - you may always re-declare the newer, missing parts in your implementation file.

OK. Let's get into something real now. My problem is: I want to implement an authentication mechanism, that will be based on the Active Directory, i.e. I want that the user may authenticate to my program with the same credentials as he uses to log in to the Windows machine at our site (and all these Windows machines are managed using Active Directory).
For those of us who do not speak windows :-) : Active Directory is basically an LDAP server, holding various administrative information for Windows. We want to treat it simply as an LDAP server that can do authentication for us.

The following code demonstrates authentication on Windows. Note that you need to link it against wldap32.lib !

#include "stdafx.h"
#include
#include
#include

bool HandleError(int rc, const char* what)
{
if (rc==0) return false; // no error
if (rc==-1) rc=LdapGetLastError();
printf("ERROR in %s: %i %s\n",what,rc,ldap_err2string(rc));
return true;
}


int main(int argc, char* argv[])
{
char *ldapHost="myLdapServer.mydomain.org";

char *userName="myUserName";
char *password="myPassword";

// initialize
LDAP *ldap=ldap_init(ldapHost,LDAP_PORT);
if (ldap==NULL) {printf("\nERROR in ldap_init\n\n"); return -1;}

// set LDAP version
unsigned ldapver=LDAP_VERSION3;
int rc=ldap_set_option(ldap,LDAP_OPT_PROTOCOL_VERSION, (void*)&ldapver);
if (HandleError(rc,"set protocol version")) return -1;

// set connection timeout, and connect
LDAP_TIMEVAL connectionTimeout;
LDAP_TIMEVAL *pConTimeout=&connectionTimeout;
connectionTimeout.tv_sec=10;
rc = ldap_connect(ldap, pConTimeout);
if (HandleError(rc,"Connect")) return -1;


// credentials for SASL/DIGEST-MD5 are passed through this
// data structure; you need #include for it!
SEC_WINNT_AUTH_IDENTITY auth;
auth.Domain=NULL; // may be left empty!
auth.DomainLength=0;
auth.User=(unsigned char*)strdup(userName);
auth.UserLength=strlen(userName);
auth.Password=(unsigned char*)strdup(password);
auth.PasswordLength=strlen(password);
auth.Flags=SEC_WINNT_AUTH_IDENTITY_ANSI;

char* bindDN=NULL; // credentials already defined above! Leave empty!!!

rc=ldap_bind_s(ldap,bindDN,(char*)&auth,LDAP_AUTH_NEGOTIATE);

if (HandleError(rc,"Bind")) return -1;

rc=ldap_unbind_s(ldap);
if (HandleError(rc,"Unbind")) return -1;

return 0;
}


As you can see, we use the standard ldap_bind_s function to perform the bind (authentication), and we pass the authentication credentials in the SEC_WINNT_AUTH_IDENTITY structure. Note that you need to change the ldapHost, userName and password to represent yours, to make the thing actually work.

In the next post I describe how to make SASL/DIGEST-MD5 authentication work with ActiveDirectory using OpenLDAP, i.e. on Linux.


(c) Piotr Golonka 2007