From 2c8a11029a3325ee492fc040b087497eb96b5374 Mon Sep 17 00:00:00 2001 From: Kirill Solomko Date: Fri, 24 Apr 2020 15:53:38 +0200 Subject: [PATCH] TT#80305 add JWT authentication for admin users * /admin_login_jwt now returns a JWT token for admin users and also the JWT token is supported in the authorization process for the admin requests Change-Id: I987640d46bd8a339a959a6b2efb65b6dce06bf8c --- lib/NGCP/Panel.pm | 18 ++++ .../Panel/Authentication/Credential/JWT.pm | 7 +- .../Authentication/Store/RoleFromRealm.pm | 2 +- lib/NGCP/Panel/Controller/Root.pm | 85 +++++++++++++++++++ lib/NGCP/Panel/Utils/Admin.pm | 23 +++-- 5 files changed, 125 insertions(+), 10 deletions(-) diff --git a/lib/NGCP/Panel.pm b/lib/NGCP/Panel.pm index 679b757086..45077128be 100644 --- a/lib/NGCP/Panel.pm +++ b/lib/NGCP/Panel.pm @@ -256,6 +256,24 @@ __PACKAGE__->config( }, use_session => 0, }, + api_admin_jwt => { + credential => { + class => '+NGCP::Panel::Authentication::Credential::JWT', + username_jwt => 'username', + username_field => 'login', + id_jwt => 'id', + id_field => 'id', + jwt_key => _get_jwt_key(), + debug => 1, + alg => 'HS256', + }, + store => { + class => 'DBIx::Class', + user_model => 'DB::admins', + store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm', + }, + use_session => 0, + }, api_admin_system => { credential => { class => 'HTTP', diff --git a/lib/NGCP/Panel/Authentication/Credential/JWT.pm b/lib/NGCP/Panel/Authentication/Credential/JWT.pm index f76e56e725..ad1dec2a8c 100644 --- a/lib/NGCP/Panel/Authentication/Credential/JWT.pm +++ b/lib/NGCP/Panel/Authentication/Credential/JWT.pm @@ -46,7 +46,12 @@ sub authenticate { my ($token) = $auth_header =~ m/Bearer\s+(.*)/; return unless ($token); - $c->log->debug("Found token: $token") if $self->debug; + if ($token =~ /^a=(.+)$/) { + $c->log->debug("Found admin token: $token") if $self->debug; + $token = $1; + } else { + $c->log->debug("Found token: $token") if $self->debug; + } my $jwt_data; try { diff --git a/lib/NGCP/Panel/Authentication/Store/RoleFromRealm.pm b/lib/NGCP/Panel/Authentication/Store/RoleFromRealm.pm index a303c196d5..909f2678e9 100644 --- a/lib/NGCP/Panel/Authentication/Store/RoleFromRealm.pm +++ b/lib/NGCP/Panel/Authentication/Store/RoleFromRealm.pm @@ -6,7 +6,7 @@ sub roles { my ($self) = @_; if ($self->auth_realm) { - for my $auth_type (qw/admin_bcrypt admin api_admin_cert api_admin_http api_admin api_admin_bcrypt/) { + for my $auth_type (qw/admin_bcrypt admin api_admin_cert api_admin_http api_admin api_admin_bcrypt api_admin_jwt/) { if ($auth_type eq $self->auth_realm) { if ($self->_user->is_ccare) { $self->_user->is_superuser ? return "ccareadmin" diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index 9a868da9d8..d5d2ade9d9 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -181,6 +181,19 @@ sub auto :Private { $c->log->warn("invalid api system login from '".$c->qs($c->req->address)."'"); } + $self->api_apply_fake_time($c); + return 1; + } elsif ($c->req->headers->header("Authorization") && + $c->req->headers->header("Authorization") =~ m/^Bearer(\s+)a=/) { + $c->log->debug("++++++ Root::auto API request with admin JWT"); + my $realm = "api_admin_jwt"; + my $res = $c->authenticate({}, $realm); + + unless ($c->user_exists) { + $c->log->debug("+++++ invalid api admin JWT login"); + # $c->log->warn("invalid api system login from '".$c->qs($c->req->address)."'"); + } + $self->api_apply_fake_time($c); return 1; } elsif ($c->req->headers->header("Authorization") && @@ -497,6 +510,78 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') { return; } +sub admin_login_jwt :Chained('/') :PathPart('admin_login_jwt') :Args(0) :Method('POST') { + my ($self, $c) = @_; + + use JSON qw/encode_json decode_json/; + use Crypt::JWT qw/encode_jwt/; + + my $user = $c->req->body_data->{username} // ''; + my $pass = $c->req->body_data->{password} // ''; + + my $key = $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{jwt_key}; + my $relative_exp = $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{relative_exp}; + my $alg = $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{alg}; + + $c->response->content_type('application/json'); + + unless ($key) { + $c->response->status(HTTP_INTERNAL_SERVER_ERROR); + $c->response->body(encode_json({ code => HTTP_INTERNAL_SERVER_ERROR, + message => "No JWT key has been configured" })."\n"); + $c->log->error("No JWT key has been configured"); + return; + } + + my $raw_key = pack('H*', $key); + + unless ($user && $pass) { + $c->response->status(HTTP_UNPROCESSABLE_ENTITY); + $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, + message => "No username or password given" })."\n"); + $c->log->error("No username or password given"); + return; + } + + my $authrs = $c->model('DB')->resultset('admins')->search({ + login => $user, + is_active => 1, + }); + + my $usr_salted_pass; + my $auth_user = $authrs->first; + my $result = {}; + + if ($auth_user && $auth_user->id) { + $usr_salted_pass = NGCP::Panel::Utils::Admin::get_usr_salted_pass($auth_user->saltedpass, $pass); + } + + if ($usr_salted_pass && $usr_salted_pass eq $auth_user->saltedpass) { + my $jwt_data = { + id => $auth_user->id, + username => $auth_user->login, + }; + $result->{jwt} = 'a='.encode_jwt( + payload => $jwt_data, + key => $raw_key, + alg => $alg, + $relative_exp ? (relative_exp => $relative_exp) : (), + ); + $result->{id} = int($auth_user->id // 0); + } else { + $c->response->status(HTTP_FORBIDDEN); + $c->response->body(encode_json({ code => HTTP_FORBIDDEN, + message => "User not found" })."\n"); + $c->log->error("User not found"); + return; + } + + $c->res->body(encode_json($result)); + $c->res->code(HTTP_OK); # 200 + + return; +} + sub api_apply_fake_time :Private { my ($self, $c) = @_; my $allow_fake_client_time = 0; diff --git a/lib/NGCP/Panel/Utils/Admin.pm b/lib/NGCP/Panel/Utils/Admin.pm index 559d602585..1a034e1bf3 100644 --- a/lib/NGCP/Panel/Utils/Admin.pm +++ b/lib/NGCP/Panel/Utils/Admin.pm @@ -28,6 +28,18 @@ sub generate_salted_hash { return $b64salt . '$' . $b64hash; } +sub get_usr_salted_pass { + my ($saltedpass, $pass) = @_; + my ($db_b64salt, $db_b64hash) = split /\$/, $saltedpass; + my $salt = de_base64($db_b64salt); + my $usr_b64hash = en_base64(bcrypt_hash({ + key_nul => 1, + cost => get_bcrypt_cost(), + salt => $salt, + }, $pass)); + return $db_b64salt . '$' . $usr_b64hash; +} + sub perform_auth { my ($c, $user, $pass, $realm, $bcrypt_realm) = @_; my $res; @@ -39,19 +51,14 @@ sub perform_auth { }) if $user; if(defined $dbadmin && defined $dbadmin->saltedpass) { $c->log->debug("login via bcrypt"); - my ($db_b64salt, $db_b64hash) = split /\$/, $dbadmin->saltedpass; - my $salt = de_base64($db_b64salt); - my $usr_b64hash = en_base64(bcrypt_hash({ - key_nul => 1, - cost => get_bcrypt_cost(), - salt => $salt, - }, $pass)); + my $saltedpass = $dbadmin->saltedpass; + my $usr_salted_pass = get_usr_salted_pass($saltedpass, $pass); # fetch again to load user into session etc (otherwise we could # simply compare the two hashes here :( $res = $c->authenticate( { login => $user, - saltedpass => $db_b64salt . '$' . $usr_b64hash, + saltedpass => $usr_salted_pass, 'dbix_class' => { searchargs => [{ -and => [