diff options
Diffstat (limited to 'Bugzilla/Auth.pm')
-rw-r--r-- | Bugzilla/Auth.pm | 451 |
1 files changed, 228 insertions, 223 deletions
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 58ac248c5..c40b3582e 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -12,9 +12,9 @@ use strict; use warnings; use fields qw( - _info_getter - _verifier - _persister + _info_getter + _verifier + _persister ); use Bugzilla::Constants; @@ -30,276 +30,281 @@ use URI; use URI::QueryParam; sub new { - my ($class, $params) = @_; - my $self = fields::new($class); + 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'}; + $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(); + $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); + $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - return $self; + # 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) = @_; - my $dbh = Bugzilla->dbh; - - # Get login info from the cookie, form, environment variables, etc. - my $login_info = $self->{_info_getter}->get_login_info(); + my ($self, $type) = @_; + my $dbh = Bugzilla->dbh; - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } + # Get login info from the cookie, form, environment variables, etc. + my $login_info = $self->{_info_getter}->get_login_info(); - # Now verify his 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); + } + # Now verify his 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); - } - - # 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); - - # trigger multi-factor auth - if ($self->{_info_getter}->{successful}->requires_verification - && $user->mfa - && !Bugzilla->sudoer - && !i_am_webservice() - ) { - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - my $uri = URI->new($cgi->self_url); - foreach my $param (qw( Bugzilla_remember Bugzilla_restrictlogin GoAheadAndLogIn )) { - $uri->query_param_delete($param); - } - $user->mfa_provider->verify_prompt({ - user => $user, - type => $type, - reason => 'Logging in as ' . $user->identity, - restrictlogin => $params->{Bugzilla_restrictlogin}, - remember => $params->{Bugzilla_remember}, - url => $uri->as_string, - postback => { - action => 'token.cgi', - token_field => 't', - fields => { - a => 'mfa_l', - }, - } - }); + 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); + + # trigger multi-factor auth + if ( $self->{_info_getter}->{successful}->requires_verification + && $user->mfa + && !Bugzilla->sudoer + && !i_am_webservice()) + { + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + my $uri = URI->new($cgi->self_url); + foreach + my $param (qw( Bugzilla_remember Bugzilla_restrictlogin GoAheadAndLogIn )) + { + $uri->query_param_delete($param); + } + $user->mfa_provider->verify_prompt({ + user => $user, + type => $type, + reason => 'Logging in as ' . $user->identity, + restrictlogin => $params->{Bugzilla_restrictlogin}, + remember => $params->{Bugzilla_remember}, + url => $uri->as_string, + postback => + {action => 'token.cgi', token_field => 't', fields => {a => 'mfa_l',},} + }); + } + + + return $self->_handle_login_result($login_info, $type); } sub mfa_verified { - my ($self, $user, $event) = @_; - require Bugzilla::Auth::Login::CGI; + my ($self, $user, $event) = @_; + require Bugzilla::Auth::Login::CGI; - my $params = Bugzilla->input_params; - $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new(); - $params->{Bugzilla_restrictlogin} = $event->{restrictlogin} if defined $event->{restrictlogin}; - $params->{Bugzilla_remember} = $event->{remember} if defined $event->{remember}; + my $params = Bugzilla->input_params; + $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new(); + $params->{Bugzilla_restrictlogin} = $event->{restrictlogin} + if defined $event->{restrictlogin}; + $params->{Bugzilla_remember} = $event->{remember} if defined $event->{remember}; - $self->_handle_login_result({ user => $user }, $event->{type}); + $self->_handle_login_result({user => $user}, $event->{type}); } sub successful_info_getter { - my ($self) = @_; + my ($self) = @_; - return $self->{_info_getter}->{successful}; + return $self->{_info_getter}->{successful}; } 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; + 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; + 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; + 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; + 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; + 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; + my ($self) = @_; + return $self->{_info_getter}->extern_id_used + || $self->{_verifier}->extern_id_used; } sub can_change_email { - return $_[0]->user_can_create_account; + return $_[0]->user_can_create_account; } 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) { - # 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} - || Bugzilla->request_cache->{dont_persist_session} - ) - ) { - $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}); - } + my ($self, $result, $login_type) = @_; + my $dbh = Bugzilla->dbh; + + my $user = $result->{user}; + my $fail_code = $result->{failure}; + + if (!$fail_code) { + + # 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} + || Bugzilla->request_cache->{dont_persist_session} + ) + ) + { + $user->{_login_token} = $self->{_persister}->persist_login($user); } - 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; + } + elsif ($fail_code == AUTH_ERROR) { + if ($result->{user_error}) { + ThrowUserError($result->{user_error}, $result->{details}); } - # 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_username_or_password", - { remaining => $remaining_attempts }); - } - # 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}); - } - 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) { - my $host = gethostbyaddr($n, AF_INET); - $address = "$host ($address)" if $host; - } - 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); - Bugzilla->audit(sprintf( - '<%s> triggered lockout of %s after %s attempts', - $address, $user->login, scalar(@$attempts) - )); - } - - $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 }); + 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_username_or_password", + {remaining => $remaining_attempts}); + } + + # 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}); + } + 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) { + my $host = gethostbyaddr($n, AF_INET); + $address = "$host ($address)" if $host; + } + 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); + Bugzilla->audit(sprintf( + '<%s> triggered lockout of %s after %s attempts', + $address, $user->login, scalar(@$attempts) + )); + } + + $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; + return $user; } 1; |