]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 893195 - Allow token based authentication for webservices
authorDave Lawrence <dlawrence@mozilla.com>
Tue, 27 Aug 2013 03:54:32 +0000 (23:54 -0400)
committerDave Lawrence <dlawrence@mozilla.com>
Tue, 27 Aug 2013 03:54:32 +0000 (23:54 -0400)
r=glob,a=sgreen

Bugzilla/Auth.pm
Bugzilla/Auth/Login.pm
Bugzilla/Auth/Login/Cookie.pm
Bugzilla/Auth/Persist/Cookie.pm
Bugzilla/WebService/Server/JSONRPC.pm
Bugzilla/WebService/Server/REST.pm
Bugzilla/WebService/Server/REST/Resources/User.pm
Bugzilla/WebService/User.pm
Bugzilla/WebService/Util.pm

index 412edd5a1d9ae6d3c6446c95c2bea04cf152e36a..c46e554a3bd362e7ae3be5b71dbc1a06df0f518f 100644 (file)
@@ -109,6 +109,15 @@ sub 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 {
     my ($self) = @_;
     my $verifier = $self->{_verifier}->{successful};
@@ -410,6 +419,14 @@ Params:      None
 Returns:     C<true> if users can change their own email address,
              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
 
 =head1 STRUCTURE
index 33d63a425cddab64d07e3e5c6a01d00a5044a8a0..ddc1726894b1838c2fbc7fb940d78534edd4d14c 100644 (file)
@@ -9,7 +9,7 @@ package Bugzilla::Auth::Login;
 
 use 5.10.1;
 use strict;
-use fields qw();
+use fields qw(_login_token);
 
 # 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
index 56bc34f406d817e8407b9a36cc420e4f4c081484..130fab8e332fc8321bd9316311ef323a55a9efbf 100644 (file)
@@ -20,7 +20,8 @@ use List::Util qw(first);
 use constant requires_persistence  => 0;
 use constant requires_verification => 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
 # it has a valid DB account or it fails.
@@ -28,24 +29,35 @@ sub get_login_info {
     my ($self) = @_;
     my $cgi = Bugzilla->cgi;
     my $dbh = Bugzilla->dbh;
+    my ($user_id, $login_cookie);
 
-    my $ip_addr      = remote_ip();
-    my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
-    my $user_id      = $cgi->cookie("Bugzilla_login");
+    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;
+        # 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;
+        }
     }
-    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'});
     }
 
