aboutsummaryrefslogtreecommitdiffstats
path: root/perllib
diff options
context:
space:
mode:
Diffstat (limited to 'perllib')
-rw-r--r--perllib/Catalyst/Authentication/Credential/2FA.pm123
-rw-r--r--perllib/FixMyStreet/App.pm17
-rw-r--r--perllib/FixMyStreet/App/Controller/Alert.pm9
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm3
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Profile.pm26
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm3
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm3
-rw-r--r--perllib/FixMyStreet/DB/Result/User.pm5
8 files changed, 180 insertions, 9 deletions
diff --git a/perllib/Catalyst/Authentication/Credential/2FA.pm b/perllib/Catalyst/Authentication/Credential/2FA.pm
new file mode 100644
index 000000000..2c2054c66
--- /dev/null
+++ b/perllib/Catalyst/Authentication/Credential/2FA.pm
@@ -0,0 +1,123 @@
+package Catalyst::Authentication::Credential::2FA;
+
+use strict;
+use warnings;
+use Auth::GoogleAuth;
+
+our $VERSION = "0.01";
+
+sub new {
+ my ($class, $config, $c, $realm) = @_;
+ my $self = { %$config };
+ bless $self, $class;
+ return $self;
+}
+
+sub authenticate {
+ my ( $self, $c, $realm, $authinfo ) = @_;
+
+ my $userfindauthinfo = {%{$authinfo}};
+ delete($userfindauthinfo->{password});
+
+ my $user_obj = $realm->find_user($userfindauthinfo, $c);
+ if (ref($user_obj)) {
+ # We don't care unless user is a superuser and has a 2FA secret
+ return $user_obj unless $user_obj->is_superuser;
+ return $user_obj unless $user_obj->get_extra_metadata('2fa_secret');
+ return $user_obj if $self->check_2fa($c, $user_obj);
+
+ $c->stash->{template} = 'auth/2faform.html';
+ $c->detach;
+ }
+}
+
+sub check_2fa {
+ my ($self, $c, $user) = @_;
+
+ if (my $code = $c->get_param('2fa_code')) {
+ my $auth = Auth::GoogleAuth->new;
+ my $secret32 = $user->get_extra_metadata('2fa_secret');
+ return 1 if $auth->verify($code, 1, $secret32);
+ $c->stash->{incorrect_code} = 1;
+ }
+ return 0;
+}
+
+__PACKAGE__;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Catalyst::Authentication::Credential::2FA - Authenticate a user
+with a two-factor authentication code.
+
+=head1 SYNOPSIS
+
+ use Catalyst qw/
+ Authentication
+ /;
+
+ package MyApp::Controller::Auth;
+
+ sub login : Local {
+ my ( $self, $c ) = @_;
+
+ $c->authenticate( { username => $c->req->param('username'),
+ password => $c->req->param('password') });
+ }
+
+=head1 DESCRIPTION
+
+This authentication credential checker takes authentication information
+(most often a username), and only passes if a valid 2FA code is then
+entered. It only works for Users that have an is_superuser flag set,
+plus store the 2FA secret in a FixMyStreet::Role::Extra metadata key.
+
+=head1 CONFIGURATION
+
+ # example
+ 'Plugin::Authentication' => {
+ default => {
+ credential => {
+ class => 'MultiFactor',
+ factors => [
+ {
+ class => 'Password',
+ password_field => 'password',
+ password_type => 'self_check',
+ },
+ {
+ class => '2FA',
+ },
+ ],
+ },
+ store => {
+ class => 'DBIx::Class',
+ user_model => 'DB::User',
+ },
+ },
+
+
+=over 4
+
+=item class
+
+The classname used for Credential. This is part of
+L<Catalyst::Plugin::Authentication> and is the method by which
+Catalyst::Authentication::Credential::2FA is loaded as the
+credential validator. For this module to be used, this must be set to
+'2FA'.
+
+=back
+
+=head1 USAGE
+
+Once configured as indicated above, authenticating using this module is a
+matter of calling $c->authenticate() as normal. If you wish to use it in
+combination with e.g. password authentication as well (so it actually is
+two-factor!), check out Catalyst::Authentication::Credential::MultiFactor.
+
+=cut
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm
index e47336b7c..3108c5c01 100644
--- a/perllib/FixMyStreet/App.pm
+++ b/perllib/FixMyStreet/App.pm
@@ -61,10 +61,19 @@ __PACKAGE__->config(
'Plugin::Authentication' => {
default_realm => 'default',
default => {
- credential => { # Catalyst::Authentication::Credential::Password
- class => 'Password',
- password_field => 'password',
- password_type => 'self_check',
+ credential => {
+ class => 'MultiFactor',
+ factors => [
+ # Catalyst::Authentication::Credential::Password
+ {
+ class => 'Password',
+ password_field => 'password',
+ password_type => 'self_check',
+ },
+ {
+ class => '2FA',
+ },
+ ],
},
store => { # Catalyst::Authentication::Store::DBIx::Class
class => 'DBIx::Class',
diff --git a/perllib/FixMyStreet/App/Controller/Alert.pm b/perllib/FixMyStreet/App/Controller/Alert.pm
index 5c9fbad1b..9d522dbc9 100644
--- a/perllib/FixMyStreet/App/Controller/Alert.pm
+++ b/perllib/FixMyStreet/App/Controller/Alert.pm
@@ -281,20 +281,25 @@ then display confirmation page.
sub send_confirmation_email : Private {
my ( $self, $c ) = @_;
+ my $user = $c->stash->{alert}->user;
+
+ # Superusers using 2FA can not log in by code
+ $c->detach( '/page_error_403_access_denied', [] ) if $user->has_2fa;
+
my $token = $c->model("DB::Token")->create(
{
scope => 'alert',
data => {
id => $c->stash->{alert}->id,
type => 'subscribe',
- email => $c->stash->{alert}->user->email
+ email => $user->email
}
}
);
$c->stash->{token_url} = $c->uri_for_email( '/A', $token->token );
- $c->send_email( 'alert-confirm.txt', { to => $c->stash->{alert}->user->email } );
+ $c->send_email( 'alert-confirm.txt', { to => $user->email } );
$c->stash->{email_type} = 'alert';
$c->stash->{template} = 'email_sent.html';
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index 455022e03..06448afde 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -232,6 +232,9 @@ sub process_login : Private {
$c->detach( '/page_error_403_access_denied', [] )
if FixMyStreet->config('SIGNUPS_DISABLED') && !$user->in_storage && !$data->{old_user_id};
+ # Superusers using 2FA can not log in by code
+ $c->detach( '/page_error_403_access_denied', [] ) if $user->has_2fa;
+
if ($data->{old_user_id}) {
# Were logged in as old_user_id, want to switch to $user
if ($user->in_storage) {
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
index 5e6fe6266..a58d2ddf6 100644
--- a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
@@ -157,14 +157,34 @@ sub generate_token : Path('/auth/generate_token') {
$c->stash->{template} = 'auth/generate_token.html';
$c->forward('/auth/get_csrf_token');
+ my $has_2fa = $c->user->get_extra_metadata('2fa_secret');
+
if ($c->req->method eq 'POST') {
$c->forward('/auth/check_csrf_token');
- my $token = mySociety::AuthToken::random_token();
- $c->user->set_extra_metadata('access_token', $token);
+
+ if ($c->get_param('generate_token')) {
+ my $token = mySociety::AuthToken::random_token();
+ $c->user->set_extra_metadata('access_token', $token);
+ $c->stash->{token_generated} = 1;
+ }
+
+ if ($c->get_param('toggle_2fa') && $c->user->is_superuser) {
+ if ($has_2fa) {
+ $c->user->unset_extra_metadata('2fa_secret');
+ $c->stash->{toggle_2fa_off} = 1;
+ } else {
+ my $auth = Auth::GoogleAuth->new;
+ $c->stash->{qr_code} = $auth->qr_code(undef, $c->user->email, 'FixMyStreet');
+ $c->stash->{secret32} = $auth->secret32;
+ $c->user->set_extra_metadata('2fa_secret', $auth->secret32);
+ $c->stash->{toggle_2fa_on} = 1;
+ }
+ }
+
$c->user->update();
- $c->stash->{token_generated} = 1;
}
+ $c->stash->{has_2fa} = $has_2fa ? 1 : 0;
$c->stash->{existing_token} = $c->user->get_extra_metadata('access_token');
}
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index 166c0614d..09430110d 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -1391,6 +1391,9 @@ sub redirect_or_confirm_creation : Private {
return 1;
}
+ # Superusers using 2FA can not log in by code
+ $c->detach( '/page_error_403_access_denied', [] ) if $report->user->has_2fa;
+
# otherwise email or text a confirm token to them.
my $thing = 'email';
if ($report->user->email_verified) {
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index c28039808..2f0ef8c0f 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -478,6 +478,9 @@ sub redirect_or_confirm_creation : Private {
return 1;
}
+ # Superusers using 2FA can not log in by code
+ $c->detach( '/page_error_403_access_denied', [] ) if $update->user->has_2fa;
+
my $data = $c->stash->{token_data};
$data->{id} = $update->id;
$data->{add_alert} = $c->get_param('add_alert') ? 1 : 0;
diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm
index d02039ac3..7e16308b9 100644
--- a/perllib/FixMyStreet/DB/Result/User.pm
+++ b/perllib/FixMyStreet/DB/Result/User.pm
@@ -395,6 +395,11 @@ sub admin_user_body_permissions {
});
}
+sub has_2fa {
+ my $self = shift;
+ return $self->is_superuser && $self->get_extra_metadata('2fa_secret');
+}
+
sub contributing_as {
my ($self, $other, $c, $bodies) = @_;
$bodies = [ keys %$bodies ] if ref $bodies eq 'HASH';