diff --git a/lib/NGCP/Panel/Block/Login/OtpRegistrationInfo.pm b/lib/NGCP/Panel/Block/Login/OtpRegistrationInfo.pm
new file mode 100644
index 0000000000..f68d7e5cfb
--- /dev/null
+++ b/lib/NGCP/Panel/Block/Login/OtpRegistrationInfo.pm
@@ -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;
diff --git a/lib/NGCP/Panel/Controller/API/OTPSecret.pm b/lib/NGCP/Panel/Controller/API/OTPSecret.pm
index 7feb3388c1..87eb144e43 100644
--- a/lib/NGCP/Panel/Controller/API/OTPSecret.pm
+++ b/lib/NGCP/Panel/Controller/API/OTPSecret.pm
@@ -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) = @_;
- #
- 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";
}
diff --git a/lib/NGCP/Panel/Controller/Administrator.pm b/lib/NGCP/Panel/Controller/Administrator.pm
index 37295c2638..17adf77c05 100644
--- a/lib/NGCP/Panel/Controller/Administrator.pm
+++ b/lib/NGCP/Panel/Controller/Administrator.pm
@@ -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){
diff --git a/lib/NGCP/Panel/Controller/Login.pm b/lib/NGCP/Panel/Controller/Login.pm
index 594af9cd83..1b0b00f1b6 100644
--- a/lib/NGCP/Panel/Controller/Login.pm
+++ b/lib/NGCP/Panel/Controller/Login.pm
@@ -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) {
diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm
index df6bd60154..758e6893f7 100644
--- a/lib/NGCP/Panel/Controller/Root.pm
+++ b/lib/NGCP/Panel/Controller/Root.pm
@@ -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);
diff --git a/lib/NGCP/Panel/Field/Password.pm b/lib/NGCP/Panel/Field/Password.pm
new file mode 100644
index 0000000000..d8b24fa3fe
--- /dev/null
+++ b/lib/NGCP/Panel/Field/Password.pm
@@ -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;
diff --git a/lib/NGCP/Panel/Form/Administrator/Admin.pm b/lib/NGCP/Panel/Form/Administrator/Admin.pm
index 15b4034cf9..368a355cb8 100644
--- a/lib/NGCP/Panel/Form/Administrator/Admin.pm
+++ b/lib/NGCP/Panel/Form/Administrator/Admin.pm
@@ -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
)],
);
diff --git a/lib/NGCP/Panel/Form/Administrator/Reseller.pm b/lib/NGCP/Panel/Form/Administrator/Reseller.pm
index 38381ae498..3780687886 100644
--- a/lib/NGCP/Panel/Form/Administrator/Reseller.pm
+++ b/lib/NGCP/Panel/Form/Administrator/Reseller.pm
@@ -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');
}
diff --git a/lib/NGCP/Panel/Form/Administrator/System.pm b/lib/NGCP/Panel/Form/Administrator/System.pm
index d7420dd0ec..7ac6267b19 100644
--- a/lib/NGCP/Panel/Form/Administrator/System.pm
+++ b/lib/NGCP/Panel/Form/Administrator/System.pm
@@ -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
)],
);
diff --git a/lib/NGCP/Panel/Form/LoginOtp.pm b/lib/NGCP/Panel/Form/LoginOtp.pm
new file mode 100644
index 0000000000..2486c34036
--- /dev/null
+++ b/lib/NGCP/Panel/Form/LoginOtp.pm
@@ -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;
+
diff --git a/lib/NGCP/Panel/Form/PasswordChangeOtp.pm b/lib/NGCP/Panel/Form/PasswordChangeOtp.pm
new file mode 100644
index 0000000000..2f59d4a3fa
--- /dev/null
+++ b/lib/NGCP/Panel/Form/PasswordChangeOtp.pm
@@ -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;
+
diff --git a/lib/NGCP/Panel/Utils/Auth.pm b/lib/NGCP/Panel/Utils/Auth.pm
index b212baddb0..947a98f9fa 100644
--- a/lib/NGCP/Panel/Utils/Auth.pm
+++ b/lib/NGCP/Panel/Utils/Auth.pm
@@ -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) = @_;
+
+ #
+ 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;
diff --git a/share/static/css/application.css b/share/static/css/application.css
index fae0aba2a8..3121d52edd 100644
--- a/share/static/css/application.css
+++ b/share/static/css/application.css
@@ -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%;
diff --git a/share/static/img/signin/otp.png b/share/static/img/signin/otp.png
new file mode 100644
index 0000000000..68bbfabf5b
Binary files /dev/null and b/share/static/img/signin/otp.png differ
diff --git a/share/templates/login/otp_registration_info.tt b/share/templates/login/otp_registration_info.tt
new file mode 100644
index 0000000000..8317d9c1cc
--- /dev/null
+++ b/share/templates/login/otp_registration_info.tt
@@ -0,0 +1,17 @@
+