}
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;
}
'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', '', '', '',
use FS::cust_main;
use FS::sales;
use Carp qw( croak );
+use Auth::GoogleAuth;
$DEBUG = 0;
$me = '[FS::access_user]';
$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')
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
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)
};
+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 '';
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';
<% 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',
<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>
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.',
);
</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>
--- /dev/null
+<& /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>