summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
authorByron Jones <glob@mozilla.com>2015-09-01 07:01:20 +0200
committerByron Jones <glob@mozilla.com>2015-09-01 07:01:20 +0200
commit421ff7f194875db9634ea783d9dd5b6111f19df3 (patch)
tree5806e9f3001fa4f33ba85aa94856b70a7f878cf8 /Bugzilla
parentbcc93f83a64a76cd73501eaefaf5fd073fbc3f0d (diff)
downloadbugzilla-421ff7f194875db9634ea783d9dd5b6111f19df3.tar.gz
bugzilla-421ff7f194875db9634ea783d9dd5b6111f19df3.tar.xz
Bug 1197073 - add support for 2fa using totp (eg. google authenticator)
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/Auth.pm21
-rw-r--r--Bugzilla/Auth/Login/Cookie.pm12
-rw-r--r--Bugzilla/Auth/Verify/DB.pm11
-rw-r--r--Bugzilla/Install.pm1
-rw-r--r--Bugzilla/Install/Requirements.pm15
-rw-r--r--Bugzilla/MFA.pm64
-rw-r--r--Bugzilla/MFA/TOTP.pm79
-rw-r--r--Bugzilla/Token.pm9
-rw-r--r--Bugzilla/User.pm38
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/User.pm10
-rw-r--r--Bugzilla/WebService/User.pm14
11 files changed, 268 insertions, 6 deletions
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 88eadbe19..a4f2dd9a9 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -33,7 +33,7 @@ use fields qw(
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Mailer;
-use Bugzilla::Util qw(datetime_from);
+use Bugzilla::Util qw(datetime_from i_am_webservice);
use Bugzilla::User::Setting ();
use Bugzilla::Auth::Login::Stack;
use Bugzilla::Auth::Verify::Stack;
@@ -93,9 +93,28 @@ sub login {
}
$user->set_authorizer($self);
+ # trigger multi-factor auth. once verified the provider calls mfa_verified()
+ if ($self->{_info_getter}->{successful}->requires_verification
+ && $user->mfa
+ && !Bugzilla->sudoer
+ && !i_am_webservice()
+ ) {
+ $user->mfa_provider->prompt({ user => $user, type => $type });
+ exit;
+ }
+
return $self->_handle_login_result($login_info, $type);
}
+sub mfa_verified {
+ my ($self, $user, $type) = @_;
+ require Bugzilla::Auth::Login::CGI;
+ $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new();
+ $self->_handle_login_result({ user => $user }, $type);
+ print Bugzilla->cgi->redirect('index.cgi');
+ exit;
+}
+
sub successful_info_getter {
my ($self) = @_;
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index 738d26b21..0b5842523 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -40,7 +40,7 @@ sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
- my ($user_id, $login_cookie);
+ my ($user_id, $login_cookie, $is_internal);
if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
$login_cookie = $cgi->cookie("Bugzilla_logincookie");
@@ -82,6 +82,7 @@ sub get_login_info {
{
ThrowUserError('auth_invalid_token', { token => $api_token });
}
+ $is_internal = 1;
}
}
}
@@ -112,6 +113,15 @@ sub get_login_info {
# If the cookie is valid, return a valid username.
if (defined $db_cookie && $login_cookie eq $db_cookie) {
+
+ # forbid logging in with a cookie if only api-keys are allowed
+ if (i_am_webservice() && !$is_internal) {
+ my $user = Bugzilla::User->new({ id => $user_id, cache => 1 });
+ if ($user->settings->{api_key_only}->{value} eq 'on') {
+ ThrowUserError('invalid_cookies_or_token');
+ }
+ }
+
# If we logged in successfully, then update the lastused
# time on the login cookie
$dbh->do("UPDATE logincookies SET lastused = NOW()
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index aaa1b6c87..ea86346e1 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -53,6 +53,7 @@ sub check_credentials {
}
my $password = $login_data->{password};
+ return { failure => AUTH_NODATA } unless defined $login_data->{password};
my $real_password_crypted = $user->cryptpassword;
# Using the internal crypted password as the salt,
@@ -105,6 +106,16 @@ sub check_credentials {
$user->update();
}
+ if (i_am_webservice() && $user->settings->{api_key_only}->{value} eq 'on') {
+ # api-key verification happens in Auth/Login/APIKey
+ # token verification happens in Auth/Login/Cookie
+ # if we get here from an api call then we must be using user/pass
+ return {
+ failure => AUTH_ERROR,
+ user_error => 'invalid_auth_method',
+ };
+ }
+
return $login_data;
}
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index d5f4f04cd..8a1113741 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -99,6 +99,7 @@ sub SETTINGS {
possible_duplicates => { options => ['on', 'off'], default => 'on' },
# 2011-10-11 glob@mozilla.com -- Bug 301656
requestee_cc => { options => ['on', 'off'], default => 'on' },
+ api_key_only => { options => ['on', 'off'], default => 'off' },
}
};
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index 41a4b04e1..e653e5f8c 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -411,6 +411,20 @@ sub OPTIONAL_MODULES {
version => '0',
feature => ['elasticsearch'],
},
+
+ # multi factor auth - totp
+ {
+ package => 'Auth-GoogleAuth',
+ module => 'Auth::GoogleAuth',
+ version => '1.01',
+ feature => ['mfa'],
+ },
+ {
+ package => 'GD-Barcode-QRcode',
+ module => 'GD::Barcode::QRcode',
+ version => '0',
+ feature => ['mfa'],
+ },
);
my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
@@ -434,6 +448,7 @@ use constant FEATURE_FILES => (
patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
updates => ['Bugzilla/Update.pm'],
memcached => ['Bugzilla/Memcache.pm'],
+ mfa => ['Bugzilla/MFA/TOTP.pm'],
);
# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
diff --git a/Bugzilla/MFA.pm b/Bugzilla/MFA.pm
new file mode 100644
index 000000000..564f124cd
--- /dev/null
+++ b/Bugzilla/MFA.pm
@@ -0,0 +1,64 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::MFA;
+use strict;
+
+sub new {
+ my ($class, $user) = @_;
+ return bless({ user => $user }, $class);
+}
+
+# abstract methods
+
+# api call, returns required data to user-prefs enrollment page
+sub enroll {}
+
+# called after the user has confirmed enrollment
+sub enrolled {}
+
+# display page with verification prompt
+sub prompt {}
+
+# throws errors if code is invalid
+sub check {}
+
+# during-login verification
+sub check_login {}
+
+
+# helpers
+
+sub property_get {
+ my ($self, $name) = @_;
+ return scalar Bugzilla->dbh->selectrow_array(
+ "SELECT value FROM profile_mfa WHERE user_id = ? AND name = ?",
+ undef, $self->{user}->id, $name);
+}
+
+sub property_set {
+ my ($self, $name, $value) = @_;
+ Bugzilla->dbh->do(
+ "INSERT INTO profile_mfa (user_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?",
+ undef, $self->{user}->id, $name, $value, $value);
+}
+
+sub property_delete {
+ my ($self, $name) = @_;
+ Bugzilla->dbh->do(
+ "DELETE FROM profile_mfa WHERE user_id = ? AND name = ?",
+ undef, $self->{user}->id, $name);
+}
+
+sub property_delete_all {
+ my ($self) = @_;
+ Bugzilla->dbh->do(
+ "DELETE FROM profile_mfa WHERE user_id",
+ undef, $self->{user}->id);
+}
+
+1;
diff --git a/Bugzilla/MFA/TOTP.pm b/Bugzilla/MFA/TOTP.pm
new file mode 100644
index 000000000..95e3d89aa
--- /dev/null
+++ b/Bugzilla/MFA/TOTP.pm
@@ -0,0 +1,79 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::MFA::TOTP;
+use strict;
+use parent 'Bugzilla::MFA';
+
+use Auth::GoogleAuth;
+use Bugzilla::Error;
+use Bugzilla::Token qw( issue_session_token );
+use Bugzilla::Util qw( template_var generate_random_password );
+use GD::Barcode::QRcode;
+use MIME::Base64 qw( encode_base64 );
+
+sub _auth {
+ my ($self) = @_;
+ return Auth::GoogleAuth->new({
+ secret => $self->property_get('secret') // $self->property_get('secret.temp'),
+ issuer => template_var('terms')->{BugzillaTitle},
+ key_id => $self->{user}->login,
+ });
+}
+
+sub enroll {
+ my ($self) = @_;
+
+ # create a new secret for the user
+ # store it in secret.temp to avoid overwriting a valid secret
+ $self->property_set('secret.temp', generate_random_password(16));
+
+ # build the qr code
+ my $auth = $self->_auth();
+ my $otpauth = $auth->qr_code(undef, undef, undef, 1);
+ my $png = GD::Barcode::QRcode->new($otpauth, { Version => 10, ModuleSize => 3 })->plot()->png();
+ return { png => encode_base64($png), secret32 => $auth->secret32 };
+}
+
+sub enrolled {
+ my ($self) = @_;
+
+ # make the temporary secret permanent
+ $self->property_set('secret', $self->property_get('secret.temp'));
+ $self->property_delete('secret.temp');
+}
+
+sub prompt {
+ my ($self, $params) = @_;
+ my $template = Bugzilla->template;
+
+ my $vars = {
+ user => $params->{user},
+ token => scalar issue_session_token('mfa', $params->{user}),
+ type => $params->{type},
+ };
+
+ print Bugzilla->cgi->header();
+ $template->process('mfa/totp/verify.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub check {
+ my ($self, $code) = @_;
+ $self->_auth()->verify($code)
+ || ThrowUserError('mfa_totp_bad_code');
+}
+
+sub check_login {
+ my ($self, $user) = @_;
+ my $cgi = Bugzilla->cgi;
+
+ $self->check($cgi->param('code') // '');
+ $user->authorizer->mfa_verified($user, $cgi->param('type'));
+}
+
+1;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index b7227144f..6e1414db6 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -219,11 +219,12 @@ sub IssuePasswordToken {
}
sub issue_session_token {
+ my ($data, $user) = @_;
# Generates a random token, adds it to the tokens table, and returns
# the token to the caller.
- my $data = shift;
- return _create_token(Bugzilla->user->id, 'session', $data);
+ $user //= Bugzilla->user;
+ return _create_token($user->id, 'session', $data);
}
sub issue_hash_token {
@@ -657,12 +658,14 @@ although they can be used separately.
=over
-=item C<issue_session_token($event)>
+=item C<issue_session_token($event [, $user])>
Description: Creates and returns a token used internally.
Params: $event - The event which needs to be stored in the DB for future
reference/checks.
+ $user - The user to bind the token with. Uses the current user
+ if not provided.
Returns: A unique token.
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 4a0c2166d..d3bb807b3 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -108,6 +108,7 @@ sub DB_COLUMNS {
$dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
'profiles.password_change_required',
'profiles.password_change_reason',
+ 'profiles.mfa',
),
}
@@ -125,6 +126,7 @@ use constant VALIDATORS => {
is_enabled => \&_check_is_enabled,
password_change_required => \&Bugzilla::Object::check_boolean,
password_change_reason => \&_check_password_change_reason,
+ mfa => \&_check_mfa,
};
sub UPDATE_COLUMNS {
@@ -138,6 +140,7 @@ sub UPDATE_COLUMNS {
is_enabled
password_change_required
password_change_reason
+ mfa
);
push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
return @cols;
@@ -266,6 +269,10 @@ sub update {
$self->derive_regexp_groups();
}
+ if (exists $changes->{mfa} && $self->mfa eq '') {
+ $dbh->do("DELETE FROM profile_mfa WHERE user_id = ?", undef, $self->id);
+ }
+
# Logout the user if necessary.
Bugzilla->logout_user($self)
if (!$options->{keep_session}
@@ -357,6 +364,13 @@ sub _check_password_change_reason {
: '';
}
+sub _check_mfa {
+ my ($self, $provider) = @_;
+ $provider = lc($provider // '');
+ return 'TOTP' if $provider eq 'totp';
+ return '';
+}
+
################################################################################
# Mutators
################################################################################
@@ -394,6 +408,15 @@ sub set_disabledtext {
$self->set('disable_mail', 1) if !$self->is_enabled;
}
+sub set_mfa {
+ my ($self, $value) = @_;
+ if ($value eq '' && $self->mfa) {
+ $self->mfa_provider->property_delete_all();
+ }
+ $self->set('mfa', $value);
+ delete $self->{mfa_provider};
+}
+
sub set_groups {
my $self = shift;
$self->_set_groups(GROUP_MEMBERSHIP, @_);
@@ -561,6 +584,21 @@ sub authorizer {
return $self->{authorizer};
}
+sub mfa { $_[0]->{mfa} }
+sub mfa_provider {
+ my ($self) = @_;
+ my $mfa = $self->{mfa} || return undef;
+ return $self->{mfa_provider} if exists $self->{mfa_provider};
+ if ($mfa eq 'TOTP') {
+ require Bugzilla::MFA::TOTP;
+ $self->{mfa_provider} = Bugzilla::MFA::TOTP->new($self);
+ }
+ else {
+ $self->{mfa_provider} = undef;
+ }
+ return $self->{mfa_provider};
+}
+
# Generate a string to identify the user by name + login if the user
# has a name or by login only if she doesn't.
sub identity {
diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm
index badbc94b2..b9ecc21ba 100644
--- a/Bugzilla/WebService/Server/REST/Resources/User.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/User.pm
@@ -58,7 +58,15 @@ sub _rest_resources {
return { $param => [ $_[0] ] };
}
}
- }
+ },
+ qr{^/user/mfa/([^/]+)/enroll$}, {
+ GET => {
+ method => 'mfa_enroll',
+ params => sub {
+ return { provider => $_[0] };
+ }
+ },
+ },
];
return $rest_resources;
}
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index 8592d809c..2d3f5f185 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -416,6 +416,20 @@ sub _login_to_hash {
return $item;
}
+#
+# MFA
+#
+
+sub mfa_enroll {
+ my ($self, $params) = @_;
+ my $provider_name = lc($params->{provider});
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ $user->set_mfa($provider_name);
+ my $provider = $user->mfa_provider // die "Unknown MTA provider\n";
+ return $provider->enroll();
+}
+
1;
__END__