MT#53706 OTP: login, show registration info

Change-Id: I20e6fba357b6bfed868edb8030009683b8df29ef
mr13.3
Rene Krenn 1 year ago
parent 4bb352e1f8
commit 0bd2c64b98

@ -0,0 +1,13 @@
package NGCP::Panel::Block::Login::OtpRegistrationInfo;
use warnings;
use strict;
use parent ("NGCP::Panel::Block::Block");
sub template {
my $self = shift;
return 'login/otp_registration_info.tt';
}
1;

@ -3,8 +3,7 @@ package NGCP::Panel::Controller::API::OTPSecret;
use parent qw/NGCP::Panel::Role::EntitiesItem NGCP::Panel::Role::API/;
use Sipwise::Base;
use Imager::QRCode qw();
use URI::Encode qw(uri_encode);
use NGCP::Panel::Utils::Auth qw();
use HTTP::Status qw(:constants);
@ -59,31 +58,11 @@ sub get_item_binary_data{
my($self, $c, $id, $item, $return_type) = @_;
#<img src="$http_base_url/chart?chs=150x150&chld=M%7c0&cht=qr&chl=otpauth://totp/$string_utils.urlEncode($inheriteduser_name,$template_encoding)@$string_utils.urlEncode($instance_name,$template_encoding)?secret=$otp_secret"/>
my $qrcode = Imager::QRCode->new(
size => 4,
margin => 3,
version => 1,
level => 'M',
casesensitive => 1,
lightcolor => Imager::Color->new(255, 255, 255),
darkcolor => Imager::Color->new(0, 0, 0),
);
my $image = $qrcode->plot(sprintf("otpauth://totp/%s@%s?secret=%s&issuer=%s",
uri_encode($item->login),
uri_encode($c->req->uri->host),
$item->otp_secret,
'NGCP', # . $c->config->{ngcp_version}
));
my $data;
$image->write(data => \$data, type => 'png')
or die $image->errstr;
my $data = NGCP::Panel::Utils::Auth::generate_otp_qr($c,$item);
my $t = time();
return \$data, 'image/png', "qrcode_$t.png";
return $data, 'image/png', "qrcode_$t.png";
}

