Commit 6ec36256 authored by Dave Lawrence's avatar Dave Lawrence

Bug 893195 - Allow token based authentication for webservices

r=glob,a=justdave
parent 87064aef
...@@ -108,6 +108,15 @@ sub can_logout { ...@@ -108,6 +108,15 @@ sub can_logout {
return $getter->can_logout; 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 { sub user_can_create_account {
my ($self) = @_; my ($self) = @_;
my $verifier = $self->{_verifier}->{successful}; my $verifier = $self->{_verifier}->{successful};
...@@ -409,6 +418,14 @@ Params: None ...@@ -409,6 +418,14 @@ Params: None
Returns: C<true> if users can change their own email address, Returns: C<true> if users can change their own email address,
C<false> otherwise. C<false> otherwise.
=item C<login_token>
Description: If a login token was used instead of a cookie then this
will return the current login token data such as user id
and the token itself.
Params: None
Returns: A hash containing C<login_token> and C<user_id>.
=back =back
=head1 STRUCTURE =head1 STRUCTURE
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
package Bugzilla::Auth::Login; package Bugzilla::Auth::Login;
use strict; use strict;
use fields qw(); use fields qw(_login_token);
# Determines whether or not a user can logout. It's really a subroutine, # Determines whether or not a user can logout. It's really a subroutine,
# but we implement it here as a constant. Override it in subclasses if # but we implement it here as a constant. Override it in subclasses if
......
...@@ -17,7 +17,8 @@ use List::Util qw(first); ...@@ -17,7 +17,8 @@ use List::Util qw(first);
use constant requires_persistence => 0; use constant requires_persistence => 0;
use constant requires_verification => 0; use constant requires_verification => 0;
use constant can_login => 0; use constant can_login => 0;
use constant is_automatic => 1;
sub is_automatic { return $_[0]->login_token ? 0 : 1; }
# Note that Cookie never consults the Verifier, it always assumes # Note that Cookie never consults the Verifier, it always assumes
# it has a valid DB account or it fails. # it has a valid DB account or it fails.
...@@ -25,24 +26,35 @@ sub get_login_info { ...@@ -25,24 +26,35 @@ sub get_login_info {
my ($self) = @_; my ($self) = @_;
my $cgi = Bugzilla->cgi; my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh; my $dbh = Bugzilla->dbh;
my ($user_id, $login_cookie);
my $ip_addr = remote_ip(); if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
my $login_cookie = $cgi->cookie("Bugzilla_logincookie"); $login_cookie = $cgi->cookie("Bugzilla_logincookie");
my $user_id = $cgi->cookie("Bugzilla_login"); $user_id = $cgi->cookie("Bugzilla_login");
# If cookies cannot be found, this could mean that they haven't # If cookies cannot be found, this could mean that they haven't
# been made available yet. In this case, look at Bugzilla_cookie_list. # been made available yet. In this case, look at Bugzilla_cookie_list.
unless ($login_cookie) { unless ($login_cookie) {
my $cookie = first {$_->name eq 'Bugzilla_logincookie'} my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}}; @{$cgi->{'Bugzilla_cookie_list'}};
$login_cookie = $cookie->value if $cookie; $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;
}
} }
unless ($user_id) {
my $cookie = first {$_->name eq 'Bugzilla_login'} # If no cookies were provided, we also look for a login token
@{$cgi->{'Bugzilla_cookie_list'}}; # passed in the parameters of a webservice
$user_id = $cookie->value if $cookie; 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();
if ($login_cookie && $user_id) { if ($login_cookie && $user_id) {
# Anything goes for these params - they're just strings which # Anything goes for these params - they're just strings which
# we're going to verify against the db # we're going to verify against the db
...@@ -75,4 +87,32 @@ sub get_login_info { ...@@ -75,4 +87,32 @@ sub get_login_info {
return { failure => AUTH_NODATA }; return { failure => AUTH_NODATA };
} }
sub login_token {
my ($self) = @_;
my $input = Bugzilla->input_params;
my $usage_mode = Bugzilla->usage_mode;
return $self->{'_login_token'} if exists $self->{'_login_token'};
if ($usage_mode ne USAGE_MODE_XMLRPC
&& $usage_mode ne USAGE_MODE_JSON)
{
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;
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
};
}
1; 1;
...@@ -13,6 +13,8 @@ use Bugzilla::Constants; ...@@ -13,6 +13,8 @@ use Bugzilla::Constants;
use Bugzilla::Util; use Bugzilla::Util;
use Bugzilla::Token; use Bugzilla::Token;
use Bugzilla::Auth::Login::Cookie qw(login_token);
use List::Util qw(first); use List::Util qw(first);
sub new { sub new {
...@@ -84,6 +86,7 @@ sub logout { ...@@ -84,6 +86,7 @@ sub logout {
my $dbh = Bugzilla->dbh; my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi; my $cgi = Bugzilla->cgi;
my $input = Bugzilla->input_params;
$param = {} unless $param; $param = {} unless $param;
my $user = $param->{user} || Bugzilla->user; my $user = $param->{user} || Bugzilla->user;
my $type = $param->{type} || LOGOUT_ALL; my $type = $param->{type} || LOGOUT_ALL;
...@@ -97,16 +100,24 @@ sub logout { ...@@ -97,16 +100,24 @@ sub logout {
# The LOGOUT_*_CURRENT options require the current login cookie. # The LOGOUT_*_CURRENT options require the current login cookie.
# If a new cookie has been issued during this run, that's the current one. # If a new cookie has been issued during this run, that's the current one.
# If not, it's the one we've received. # If not, it's the one we've received.
my @login_cookies;
my $cookie = first {$_->name eq 'Bugzilla_logincookie'} my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}}; @{$cgi->{'Bugzilla_cookie_list'}};
my $login_cookie;
if ($cookie) { if ($cookie) {
$login_cookie = $cookie->value; push(@login_cookies, $cookie->value);
}
elsif ($cookie = $cgi->cookie("Bugzilla_logincookie")) {
push(@login_cookies, $cookie);
} }
else {
$login_cookie = $cgi->cookie("Bugzilla_logincookie") || ''; # If we are a webservice using a token instead of cookie
# then add that as well to the login cookies to delete
if (my $login_token = $user->authorizer->login_token) {
push(@login_cookies, $login_token->{'login_token'});
} }
trick_taint($login_cookie);
# Make sure that @login_cookies is not empty to not break SQL statements.
push(@login_cookies, '') unless @login_cookies;
# These queries use both the cookie ID and the user ID as keys. Even # These queries use both the cookie ID and the user ID as keys. Even
# though we know the userid must match, we still check it in the SQL # though we know the userid must match, we still check it in the SQL
...@@ -115,12 +126,18 @@ sub logout { ...@@ -115,12 +126,18 @@ sub logout {
# logged in and got the same cookie, we could be logging the other # logged in and got the same cookie, we could be logging the other
# user out here. Yes, this is very very very unlikely, but why take # user out here. Yes, this is very very very unlikely, but why take
# chances? - bbaetz # chances? - bbaetz
map { trick_taint($_) } @login_cookies;
@login_cookies = map { $dbh->quote($_) } @login_cookies;
if ($type == LOGOUT_KEEP_CURRENT) { if ($type == LOGOUT_KEEP_CURRENT) {
$dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?", $dbh->do("DELETE FROM logincookies WHERE " .
undef, $login_cookie, $user->id); $dbh->sql_in('cookie', \@login_cookies, 1) .
" AND userid = ?",
undef, $user->id);
} elsif ($type == LOGOUT_CURRENT) { } elsif ($type == LOGOUT_CURRENT) {
$dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?", $dbh->do("DELETE FROM logincookies WHERE " .
undef, $login_cookie, $user->id); $dbh->sql_in('cookie', \@login_cookies) .
" AND userid = ?",
undef, $user->id);
} else { } else {
die("Invalid type $type supplied to logout()"); die("Invalid type $type supplied to logout()");
} }
...@@ -128,7 +145,6 @@ sub logout { ...@@ -128,7 +145,6 @@ sub logout {
if ($type != LOGOUT_KEEP_CURRENT) { if ($type != LOGOUT_KEEP_CURRENT) {
clear_browser_cookies(); clear_browser_cookies();
} }
} }
sub clear_browser_cookies { sub clear_browser_cookies {
......
...@@ -23,7 +23,7 @@ BEGIN { ...@@ -23,7 +23,7 @@ BEGIN {
use Bugzilla::Error; use Bugzilla::Error;
use Bugzilla::WebService::Constants; use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(taint_data); use Bugzilla::WebService::Util qw(taint_data fix_credentials);
use Bugzilla::Util qw(correct_urlbase trim disable_utf8); use Bugzilla::Util qw(correct_urlbase trim disable_utf8);
use HTTP::Message; use HTTP::Message;
...@@ -349,6 +349,10 @@ sub _argument_type_check { ...@@ -349,6 +349,10 @@ sub _argument_type_check {
} }
} }
# Update the params to allow for several convenience key/values
# use for authentication
fix_credentials($params);
Bugzilla->input_params($params); Bugzilla->input_params($params);
if ($self->request->method eq 'POST') { if ($self->request->method eq 'POST') {
......
...@@ -82,6 +82,7 @@ our @ISA = qw(XMLRPC::Deserializer); ...@@ -82,6 +82,7 @@ our @ISA = qw(XMLRPC::Deserializer);
use Bugzilla::Error; use Bugzilla::Error;
use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST); use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST);
use Bugzilla::WebService::Util qw(fix_credentials);
use Scalar::Util qw(tainted); use Scalar::Util qw(tainted);
sub deserialize { sub deserialize {
...@@ -105,6 +106,11 @@ sub deserialize { ...@@ -105,6 +106,11 @@ sub deserialize {
my $params = $som->paramsin; my $params = $som->paramsin;
# This allows positional parameters for Testopia. # This allows positional parameters for Testopia.
$params = {} if ref $params ne 'HASH'; $params = {} if ref $params ne 'HASH';
# Update the params to allow for several convenience key/values
# use for authentication
fix_credentials($params);
Bugzilla->input_params($params); Bugzilla->input_params($params);
return $som; return $som;
} }
......
...@@ -20,6 +20,8 @@ use Bugzilla::WebService::Util qw(filter validate translate params_to_objects); ...@@ -20,6 +20,8 @@ use Bugzilla::WebService::Util qw(filter validate translate params_to_objects);
use List::Util qw(min); use List::Util qw(min);
use List::Util qw(first);
# Don't need auth to login # Don't need auth to login
use constant LOGIN_EXEMPT => { use constant LOGIN_EXEMPT => {
login => 1, login => 1,
...@@ -74,14 +76,25 @@ sub login { ...@@ -74,14 +76,25 @@ sub login {
$input_params->{'Bugzilla_password'} = $params->{password}; $input_params->{'Bugzilla_password'} = $params->{password};
$input_params->{'Bugzilla_remember'} = $remember; $input_params->{'Bugzilla_remember'} = $remember;
Bugzilla->login(); my $user = Bugzilla->login();
return { id => $self->type('int', Bugzilla->user->id) };
my $result = { id => $self->type('int', $user->id) };
# We will use the stored cookie value combined with the user id
# to create a token that can be used with future requests in the
# query parameters
my $login_cookie = first { $_->name eq 'Bugzilla_logincookie' }
@{ Bugzilla->cgi->{'Bugzilla_cookie_list'} };
if ($login_cookie) {
$result->{'token'} = $user->id . "-" . $login_cookie->value;
}
return $result;
} }
sub logout { sub logout {
my $self = shift; my $self = shift;
Bugzilla->logout; Bugzilla->logout;
return undef;
} }
################# #################
...@@ -439,10 +452,12 @@ management of cookies across sessions. ...@@ -439,10 +452,12 @@ management of cookies across sessions.
=item B<Returns> =item B<Returns>
On success, a hash containing one item, C<id>, the numeric id of the On success, a hash containing two items, C<id>, the numeric id of the
user that was logged in. A set of http cookies is also sent with the user that was logged in, and a C<token> which can be passed in
response. These cookies must be sent along with any future requests the parameters as authentication in other calls. A set of http cookies
to the webservice, for the duration of the session. is also sent with the response. These cookies *or* the token can be sent
along with any future requests to the webservice, for the duration of the
session.
=item B<Errors> =item B<Errors>
......
...@@ -20,6 +20,7 @@ our @EXPORT_OK = qw( ...@@ -20,6 +20,7 @@ our @EXPORT_OK = qw(
validate validate
translate translate
params_to_objects params_to_objects
fix_credentials
); );
sub filter ($$;$) { sub filter ($$;$) {
...@@ -143,6 +144,22 @@ sub params_to_objects { ...@@ -143,6 +144,22 @@ sub params_to_objects {
return \@objects; return \@objects;
} }
sub fix_credentials {
my ($params) = @_;
# Allow user to pass in login=foo&password=bar as a convenience
# even if not calling User.login. We also do not delete them as
# User.login requires "login" and "password".
if (exists $params->{'login'} && exists $params->{'password'}) {
$params->{'Bugzilla_login'} = $params->{'login'};
$params->{'Bugzilla_password'} = $params->{'password'};
}
# Allow user to pass token=12345678 as a convenience which becomes
# "Bugzilla_token" which is what the auth code looks for.
if (exists $params->{'token'}) {
$params->{'Bugzilla_token'} = $params->{'token'};
}
}
__END__ __END__
=head1 NAME =head1 NAME
...@@ -205,3 +222,9 @@ parameters passed to a WebService method (the first parameter to this function). ...@@ -205,3 +222,9 @@ parameters passed to a WebService method (the first parameter to this function).
Helps make life simpler for WebService methods that internally create objects Helps make life simpler for WebService methods that internally create objects
via both "ids" and "names" fields. Also de-duplicates objects that were loaded via both "ids" and "names" fields. Also de-duplicates objects that were loaded
by both "ids" and "names". Returns an arrayref of objects. by both "ids" and "names". Returns an arrayref of objects.
=head2 fix_credentials
Allows for certain parameters related to authentication such as Bugzilla_login,
Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in.
This function converts the shorter versions to their respective internal names.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment