1 package RT::Authen::ExternalAuth;
7 RT::Authen::ExternalAuth - RT Authentication using External Sources
11 A complete package for adding external authentication mechanisms
12 to RT. It currently supports LDAP via Net::LDAP and External Database
13 authentication for any database with an installed DBI driver.
15 It also allows for authenticating cookie information against an
16 external database through the use of the RT-Authen-CookieAuth extension.
20 ok(require RT::Authen::ExternalAuth);
26 use RT::Authen::ExternalAuth::LDAP;
27 use RT::Authen::ExternalAuth::DBI;
32 my ($session,$given_user,$given_pass) = @_;
34 unless(defined($RT::ExternalAuthPriority)) {
35 return (0, "ExternalAuthPriority not defined, please check your configuration file.");
38 my $no_info_check = 0;
39 unless(defined($RT::ExternalInfoPriority)) {
40 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled cannot be externally-sourced");
44 # This may be used by single sign-on (SSO) authentication mechanisms for bypassing a password check.
48 # Should have checked if user is already logged in before calling this function,
49 # but just in case, we'll check too.
50 return (0, "User already logged in!") if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id);
51 # We don't have a logged in user. Let's try all our available methods in order.
52 # last if success, next if not.
54 # Get the prioritised list of external authentication services
55 my @auth_services = @$RT::ExternalAuthPriority;
57 # For each of those services..
58 foreach my $service (@auth_services) {
62 # Get the full configuration for that service as a hashref
63 my $config = $RT::ExternalSettings->{$service};
64 $RT::Logger->debug( "Attempting to use external auth service:",
67 # $username will be the final username we decide to check
68 # This will not necessarily be $given_user
71 #############################################################
72 ####################### SSO Check ###########################
73 #############################################################
74 if ($config->{'type'} eq 'cookie') {
75 # Currently, Cookie authentication is our only SSO method
76 $username = RT::Authen::ExternalAuth::DBI::GetCookieAuth($config);
78 #############################################################
80 # If $username is defined, we have a good SSO $username and can
81 # safely bypass the password checking later on; primarily because
82 # it's VERY unlikely we even have a password to check if an SSO succeeded.
84 if(defined($username)) {
85 $RT::Logger->debug("Pass not going to be checked, attempting SSO");
89 # SSO failed and no $user was passed for a login attempt
90 # We only don't return here because the next iteration could be an SSO attempt
91 unless(defined($given_user)) {
92 $RT::Logger->debug("SSO Failed and no user to test with. Nexting");
96 # We don't have an SSO login, so we will be using the credentials given
97 # on RT's login page to do our authentication.
98 $username = $given_user;
100 # Don't continue unless the service works.
101 # next unless RT::Authen::ExternalAuth::TestConnection($config);
103 # Don't continue unless the $username exists in the external service
105 $RT::Logger->debug("Calling UserExists with \$username ($username) and \$service ($service)");
106 next unless RT::Authen::ExternalAuth::UserExists($username, $service);
109 ####################################################################
110 ########## Load / Auto-Create ######################################
111 ####################################################################
112 # We are now sure that we're talking about a valid RT user.
113 # If the user already exists, load up their info. If they don't
114 # then we need to create the user in RT.
116 # Does user already exist internally to RT?
117 $session->{'CurrentUser'} = RT::CurrentUser->new();
118 $session->{'CurrentUser'}->Load($username);
120 # Unless we have loaded a valid user with a UserID create one.
121 unless ($session->{'CurrentUser'}->Id) {
122 my $UserObj = RT::User->new($RT::SystemUser);
124 $UserObj->Create(%{ref($RT::AutoCreate) ? $RT::AutoCreate : {}},
129 $RT::Logger->error( "Couldn't create user $username: $msg" );
132 $RT::Logger->info( "Autocreated external user",
138 $RT::Logger->debug("Loading new user (",
140 ") into current session");
141 $session->{'CurrentUser'}->Load($username);
144 ####################################################################
145 ########## Authentication ##########################################
146 ####################################################################
147 # If we successfully used an SSO service, then authentication
148 # succeeded. If we didn't then, success is determined by a password
152 $RT::Logger->debug("Password check bypassed due to SSO method being in use");
155 $RT::Logger->debug("Password validation required for service - Executing...");
156 $success = RT::Authen::ExternalAuth::GetAuth($service,$username,$given_pass);
159 $RT::Logger->debug("Password Validation Check Result: ",$success);
161 # If the password check succeeded then this is our authoritative service
162 # and we proceed to user information update and login.
166 # If we got here and don't have a user loaded we must have failed to
167 # get a full, valid user from an authoritative external source.
168 unless ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
169 delete $session->{'CurrentUser'};
170 return (0, "No User");
174 delete $session->{'CurrentUser'};
175 return (0, "Password Invalid");
178 # Otherwise we succeeded.
179 $RT::Logger->debug("Authentication successful. Now updating user information and attempting login.");
181 ####################################################################################################
182 ############################### The following is auth-method agnostic ##############################
183 ####################################################################################################
185 # If we STILL have a completely valid RT user to play with...
186 # and therefore password has been validated...
187 if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
189 # Even if we have JUST created the user in RT, we are going to
190 # reload their information from an external source. This allows us
191 # to be sure that the user the cookie gave us really does exist in
192 # the database, but more importantly, UpdateFromExternal will check
193 # whether the user is disabled or not which we have not been able to
194 # do during auto-create
196 # These are not currently used, but may be used in the future.
197 my $info_updated = 0;
198 my $info_updated_msg = "User info not updated";
200 unless($no_info_check) {
201 # Note that UpdateUserInfo does not care how we authenticated the user
202 # It will look up user info from whatever is specified in $RT::ExternalInfoPriority
203 ($info_updated,$info_updated_msg) = RT::Authen::ExternalAuth::UpdateUserInfo($session->{'CurrentUser'}->Name);
206 # Now that we definitely have up-to-date user information,
207 # if the user is disabled, kick them out. Now!
208 if ($session->{'CurrentUser'}->UserObj->Disabled) {
209 delete $session->{'CurrentUser'};
210 return (0, "User account disabled, login denied");
214 # If we **STILL** have a full user and the session hasn't already been deleted
215 # This If/Else is logically unnecessary, but it doesn't hurt to leave it here
216 # just in case. Especially to be a double-check to future modifications.
217 if ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
219 $RT::Logger->info( "Successful login for",
220 $session->{'CurrentUser'}->Name,
222 $ENV{'REMOTE_ADDR'});
223 # Do not delete the session. User stays logged in and
224 # autohandler will not check the password again
226 # Make SURE the session is deleted.
227 delete $session->{'CurrentUser'};
228 return (0, "Failed to authenticate externally");
229 # This will cause autohandler to request IsPassword
230 # which will in turn call IsExternalPassword
233 return (1, "Successful login");
237 my $username = shift;
239 # Prepare for the worst...
242 my $msg = "User NOT updated";
244 my $user_disabled = RT::Authen::ExternalAuth::UserDisabled($username);
246 my $UserObj = RT::User->new($RT::SystemUser);
247 $UserObj->Load($username);
249 # If user is disabled, set the RT::Principle to disabled and return out of the function.
250 # I think it's a waste of time and energy to update a user's information if they are disabled
251 # and it could be a security risk if they've updated their external information with some
252 # carefully concocted code to try to break RT - worst case scenario, but they have been
253 # denied access after all, don't take any chances.
255 # If someone gives me a good enough reason to do it,
256 # then I'll update all the info for disabled users
258 if ($user_disabled) {
259 # Make sure principle is disabled in RT
260 my ($val, $message) = $UserObj->SetDisabled(1);
261 # Log what has happened
262 $RT::Logger->info("User marked as DISABLED (",
264 ") per External Service",
265 "($val, $message)\n");
266 $msg = "User Disabled";
268 return ($updated, $msg);
271 # Make sure principle is not disabled in RT
272 my ($val, $message) = $UserObj->SetDisabled(0);
273 # Log what has happened
274 $RT::Logger->info("User marked as ENABLED (",
276 ") per External Service",
277 "($val, $message)\n");
279 # Update their info from external service using the username as the lookup key
280 # CanonicalizeUserInfo will work out for itself which service to use
281 # Passing it a service instead could break other RT code
282 my %args = (Name => $username);
283 $UserObj->CanonicalizeUserInfo(\%args);
285 # For each piece of information returned by CanonicalizeUserInfo,
286 # run the Set method for that piece of info to change it for the user
287 foreach my $key (sort(keys(%args))) {
288 next unless $args{$key};
289 my $method = "Set$key";
290 # We do this on the UserObj from above, not self so that there
291 # are no permission restrictions on setting information
292 my ($method_success,$method_msg) = $UserObj->$method($args{$key});
294 # If your user information is not getting updated,
295 # uncomment the following logging statements
296 if ($method_success) {
297 # At DEBUG level, log that method succeeded
298 # $RT::Logger->debug((caller(0))[3],"$method Succeeded. $method_msg");
300 # At DEBUG level, log that method failed
301 # $RT::Logger->debug((caller(0))[3],"$method Failed. $method_msg");
305 # Confirm update success
307 $RT::Logger->debug( "UPDATED user (",
309 ") from External Service\n");
310 $msg = 'User updated';
312 return ($updated, $msg);
317 # Request a username/password check from the specified service
318 # This is only valid for non-SSO services.
320 my ($service,$username,$password) = @_;
324 # Get the full configuration for that service as a hashref
325 my $config = $RT::ExternalSettings->{$service};
327 # And then act accordingly depending on what type of service it is.
328 # Right now, there is only code for DBI and LDAP non-SSO services
329 if ($config->{'type'} eq 'db') {
330 $success = RT::Authen::ExternalAuth::DBI::GetAuth($service,$username,$password);
331 $RT::Logger->debug("DBI password validation result:",$success);
332 } elsif ($config->{'type'} eq 'ldap') {
333 $success = RT::Authen::ExternalAuth::LDAP::GetAuth($service,$username,$password);
334 $RT::Logger->debug("LDAP password validation result:",$success);
336 $RT::Logger->error("Invalid service type for GetAuth:",$service);
344 # Request a username/password check from the specified service
345 # This is only valid for non-SSO services.
347 my ($username,$service) = @_;
351 # Get the full configuration for that service as a hashref
352 my $config = $RT::ExternalSettings->{$service};
354 # And then act accordingly depending on what type of service it is.
355 # Right now, there is only code for DBI and LDAP non-SSO services
356 if ($config->{'type'} eq 'db') {
357 $success = RT::Authen::ExternalAuth::DBI::UserExists($username,$service);
358 } elsif ($config->{'type'} eq 'ldap') {
359 $success = RT::Authen::ExternalAuth::LDAP::UserExists($username,$service);
361 $RT::Logger->debug("Invalid service type for UserExists:",$service);
369 my $username = shift;
370 my $user_disabled = 0;
372 my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
374 # For each named service in the list
375 # Check to see if the user is found in the external service
376 # If not found, jump to next service
377 # If found, check to see if user is considered disabled by the service
378 # Then update the user's info in RT and return
379 foreach my $service (@info_services) {
381 # Get the external config for this service as a hashref
382 my $config = $RT::ExternalSettings->{$service};
384 # If the config doesn't exist, don't bother doing anything, skip to next in list.
385 unless(defined($config)) {
386 $RT::Logger->debug("You haven't defined a configuration for the service named \"",
388 "\" so I'm not going to try to get user information from it. Skipping...");
392 # If it's a DBI config:
393 if ($config->{'type'} eq 'db') {
395 unless(RT::Authen::ExternalAuth::DBI::UserExists($username,$service)) {
396 $RT::Logger->debug("User (",
398 ") doesn't exist in service (",
400 ") - Cannot update information - Skipping...");
403 $user_disabled = RT::Authen::ExternalAuth::DBI::UserDisabled($username,$service);
405 } elsif ($config->{'type'} eq 'ldap') {
407 unless(RT::Authen::ExternalAuth::LDAP::UserExists($username,$service)) {
408 $RT::Logger->debug("User (",
410 ") doesn't exist in service (",
412 ") - Cannot update information - Skipping...");
415 $user_disabled = RT::Authen::ExternalAuth::LDAP::UserDisabled($username,$service);
417 } elsif ($config->{'type'} eq 'cookie') {
418 RT::Logger->error("You cannot use SSO Cookies as an information service.");
421 # The type of external service doesn't currently have any methods associated with it. Or it's a typo.
422 RT::Logger->error("Invalid type specification for config %config->{'name'}");
423 # Drop out to next service in list
428 return $user_disabled;
431 sub CanonicalizeUserInfo {
433 # Careful, this $args hashref was given to RT::User::CanonicalizeUserInfo and
434 # then transparently passed on to this function. The whole purpose is to update
435 # the original hash as whatever passed it to RT::User is expecting to continue its
436 # code with an update args hash.
442 my %params = (Name => undef,
443 EmailAddress => undef,
446 $RT::Logger->debug( (caller(0))[3],
450 join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
451 sort(keys(%$args))));
453 # Get the list of defined external services
454 my @info_services = $RT::ExternalInfoPriority ? @{$RT::ExternalInfoPriority} : undef;
455 # For each external service...
456 foreach my $service (@info_services) {
458 $RT::Logger->debug( "Attempting to get user info using this external service:",
461 # Get the config for the service so that we know what attrs we can canonicalize
462 my $config = $RT::ExternalSettings->{$service};
464 if($config->{'type'} eq 'cookie'){
465 $RT::Logger->debug("You cannot use SSO cookies as an information service!");
469 # For each attr we've been told to canonicalize in the match list
470 my $attr_map = (defined $config->{'lookup_attr_map'}) ? $config->{'lookup_attr_map'} : $config->{'attr_map'};
471 foreach my $rt_attr (@{$config->{'attr_match_list'}}) {
472 # Jump to the next attr in $args if this one isn't in the attr_match_list
473 $RT::Logger->debug( "Attempting to use this canonicalization key:",$rt_attr);
474 unless(defined($args->{$rt_attr})) {
475 $RT::Logger->debug("This attribute (",
477 ") is null or incorrectly defined in the attr_map for this service (",
483 # Else, use it as a canonicalization key and lookup the user info
484 my $key = $attr_map->{$rt_attr};
485 my $value = $args->{$rt_attr};
487 # Check to see that the key being asked for is defined in the config's attr_map
489 my ($attr_key, $attr_value);
490 while (($attr_key, $attr_value) = each %$attr_map) {
491 $valid = 1 if ($key eq $attr_value);
494 $RT::Logger->debug( "This key (",
496 "is not a valid attribute key (",
502 # Use an if/elsif structure to do a lookup with any custom code needed
503 # for any given type of external service, or die if no code exists for
504 # the service requested.
506 if($config->{'type'} eq 'ldap'){
507 ($found, %params) = RT::Authen::ExternalAuth::LDAP::CanonicalizeUserInfo($service,$key,$value);
508 } elsif ($config->{'type'} eq 'db') {
509 ($found, %params) = RT::Authen::ExternalAuth::DBI::CanonicalizeUserInfo($service,$key,$value);
511 $RT::Logger->debug( (caller(0))[3],
514 "a valid information service");
517 # Don't Check any more attributes
520 # Don't Check any more services
524 # If found, Canonicalize Email Address and
525 # update the args hash that we were given the hashref for
527 # It's important that we always have a canonical email address
528 if ($params{'EmailAddress'}) {
529 $params{'EmailAddress'} = $UserObj->CanonicalizeEmailAddress($params{'EmailAddress'});
531 %$args = (%$args, %params);
534 $RT::Logger->info( (caller(0))[3],
536 join(", ", map {sprintf("%s: %s", $_, $args->{$_})}
537 sort(keys(%$args))));
539 ### HACK: The config var below is to overcome the (IMO) bug in
540 ### RT::User::Create() which expects this function to always
541 ### return true or rejects the user for creation. This should be
542 ### a different config var (CreateUncanonicalizedUsers) and
543 ### should be honored in RT::User::Create()
544 return($found || $RT::AutoCreateNonExternalUsers);
549 no warnings 'redefine';
550 *RT::User::CanonicalizeUserInfo = sub {
553 return ( CanonicalizeUserInfo( $self, $args ) );