google authenticator support, RT#86743
authorIvan Kohler <ivan@freeside.biz>
Thu, 8 Sep 2022 00:22:51 +0000 (17:22 -0700)
committerIvan Kohler <ivan@freeside.biz>
Thu, 8 Sep 2022 00:22:51 +0000 (17:22 -0700)
FS/FS/AuthCookieHandler.pm
FS/FS/Schema.pm
FS/FS/access_user.pm
debian/control
httemplate/browse/access_user.html
httemplate/edit/process/access_user.html
httemplate/loginout/login.html
httemplate/pref/pref.html
httemplate/pref/set_totp_secret32.html [new file with mode: 0644]

index 93d8ea6..b7d0dbf 100644 (file)
@@ -13,13 +13,13 @@ sub useragent_ip {
 }
 
 sub authen_cred {
-  my( $self, $r, $username, $password ) = @_;
+  my( $self, $r, $username, $password, $totp_code ) = @_;
 
   preuser_setup();
 
   my $info = {};
 
-  unless ( FS::Auth->authenticate($username, $password, $info) ) {
+  unless ( FS::Auth->authenticate($username, $password, $totp_code, $info) ) {
     warn "failed auth $username from ". $self->useragent_ip($r). "\n";
     return undef;
   }
index d884036..61b793b 100644 (file)
@@ -5931,6 +5931,7 @@ sub tables_hashref {
         'username',           'varchar',     '', $char_d, '', '',
         '_password',          'varchar', 'NULL', $char_d, '', '',
         '_password_encoding', 'varchar', 'NULL', $char_d, '', '',
+        'totp_secret32',         'char', 'NULL',      32, '', '',
         'last',               'varchar', 'NULL', $char_d, '', '', 
         'first',              'varchar', 'NULL', $char_d, '', '', 
         'user_custnum',           'int', 'NULL',      '', '', '',
index f23aa77..270f8bb 100644 (file)
@@ -13,6 +13,7 @@ use FS::agent;
 use FS::cust_main;
 use FS::sales;
 use Carp qw( croak );
+use Auth::GoogleAuth;
 
 $DEBUG = 0;
 $me = '[FS::access_user]';
@@ -239,6 +240,7 @@ sub check {
     $self->ut_numbern('usernum')
     || $self->ut_alpha_lower('username')
     || $self->ut_textn('_password')
+    || $self->ut_alphan('totp_secret32')
     || $self->ut_textn('last')
     || $self->ut_textn('first')
     || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum')
@@ -733,6 +735,44 @@ sub change_password_fields {
   FS::Auth->auth_class->change_password_fields( @_ );
 }
 
+=item google_auth
+
+=cut
+
+sub google_auth {
+  my( $self ) = @_;
+  my $issuer = FS::Conf->new->config('company_name'). ' Freeside';
+  my $label = $issuer. ':'. $self->username;
+
+  Auth::GoogleAuth->new({
+    secret => $self->totp_secret32,
+    issuer => $issuer,
+    key_id => $label,
+  });
+
+}
+
+=item set_totp_secret32
+
+=cut
+
+sub set_totp_secret32 {
+  my( $self ) = @_;
+
+  $self->totp_secret32( $self->google_auth->generate_secret32 );
+  $self->replace;
+}
+
+=item totp_qr_code_url
+
+=cut
+
+sub totp_qr_code_url {
+  my( $self ) = @_;
+
+  $self->google_auth->qr_code;
+}
+
 =item locale
 
 =cut
index 479a150..043294f 100644 (file)
@@ -109,7 +109,7 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,
  libspreadsheet-xlsx-perl, libpod-simple-perl, libwebservice-northern911-perl,
  liblocale-codes-perl, liblocale-po-perl, libgeo-uscensus-geocoding-perl,
  libnet-sftp-foreign-perl, libpdf-webkit-perl, libgeo-shapelib-perl,
- libgeo-json-perl
+ libgeo-json-perl, libauth-googleauth-perl
 Conflicts: libparams-classify-perl (>= 0.013-6)
 Replaces: freeside (<<4)
 Breaks: freeside (<<4)
index 446bfe0..6587627 100644 (file)
@@ -49,6 +49,11 @@ my $groups_sub = sub {
 
 };
 
+my $goog_auth_sub = sub {
+  my $access_user = shift;
+  $access_user->totp_secret32 ? 'Enabled' : '';
+};
+
 my $installer_sub = sub {
   my $access_user = shift;
   my @sched_item = $access_user->sched_item or return '';
@@ -66,11 +71,23 @@ my $count_query = 'SELECT COUNT(*) FROM access_user';
 my $link = [ $p.'edit/access_user.html?', 'usernum' ];
 
 my @header = (
-  'Username', 'Full name', 'Groups',    'Installer',    'Customer' );
+  'Username',
+  'Full name',
+  'Groups',
+  'Google Auth',
+  'Installer',
+  'Customer',
+);
 my @fields = (
-  'username', 'name',      $groups_sub, $installer_sub, $cust_sub, );
-my $align = 'lllcl';
-my @links = ( $link, $link, $link, '', '', $cust_link );
+  'username',
+  'name',
+  $groups_sub,
+  $goog_auth_sub,
+  $installer_sub,
+  $cust_sub,
+);
+my $align = 'lllccl';
+my @links = ( $link, $link, $link, '', '', '', $cust_link );
 
 #if ( FS::Conf->new->config('ticket_system') ) {
 #  push @header, 'Ticketing';
index c272620..8e264c1 100644 (file)
@@ -5,7 +5,7 @@
 <%   include( 'elements/process.html',
                  'table'          => 'access_user',
                  'viewall_dir'    => 'browse',
-                 'copy_on_empty'  => [ '_password', '_password_encoding' ],
+                 'copy_on_empty'  => [ '_password', '_password_encoding', 'totp_secret32' ],
                  'clear_on_error' => [ '_password', '_password2' ],
                  'process_m2m'    => { 'link_table'   => 'access_usergroup',
                                        'target_table' => 'access_group',
index 72e9525..1785ea7 100644 (file)
         <TD ALIGN="right">Password: </TD>
         <TD><INPUT TYPE="password" NAME="credential_1" SIZE="13"></TD>
       </TR>
+      <TR>
+        <TD ALIGN="right">One-time code: </TD>
+        <TD><INPUT TYPE="text" NAME="credential_2" SIZE="13"></TD>
+      </TR>
     </TABLE>
     <BR>
  
@@ -42,7 +46,7 @@
 my %error = (
   'no_cookie'       => '', #First login, don't display an error
   'bad_cookie'      => 'Bad Cookie', #timed out?
-  'bad_credentials' => 'Incorrect username / password',
+  'bad_credentials' => 'Incorrect username / password / one-time code',
   #'logout'          => 'You have been logged out.',
 );
 
index 56fde6d..5f68d3e 100644 (file)
     </TABLE>
     <BR>
 
+    <FONT CLASS="fsinnerbox-title"><% emt('Google Authenticator') %></FONT>
+    <TABLE CLASS="fsinnerbox">
+      <TR>
+%       if ( $curuser->totp_secret32 ) {
+          <TD><IMG SRC="<% $curuser->totp_qr_code_url %>"</IMG></TD>
+%       } else {
+          <TD><A HREF="<%$p%>pref/set_totp_secret32.html">Enable</A></TD>
+%       }
+      </TR>
+    </TABLE>
+    <BR>
+
 % }
 
 <FONT CLASS="fsinnerbox-title"><% emt("Interface") %></FONT>
diff --git a/httemplate/pref/set_totp_secret32.html b/httemplate/pref/set_totp_secret32.html
new file mode 100644 (file)
index 0000000..f5676bc
--- /dev/null
@@ -0,0 +1,19 @@
+<& /elements/header.html, mt('Google Authenticator for [_1]', $FS::CurrentUser::CurrentUser->username) &>
+
+Scan this code with the Google Authenticator application on your phone.
+<BR><BR>
+
+<IMG SRC="<% $access_user->totp_qr_code_url %>"></IMG>
+<BR><BR>
+
+Future logins will require a 6-digit code generated by the application.
+
+<& /elements/footer.html &>
+<%init>
+
+my $access_user = $FS::CurrentUser::CurrentUser;
+
+my $error = $access_user->set_totp_secret32 unless length($access_user->totp_secret32);
+die $error if $error; #better error handling for this "shouldn't happen" case?
+
+</%init>