diff options
Diffstat (limited to 'Bugzilla/Auth/Login')
-rw-r--r-- | Bugzilla/Auth/Login/APIKey.pm | 35 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/CGI.pm | 118 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Cookie.pm | 185 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Env.pm | 27 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Stack.pm | 126 |
5 files changed, 255 insertions, 236 deletions
diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm index 63e35578a..79c16825e 100644 --- a/Bugzilla/Auth/Login/APIKey.pm +++ b/Bugzilla/Auth/Login/APIKey.pm @@ -26,28 +26,29 @@ use constant can_logout => 0; # This method is only available to web services. An API key can never # be used to authenticate a Web request. sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my ($user_id, $login_cookie); + my ($self) = @_; + my $params = Bugzilla->input_params; + my ($user_id, $login_cookie); - my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); - if (!i_am_webservice() || !$api_key_text) { - return { failure => AUTH_NODATA }; - } + my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); + if (!i_am_webservice() || !$api_key_text) { + return {failure => AUTH_NODATA}; + } - my $api_key = Bugzilla::User::APIKey->new({ name => $api_key_text }); + my $api_key = Bugzilla::User::APIKey->new({name => $api_key_text}); - if (!$api_key or $api_key->api_key ne $api_key_text) { - # The second part checks the correct capitalisation. Silly MySQL - ThrowUserError("api_key_not_valid"); - } - elsif ($api_key->revoked) { - ThrowUserError('api_key_revoked'); - } + if (!$api_key or $api_key->api_key ne $api_key_text) { - $api_key->update_last_used(); + # The second part checks the correct capitalisation. Silly MySQL + ThrowUserError("api_key_not_valid"); + } + elsif ($api_key->revoked) { + ThrowUserError('api_key_revoked'); + } - return { user_id => $api_key->user_id }; + $api_key->update_last_used(); + + return {user_id => $api_key->user_id}; } 1; diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm index 6003d62a5..0824f1ebd 100644 --- a/Bugzilla/Auth/Login/CGI.pm +++ b/Bugzilla/Auth/Login/CGI.pm @@ -21,65 +21,71 @@ use Bugzilla::Error; use Bugzilla::Token; sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - - my $login = trim(delete $params->{'Bugzilla_login'}); - my $password = delete $params->{'Bugzilla_password'}; - # The token must match the cookie to authenticate the request. - my $login_token = delete $params->{'Bugzilla_login_token'}; - my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie'); - - my $valid = 0; - # If the web browser accepts cookies, use them. - if ($login_token && $login_cookie) { - my ($time, undef) = split(/-/, $login_token); - # Regenerate the token based on the information we have. - my $expected_token = issue_hash_token(['login_request', $login_cookie], $time); - $valid = 1 if $expected_token eq $login_token; - $cgi->remove_cookie('Bugzilla_login_request_cookie'); - } - # WebServices and other local scripts can bypass this check. - # This is safe because we won't store a login cookie in this case. - elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { - $valid = 1; - } - # Else falls back to the Referer header and accept local URLs. - # Attachments are served from a separate host (ideally), and so - # an evil attachment cannot abuse this check with a redirect. - elsif (my $referer = $cgi->referer) { - my $urlbase = correct_urlbase(); - $valid = 1 if $referer =~ /^\Q$urlbase\E/; - } - # If the web browser doesn't accept cookies and the Referer header - # is missing, we have no way to make sure that the authentication - # request comes from the user. - elsif ($login && $password) { - ThrowUserError('auth_untrusted_request', { login => $login }); - } - - if (!defined($login) || !defined($password) || !$valid) { - return { failure => AUTH_NODATA }; - } - - return { username => $login, password => $password }; + my ($self) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + my $login = trim(delete $params->{'Bugzilla_login'}); + my $password = delete $params->{'Bugzilla_password'}; + + # The token must match the cookie to authenticate the request. + my $login_token = delete $params->{'Bugzilla_login_token'}; + my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie'); + + my $valid = 0; + + # If the web browser accepts cookies, use them. + if ($login_token && $login_cookie) { + my ($time, undef) = split(/-/, $login_token); + + # Regenerate the token based on the information we have. + my $expected_token = issue_hash_token(['login_request', $login_cookie], $time); + $valid = 1 if $expected_token eq $login_token; + $cgi->remove_cookie('Bugzilla_login_request_cookie'); + } + + # WebServices and other local scripts can bypass this check. + # This is safe because we won't store a login cookie in this case. + elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { + $valid = 1; + } + + # Else falls back to the Referer header and accept local URLs. + # Attachments are served from a separate host (ideally), and so + # an evil attachment cannot abuse this check with a redirect. + elsif (my $referer = $cgi->referer) { + my $urlbase = correct_urlbase(); + $valid = 1 if $referer =~ /^\Q$urlbase\E/; + } + + # If the web browser doesn't accept cookies and the Referer header + # is missing, we have no way to make sure that the authentication + # request comes from the user. + elsif ($login && $password) { + ThrowUserError('auth_untrusted_request', {login => $login}); + } + + if (!defined($login) || !defined($password) || !$valid) { + return {failure => AUTH_NODATA}; + } + + return {username => $login, password => $password}; } sub fail_nodata { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - - if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { - ThrowUserError('login_required'); - } - - print $cgi->header(); - $template->process("account/auth/login.html.tmpl", - { 'target' => $cgi->url(-relative=>1) }) - || ThrowTemplateError($template->error()); - exit; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { + ThrowUserError('login_required'); + } + + print $cgi->header(); + $template->process("account/auth/login.html.tmpl", + {'target' => $cgi->url(-relative => 1)}) + || ThrowTemplateError($template->error()); + exit; } 1; diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm index c09f08d24..1983dbd4c 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -23,121 +23,124 @@ use List::Util qw(first); use constant requires_persistence => 0; use constant requires_verification => 0; -use constant can_login => 0; +use constant can_login => 0; sub is_automatic { return $_[0]->login_token ? 0 : 1; } # Note that Cookie never consults the Verifier, it always assumes # it has a valid DB account or it fails. sub get_login_info { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my ($user_id, $login_cookie); - - if (!Bugzilla->request_cache->{auth_no_automatic_login}) { - $login_cookie = $cgi->cookie("Bugzilla_logincookie"); - $user_id = $cgi->cookie("Bugzilla_login"); - - # If cookies cannot be found, this could mean that they haven't - # been made available yet. In this case, look at Bugzilla_cookie_list. - unless ($login_cookie) { - my $cookie = first {$_->name eq 'Bugzilla_logincookie'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $login_cookie = $cookie->value if $cookie; - } - unless ($user_id) { - my $cookie = first {$_->name eq 'Bugzilla_login'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $user_id = $cookie->value if $cookie; - } - - # If the call is for a web service, and an api token is provided, check - # it is valid. - if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { - my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; - my ($token_user_id, undef, undef, $token_type) - = Bugzilla::Token::GetTokenData($api_token); - if (!defined $token_type - || $token_type ne 'api_token' - || $user_id != $token_user_id) - { - ThrowUserError('auth_invalid_token', { token => $api_token }); - } - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my ($user_id, $login_cookie); + + if (!Bugzilla->request_cache->{auth_no_automatic_login}) { + $login_cookie = $cgi->cookie("Bugzilla_logincookie"); + $user_id = $cgi->cookie("Bugzilla_login"); + + # If cookies cannot be found, this could mean that they haven't + # been made available yet. In this case, look at Bugzilla_cookie_list. + unless ($login_cookie) { + my $cookie = first { $_->name eq 'Bugzilla_logincookie' } + @{$cgi->{'Bugzilla_cookie_list'}}; + $login_cookie = $cookie->value if $cookie; + } + unless ($user_id) { + my $cookie = first { $_->name eq 'Bugzilla_login' } + @{$cgi->{'Bugzilla_cookie_list'}}; + $user_id = $cookie->value if $cookie; } - # If no cookies were provided, we also look for a login token - # passed in the parameters of a webservice - my $token = $self->login_token; - if ($token && (!$login_cookie || !$user_id)) { - ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'}); + # If the call is for a web service, and an api token is provided, check + # it is valid. + if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { + my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; + my ($token_user_id, undef, undef, $token_type) + = Bugzilla::Token::GetTokenData($api_token); + if ( !defined $token_type + || $token_type ne 'api_token' + || $user_id != $token_user_id) + { + ThrowUserError('auth_invalid_token', {token => $api_token}); + } } + } + + # If no cookies were provided, we also look for a login token + # passed in the parameters of a webservice + my $token = $self->login_token; + if ($token && (!$login_cookie || !$user_id)) { + ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'}); + } - my $ip_addr = remote_ip(); + my $ip_addr = remote_ip(); - if ($login_cookie && $user_id) { - # Anything goes for these params - they're just strings which - # we're going to verify against the db - trick_taint($ip_addr); - trick_taint($login_cookie); - detaint_natural($user_id); + if ($login_cookie && $user_id) { - my $db_cookie = - $dbh->selectrow_array('SELECT cookie + # Anything goes for these params - they're just strings which + # we're going to verify against the db + trick_taint($ip_addr); + trick_taint($login_cookie); + detaint_natural($user_id); + + my $db_cookie = $dbh->selectrow_array( + 'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ? - AND (ipaddr = ? OR ipaddr IS NULL)', - undef, ($login_cookie, $user_id, $ip_addr)); - - # If the cookie or token is valid, return a valid username. - # If they were not valid and we are using a webservice, then - # throw an error notifying the client. - if (defined $db_cookie && $login_cookie eq $db_cookie) { - # If we logged in successfully, then update the lastused - # time on the login cookie - $dbh->do("UPDATE logincookies SET lastused = NOW() - WHERE cookie = ?", undef, $login_cookie); - return { user_id => $user_id }; - } - elsif (i_am_webservice()) { - ThrowUserError('invalid_cookies_or_token'); - } + AND (ipaddr = ? OR ipaddr IS NULL)', undef, + ($login_cookie, $user_id, $ip_addr) + ); + + # If the cookie or token is valid, return a valid username. + # If they were not valid and we are using a webservice, then + # throw an error notifying the client. + if (defined $db_cookie && $login_cookie eq $db_cookie) { + + # If we logged in successfully, then update the lastused + # time on the login cookie + $dbh->do( + "UPDATE logincookies SET lastused = NOW() + WHERE cookie = ?", undef, $login_cookie + ); + return {user_id => $user_id}; } - - # Either the cookie or token is invalid and we are not authenticating - # via a webservice, or we did not receive a cookie or token. We don't - # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to - # actually throw an error when it gets a bad cookie or token. It should just - # look like there was no cookie or token to begin with. - return { failure => AUTH_NODATA }; + elsif (i_am_webservice()) { + ThrowUserError('invalid_cookies_or_token'); + } + } + + # Either the cookie or token is invalid and we are not authenticating + # via a webservice, or we did not receive a cookie or token. We don't + # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to + # actually throw an error when it gets a bad cookie or token. It should just + # look like there was no cookie or token to begin with. + return {failure => AUTH_NODATA}; } sub login_token { - my ($self) = @_; - my $input = Bugzilla->input_params; - my $usage_mode = Bugzilla->usage_mode; + my ($self) = @_; + my $input = Bugzilla->input_params; + my $usage_mode = Bugzilla->usage_mode; - return $self->{'_login_token'} if exists $self->{'_login_token'}; + return $self->{'_login_token'} if exists $self->{'_login_token'}; - if (!i_am_webservice()) { - return $self->{'_login_token'} = undef; - } + if (!i_am_webservice()) { + return $self->{'_login_token'} = undef; + } - # Check if a token was passed in via requests for WebServices - my $token = trim(delete $input->{'Bugzilla_token'}); - return $self->{'_login_token'} = undef if !$token; + # Check if a token was passed in via requests for WebServices + my $token = trim(delete $input->{'Bugzilla_token'}); + return $self->{'_login_token'} = undef if !$token; - my ($user_id, $login_token) = split('-', $token, 2); - if (!detaint_natural($user_id) || !$login_token) { - return $self->{'_login_token'} = undef; - } + my ($user_id, $login_token) = split('-', $token, 2); + if (!detaint_natural($user_id) || !$login_token) { + return $self->{'_login_token'} = undef; + } - return $self->{'_login_token'} = { - user_id => $user_id, - login_token => $login_token - }; + return $self->{'_login_token'} + = {user_id => $user_id, login_token => $login_token}; } 1; diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm index 653df2bb3..5fc33921b 100644 --- a/Bugzilla/Auth/Login/Env.pm +++ b/Bugzilla/Auth/Login/Env.pm @@ -16,28 +16,31 @@ use parent qw(Bugzilla::Auth::Login); use Bugzilla::Constants; use Bugzilla::Error; -use constant can_logout => 0; -use constant can_login => 0; +use constant can_logout => 0; +use constant can_login => 0; use constant requires_persistence => 0; use constant requires_verification => 0; -use constant is_automatic => 1; -use constant extern_id_used => 1; +use constant is_automatic => 1; +use constant extern_id_used => 1; sub get_login_info { - my ($self) = @_; + my ($self) = @_; - my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; - my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; - my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; + my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; + my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; + my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; - return { failure => AUTH_NODATA } if !$env_email; + return {failure => AUTH_NODATA} if !$env_email; - return { username => $env_email, extern_id => $env_id, - realname => $env_realname }; + return { + username => $env_email, + extern_id => $env_id, + realname => $env_realname + }; } sub fail_nodata { - ThrowCodeError('env_no_email'); + ThrowCodeError('env_no_email'); } 1; diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm index dc35998e4..7786f26c8 100644 --- a/Bugzilla/Auth/Login/Stack.pm +++ b/Bugzilla/Auth/Login/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Login); use fields qw( - _stack - successful + _stack + successful ); use Hash::Util qw(lock_keys); use Bugzilla::Hook; @@ -22,81 +22,87 @@ use Bugzilla::Constants; use List::MoreUtils qw(any); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - my $list = shift; - my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); - lock_keys(%methods); - Bugzilla::Hook::process('auth_login_methods', { modules => \%methods }); - - $self->{_stack} = []; - foreach my $login_method (split(',', $list)) { - my $module = $methods{$login_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + my $list = shift; + my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); + lock_keys(%methods); + Bugzilla::Hook::process('auth_login_methods', {modules => \%methods}); + + $self->{_stack} = []; + foreach my $login_method (split(',', $list)) { + my $module = $methods{$login_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub get_login_info { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - # See Bugzilla::WebService::Server::JSONRPC for where and why - # auth_no_automatic_login is used. - if (Bugzilla->request_cache->{auth_no_automatic_login}) { - next if $object->is_automatic; - } - $result = $object->get_login_info(@_); - $self->{successful} = $object; - - # We only carry on down the stack if this method denied all knowledge. - last unless ($result->{failure} - && ($result->{failure} eq AUTH_NODATA - || $result->{failure} eq AUTH_NO_SUCH_USER)); - - # If none of the methods succeed, it's undef. - $self->{successful} = undef; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + + # See Bugzilla::WebService::Server::JSONRPC for where and why + # auth_no_automatic_login is used. + if (Bugzilla->request_cache->{auth_no_automatic_login}) { + next if $object->is_automatic; } - return $result; + $result = $object->get_login_info(@_); + $self->{successful} = $object; + + # We only carry on down the stack if this method denied all knowledge. + last + unless ($result->{failure} + && ( $result->{failure} eq AUTH_NODATA + || $result->{failure} eq AUTH_NO_SUCH_USER)); + + # If none of the methods succeed, it's undef. + $self->{successful} = undef; + } + return $result; } sub fail_nodata { - my $self = shift; - # We fail from the bottom of the stack. - my @reverse_stack = reverse @{$self->{_stack}}; - foreach my $object (@reverse_stack) { - # We pick the first object that actually has the method - # implemented. - if ($object->can('fail_nodata')) { - $object->fail_nodata(@_); - } + my $self = shift; + + # We fail from the bottom of the stack. + my @reverse_stack = reverse @{$self->{_stack}}; + foreach my $object (@reverse_stack) { + + # We pick the first object that actually has the method + # implemented. + if ($object->can('fail_nodata')) { + $object->fail_nodata(@_); } + } } sub can_login { - my ($self) = @_; - # We return true if any method can log in. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_login; - } - return 0; + my ($self) = @_; + + # We return true if any method can log in. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_login; + } + return 0; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows users to create accounts. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows users to create accounts. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; |