TT#126601 Implement /api/authtokens endpoint

* the endpoint will receive "type" (expires|onetime)
	    and "expires" (positive integer representing seconds)
	  * type will define the expiray method for the token;
	    onetime: the token expires as soon as it's used, or
	    after "expires" seconds if not used
	    expires: the token can be used multiple times until
	    it expires according to the "expires" param value
	  * login_jwt endpoint for generating the JWT token for
	    subscribers has been enhanced to accept the "token"
	    param, containing the token generated using the
	    /api/authtokens endpoint
	  * admin_login_jwt endpoint for generating the JWT token
	    for admins has been enhanced to accept the "token"
	    param, containing the token generated using the
	    /api/authtokens endpoint
	  * login_jwt and amin_login_jwt will respond with 403
	    "Forbidden" if the token role stored in Redis does
	    not match the role of the user that generated it
	  * /api/authtokens is hidden from documentation for now

Change-Id: I4eb76c2b08f2e24774fa84ba0ccf7412ce8670e8
(cherry picked from commit 9b422ddabf)
mr9.5.4
Flaviu Mates 4 years ago committed by Kirill Solomko
parent 944823b780
commit db8f0feff6

@ -0,0 +1,74 @@
package NGCP::Panel::Controller::API::AuthTokens;
use Sipwise::Base;
use Data::HAL qw();
use Data::HAL::Link qw();
use File::Basename;
use File::Find::Rule;
use HTTP::Headers qw();
use HTTP::Status qw(:constants);
sub allowed_methods{
return [qw/POST OPTIONS/];
}
use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::AuthTokens/;
sub api_description {
return '';
};
sub query_params {
return [
];
}
sub resource_name{
return 'authtokens';
}
sub dispatch_path{
return '/api/authtokens/';
}
sub relation{
return 'http://purl.org/sipwise/ngcp-api/#rel-authtokens';
}
__PACKAGE__->set_config({
allowed_roles => [qw/admin reseller ccare ccareadmin subscriber subscriberadmin/],
});
sub POST :Allow {
my ($self, $c) = @_;
my $resource = $self->get_valid_post_data(
c => $c,
media_type => 'application/json',
);
return unless $resource;
my $form = $self->get_form($c);
return unless $self->validate_form(
c => $c,
resource => $resource,
form => $form,
);
if($c->user->roles eq "reseller") {
$resource->{reseller_id} = $c->user->reseller_id;
}
my $res = {};
$res->{token} = $self->generate_auth_token($c, $resource);
$c->response->status(HTTP_CREATED);
$c->response->body(JSON::to_json($res));
return;
}
1;
# vim: set tabstop=4 expandtab:

@ -81,6 +81,7 @@ sub GET : Allow {
"PbxDeviceProfilePreferenceDefs" => 1, "PbxDeviceProfilePreferenceDefs" => 1,
"PbxFieldDevicePreferenceDefs" => 1, "PbxFieldDevicePreferenceDefs" => 1,
"MetaConfigDefs" => 1, "MetaConfigDefs" => 1,
"AuthTokens" => 1,
}; };
my $colls = NGCP::Panel::Utils::API::get_collections_files; my $colls = NGCP::Panel::Utils::API::get_collections_files;

