Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Authenticating to Active Directory with python-ldap always returns (97, [])

Similar to this question, I am trying to perform simple authentication to a 2003 Active Directory using python ldap (CentOS 6.2 x86_64, Python 2.6.6, python-ldap 2.3.10).

Despite following all the usual steps in the init, including

conn.set_option(ldap.OPT_REFERRALS, 0)

if I pass the correct credentials I always get a (97, []) returned:

>>> import ldap
>>> conn = ldap.initialize('ldap://ad.server.domain.com')
>>> conn.protocol_version = 3
>>> conn.set_option(ldap.OPT_REFERRALS, 0)
>>> conn.simple_bind_s('[email protected]', 'WrongPassword')
ldap.INVALID_CREDENTIALS: {'info': '80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 52e, vece', 'desc': 'Invalid credentials'}
>>> conn.simple_bind_s('[email protected]', 'CorrectPassword')
(97, [])

Error code 97 is not a success; it's the LDAP_REFERRAL_LIMIT_EXCEEDED error being returned from AD. Nor can I use it as a de facto success indicator, because:

>>> conn.simple_bind_s('', 'CorrectPassword')
(97, [])
>>> conn.simple_bind_s('', '')
(97, [])

Even more frustrating is that this script is a migration from an old Perl script using Net::LDAP, which does return 0 for a successful authenticated bind to the same AD and server.

All the information I can find on python-ldap indicates that what I am doing should Just Work; I would be inclined to think there's something wrong with the AD servers, but the Perl script does return the correct LDAP code on a successful bind.

I have tested python-ldap 2.2.0 and python 2.4.4 on an old CentOS 5.5 box I had lying around and it "fails" in exactly the same way.

Does anyone know what I am missing?

EDIT: Per request, here is the Perl script that works. Net::LDAP returns the return code from the LDAP server, and the AD server is returning 0x00, "Successful request", AFAICT.

#!/usr/bin/perl -w
use strict;
use Net::LDAP;

## Corporate subdomains
my @domains = ("americas", "asia", "europe");

# AD connect timeout
my $timeout = 10;
# Set AD server info.
my $port = "389";
my $host = "server.domain.com";

my $user = shift @ARGV;
chomp $user;

my $password = <STDIN>;
$password =~ s/\r\n//;
chomp $password;

my $ldap = Net::LDAP->new($host, port => $port, timeout => $timeout ) ||
        die "Unable to connect to LDAP server";

my $bind_return = 1;
foreach (@domains) {
        my $result = $ldap->bind( "$user\@$_.domain.com", password => $password );
        if( $result->code == 0) {
                $bind_return = 0;
                last;
        }
}

## Unbind and return
$ldap->unbind;

if ($bind_return) { print "Authentication Failed.  Access Denied\n" }
exit $bind_return;
like image 471
Chris Doherty Avatar asked May 23 '12 18:05

Chris Doherty


People also ask

How do I use LDAP authentication in Python?

In order to use LDAP with Python we need to import the Server and the Connection object, and any additional constant we will use in our LDAP. As you might remember from the LDAP Protocol diagram the authentication operation is called Bind.

What are three ways to LDAP authenticate?

LDAP v3 supports three types of authentication: anonymous, simple and SASL authentication.


1 Answers

Michael Ströder, the author of the python-ldap library, enlightened me thus:

The 97 is not the LDAP result code. It's the result type ldap.RES_BIND. Normally you don't have to look at the results returned by LDAPObject.simple_bind_s() (unless you want to extract the bind response controls).

If the LDAP result code is not 0 the accompanying exception is raised like ldap.INVALID_CREDENTIALS in your example.

So your code should look like this:

try:
  conn.simple_bind_s('[email protected]', 'WrongPassword')
except ldap.INVALID_CREDENTIALS:
  user_error_msg('wrong password provided')

The reason for these results:

>>> conn.simple_bind_s('', 'CorrectPassword')
(97, [])
>>> conn.simple_bind_s('', '')
(97, [])

is that out of the box 2003 Active Directory allows anonymous binds. So not providing a user id at all will still pass a simple bind check, if the only thing being tested is whether simple_bind_s() throws an error.

2003 Active Directory does require authentication for any searches that aren't attributes of the rootDSE, so for our internal purposes we added a trivial search to the try: block:

try:
  conn.simple_bind_s('[email protected]', 'SubmittedPassword')
  conn.search_st('DC=domain,DC=com', ldap.SCOPE_SUBTREE, '(objectClass=container)', 'name', 0, 30)
except ldap.INVALID_CREDENTIALS:
  user_error_msg('wrong password provided')
like image 77
Chris Doherty Avatar answered Oct 02 '22 09:10

Chris Doherty