MT#53706 rail for generating OTP secret QR code png

Change-Id: I2d8d4ab1d3967a5c7ac720e09278443c0d098866
mr13.3
Rene Krenn 2 months ago
parent b0b646db5b
commit 4bb352e1f8

1
debian/control vendored

@ -77,6 +77,7 @@ Depends:
libhtml-formhandler-perl (>= 0.40026),
libhtml-parser-perl,
libhttp-message-perl,
libimager-qrcode-perl,
libio-compress-lzma-perl,
libio-socket-ip-perl,
libio-string-perl,

@ -0,0 +1,91 @@
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 HTTP::Status qw(:constants);
__PACKAGE__->set_config({
GET => {
'ReturnContentType' => ['image/png'],#,
},
allowed_roles => [qw/admin reseller ccareadmin ccare/],
});
sub allowed_methods {
return [qw/GET OPTIONS HEAD/];
}
sub item_name {
return 'otpsecret';
}
sub resource_name {
return 'otpsecret';
}
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 => [
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' ],
});
return $item_rs;
}
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 $t = time();
return \$data, 'image/png', "qrcode_$t.png";
}
1;

@ -31,10 +31,18 @@ sub login_index :Path Form {
$c->log->debug("login form validated");
my $user = $form->field('username')->value;
my $pass = $form->field('password')->value;
my $otp;
$c->log->debug("Login::index user=$user, pass=****, realm=$realm");
my $res;
if($realm eq 'admin') {
$res = NGCP::Panel::Utils::Auth::perform_auth($c, $user, $pass, 'admin', 'admin_bcrypt');
$res = NGCP::Panel::Utils::Auth::perform_auth(
c => $c,
user => $user,
pass => $pass,
otp => $otp,
realm => 'admin',
bcrypt_realm => 'admin_bcrypt',
);
} elsif($realm eq 'subscriber') {
my ($u, $d, $t) = split /\@/, $user;
if(defined $t) {
@ -294,12 +302,20 @@ sub change_password :Chained('/') :PathPart('changepassword') Args(0) {
$c->log->debug("login form validated");
my $user = $form->field('username')->value;
my $pass = $form->field('password')->value;
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");
my $res;
if($realm eq 'admin') {
$res = NGCP::Panel::Utils::Auth::perform_auth($c, $user, $pass, 'admin', 'admin_bcrypt');
$res = NGCP::Panel::Utils::Auth::perform_auth(
c => $c,
user => $user,
pass => $pass,
otp => $otp,
realm => 'admin',
bcrypt_realm => 'admin_bcrypt',
);
} elsif($realm eq 'subscriber') {
my ($u, $d, $t) = split /\@/, $user;
if(defined $t) {

@ -275,7 +275,15 @@ sub auto :Private {
my ($user, $pass) = $c->req->headers->authorization_basic;
my ($otp) = $c->request->header('X-OTP');
#$c->log->debug("user: " . $user . " pass: " . $pass);
my $res = NGCP::Panel::Utils::Auth::perform_auth($c, $user, $pass, "api_admin" , "api_admin_bcrypt");
my $res = NGCP::Panel::Utils::Auth::perform_auth(
c => $c,
user => $user,
pass => $pass,
otp => $otp,
skip_otp => ($c->req->uri->path =~ m|^/api/otpsecret/?$| ? 1 : 0),
realm => 'api_admin',
bcrypt_realm => 'api_admin_bcrypt',
);
if ($res && $res == -2) {
$c->detach(qw(API::Root banned_user), [$user]);
@ -733,10 +741,14 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
$c->log->info("User not found");
return;
}
if ($res
and $auth_user->enable_2fa
and not verify_otp($auth_user->otp_secret,$otp,time())) {
$res = 0;
if ($auth_user->enable_2fa
and not verify_otp($c,$auth_user->otp_secret,$otp,time())) {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({
code => HTTP_FORBIDDEN,
message => "Invalid OTP" })."\n");
$c->log->info("Invalid OTP");
return;
}
}
} else {

@ -77,7 +77,23 @@ sub get_usr_salted_pass {
}
sub perform_auth {
my ($c, $user, $pass, $realm, $bcrypt_realm) = @_;
my %params = @_;
my ($c,
$user,
$pass,
$otp,
$skip_otp,
$realm,
$bcrypt_realm) = @params{qw/
c
user
pass
otp
skip_otp
realm
bcrypt_realm
/};
my $res;
my $log_failed_login_attempt = 1;
@ -158,8 +174,9 @@ sub perform_auth {
}
if ($res
and not $skip_otp
and $dbadmin->enable_2fa
and not verify_otp($dbadmin->otp_secret,$otp,time())) {
and not verify_otp($c,$dbadmin->otp_secret,$otp,time())) {
$res = -3;
}
@ -811,7 +828,15 @@ sub create_otp_secret {
sub verify_otp {
my ($otp_secret, $otp, $time) = @_;
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 // ""));
return 0 unless $otp;
return 0 unless $otp_secret;
my $secret = MIME::Base32::decode($otp_secret);
my $window = $OTP_WINDOW;

@ -46,6 +46,7 @@ $ua = Test::Collection->new()->ua();
activesubscriberpreferences => 1,
admincerts => 1,
admins => 1,
otpsecret => 1,
applyrewrites => 1,
authtokens => 1,
autoattendants => 1,

Loading…
Cancel
Save