@ -503,6 +503,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
use JSON qw/encode_json decode_json/; use JSON qw/encode_json decode_json/;
use Crypt::JWT qw/encode_jwt/; use Crypt::JWT qw/encode_jwt/;
my $auth_token = $c->req->body_data->{token} // '';
my $user = $c->req->body_data->{username} // ''; my $user = $c->req->body_data->{username} // '';
my $pass = $c->req->body_data->{password} // ''; my $pass = $c->req->body_data->{password} // '';
@ -522,82 +523,112 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
my $raw_key = pack('H*', $key); my $raw_key = pack('H*', $key);
unless ($user && $pass) { my $auth_user;
$c->response->status(HTTP_UNPROCESSABLE_ENTITY); if ($auth_token) {
$c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}});
message => "No username or password given" })."\n"); unless ($redis) {
$c->log->error("No username or password given"); $c->response->status(HTTP_INTERNAL_SERVER_ERROR);
return; $c->response->body(encode_json({ code => HTTP_INTERNAL_SERVER_ERROR,
} message => "Internal Server Error" })."\n");
$c->log->error("Could not connect to Redis");
if ($pass =~ /[^[:ascii:]]/) { return;
$c->response->status(HTTP_UNPROCESSABLE_ENTITY); }
$c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, my $type = $redis->hget("auth_token:$auth_token", "type");
message => "'password' contains invalid characters" })."\n"); my $role = $redis->hget("auth_token:$auth_token", "role");
$c->log->error("'password' contains invalid characters"); my $user_id = $redis->hget("auth_token:$auth_token", "user_id");
return; $redis->del("auth_token:$auth_token") if ($type eq 'onetime');
} unless (grep {$role eq $_} qw/subscriber subscriberadmin/) {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({ code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->error("Wrong auth_token role");
return;
}
my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
'me.id' => $user_id,
'voip_subscriber.status' => 'active',
'contract.status' => 'active',
}, {
join => ['contract', 'voip_subscriber'],
});
$auth_user = $authrs->first if ($authrs->first);
} else {
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 ($u, $d, $t) = split(/\@/, $user, 3); if ($pass =~ /[^[:ascii:]]/) {
if(defined $t) { $c->response->status(HTTP_UNPROCESSABLE_ENTITY);
# in case username is an email address $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY,
$u = $u . '@' . $d; message => "'password' contains invalid characters" })."\n");
$d = $t; $c->log->error("'password' contains invalid characters");
} return;
unless(defined $d) { }
$d = $c->req->uri->host;
}
my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
webusername => $u,
'voip_subscriber.status' => 'active',
'domain.domain' => $d,
'contract.status' => 'active',
}, {
join => ['domain', 'contract', 'voip_subscriber'],
});
my $auth_user; my ($u, $d, $t) = split(/\@/, $user, 3);
if ($authrs->first) { if(defined $t) {
my $password = $authrs->first->webpassword; # in case username is an email address
if (length $password > 40) { $u = $u . '@' . $d;
my @splitted_pass = split /\$/, $password; $d = $t;
if (scalar @splitted_pass == 3) { }
#password is bcrypted with lower cost unless(defined $d) {
my ($cost, $db_b64salt, $db_b64hash) = @splitted_pass; $d = $c->req->uri->host;
my $salt = de_base64($db_b64salt); }
my $usr_b64hash = en_base64(bcrypt_hash({ my $authrs = $c->model('DB')->resultset('provisioning_voip_subscribers')->search({
key_nul => 1, webusername => $u,
cost => $cost, 'voip_subscriber.status' => 'active',
salt => $salt, 'domain.domain' => $d,
}, $pass)); 'contract.status' => 'active',
if ($db_b64hash eq $usr_b64hash) { }, {
#upgrade password to bigger cost join => ['domain', 'contract', 'voip_subscriber'],
$salt = rand_bits(128); });
my $b64salt = en_base64($salt);
my $b64hash = en_base64(bcrypt_hash({ if ($authrs->first) {
my $password = $authrs->first->webpassword;
if (length $password > 40) {
my @splitted_pass = split /\$/, $password;
if (scalar @splitted_pass == 3) {
#password is bcrypted with lower cost
my ($cost, $db_b64salt, $db_b64hash) = @splitted_pass;
my $salt = de_base64($db_b64salt);
my $usr_b64hash = en_base64(bcrypt_hash({
key_nul => 1,
cost => $cost,
salt => $salt,
}, $pass));
if ($db_b64hash eq $usr_b64hash) {
#upgrade password to bigger cost
$salt = rand_bits(128);
my $b64salt = en_base64($salt);
my $b64hash = en_base64(bcrypt_hash({
key_nul => 1,
cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(),
salt => $salt,
}, $pass));
$authrs->first->update({webpassword => $b64salt . '$' . $b64hash});
$auth_user = $authrs->first;
}
}
elsif (scalar @splitted_pass == 2) {
#password is bcrypted with proper cost
my ($db_b64salt, $db_b64hash) = @splitted_pass;
my $salt = de_base64($db_b64salt);
my $usr_b64hash = en_base64(bcrypt_hash({
key_nul => 1, key_nul => 1,
cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(), cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(),
salt => $salt, salt => $salt,
}, $pass)); }, $pass));
$authrs->first->update({webpassword => $b64salt . '$' . $b64hash}); $auth_user = $authrs->search({webpassword => $db_b64salt . '$' . $usr_b64hash})->first;
$auth_user = $authrs->first;
} }
} }
elsif (scalar @splitted_pass == 2) { else {
#password is bcrypted with proper cost $auth_user = $authrs->search({webpassword => $pass})->first;
my ($db_b64salt, $db_b64hash) = @splitted_pass;
my $salt = de_base64($db_b64salt);
my $usr_b64hash = en_base64(bcrypt_hash({
key_nul => 1,
cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(),
salt => $salt,
}, $pass));
$auth_user = $authrs->search({webpassword => $db_b64salt . '$' . $usr_b64hash})->first;
} }
} }
else {
$auth_user = $authrs->search({webpassword => $pass})->first;
}
} }
my $result = {}; my $result = {};
@ -634,6 +665,7 @@ sub admin_login_jwt :Chained('/') :PathPart('admin_login_jwt') :Args(0) :Method(
use JSON qw/encode_json decode_json/; use JSON qw/encode_json decode_json/;
use Crypt::JWT qw/encode_jwt/; use Crypt::JWT qw/encode_jwt/;
my $auth_token = $c->req->body_data->{token} // '';
my $user = $c->req->body_data->{username} // ''; my $user = $c->req->body_data->{username} // '';
my $pass = $c->req->body_data->{password} // ''; my $pass = $c->req->body_data->{password} // '';
@ -653,36 +685,72 @@ sub admin_login_jwt :Chained('/') :PathPart('admin_login_jwt') :Args(0) :Method(
my $raw_key = pack('H*', $key); my $raw_key = pack('H*', $key);
unless ($user && $pass) { my $auth_user;
$c->response->status(HTTP_UNPROCESSABLE_ENTITY); if ($auth_token) {
$c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}});
message => "No username or password given" })."\n"); unless ($redis) {
$c->log->error("No username or password given"); $c->response->status(HTTP_INTERNAL_SERVER_ERROR);
return; $c->response->body(encode_json({ code => HTTP_INTERNAL_SERVER_ERROR,
} message => "Internal Server Error" })."\n");
$c->log->error("Could not connect to Redis");
return;
}
my $type = $redis->hget("auth_token:$auth_token", "type");
my $role = $redis->hget("auth_token:$auth_token", "role");
my $user_id = $redis->hget("auth_token:$auth_token", "user_id");
$redis->del("auth_token:$auth_token") if ($type eq 'onetime');
unless (grep {$role eq $_} qw/admin reseller ccare ccareadmin/) {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({ code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->error("Wrong auth_token role");
return;
}
my $authrs = $c->model('DB')->resultset('admins')->search({
id => $user_id,
is_active => 1,
});
$auth_user = $authrs->first if ($authrs->first);
} else {
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;
}
if ($pass =~ /[^[:ascii:]]/) { if ($pass =~ /[^[:ascii:]]/) {
$c->response->status(HTTP_UNPROCESSABLE_ENTITY); $c->response->status(HTTP_UNPROCESSABLE_ENTITY);
$c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY, $c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY,
message => "'password' contains invalid characters" })."\n"); message => "'password' contains invalid characters" })."\n");
$c->log->error("'password' contains invalid characters"); $c->log->error("'password' contains invalid characters");
return; return;
} }
my $authrs = $c->model('DB')->resultset('admins')->search({ my $authrs = $c->model('DB')->resultset('admins')->search({
login => $user, login => $user,
is_active => 1, is_active => 1,
}); });
my $usr_salted_pass; my $usr_salted_pass;
my $auth_user = $authrs->first; $auth_user = $authrs->first;
my $result = {};
if ($auth_user && $auth_user->id) { if ($auth_user && $auth_user->id) {
$usr_salted_pass = NGCP::Panel::Utils::Auth::get_usr_salted_pass($auth_user->saltedpass, $pass); $usr_salted_pass = NGCP::Panel::Utils::Auth::get_usr_salted_pass($auth_user->saltedpass, $pass);
}
unless ($usr_salted_pass && $usr_salted_pass eq $auth_user->saltedpass) {
$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;
}
} }
if ($usr_salted_pass && $usr_salted_pass eq $auth_user->saltedpass) { my $result = {};
if ($auth_user) {
my $jwt_data = { my $jwt_data = {
id => $auth_user->id, id => $auth_user->id,
username => $auth_user->login, username => $auth_user->login,

@ -0,0 +1,29 @@
package NGCP::Panel::Form::AuthToken;
use HTML::FormHandler::Moose;
use NGCP::Panel::Utils::Form;
extends 'HTML::FormHandler';
has_field 'type' => (
type => 'Select',
options => [
{ label => 'Onetime', value => 'onetime' },
{ label => 'Expires', value => 'expires' },
],
label => 'Type',
required => 1,
);
has_field 'expires' => (
type => '+NGCP::Panel::Field::PosInteger',
required => 1,
label => 'Expires',
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/type expires/],
);
1;

@ -0,0 +1,35 @@
package NGCP::Panel::Role::API::AuthTokens;
use Sipwise::Base;
use NGCP::Panel::Utils::Redis;
use parent 'NGCP::Panel::Role::API';
use Redis;
use UUID;
sub get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::get("NGCP::Panel::Form::AuthToken", $c);
}
sub generate_auth_token {
my ($self, $c, $resource) = @_;
my ($uuid_bin, $uuid_string);
UUID::generate($uuid_bin);
UUID::unparse($uuid_bin, $uuid_string);
#remove '-' from the token
$uuid_string =~ s/\-//g;
my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}});
return unless $redis;
$redis->hset("auth_token:$uuid_string", 'type', $resource->{type});
$redis->hset("auth_token:$uuid_string", 'role', $c->user->roles);
$redis->hset("auth_token:$uuid_string", 'user_id', $c->user->id);
$redis->expire("auth_token:$uuid_string", $resource->{expires});
return $uuid_string;
}
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,23 @@
package NGCP::Panel::Utils::Redis;
use warnings;
use strict;
use Redis;
sub get_redis_connection {
my ($c, $params) = @_;
my $redis = Redis->new(
server => $c->config->{redis}->{central_url},
reconnect => 10, every => 500000, # 500ms
cnx_timeout => 3,
);
unless ($redis) {
$c->log->error("Failed to connect to central redis url " . $c->config->{redis}->{central_url});
return;
}
$redis->select($params->{database});
return $redis;
}
1;

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

Loading…
Cancel
Save