+    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
@@ -78,4 +90,32 @@ sub get_login_info {
     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
+        && $usage_mode ne USAGE_MODE_REST) {
+        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;
index 15a2d490e2b47d2a6097957cfc0c8686618cd375..9681bcea256b93c8b34ac9c2ed3ab4e1b3e1b2e3 100644 (file)
@@ -15,6 +15,8 @@ use Bugzilla::Constants;
 use Bugzilla::Util;
 use Bugzilla::Token;
 
+use Bugzilla::Auth::Login::Cookie qw(login_token);
+
 use List::Util qw(first);
 
 sub new {
@@ -86,6 +88,7 @@ sub logout {
 
     my $dbh = Bugzilla->dbh;
     my $cgi = Bugzilla->cgi;
+    my $input = Bugzilla->input_params;
     $param = {} unless $param;
     my $user = $param->{user} || Bugzilla->user;
     my $type = $param->{type} || LOGOUT_ALL;
@@ -99,16 +102,23 @@ sub logout {
     # 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 not, it's the one we've received.
+    my @login_cookies;
     my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
                        @{$cgi->{'Bugzilla_cookie_list'}};
-    my $login_cookie;
     if ($cookie) {
-        $login_cookie = $cookie->value;
+        push(@login_cookies, $cookie->value);
     }
     else {
-        $login_cookie = $cgi->cookie("Bugzilla_logincookie");
+        push(@login_cookies, $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);
+
+    return if !@login_cookies;
 
     # 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
@@ -117,12 +127,18 @@ sub logout {
     # 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
     # chances? - bbaetz
+    map { trick_taint($_) } @login_cookies;
+    @login_cookies = map { $dbh->quote($_) } @login_cookies;
     if ($type == LOGOUT_KEEP_CURRENT) {
-        $dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
-                 undef, $login_cookie, $user->id);
+        $dbh->do("DELETE FROM logincookies WHERE " .
+                 $dbh->sql_in('cookie', \@login_cookies, 1) .
+                 " AND userid = ?",
+                 undef, $user->id);
     } elsif ($type == LOGOUT_CURRENT) {
-        $dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
-                 undef, $login_cookie, $user->id);
+        $dbh->do("DELETE FROM logincookies WHERE " .
+                 $dbh->sql_in('cookie', \@login_cookies) .
+                 " AND userid = ?",
+                 undef, $user->id);
     } else {
         die("Invalid type $type supplied to logout()");
     }
index e7b3fe7e712652e33b0cef38a0d6ecd1573a0863..5290caa5d08e858d0116bfa2864878cfa02a83cc 100644 (file)
@@ -25,7 +25,7 @@ BEGIN {
 
 use Bugzilla::Error;
 use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(taint_data);
+use Bugzilla::WebService::Util qw(taint_data fix_credentials);
 use Bugzilla::Util;
 
 use HTTP::Message;
@@ -373,6 +373,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);
 
     if ($self->request->method eq 'POST') {
index 454749b5d1dc897f33d679e4eba5e37f46254c78..4145455ecb517768b4dba77d386a174b37c66be3 100644 (file)
@@ -16,7 +16,7 @@ use Bugzilla;
 use Bugzilla::Constants;
 use Bugzilla::Error;
 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 html_quote);
 
 # Load resource modules
@@ -69,7 +69,7 @@ sub handle {
 
     my $params = $self->_retrieve_json_params;
 
-    $self->_fix_credentials($params);
+    fix_credentials($params);
 
     # Fix includes/excludes for each call
     rest_include_exclude($params);
@@ -131,7 +131,7 @@ sub response {
 
     # If accessing through web browser, then display in readable format
     if ($self->content_type eq 'text/html') {
-        $result = $self->json->pretty->canonical->encode($result);
+        $result = $self->json->pretty->canonical->allow_nonref->encode($result);
 
         my $template = Bugzilla->template;
         $content = "";
@@ -162,8 +162,15 @@ sub handle_login {
     # explicitly gives that site their username and password. (This is
     # particularly important for JSONP, which would allow a remote site
     # to use private data without the user's knowledge, unless we had this
-    # protection in place.)
-    if (!grep($_ eq $self->request->method, ('POST', 'PUT'))) {
+    # protection in place.) We do allow this for GET /login as we need to
+    # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we
+    # can also use for Bugzilla_token support. This is OK as it requires
+    # a login and password to be supplied and will fail if they are not
+    # valid for the user.
+    if (!grep($_ eq $self->request->method, ('POST', 'PUT'))
+        && !($self->bz_class_name eq 'Bugzilla::WebService::User'
+            && $self->bz_method_name eq 'login'))
+    {
         # XXX There's no particularly good way for us to get a parameter
         # to Bugzilla->login at this point, so we pass this information
         # around using request_cache, which is a bit of a hack. The
@@ -424,15 +431,6 @@ sub _find_resource {
     return $handler_found;
 }
 
-sub _fix_credentials {
-    my ($self, $params) = @_;
-    # Allow user to pass in &username=foo&password=bar
-    if (exists $params->{'username'} && exists $params->{'password'}) {
-        $params->{'Bugzilla_login'}    = delete $params->{'username'};
-        $params->{'Bugzilla_password'} = delete $params->{'password'};
-    }
-}
-
 sub _best_content_type {
     my ($self, @types) = @_;
     return ($self->_simple_content_negotiation(@types))[0] || '*/*';
@@ -545,15 +543,23 @@ if you have a Bugzilla account by providing your login credentials.
 
 =over
 
-=item Username and password
+=item Login name and password
 
 Pass in as query parameters of any request:
 
-username=fred@bedrock.com&password=ilovewilma
+login=fred@example.com&password=ilovecheese
 
 Remember to URL encode any special characters, which are often seen in passwords and to
 also enable SSL support.
 
+=item Login token
+
+By calling GET /login?login=fred@example.com&password=ilovecheese, you get back
+a C<token> value which can then be passed to each subsequent call as
+authentication. This is useful for third party clients that cannot use cookies
+and do not want to store a user's login and password in the client. You can also
+pass in "token" as a convenience.
+
 =back
 
 =head1 ERRORS
index e2a2ea2609999ea8f35c9d5a70e8777acd1fdeba..539a9313a5204ae957f7813fd84ef3d501b10be7 100644 (file)
@@ -19,6 +19,16 @@ BEGIN {
 
 sub _rest_resources {
     my $rest_resources = [
+        qr{^/login$}, {
+            GET => {
+                method => 'login'
+            }
+        },
+        qr{^/logout$}, {
+            GET => {
+                method => 'logout'
+            }
+        },
         qr{^/valid_login$}, {
             GET => {
                 method => 'valid_login'
index 44938a97a4502d5e2d7993a010c3450dc52ad9f1..ba8640f3d7ee729823f1ea1168234b1fda8940c2 100644 (file)
@@ -19,6 +19,8 @@ use Bugzilla::User;
 use Bugzilla::Util qw(trim);
 use Bugzilla::WebService::Util qw(filter validate translate params_to_objects);
 
+use List::Util qw(first);
+
 # Don't need auth to login
 use constant LOGIN_EXEMPT => {
     login => 1,
@@ -73,14 +75,25 @@ sub login {
     $input_params->{'Bugzilla_password'} = $params->{password};
     $input_params->{'Bugzilla_remember'} = $remember;
 
-    Bugzilla->login();
-    return { id => $self->type('int', Bugzilla->user->id) };
+    my $user = Bugzilla->login();
+
+    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 {
     my $self = shift;
     Bugzilla->logout;
-    return undef;
 }
 
 sub valid_login {
@@ -448,10 +461,12 @@ management of cookies across sessions.
 
 =item B<Returns>
 
-On success, a hash containing one item, C<id>, the numeric id of the
-user that was logged in.  A set of http cookies is also sent with the
-response.  These cookies must be sent along with any future requests
-to the webservice, for the duration of the session.
+On success, a hash containing two items, C<id>, the numeric id of the
+user that was logged in, and a C<token> which can be passed in
+the parameters as authentication in other calls. A set of http cookies
+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>
 
index c24f95923dcc44eb39b2886b294ab6bf45b0583a..be72515329fdef16d026cfa0ce1b81b4f8eeea5a 100644 (file)
@@ -23,6 +23,7 @@ our @EXPORT_OK = qw(
     validate
     translate
     params_to_objects
+    fix_credentials
 );
 
 sub filter ($$;$) {
@@ -146,6 +147,22 @@ sub params_to_objects {
     return \@objects;
 }
 
+sub fix_credentials {
+    my ($params) = @_;
+    # Allow user to pass in login=foo&password=bar as a convenience
+    # even if not calling GET /login. We also do not delete them as
+    # GET /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__
 
 =head1 NAME
@@ -209,6 +226,12 @@ Helps make life simpler for WebService methods that internally create objects
 via both "ids" and "names" fields. Also de-duplicates objects that were loaded
 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.
+
 =head1 B<Methods in need of POD>
 
 =over