From 4bb352e1f8e6a7cdf7f39e129fd316d3b0b13194 Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Thu, 27 Feb 2025 13:49:02 +0100 Subject: [PATCH] MT#53706 rail for generating OTP secret QR code png Change-Id: I2d8d4ab1d3967a5c7ac720e09278443c0d098866 --- debian/control | 1 + lib/NGCP/Panel/Controller/API/OTPSecret.pm | 91 ++++++++++++++++++++++ lib/NGCP/Panel/Controller/Login.pm | 20 ++++- lib/NGCP/Panel/Controller/Root.pm | 22 ++++-- lib/NGCP/Panel/Utils/Auth.pm | 31 +++++++- t/api-rest/api-root.t | 1 + 6 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/OTPSecret.pm diff --git a/debian/control b/debian/control index 090dec2aaa..cdbb75f6b9 100644 --- a/debian/control +++ b/debian/control @@ -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, diff --git a/lib/NGCP/Panel/Controller/API/OTPSecret.pm b/lib/NGCP/Panel/Controller/API/OTPSecret.pm new file mode 100644 index 0000000000..7feb3388c1 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/OTPSecret.pm @@ -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) = @_; + + # + 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; + diff --git a/lib/NGCP/Panel/Controller/Login.pm b/lib/NGCP/Panel/Controller/Login.pm index 43ad80bfcc..594af9cd83 100644 --- a/lib/NGCP/Panel/Controller/Login.pm +++ b/lib/NGCP/Panel/Controller/Login.pm @@ -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) { diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index cec7167781..df6bd60154 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -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 { diff --git a/lib/NGCP/Panel/Utils/Auth.pm b/lib/NGCP/Panel/Utils/Auth.pm index 9e667eeecb..b212baddb0 100644 --- a/lib/NGCP/Panel/Utils/Auth.pm +++ b/lib/NGCP/Panel/Utils/Auth.pm @@ -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; diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index e26fe84dba..a1e878839c 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -46,6 +46,7 @@ $ua = Test::Collection->new()->ua(); activesubscriberpreferences => 1, admincerts => 1, admins => 1, + otpsecret => 1, applyrewrites => 1, authtokens => 1, autoattendants => 1,