MT#62568 subscriber 2FA impl

Change-Id: Iada4c3a6b0d61babe50178b31c773f5b5897c8c1
mr13.4
Rene Krenn 11 months ago
parent c4542d04a2
commit 00a007da16

@ -9,9 +9,9 @@ use HTTP::Status qw(:constants);
__PACKAGE__->set_config({
GET => {
'ReturnContentType' => ['image/png'],#,
'ReturnContentType' => [ 'image/png', 'text/plain' ],#,
},
allowed_roles => [qw/admin reseller ccareadmin ccare/],
allowed_roles => [qw/admin reseller ccareadmin ccare subscriberadmin subscriber/],
});
sub allowed_methods {
@ -27,37 +27,75 @@ sub resource_name {
}
sub item_by_id_valid {
my ($self, $c) = @_;
my $item_rs = $self->item_rs($c);
my $item = $item_rs->first;
$self->error($c, HTTP_BAD_REQUEST, "no OTP") unless $item;
return $item;
}
sub _item_rs {
my ($self, $c) = @_;
my $where;
my $item_rs = $c->model('DB')->resultset('admins')->search({
-and => [
my $item_rs;
if ($c->user->auth_realm =~ /admin/) {
$item_rs = $c->model('DB')->resultset('admins')->search({
-and => [
id => $c->user->id,
enable_2fa => 1,
show_otp_registration_info => 1,
\[ 'length(`me`.`otp_secret`) > ?', '0' ],
]
},{
});
} elsif ($c->user->auth_realm =~ /subscriber/) {
$item_rs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
id => $c->user->id,
enable_2fa => 1,
show_otp_registration_info => 1,
\[ 'length(`me`.`otp_secret`) > ?', '0' ],
]
},{
#'+select' => { '' => \[ 'length(`me`.`otp_secret`)' ], -as => 'otp_secret_length' },
#select => [ { length => 'otp_secret' } ],
#s => [ 'otp_secret_length' ],
});
},{
});
my $show = (NGCP::Panel::Utils::Auth::get_subscriber_enable_2fa($c,$item_rs->first)
and NGCP::Panel::Utils::Auth::get_subscriber_show_otp_registration_info($c,$item_rs->first) ? 1 : 0);
$item_rs = $item_rs->search({
-and => [
\[ '1 = ?', $show ],
],
},{
});
}
return $item_rs;
}
sub get_item_binary_data{
sub return_requested_type {
my ($self, $c, $id, $item, $return_type) = @_;
#$c->log->debug("return_requested_type: " . $return_type);
if ($return_type eq 'text/plain') {
$c->response->status(200);
$c->response->content_type($return_type);
$c->response->body(NGCP::Panel::Utils::Auth::get_otp_secret($c,$item));
return;
} elsif ($return_type eq 'image/png') {
return NGCP::Panel::Role::API::return_requested_type($self, $c, $id, $item, $return_type);
} else {
$self->error($c, HTTP_BAD_REQUEST, 'unsupported accept content type');
}
}
sub get_item_binary_data {
my($self, $c, $id, $item, $return_type) = @_;
#$c->log->debug("get_item_binary_data");
my $data = NGCP::Panel::Utils::Auth::generate_otp_qr($c,$item);
my $t = time();

@ -49,6 +49,7 @@ sub login_index :Path Form {
$c->log->debug("Login::index user=$user, pass=****, realm=$realm");
}
my $res;
my ($u, $d, $t);
if($realm eq 'admin') {
$res = NGCP::Panel::Utils::Auth::perform_auth(
c => $c,
@ -59,7 +60,7 @@ sub login_index :Path Form {
bcrypt_realm => 'admin_bcrypt',
);
} elsif($realm eq 'subscriber') {
my ($u, $d, $t) = split /\@/, $user;
($u, $d, $t) = split /\@/, $user;
if(defined $t) {
# in case username is an email address
$u = $u . '@' . $d;
@ -68,23 +69,47 @@ sub login_index :Path Form {
unless(defined $d) {
$d = $c->req->uri->host;
}
$res = NGCP::Panel::Utils::Auth::perform_subscriber_auth($c, $u, $d, $pass);
$res = NGCP::Panel::Utils::Auth::perform_subscriber_auth(
c => $c,
user => $u,
domain => $d,
pass => $pass,
otp => $otp,
);
}
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('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)}),
);
if($realm eq 'admin') {
my $dbadmin = $c->model('DB')->resultset('admins')->search({
login => $user,
})->first;
$c->stash(show_otp_registration_info => $dbadmin->show_otp_registration_info);
if ($dbadmin->show_otp_registration_info) {
$c->stash(
otp_secret_qr_base64 => encode_base64(${NGCP::Panel::Utils::Auth::generate_otp_qr($c,$dbadmin)}),
);
}
} elsif($realm eq 'subscriber') {
my $sub = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
webusername => $u,
'voip_subscriber.status' => 'active',
($c->config->{features}->{multidomain} ? ('domain.domain' => $d) : ()),
'contract.status' => 'active',
}, {
join => ['domain', 'contract', 'voip_subscriber'],
})->first;
my $show_otp_registration_info = NGCP::Panel::Utils::Auth::get_subscriber_show_otp_registration_info($c,$sub);
$c->stash(show_otp_registration_info => $show_otp_registration_info);
if ($show_otp_registration_info) {
$c->stash(
otp_secret_qr_base64 => encode_base64(${NGCP::Panel::Utils::Auth::generate_otp_qr($c,$sub)}),
);
}
}
} elsif($res && $res == -2) {
$c->log->warn("invalid http login from '".$c->qs($c->req->address)."'");
@ -353,6 +378,7 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
$c->log->debug("Password change user=$user, pass=****, realm=$realm");
}
my $res;
my ($u, $d, $t);
if($realm eq 'admin') {
$res = NGCP::Panel::Utils::Auth::perform_auth(
c => $c,
@ -363,7 +389,7 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
bcrypt_realm => 'admin_bcrypt',
);
} elsif($realm eq 'subscriber') {
my ($u, $d, $t) = split /\@/, $user;
($u, $d, $t) = split /\@/, $user;
if(defined $t) {
# in case username is an email address
$u = $u . '@' . $d;
@ -372,7 +398,13 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
unless(defined $d) {
$d = $c->req->uri->host;
}
$res = NGCP::Panel::Utils::Auth::perform_subscriber_auth($c, $u, $d, $pass);
$res = NGCP::Panel::Utils::Auth::perform_subscriber_auth(
c => $c,
user => $u,
domain => $d,
pass => $pass,
otp => $otp,
);
}
if(defined $res && $res == -3) {
@ -383,14 +415,32 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
$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)}),
);
if($realm eq 'admin') {
my $dbadmin = $c->model('DB')->resultset('admins')->search({
login => $user,
})->first;
$c->stash(show_otp_registration_info => $dbadmin->show_otp_registration_info);
if ($dbadmin->show_otp_registration_info) {
$c->stash(
otp_secret_qr_base64 => encode_base64(${NGCP::Panel::Utils::Auth::generate_otp_qr($c,$dbadmin)}),
);
}
} elsif($realm eq 'subscriber') {
my $sub = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
webusername => $u,
'voip_subscriber.status' => 'active',
($c->config->{features}->{multidomain} ? ('domain.domain' => $d) : ()),
'contract.status' => 'active',
}, {
join => ['domain', 'contract', 'voip_subscriber'],
})->first;
my $show_otp_registration_info = NGCP::Panel::Utils::Auth::get_subscriber_show_otp_registration_info($c,$sub);
$c->stash(show_otp_registration_info => $show_otp_registration_info);
if ($show_otp_registration_info) {
$c->stash(
otp_secret_qr_base64 => encode_base64(${NGCP::Panel::Utils::Auth::generate_otp_qr($c,$sub)}),
);
}
}
} elsif($res && $res == -2) {
$c->log->warn("invalid http login from '".$c->qs($c->req->address)."'");

@ -231,6 +231,7 @@ sub auto :Private {
}
my ($username,$password) = $c->req->headers->authorization_basic;
my ($otp) = $c->request->header('X-OTP');
unless ($username && defined $password) {
$c->user->logout if ($c->user);
@ -245,7 +246,14 @@ sub auto :Private {
if ($d) {
$c->req->headers->authorization_basic($u,$password);
}
my $res = NGCP::Panel::Utils::Auth::perform_subscriber_auth($c, $u, $d, $password);
my $res = NGCP::Panel::Utils::Auth::perform_subscriber_auth(
c => $c,
user => $u,
domain => $d,
pass => $password,
otp => $otp,
skip_otp => ($c->req->uri->path =~ m|^/api/otpsecret/?$| ? 1 : 0),
);
if ($res && $res == -2) {
$c->detach(qw(API::Root banned_user), [$username]);
@ -809,6 +817,23 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
} else {
$auth_user = $authrs->search({webpassword => $pass})->first;
}
if (NGCP::Panel::Utils::Auth::get_subscriber_enable_2fa($c,$auth_user)) {
if (NGCP::Panel::Utils::Auth::verify_otp($c,
NGCP::Panel::Utils::Auth::get_subscriber_otp_secret($c,$auth_user),
$otp,time())) {
NGCP::Panel::Utils::Auth::set_subscriber_show_otp_registration_info($c,$auth_user,0)
if NGCP::Panel::Utils::Auth::get_subscriber_show_otp_registration_info($c,$auth_user);
} else {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({
code => HTTP_FORBIDDEN,
message => "Invalid OTP" })."\n");
$c->log->info("Invalid OTP");
return;
}
}
}
}
}

