TT#130203 unify admin and subscriber JWT tokens

* /login_jwt is now the only endpoint to issue JWT tokens
* JWT token admin/subscriber is provided based on the
  NGCP_REALM/NGCP_API_REALM fcgi env values
  (e.g.: https://localhost:1443/login_jwt = admin JWT token and
   https://localhost/login_jwt = subscriber JWT token)
* Authorization: Bearer a= prefix is deprecated
* Clients cannot use subscriber JWT token to access admin
  NGCP_API_REALM https://localhost:1443/api/...
  and vice-versa

Change-Id: I46edf4c7aaf7bb835dc4ac6b7535aa2d6b5ac136
mr10.0
Kirill Solomko 4 years ago committed by Hans-Peter Herzog
parent 181841eb50
commit fc9c71a88e

@ -46,16 +46,16 @@ sub authenticate {
my ($token) = $auth_header =~ m/Bearer\s+(.*)/;
return unless ($token);
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;
}
$c->log->debug("Found token: $token") if $self->debug;
my $jwt_data;
try {
$jwt_data = decode_jwt(token=>$token, key=>$self->jwt_key, accepted_alg => $self->alg);
if ($jwt_data->{$self->id_field} && $jwt_data->{$self->id_field} eq 'uuid') {
$c->log->debug('decoded subscriber JWT token');
} else {
$c->log->debug('decoded admin JWT token');
}
} catch ($e) {
# something happened
$c->log->debug("Error decoding token: $e") if $self->debug;

@ -190,30 +190,27 @@ sub auto :Private {
$c->log->warn("invalid api system login from '".$c->qs($c->req->address)."'");
}
$self->api_apply_fake_time($c);
return $self->check_user_access($c);
} 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 $self->check_user_access($c);
} elsif ($c->req->headers->header("Authorization") &&
$c->req->headers->header("Authorization") =~ m/^Bearer /) {
$c->log->debug("++++++ Root::auto API request with JWT");
my $realm = "api_subscriber_jwt";
my $res = $c->authenticate({}, $realm);
my $ngcp_api_realm = $c->request->env->{NGCP_API_REALM} // "";
if ($ngcp_api_realm eq 'subscriber') {
$c->log->debug("++++++ Root::auto API request with JWT");
my $realm = "api_subscriber_jwt";
my $res = $c->authenticate({}, $realm);
unless ($c->user_exists) {
$c->log->debug("+++++ invalid api subscriber JWT login");
}
} else {
$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 subscriber JWT login");
# $c->log->warn("invalid api system login from '".$c->qs($c->req->address)."'");
unless ($c->user_exists) {
$c->log->debug("+++++ invalid api admin JWT login");
}
}
$self->api_apply_fake_time($c);
@ -298,7 +295,7 @@ sub auto :Private {
}
} elsif (!$c->user_exists &&
$c->req->headers->header("Authorization") &&
$c->req->headers->header("Authorization") =~ m/^Bearer(\s+)a=/) {
$c->req->headers->header("Authorization") =~ m/^Bearer /) {
$c->log->debug("++++++ Root::auto UI request with admin JWT");
my $realm = "admin_jwt";
@ -460,7 +457,7 @@ sub check_user_access {
my $path = $c->req->uri->path;
if ($path =~ /^\/(login|logout|login_jwt|admin_login_jwt)$/) {
if ($path =~ /^\/(login|logout|login_jwt)$/) {
return 1;
}
@ -506,10 +503,17 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
my $auth_token = $c->req->body_data->{token} // '';
my $user = $c->req->body_data->{username} // '';
my $pass = $c->req->body_data->{password} // '';
my $key = $c->config->{'Plugin::Authentication'}{api_subscriber_jwt}{credential}{jwt_key};
my $relative_exp = $c->config->{'Plugin::Authentication'}{api_subscriber_jwt}{credential}{relative_exp};
my $alg = $c->config->{'Plugin::Authentication'}{api_subscriber_jwt}{credential}{alg};
my $ngcp_realm = $c->request->env->{NGCP_REALM} // 'admin';
my $key = $ngcp_realm eq 'admin'
? $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{jwt_key}
: $c->config->{'Plugin::Authentication'}{api_subscriber_jwt}{credential}{jwt_key};
my $relative_exp = $ngcp_realm eq 'admin'
? $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{relative_exp}
: $c->config->{'Plugin::Authentication'}{api_subscriber_jwt}{credential}{relative_exp};
my $alg = $ngcp_realm eq 'admin'
? $c->config->{'Plugin::Authentication'}{api_admin_jwt}{credential}{alg}
: $c->config->{'Plugin::Authentication'}{api_subscriber_jwt}{credential}{alg};
$c->response->content_type('application/json');
@ -521,9 +525,18 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
return;
}
unless ($ngcp_realm eq 'admin' || $ngcp_realm eq 'subscriber') {
$c->response->status(HTTP_UNPROCESSABLE_ENTITY);
$c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY,
message => "Invalid realm" })."\n");
$c->log->error("Invalid realm");
return;
}
my $auth_user;
if ($auth_token) {
my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}});
unless ($redis) {
$c->response->status(HTTP_INTERNAL_SERVER_ERROR);
$c->response->body(encode_json({ code => HTTP_INTERNAL_SERVER_ERROR,
@ -531,25 +544,47 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
$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/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;
if ($ngcp_realm eq 'admin') {
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 (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);
}
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);
@ -568,177 +603,130 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
}
my ($u, $d, $t) = split(/\@/, $user, 3);
if(defined $t) {
# in case username is an email address
$u = $u . '@' . $d;
$d = $t;
}
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'],
});
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({
if ($ngcp_realm eq 'admin') {
my $authrs = $c->model('DB')->resultset('admins')->search({
login => $user,
is_active => 1,
});
my $usr_salted_pass;
$auth_user = $authrs->first;
if ($auth_user && $auth_user->id) {
$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;
}
} else {
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'],
});
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,
cost => NGCP::Panel::Utils::Auth::get_bcrypt_cost(),
salt => $salt,
}, $pass));
$authrs->first->update({webpassword => $b64salt . '$' . $b64hash});
$auth_user = $authrs->first;
$auth_user = $authrs->search({webpassword => $db_b64salt . '$' . $usr_b64hash})->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,
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;
}
}
else {
$auth_user = $authrs->search({webpassword => $pass})->first;
}
}
}
my $result = {};
if ($auth_user && $auth_user->voip_subscriber) {
my $jwt_data = {
subscriber_uuid => $auth_user->uuid,
username => $auth_user->webusername,
typ => 'JWT',
};
$result->{jwt} = encode_jwt(
payload => $jwt_data,
key => $key,
alg => $alg,
$relative_exp ? (relative_exp => $relative_exp) : (),
extra_headers => { typ => 'JWT' },
);
$result->{subscriber_id} = int($auth_user->voip_subscriber->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 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 $auth_token = $c->req->body_data->{token} // '';
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 $auth_user;
if ($auth_token) {
my $redis = NGCP::Panel::Utils::Redis::get_redis_connection($c, {database => $c->config->{'Plugin::Session'}->{redis_db}});
unless ($redis) {
$c->response->status(HTTP_INTERNAL_SERVER_ERROR);
$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/) {
if ($ngcp_realm eq 'admin') {
if ($auth_user) {
my $jwt_data = {
id => $auth_user->id,
username => $auth_user->login,
};
$result->{jwt} = encode_jwt(
payload => $jwt_data,
key => $key,
alg => $alg,
$relative_exp ? (relative_exp => $relative_exp) : (),
extra_headers => { typ => 'JWT' },
);
$result->{id} = int($auth_user->id // 0);
} else {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({ code => HTTP_FORBIDDEN,
message => "Forbidden!" })."\n");
$c->log->error("Wrong auth_token role");
message => "User not found" })."\n");
$c->log->error("User not found");
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:]]/) {
$c->response->status(HTTP_UNPROCESSABLE_ENTITY);
$c->response->body(encode_json({ code => HTTP_UNPROCESSABLE_ENTITY,
message => "'password' contains invalid characters" })."\n");
$c->log->error("'password' contains invalid characters");
return;
}
my $authrs = $c->model('DB')->resultset('admins')->search({
login => $user,
is_active => 1,
});
my $usr_salted_pass;
$auth_user = $authrs->first;
if ($auth_user && $auth_user->id) {
$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) {
if ($auth_user && $auth_user->voip_subscriber) {
my $jwt_data = {
subscriber_uuid => $auth_user->uuid,
username => $auth_user->webusername,
};
$result->{jwt} = encode_jwt(
payload => $jwt_data,
key => $key,
alg => $alg,
$relative_exp ? (relative_exp => $relative_exp) : (),
extra_headers => { typ => 'JWT' },
);
$result->{subscriber_id} = int($auth_user->voip_subscriber->id // 0);
} else {
$c->response->status(HTTP_FORBIDDEN);
$c->response->body(encode_json({ code => HTTP_FORBIDDEN,
message => "User not found" })."\n");
@ -747,29 +735,6 @@ sub admin_login_jwt :Chained('/') :PathPart('admin_login_jwt') :Args(0) :Method(
}
}
my $result = {};
if ($auth_user) {
my $jwt_data = {
id => $auth_user->id,
username => $auth_user->login,
typ => 'JWT',
};
$result->{jwt} = 'a='.encode_jwt(
payload => $jwt_data,
key => $key,
alg => $alg,
$relative_exp ? (relative_exp => $relative_exp) : (),
extra_headers => { typ => 'JWT' },
);
$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

Loading…
Cancel
Save