@ -37,13 +37,15 @@ sub list_admin :PathPart('administrator') :Chained('/') :CaptureArgs(0) {
{ name => "email", search => 1, title => $c->loc("Email") },
$c->user->roles eq 'admin' || $c->user->roles eq 'reseller' ?
(
{ name => "auth_mode", title => $c->loc("Authentication Mode")},
{ name => "acl_role.role", title => $c->loc("Role")},
{ name => "is_master", title => $c->loc("Master") },
{ name => "is_active", title => $c->loc("Active") },
{ name => "read_only", title => $c->loc("Read Only") },
{ name => "show_passwords", title => $c->loc("Show Passwords") },
{ name => "call_data", title => $c->loc("Show CDRs") },
{ name => "billing_data", title => $c->loc("Show Billing Info") },
#{ name => "show_passwords", title => $c->loc("Show Passwords") },
#{ name => "call_data", title => $c->loc("Show CDRs") },
#{ name => "billing_data", title => $c->loc("Show Billing Info") },
{ name => "enable_2fa", title => $c->loc("2FA") },
{ name => "can_reset_password", title => $c->loc("Can Reset Password") },
) : ()
);
@ -152,6 +154,15 @@ sub create :Chained('list_admin') :PathPart('create') :Args(0) :AllowedRole(admi
$form->values->{md5pass} = undef;
$form->values->{auth_mode} ||= 'local';
$form->values->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($password);
if ($form->values->{enable_2fa}) {
$form->values->{enable_2fa} = 1;
$form->values->{otp_secret} = NGCP::Panel::Utils::Auth::create_otp_secret();
$form->values->{show_otp_registration_info} = 1;
} else {
$form->values->{enable_2fa} = 0;
$form->values->{otp_secret} = undef;
$form->values->{show_otp_registration_info} = 0;
}
if ($form->values->{role_id}) {
$form->values->%* = ( $form->values->%*, NGCP::Panel::Utils::UserRole::resolve_flags($c, $form->values->{role_id}) );
@ -267,6 +278,17 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
$form->values->{md5pass} = undef;
$form->values->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($password);
}
if ($form->values->{enable_2fa}) {
$form->values->{enable_2fa} = 1;
unless ($c->stash->{administrator}->otp_secret) {
$form->values->{otp_secret} = NGCP::Panel::Utils::Auth::create_otp_secret();
$form->values->{show_otp_registration_info} = 1;
}
} else {
$form->values->{enable_2fa} = 0;
}
#should be after other fields, to remove all added values, e.g. reseller_id
if($c->stash->{administrator}->login eq NGCP::Panel::Utils::Auth::get_special_admin_login()) {
foreach my $field ($form->fields){

@ -6,6 +6,7 @@ use strict;
use parent 'Catalyst::Controller';
use TryCatch;
use UUID;
use MIME::Base64;
use NGCP::Panel::Form;
@ -16,11 +17,20 @@ use NGCP::Panel::Utils::Subscriber;
sub login_index :Path Form {
my ( $self, $c, $realm ) = @_;
$realm = 'subscriber'
unless($realm && $realm eq 'admin');
my $posted = ($c->req->method eq 'POST');
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Login", $c);
my $form;
if ($c->request->params->{otp}) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::LoginOtp", $c);
} else {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::Login", $c);
}
if (not $realm
or $realm ne 'admin') {
$realm = 'subscriber';
}
$form->process(
posted => $posted,
params => $c->request->params,
@ -32,7 +42,12 @@ sub login_index :Path Form {
my $user = $form->field('username')->value;
my $pass = $form->field('password')->value;
my $otp;
$c->log->debug("Login::index user=$user, pass=****, realm=$realm");
if ($form->field('otp')) {
$otp = $form->field('otp')->value;
$c->log->debug("Login::index user=$user, pass=****, otp=$otp, realm=$realm");
} else {
$c->log->debug("Login::index user=$user, pass=****, realm=$realm");
}
my $res;
if($realm eq 'admin') {
$res = NGCP::Panel::Utils::Auth::perform_auth(
@ -56,9 +71,27 @@ sub login_index :Path Form {
$res = NGCP::Panel::Utils::Auth::perform_subscriber_auth($c, $u, $d, $pass);
}
if($res) {
if(defined $res && $res == -3) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::LoginOtp", $c);
$form->field('username')->value($user);
$form->field('password')->value($pass);
$form->field('otp')->value(undef);
$form->add_form_error($c->loc('Invalid one-time code')) if $otp;
my $dbadmin = $c->model('DB')->resultset('admins')->search({
login => $user,
})->first;
$c->stash(show_otp_registration_info => $dbadmin->show_otp_registration_info);
if ($dbadmin && $dbadmin->show_otp_registration_info) {
$c->stash(
otp_secret_qr_base64 => encode_base64(${NGCP::Panel::Utils::Auth::generate_otp_qr($c,$dbadmin)}),
);
}
} elsif($res && $res == -2) {
$c->log->warn("invalid http login from '".$c->qs($c->req->address)."'");
$c->log->debug("Login::index auth failed");
$form->add_form_error($c->loc('User banned'))
} elsif($res) {
# auth ok
if ($realm eq 'admin') {
use Crypt::JWT qw/encode_jwt/;
@ -99,7 +132,8 @@ sub login_index :Path Form {
} else {
$c->log->warn("invalid http login from '".$c->qs($c->req->address)."'");
$c->log->debug("Login::index auth failed");
$form->add_form_error($c->loc('Invalid username/password'));
$form->add_form_error($c->loc('Invalid username/password'))
}
} else {
# initial get
@ -291,7 +325,14 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
$c->user->logout if $c->user;
my $posted = ($c->req->method eq 'POST');
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::PasswordChange", $c);
my $form;
if ($c->request->params->{otp}) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::PasswordChangeOtp", $c);
} else {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::PasswordChange", $c);
}
$form->process(
posted => $posted,
params => $c->request->params,
@ -305,7 +346,12 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
my $otp;
my $new_pass = $form->field('new_password')->value;
my $new_pass2 = $form->field('new_password2')->value;
$c->log->debug("Password change user=$user, realm=$realm");
if ($form->field('otp')) {
$otp = $form->field('otp')->value;
$c->log->debug("Password change user=$user, pass=****, otp=$otp, realm=$realm");
} else {
$c->log->debug("Password change user=$user, pass=****, realm=$realm");
}
my $res;
if($realm eq 'admin') {
$res = NGCP::Panel::Utils::Auth::perform_auth(
@ -329,7 +375,28 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
$res = NGCP::Panel::Utils::Auth::perform_subscriber_auth($c, $u, $d, $pass);
}
if($res) {
if(defined $res && $res == -3) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::PasswordChangeOtp", $c);
$form->field('username')->value($user);
$form->field('password')->value($pass);
$form->field('otp')->value(undef);
$form->field('new_password')->value($new_pass);
$form->field('new_password')->value($new_pass2);
$form->add_form_error($c->loc('Invalid one-time code')) if $otp;
my $dbadmin = $c->model('DB')->resultset('admins')->search({
login => $user,
})->first;
$c->stash(show_otp_registration_info => $dbadmin->show_otp_registration_info);
if ($dbadmin && $dbadmin->show_otp_registration_info) {
$c->stash(
otp_secret_qr_base64 => encode_base64(${NGCP::Panel::Utils::Auth::generate_otp_qr($c,$dbadmin)}),
);
}
} elsif($res && $res == -2) {
$c->log->warn("invalid http login from '".$c->qs($c->req->address)."'");
$c->log->debug("Login::index auth failed");
$form->add_form_error($c->loc('User banned'))
} elsif($res) {
# auth ok
if ($pass eq $new_pass) {

@ -52,6 +52,9 @@ sub auto :Private {
$c->log->debug("Root::auto enable cache");
NGCP::Panel::Form::dont_use_cache(0);
$is_api_request = 1;
#} elsif ('NGCP::Panel::Controller::Login' eq $c->controller->catalyst_component_name) {
# $c->log->debug("Root::auto disable cache");
# NGCP::Panel::Form::dont_use_cache(0);
} else {
$c->log->debug("Root::auto disable cache");
NGCP::Panel::Form::dont_use_cache(1);

@ -0,0 +1,35 @@
package NGCP::Panel::Field::Password;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler::Field::Password';
sub fif {
my ( $self, $result ) = @_;
return if ( $self->inactive && !$self->_active );
#return '' if $self->password;
return unless $result || $self->has_result;
my $lresult = $result || $self->result;
if ( ( $self->has_result && $self->has_input && !$self->fif_from_value ) ||
( $self->fif_from_value && !defined $lresult->value ) )
{
return defined $lresult->input ? $lresult->input : '';
}
if ( $lresult->has_value ) {
my $value;
if( $self->_can_deflate ) {
$value = $self->_apply_deflation($lresult->value);
}
else {
$value = $lresult->value;
}
return ( defined $value ? $value : '' );
}
elsif ( defined $self->value ) {
# this is because checkboxes and submit buttons have their own 'value'
# needs to be fixed in some better way
return $self->value;
}
return '';
}
no Moose;
1;

@ -14,7 +14,7 @@ has_block 'fields' => (
tag => 'div',
class => [qw(modal-body)],
render_list => [qw(
reseller login auth_mode password email role_id is_master is_active read_only show_passwords call_data billing_data can_reset_password
reseller login auth_mode password email role_id is_master is_active read_only enable_2fa show_passwords call_data billing_data can_reset_password
)],
);

@ -23,7 +23,7 @@ has_field 'auth_mode' => (
has_field 'password' => (type => 'Password', required => 1, label => 'Password');
has_field 'email' => (type => 'Email', required => 0, label => 'Email', maxlength => 255);
for (qw(is_active show_passwords call_data billing_data
is_master is_ccare read_only can_reset_password)) {
is_master is_ccare read_only enable_2fa can_reset_password)) {
has_field $_ => (type => 'Boolean', default_method => \&_set_default);
}
has_field 'save' => (type => 'Submit', element_class => [qw(btn btn-primary)],);
@ -31,7 +31,7 @@ has_block 'fields' => (
tag => 'div',
class => [qw(modal-body)],
render_list => [qw(
login auth_mode password email is_master is_active read_only show_passwords call_data billing_data can_reset_password
login auth_mode password enable_2fa email is_master is_active enable_2fa read_only show_passwords call_data billing_data can_reset_password
)],
);
has_block 'actions' => (tag => 'div', class => [qw(modal-footer)], render_list => [qw(save)],);
@ -47,6 +47,12 @@ sub _set_default {
$field->default(1);
}
if (grep { $field->name eq $_ }
qw(is_active show_passwords call_data billing_data)) {
$field->default(0);
}
if ($field->name eq 'auth_mode') {
$field->default('local');
}

@ -12,7 +12,7 @@ has_block 'fields' => (
tag => 'div',
class => [qw(modal-body)],
render_list => [qw(
reseller login auth_mode password email role_id is_master is_active read_only show_passwords call_data billing_data can_reset_password
reseller login auth_mode password email role_id is_master is_active read_only enable_2fa show_passwords call_data billing_data can_reset_password
)],
);

@ -0,0 +1,56 @@
package NGCP::Panel::Form::LoginOtp;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
use NGCP::Panel::Field::Password qw();
has '+widget_wrapper' => ( default => 'Bootstrap' );
sub build_render_list {
my $self = shift;
my @list = qw(username password);
push(@list,'otp_registration_info') if $self->{ctx}->stash->{'show_otp_registration_info'};
push(@list,"otp","submit");
return \@list;
}
sub build_form_tags {{ error_class => 'label label-secondary'}}
has_field 'username' => (
type => 'Text',
required => 1,
element_attr => { readonly => 1, placeholder => 'Username' },
element_class => [qw/login username-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_field 'password' => (
type => '+NGCP::Panel::Field::Password',
required => 1,
element_attr => { readonly => 1, placeholder => 'Password' },
element_class => [qw/login password-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_block 'otp_registration_info' => (
type => '+NGCP::Panel::Block::Login::OtpRegistrationInfo',
);
has_field 'otp' => (
type => 'Text',
required => 1,
element_attr => { placeholder => 'One-Time Code' },
element_class => [qw/login otp-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_field 'submit' => (
type => 'Submit',
value => 'Sign In',
label => '',
element_class => [qw/button btn btn-primary btn-large/],
);
1;

@ -0,0 +1,71 @@
package NGCP::Panel::Form::PasswordChangeOtp;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
has '+widget_wrapper' => ( default => 'Bootstrap' );
sub build_render_list {
my $self = shift;
my @list = qw(username password);
push(@list,'otp_registration_info') if $self->{ctx}->stash->{'show_otp_registration_info'};
push(@list,"otp","new_password","new_password2", "submit");
return \@list;
}
sub build_form_tags {{ error_class => 'label label-secondary'}}
has_field 'username' => (
type => 'Text',
required => 1,
element_attr => { readonly => 1, placeholder => 'Username' },
element_class => [qw/login username-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_field 'password' => (
type => '+NGCP::Panel::Field::Password',
required => 1,
element_attr => { readonly => 1, placeholder => 'Password' },
element_class => [qw/login password-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_block 'otp_registration_info' => (
type => '+NGCP::Panel::Block::Login::OtpRegistrationInfo',
);
has_field 'otp' => (
type => 'Text',
required => 1,
element_attr => { placeholder => 'One-Time Code' },
element_class => [qw/login otp-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_field 'new_password' => (
type => 'Password',
required => 1,
element_attr => { placeholder => 'New Password' },
element_class => [qw/login password-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_field 'new_password2' => (
type => 'Password',
required => 1,
element_attr => { placeholder => 'New Password Again' },
element_class => [qw/login password-field/],
wrapper_class => [qw/login-fields field control-group/],
);
has_field 'submit' => (
type => 'Submit',
value => 'Submit',
label => '',
element_class => [qw/button btn btn-primary btn-large/],
);
1;

@ -9,6 +9,8 @@ use UUID;
use Bytes::Random::Secure qw();
use MIME::Base32 qw();
use Digest::HMAC_SHA1 qw();
use Imager::QRCode qw();
use URI::Encode qw(uri_encode);
use NGCP::Panel::Utils::Ldap qw(
auth_ldap_simple
@ -175,12 +177,18 @@ sub perform_auth {
if ($res
and not $skip_otp
and $dbadmin->enable_2fa
and not verify_otp($c,$dbadmin->otp_secret,$otp,time())) {
$res = -3;
and $dbadmin->enable_2fa) {
if (verify_otp($c,$dbadmin->otp_secret,$otp,time())) {
$dbadmin->update({
show_otp_registration_info => 0,
}) if ($dbadmin->show_otp_registration_info);
} else {
$res = -3;
}
}
$res > 0 ? do {
(defined $res and $res > 0) ? do {
clear_failed_login_attempts($c, $user, 'admin');
reset_ban_increment_stage($c, $user, 'admin');
}
@ -826,14 +834,49 @@ sub create_otp_secret {
}
sub generate_otp_qr {
my ($c,$admin) = @_;
#<img src="$http_base_url/chart?chs=150x150&chld=M%7c0&cht=qr&chl=otpauth://totp/$string_utils.urlEncode($inheriteduser_name,$template_encoding)@$string_utils.urlEncode($instance_name,$template_encoding)?secret=$otp_secret"/>
my $qrcode = Imager::QRCode->new(
size => 4,
margin => 3,
version => 1,
level => 'M',
casesensitive => 1,
#i_background => Imager::Color->new("#FFF"),
#background => Imager::Color->new("#FFF"),
lightcolor => Imager::Color->new(255, 255, 255, 0.0),
darkcolor => Imager::Color->new(0, 0, 0),
);
my $image = $qrcode->plot(sprintf("otpauth://totp/%s@%s?secret=%s&issuer=%s",
uri_encode($admin->login),
uri_encode($c->req->uri->host),
$admin->otp_secret,
'NGCP', # . $c->config->{ngcp_version}
));
my $data;
$image->write(data => \$data, type => 'png')
or die $image->errstr;
return \$data;
}
sub verify_otp {
my ($c, $otp_secret, $otp, $time) = @_;
#$c->log->debug("verify otp: $otp, secret $otp_secret");
#return 1;
$c->log->debug("verify otp: " . ($otp // "") . ", secret " . ($otp_secret // ""));
my @dbg_msg = ();
push(@dbg_msg,$otp) if $otp;
push(@dbg_msg,"no otp provided") unless $otp;
push(@dbg_msg,'secret ' . $otp_secret) if $otp_secret;
push(@dbg_msg,'no secret') unless $otp_secret;
push(@dbg_msg,'time ' . $time) if $time;
$c->log->debug("verify otp: " . join(', ',@dbg_msg));
return 0 unless $otp;
return 0 unless $otp_secret;
@ -843,7 +886,7 @@ sub verify_otp {
for my $i (-int(($OTP_WINDOW - 1) / 2) .. int($OTP_WINDOW / 2)) {
my $hash = _verify_otp_bytes($secret, int($time / $OTP_STEP_SIZE) + $i);
return 1 if $hash == $otp;
return 1 if $hash eq $otp;
}
return 0;

@ -1090,6 +1090,9 @@ body.login {
.login-fields .password-field {
background: url(../img/signin/password.png) no-repeat;
}
.login-fields .otp-field {
background: url(../img/signin/otp.png) no-repeat;
}
.login-actions {
float: left;
width: 100%;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,17 @@
<table width="100%" cellspacing="0" cellpadding="0"><tbody><tr><td>
1. Install the <b>Google Authenticator</b> app on you mobile device.
</td></tr>
<tr><td align="left">
<a target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" style="border-radius: 10px; width: 140px; height: 50px;"></a>
<a target="_blank" href="https://apps.apple.com/us/app/google-authenticator/id388497605?itsct=apps_box_badge&itscg=30200"><img src="https://tools.applemediaservices.com/api/badges/download-on-the-app-store/black/en-us?size=140x34&releaseDate=1284940800&h=7fc6b39acc8ae5a42ad4b87ff8c7f88d" alt="Download on the App Store" style="border-radius: 10px; width: 140px; height: 34px; padding:8px;"></a>
</td></tr>
<tr><td>
2. Open the app to register your NGCP account (scan QR code below).
</td></tr>
<tr><td align="left">
<img src='data:image/png;base64,[% otp_secret_qr_base64 %]'/>
</td></tr>
<tr><td>
3. Use the app to generate the requested verification code.
</td></tr>
</tbody></table>
Loading…
Cancel
Save