@ -11,6 +11,7 @@ use MIME::Base32 qw();
use Digest::HMAC_SHA1 qw();
use Imager::QRCode qw();
use URI::Encode qw(uri_encode);
use NGCP::Panel::Utils::Preferences qw();
use NGCP::Panel::Utils::Ldap qw(
auth_ldap_simple
@ -36,6 +37,8 @@ our $OTP_SECRET_LENGTH = 20; #160 Bits
our $OTP_WINDOW = 3;
our $OTP_STEP_SIZE = 30; #secs
my $lock_timeout = 5;
sub check_password {
my $pass = shift // return;
@ -208,7 +211,22 @@ sub is_salted_hash {
}
sub perform_subscriber_auth {
my ($c, $user, $domain, $pass) = @_;
my %params = @_;
my ($c,
$user,
$domain,
$pass,
$otp,
$skip_otp) = @params{qw/
c
user
domain
pass
otp
skip_otp
/};
my $res;
if ($pass && $pass =~ /[^[:ascii:]]/) {
@ -298,6 +316,17 @@ sub perform_subscriber_auth {
}
}
if ($res
and not $skip_otp
and get_subscriber_enable_2fa($c,$sub)) {
if (verify_otp($c,get_subscriber_otp_secret($c,$sub),$otp,time())) {
NGCP::Panel::Utils::Auth::set_subscriber_show_otp_registration_info($c,$sub,0)
if NGCP::Panel::Utils::Auth::get_subscriber_show_otp_registration_info($c,$sub);
} else {
$res = -3;
}
}
$res ? do {
clear_failed_login_attempts($c, $userdom, 'subscriber');
reset_ban_increment_stage($c, $userdom, 'subscriber');
@ -834,9 +863,122 @@ sub create_otp_secret {
}
sub get_subscriber_enable_2fa {
my ($c,$prov_subscriber) = @_;
return 0 unless $prov_subscriber;
my $rs = NGCP::Panel::Utils::Preferences::get_chained_preference_rs(
$c,
'enable_2fa',
$prov_subscriber,
{
type => 'usr',
'order' => [qw/usr prof dom/]
},
);
if ($rs->first) {
return 1 if $rs->first->value;
}
return 0;
}
sub get_subscriber_otp_secret {
my ($c,$prov_subscriber) = @_;
return unless $prov_subscriber;
$prov_subscriber = $c->model('DB')->resultset('provisioning_voip_subscribers')->find({
id => $prov_subscriber->id
},{ for => \"update wait $lock_timeout" });
my $otp_secret;
my $rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c,
prov_subscriber => $prov_subscriber,
attribute => 'otp_secret',
);
if ($rs->first) {
$otp_secret = $rs->first->value;
} else {
$otp_secret = create_otp_secret();
$rs->create({ value => $otp_secret });
}
return $otp_secret;
}
sub get_subscriber_show_otp_registration_info {
my ($c,$prov_subscriber) = @_;
return unless $prov_subscriber;
$prov_subscriber = $c->model('DB')->resultset('provisioning_voip_subscribers')->find({
id => $prov_subscriber->id
},{ for => \"update wait $lock_timeout" });
my $show_otp_registration_info;
my $rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c,
prov_subscriber => $prov_subscriber,
attribute => 'show_otp_registration_info',
);
if ($rs->first) {
$show_otp_registration_info = $rs->first->value;
} else {
$show_otp_registration_info = 1;
$rs->create({ value => $show_otp_registration_info });
}
return $show_otp_registration_info;
}
sub set_subscriber_show_otp_registration_info {
my ($c,$prov_subscriber,$value) = @_;
return unless $prov_subscriber;
$prov_subscriber = $c->model('DB')->resultset('provisioning_voip_subscribers')->find({
id => $prov_subscriber->id
},{ for => \"update wait $lock_timeout" });
my $rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c,
prov_subscriber => $prov_subscriber,
attribute => 'show_otp_registration_info',
);
if ($rs->first) {
$rs->first->update({ value => $value });
} else {
$rs->create({ value => $value });
}
return $value;
}
sub get_otp_secret {
my ($c,$user) = @_;
if (ref $user eq 'NGCP::Panel::Model::DB::admins') {
return $user->otp_secret;
} elsif (ref $user eq 'NGCP::Panel::Model::DB::provisioning_voip_subscribers') {
return get_subscriber_otp_secret($c,$user);
}
}
sub generate_otp_qr {
my ($c,$admin) = @_;
my ($c,$user) = @_;
#<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(
@ -851,12 +993,26 @@ sub generate_otp_qr {
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}
));
#$c->log->debug("xxx: " . ref $user );
my $image;
if (ref $user eq 'NGCP::Panel::Model::DB::admins') {
$image = $qrcode->plot(sprintf("otpauth://totp/%s@%s?secret=%s&issuer=%s",
uri_encode($user->login),
uri_encode($c->req->uri->host),
$user->otp_secret,
'NGCP', # . $c->config->{ngcp_version}
));
} elsif (ref $user eq 'NGCP::Panel::Model::DB::provisioning_voip_subscribers') {
#$c->log->debug("xxx: " . $user->webusername );
#$c->log->debug("yyy: " . $user->domain->domain );
$image = $qrcode->plot(sprintf("otpauth://totp/%s@%s?secret=%s&issuer=%s",
uri_encode($user->webusername),
uri_encode($user->domain->domain),
get_subscriber_otp_secret($c,$user),
'NGCP', # . $c->config->{ngcp_version}
));
}
my $data;
$image->write(data => \$data, type => 'png')

Loading…
Cancel
Save