# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. package Bugzilla::Auth; use 5.10.1; use strict; use warnings; use fields qw( _info_getter _verifier _persister ); use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Mailer; use Bugzilla::Util qw(datetime_from); use Bugzilla::User::Setting (); use Bugzilla::Auth::Login::Stack; use Bugzilla::Auth::Verify::Stack; use Bugzilla::Auth::Persist::Cookie; use Socket; sub new { my ($class, $params) = @_; my $self = fields::new($class); $params ||= {}; $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); # If we ever have any other login persistence methods besides cookies, # this could become more configurable. $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); return $self; } sub login { my ($self, $type) = @_; # Get login info from the cookie, form, environment variables, etc. my $login_info = $self->{_info_getter}->get_login_info(); if ($login_info->{failure}) { return $self->_handle_login_result($login_info, $type); } # Now verify their username and password against the DB, LDAP, etc. if ($self->{_info_getter}->{successful}->requires_verification) { $login_info = $self->{_verifier}->check_credentials($login_info); if ($login_info->{failure}) { return $self->_handle_login_result($login_info, $type); } $login_info = $self->{_verifier}->{successful}->create_or_update_user($login_info); } else { $login_info = $self->{_verifier}->create_or_update_user($login_info); } if ($login_info->{failure}) { return $self->_handle_login_result($login_info, $type); } # Make sure the user isn't disabled. my $user = $login_info->{user}; if (!$user->is_enabled) { return $self->_handle_login_result({failure => AUTH_DISABLED, user => $user}, $type); } $user->set_authorizer($self); return $self->_handle_login_result($login_info, $type); } sub can_change_password { my ($self) = @_; my $verifier = $self->{_verifier}->{successful}; $verifier ||= $self->{_verifier}; my $getter = $self->{_info_getter}->{successful}; $getter = $self->{_info_getter} if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); return $verifier->can_change_password && $getter->user_can_create_account; } sub can_login { my ($self) = @_; my $getter = $self->{_info_getter}->{successful}; $getter = $self->{_info_getter} if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); return $getter->can_login; } sub can_logout { my ($self) = @_; my $getter = $self->{_info_getter}->{successful}; # If there's no successful getter, we're not logged in, so of # course we can't log out! return 0 unless $getter; return $getter->can_logout; } sub login_token { my ($self) = @_; my $getter = $self->{_info_getter}->{successful}; if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) { return $getter->login_token; } return undef; } sub user_can_create_account { my ($self) = @_; my $verifier = $self->{_verifier}->{successful}; $verifier ||= $self->{_verifier}; my $getter = $self->{_info_getter}->{successful}; $getter = $self->{_info_getter} if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); return $verifier->user_can_create_account && $getter->user_can_create_account; } sub extern_id_used { my ($self) = @_; return $self->{_info_getter}->extern_id_used || $self->{_verifier}->extern_id_used; } sub can_change_email { my $can = 1; Bugzilla::Hook::process('can_change_email', {can => \$can}); return $can ? $_[0]->user_can_create_account : 0; } sub _handle_login_result { my ($self, $result, $login_type) = @_; my $dbh = Bugzilla->dbh; my $user = $result->{user}; my $fail_code = $result->{failure}; if (!$fail_code) { ## REDHAT EXTENSION BEGIN 1434224 my $log = "User " . $user->login . " logged in with " . ref $self->{_info_getter}->{successful}; if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { $log .= " on agent " . ($ENV{HTTP_USER_AGENT} || 'NONE'); $log .= " from referer " . ($ENV{HTTP_REFERER} || 'NONE'); } Bugzilla->logger->info($log); ## REDHAT EXTENSION END 1434224 # We don't persist logins over GET requests in the WebService, # because the persistance information can't be re-used again. # (See Bugzilla::WebService::Server::JSONRPC for more info.) if ($self->{_info_getter}->{successful}->requires_persistence and !Bugzilla->request_cache->{auth_no_automatic_login}) { $user->{_login_token} = $self->{_persister}->persist_login($user); } } elsif ($fail_code == AUTH_ERROR) { if ($result->{user_error}) { ThrowUserError($result->{user_error}, $result->{details}); } else { ThrowCodeError($result->{error}, $result->{details}); } } elsif ($fail_code == AUTH_NODATA) { $self->{_info_getter}->fail_nodata($self) if $login_type == LOGIN_REQUIRED; # If we're not LOGIN_REQUIRED, we just return the default user. $user = Bugzilla->user; } # The username/password may be wrong # Don't let the user know whether the username exists or whether # the password was just wrong. (This makes it harder for a cracker # to find account names by brute force) elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) { my $remaining_attempts = MAX_LOGIN_ATTEMPTS - ($result->{failure_count} || 0); ThrowUserError("invalid_login_or_password", {remaining => $remaining_attempts}); } ## REDHAT EXTENSION 1262651 BEGIN elsif ($fail_code == AUTH_RH_RADIUS_LOGINFAILED) { my $remaining_attempts = MAX_LOGIN_ATTEMPTS - ($result->{failure_count} || 0); ThrowUserError("rh_auth_radius_failed", {remaining => $remaining_attempts}); } ## REDHAT EXTENSION 1262651 BEGIN # The account may be disabled elsif ($fail_code == AUTH_DISABLED) { $self->{_persister}->logout(); # XXX This is NOT a good way to do this, architecturally. $self->{_persister}->clear_browser_cookies(); # and throw a user error ThrowUserError( "account_disabled", { 'disabled_reason' => $result->{user}->disabledtext, 'account' => $result->{user}->login } ); } elsif ($fail_code == AUTH_LOCKOUT) { my $attempts = $user->account_ip_login_failures; # We want to know when the account will be unlocked. This is # determined by the 5th-from-last login failure (or more/less than # 5th, if MAX_LOGIN_ATTEMPTS is not 5). my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS]; my $unlock_at = datetime_from($determiner->{login_time}, Bugzilla->local_timezone); $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL); # If we were *just* locked out, notify the maintainer about the # lockout. if ($result->{just_locked_out}) { # We're sending to the maintainer, who may be not a Bugzilla # account, but just an email address. So we use the # installation's default language for sending the email. my $default_settings = Bugzilla::User::Setting::get_defaults(); my $template = Bugzilla->template_inner($default_settings->{lang}->{default_value}); my $address = $attempts->[0]->{ip_addr}; # Note: inet_aton will only resolve IPv4 addresses. # For IPv6 we'll need to use inet_pton which requires Perl 5.12. my $n = inet_aton($address); if ($n) { $address = gethostbyaddr($n, AF_INET) . " ($address)"; } my $vars = { locked_user => $user, attempts => $attempts, unlock_at => $unlock_at, address => $address, }; my $message; $template->process('email/lockout.txt.tmpl', $vars, \$message) || ThrowTemplateError($template->error); MessageToMTA($message); } $unlock_at->set_time_zone($user->timezone); ThrowUserError('account_locked', {ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at}); } # If we get here, then we've run out of options, which shouldn't happen. else { ThrowCodeError("authres_unhandled", {value => $fail_code}); } return $user; } 1; __END__ =head1 NAME Bugzilla::Auth - An object that authenticates the login credentials for a user. =head1 DESCRIPTION Handles authentication for Bugzilla users. Authentication from Bugzilla involves two sets of modules. One set is used to obtain the username/password (from CGI, email, etc), and the other set uses this data to authenticate against the datasource (the Bugzilla DB, LDAP, PAM, etc.). Modules for obtaining the username/password are subclasses of L, and modules for authenticating are subclasses of L. =head1 AUTHENTICATION ERROR CODES Whenever a method in the C family fails in some way, it will return a hashref containing at least a single key called C. C will point to an integer error code, and depending on the error code the hashref may contain more data. The error codes are explained here below. =head2 C Insufficient login data was provided by the user. This may happen in several cases, such as cookie authentication when the cookie is not present. =head2 C An error occurred when trying to use the login mechanism. The hashref will also contain an C element, which is the name of an error from C