1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4#
5# This Source Code Form is "Incompatible With Secondary Licenses", as
6# defined by the Mozilla Public License, v. 2.0.
7
8package Bugzilla::Auth::Persist::Cookie;
9
10use 5.10.1;
11use strict;
12use warnings;
13
14use fields qw();
15
16use Bugzilla::Constants;
17use Bugzilla::Util;
18use Bugzilla::Token;
19
20use List::Util qw(first);
21
22sub new {
23    my ($class) = @_;
24    my $self = fields::new($class);
25    return $self;
26}
27
28sub persist_login {
29    my ($self, $user) = @_;
30    my $dbh = Bugzilla->dbh;
31    my $cgi = Bugzilla->cgi;
32    my $input_params = Bugzilla->input_params;
33
34    my $ip_addr;
35    if ($input_params->{'Bugzilla_restrictlogin'}) {
36        $ip_addr = remote_ip();
37        # The IP address is valid, at least for comparing with itself in a
38        # subsequent login
39        trick_taint($ip_addr);
40    }
41
42    $dbh->bz_start_transaction();
43
44    my $login_cookie =
45        Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
46
47    $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
48              VALUES (?, ?, ?, NOW())",
49              undef, $login_cookie, $user->id, $ip_addr);
50
51    # Issuing a new cookie is a good time to clean up the old
52    # cookies.
53    $dbh->do("DELETE FROM logincookies WHERE lastused < "
54             . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-',
55                                   MAX_LOGINCOOKIE_AGE, 'DAY'));
56
57    $dbh->bz_commit_transaction();
58
59    # We do not want WebServices to generate login cookies.
60    # All we need is the login token for User.login.
61    return $login_cookie if i_am_webservice();
62
63    # Prevent JavaScript from accessing login cookies.
64    my %cookieargs = ('-httponly' => 1);
65
66    # Remember cookie only if admin has told so
67    # or admin didn't forbid it and user told to remember.
68    if ( Bugzilla->params->{'rememberlogin'} eq 'on' ||
69         (Bugzilla->params->{'rememberlogin'} ne 'off' &&
70          $input_params->{'Bugzilla_remember'} &&
71          $input_params->{'Bugzilla_remember'} eq 'on') )
72    {
73        # Not a session cookie, so set an infinite expiry
74        $cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
75    }
76    if (Bugzilla->params->{'ssl_redirect'}) {
77        # Make these cookies only be sent to us by the browser during
78        # HTTPS sessions, if we're using SSL.
79        $cookieargs{'-secure'} = 1;
80    }
81
82    $cgi->send_cookie(-name => 'Bugzilla_login',
83                      -value => $user->id,
84                      %cookieargs);
85    $cgi->send_cookie(-name => 'Bugzilla_logincookie',
86                      -value => $login_cookie,
87                      %cookieargs);
88}
89
90sub logout {
91    my ($self, $param) = @_;
92
93    my $dbh = Bugzilla->dbh;
94    my $cgi = Bugzilla->cgi;
95    my $input = Bugzilla->input_params;
96    $param = {} unless $param;
97    my $user = $param->{user} || Bugzilla->user;
98    my $type = $param->{type} || LOGOUT_ALL;
99
100    if ($type == LOGOUT_ALL) {
101        $dbh->do("DELETE FROM logincookies WHERE userid = ?",
102                 undef, $user->id);
103        return;
104    }
105
106    # The LOGOUT_*_CURRENT options require the current login cookie.
107    # If a new cookie has been issued during this run, that's the current one.
108    # If not, it's the one we've received.
109    my @login_cookies;
110    my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
111                       @{$cgi->{'Bugzilla_cookie_list'}};
112    if ($cookie) {
113        push(@login_cookies, $cookie->value);
114    }
115    elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) {
116        push(@login_cookies, $cookie);
117    }
118
119    # If we are a webservice using a token instead of cookie
120    # then add that as well to the login cookies to delete
121    if (my $login_token = $user->authorizer->login_token) {
122        push(@login_cookies, $login_token->{'login_token'});
123    }
124
125    # Make sure that @login_cookies is not empty to not break SQL statements.
126    push(@login_cookies, '') unless @login_cookies;
127
128    # These queries use both the cookie ID and the user ID as keys. Even
129    # though we know the userid must match, we still check it in the SQL
130    # as a sanity check, since there is no locking here, and if the user
131    # logged out from two machines simultaneously, while someone else
132    # logged in and got the same cookie, we could be logging the other
133    # user out here. Yes, this is very very very unlikely, but why take
134    # chances? - bbaetz
135    map { trick_taint($_) } @login_cookies;
136    @login_cookies = map { $dbh->quote($_) } @login_cookies;
137    if ($type == LOGOUT_KEEP_CURRENT) {
138        $dbh->do("DELETE FROM logincookies WHERE " .
139                 $dbh->sql_in('cookie', \@login_cookies, 1) .
140                 " AND userid = ?",
141                 undef, $user->id);
142    } elsif ($type == LOGOUT_CURRENT) {
143        $dbh->do("DELETE FROM logincookies WHERE " .
144                 $dbh->sql_in('cookie', \@login_cookies) .
145                 " AND userid = ?",
146                 undef, $user->id);
147    } else {
148        die("Invalid type $type supplied to logout()");
149    }
150
151    if ($type != LOGOUT_KEEP_CURRENT) {
152        clear_browser_cookies();
153    }
154
155}
156
157sub clear_browser_cookies {
158    my $cgi = Bugzilla->cgi;
159    $cgi->remove_cookie('Bugzilla_login');
160    $cgi->remove_cookie('Bugzilla_logincookie');
161    $cgi->remove_cookie('sudo');
162}
163
1641;
165