LP#1786552: LDAP bind user option
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / AuthProxy / LDAP_Auth.pm
1 package OpenILS::Application::AuthProxy::LDAP_Auth;
2 use strict;
3 use warnings;
4 use base 'OpenILS::Application::AuthProxy::AuthBase';
5 use OpenILS::Event;
6 use Net::LDAP;
7 use OpenSRF::Utils::SettingsClient;
8 use OpenSRF::Utils::Logger qw(:logger);
9
10 # default config var (override in configuration xml)
11 my $id_attr = 'uid';
12
13 sub authenticate {
14     my ( $self, $args ) = @_;
15     my $username = $args->{'username'};
16     my $password = $args->{'password'};
17
18     if (!$username) {
19         $logger->debug("User login failed: No username provided");
20         return OpenILS::Event->new( 'LOGIN_FAILED' );
21     }
22     if (!$password) {
23         $logger->debug("User login failed: No password provided");
24         return OpenILS::Event->new( 'LOGIN_FAILED' );
25     }
26
27     my $hostname_is_ldap = 0;
28     my $reached_ldap     = 0;
29     my $user_in_ldap     = 0;
30     my $login_succeeded  = 0;
31
32     my $hostname    = $self->{'hostname'};
33     my $basedn      = $self->{'basedn'};
34     my $authid      = $self->{'authid'};
35     my $authid_pass = $self->{'password'};
36     $id_attr        = $self->{'id_attr'} || $id_attr;
37     my $bind_attr   = $self->{'bind_attr'} || $id_attr; # use id_attr to bind if bind_attr not set
38
39     # bind_attr: name of LDAP attribute containing user's LDAP username
40     # id_attr: name of LDAP attribute containing user's Evergreen username
41     #
42     # Normally the LDAP username *is* the Evergreen username, in which case
43     # we don't need the extra step of getting the username from the LDAP entry.
44     # Thus, bind_attr and id_attr are the same.  (This is the default scenario.)
45     #
46     # However, suppose we have two college libraries in a consortium.  Each
47     # college has its own LDAP-based SSO solution.  An LDAP username like
48     # "jsmith" may be in use at both libraries, for two different patrons.
49     # In this case, we can't use the LDAP username as the EG username, since
50     # every patron must have a unique username in EG.
51     #
52     # Here's how we handle this second scenario:
53     #
54     # 1. The user logs in with their LDAP username.
55     # 2. EG makes a bind request to the LDAP server using the LDAP username,
56     # which is in the LDAP attribute specified by bind_attr.
57     # 3. If the bind succeeds, we pull the user's EG username from the LDAP
58     # attribute specified by id_attr, and pass it along so that EG looks up the
59     # correct user.
60     #
61     # If bind_attr is not set, or if it specifies the same LDAP attribute as
62     # id_attr, we fallback to the default scenario.
63     #
64     my $username_from_ldap = $bind_attr eq $id_attr ? 0 : 1;
65
66     # When the EG username is retrieved from the LDAP server, we want to ensure
67     # that we bind using the actual username provided by the user.
68     if ($username_from_ldap) {
69         $username = $args->{'provided_username'} || $username;
70     }
71
72     my $ldap;
73     my $ldap_search;
74     if ( $ldap = Net::LDAP->new($hostname) ) {
75         $hostname_is_ldap = 1;
76         if ( $ldap->bind( $authid, password => $authid_pass )->code == 0 ) {
77             $reached_ldap = 1;
78             # verify username and lookup user's DN
79             $ldap_search = $ldap->search( base => $basedn,
80                                              filter => "($bind_attr=$username)" );
81             if ( $ldap_search->count != 0 ) {
82                 $user_in_ldap = 1;
83
84                 # verify password (bind check)
85                 my $binddn = $ldap_search->entry(0)->dn();
86                 if ( $ldap->bind( $binddn, password => $password )
87                     ->code == 0 ) {
88                     $login_succeeded = 1;
89                 }
90             }
91         }
92     }
93
94     if ( $login_succeeded ) {
95         if ($username_from_ldap) {
96             my $id_attr_val = $ldap_search->entry(0)->get_value($id_attr);
97             return OpenILS::Event->new('SUCCESS', payload => $id_attr_val);
98         } else {
99             return OpenILS::Event->new('SUCCESS');
100         }
101     } elsif ( !$hostname_is_ldap ) {
102         # TODO: custom failure events?
103         $logger->debug("User login failed: Incorrect LDAP hostname");
104         return OpenILS::Event->new( 'LOGIN_FAILED' );
105     } elsif ( !$reached_ldap ) {
106         $logger->debug("User login failed: The LDAP server is misconfigured or unavailable");
107         return OpenILS::Event->new( 'LOGIN_FAILED' );
108     } elsif ( !$user_in_ldap ) {
109         $logger->debug("User login failed: Username $username not in LDAP");
110         return OpenILS::Event->new( 'LOGIN_FAILED' );
111     } else {
112         $logger->debug("User login failed: Incorrect LDAP password");
113         return OpenILS::Event->new( 'LOGIN_FAILED' );
114     }
115 }
116
117 1;