TT#16003 implement JWT auth

- read key from specific config file
- key is hex encoded
+ fix: add libcryptx-perl as dependency (actually dep of libcrypt-jwt-perl)
+ fix: typo in perl (comma instead of assignment)
+ fix: chomp any preceeding newlines from jwt_secret

Change-Id: I6c6bd4dc0d7fa7fa43868afb13b4d8d838d90564
changes/79/13179/9
Gerhard Jungwirth 8 years ago
parent fd79802c3d
commit ba3548d825

2
debian/control vendored

@ -27,8 +27,10 @@ Depends:
libconfig-general-perl,
libconvert-ascii85-perl,
libcrypt-eksblowfish-perl,
libcrypt-jwt-perl,
libcrypt-rc4-perl,
libcrypt-rijndael-perl,
libcryptx-perl,
libdata-compare-perl,
libdata-entropy-perl,
libdata-hal-perl,

@ -132,7 +132,7 @@ __PACKAGE__->config(
class => 'DBIx::Class',
user_model => 'DB::admins',
id_field => 'id',
store_user_class => 'NGCP::Panel::AuthenticationStore::RoleFromRealm',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
use_userdata_from_session => 1,
}
},
@ -147,7 +147,7 @@ __PACKAGE__->config(
class => 'DBIx::Class',
user_model => 'DB::admins',
id_field => 'id',
store_user_class => 'NGCP::Panel::AuthenticationStore::RoleFromRealm',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
use_userdata_from_session => 1,
}
},
@ -162,7 +162,7 @@ __PACKAGE__->config(
class => 'DBIx::Class',
user_model => 'DB::admins',
id_field => 'id',
store_user_class => 'NGCP::Panel::AuthenticationStore::RoleFromRealm',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
},
use_session => 0,
},
@ -180,7 +180,7 @@ __PACKAGE__->config(
class => 'DBIx::Class',
user_model => 'DB::admins',
id_field => 'id',
store_user_class => 'NGCP::Panel::AuthenticationStore::RoleFromRealm',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
},
use_session => 0,
},
@ -196,11 +196,29 @@ __PACKAGE__->config(
store => {
class => 'DBIx::Class',
user_model => 'DB::provisioning_voip_subscribers',
store_user_class => 'NGCP::Panel::AuthenticationStore::RoleFromRealm',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
# use_userdata_from_session => 1,
},
use_session => 0,
},
api_subscriber_jwt => {
credential => {
class => '+NGCP::Panel::Authentication::Credential::JWT',
username_jwt => 'username',
username_field => 'webusername',
id_jwt => 'subscriber_uuid',
id_field => 'uuid',
jwt_key => _get_jwt_key(),
debug => 1,
alg => 'HS256',
},
store => {
class => 'DBIx::Class',
user_model => 'DB::provisioning_voip_subscribers',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
},
use_session => 0,
},
api_admin_system => {
credential => {
class => 'HTTP',
@ -210,7 +228,7 @@ __PACKAGE__->config(
password_type => 'clear',
},
store => {
class => '+NGCP::Panel::AuthenticationStore::System',
class => '+NGCP::Panel::Authentication::Store::System',
file => '/etc/default/ngcp-api',
group => 'auth_system',
},
@ -226,7 +244,7 @@ __PACKAGE__->config(
class => 'DBIx::Class',
user_model => 'DB::provisioning_voip_subscribers',
id_field => 'id',
store_user_class => 'NGCP::Panel::AuthenticationStore::RoleFromRealm',
store_user_class => 'NGCP::Panel::Authentication::Store::RoleFromRealm',
use_userdata_from_session => 1,
}
}
@ -247,6 +265,14 @@ sub get_ngcp_version {
return $content;
}
sub _get_jwt_key {
my $content = File::Slurp::read_file("/etc/ngcp-panel/jwt_secret", err_mode => 'quiet');
$content //= '';
$content =~ s/\n//; # remove newline before
chomp($content);
return $content;
}
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,97 @@
package NGCP::Panel::Authentication::Credential::JWT;
use warnings;
use strict;
use base "Class::Accessor::Fast";
__PACKAGE__->mk_accessors(qw/
debug
username_jwt
username_field
id_jwt
id_field
jwt_key
alg
/);
use Crypt::JWT qw/decode_jwt/;
use TryCatch;
use Catalyst::Exception ();
sub new {
my ( $class, $config, $c, $realm ) = @_;
my $self = {
# defaults:
username_jwt => 'username',
username_field => 'username',
id_jwt => 'id',
id_field => 'id',
alg => 'HS256',
#
%{ $config },
%{ $realm->{config} }, # additional info, actually unused
};
bless $self, $class;
return $self;
}
sub authenticate {
my ( $self, $c, $realm, $authinfo ) = @_;
$c->log->debug("CredentialJWT::authenticate() called from " . $c->request->uri) if $self->debug;
my $auth_header = $c->req->header('Authorization');
return unless ($auth_header);
my ($token) = $auth_header =~ m/Bearer\s+(.*)/;
return unless ($token);
$c->log->debug("Found token: $token") if $self->debug;
my $jwt_data;
try {
my $raw_key = pack('H*', $self->jwt_key);
$jwt_data = decode_jwt(token=>$token, key=>$raw_key, accepted_alg => $self->alg);
} catch ($e) {
# smt happended
$c->log->debug("Error decoding token: $e") if $self->debug;
return;
}
my $user_data = {
%{ $authinfo // {} },
$self->username_field => $jwt_data->{$self->username_jwt},
$self->id_field => $jwt_data->{$self->id_jwt},
};
my $user_obj = $realm->find_user($user_data, $c);
if (ref $user_obj) {
return $user_obj;
} else {
$c->log->debug("Failed to find_user") if $self->debug;
return;
}
}
1;
__END__
=head1 NAME
NGCP::Panel::Authentication::Credential::JWT
=head1 DESCRIPTION
This authentication credential checker tries to read a JSON Web Token (JWT)
from the current request, verifies its signature and looks up the user
in the configured authentication store.
=head1 LICENSE
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=head1 AUTHOR
Gerhard Jungwirth C<< <gjungwirth@sipwise.com> >>

@ -1,4 +1,4 @@
package NGCP::Panel::AuthenticationStore::RoleFromRealm;
package NGCP::Panel::Authentication::Store::RoleFromRealm;
use Sipwise::Base;
use parent 'Catalyst::Authentication::Store::DBIx::Class::User';
@ -12,7 +12,7 @@ sub roles {
: return "reseller";
}
}
foreach my $auth_type (qw/subscriber api_subscriber_http/) {
foreach my $auth_type (qw/subscriber api_subscriber_http api_subscriber_jwt/) { # TODO: simplify this
if ($auth_type eq $self->auth_realm) {
$self->_user->admin ? return "subscriberadmin"
: return "subscriber";

@ -1,8 +1,8 @@
package NGCP::Panel::AuthenticationStore::System;
package NGCP::Panel::Authentication::Store::System;
use Sipwise::Base;
use Moose;
use namespace::autoclean;
use NGCP::Panel::AuthenticationStore::SystemRole;
use NGCP::Panel::Authentication::Store::SystemRole;
use Config::Tiny;
with 'MooseX::Emulate::Class::Accessor::Fast';
@ -43,7 +43,7 @@ sub new {
} },
port => $data{port},
user_class => $config->{user_class} ||
"NGCP::Panel::AuthenticationStore::SystemRole",
"NGCP::Panel::Authentication::Store::SystemRole",
}, $class;
return $self;
@ -63,12 +63,12 @@ sub find_user {
if (ref($user) eq "HASH") {
return $self->user_class->new($user);
} elsif (ref($user) && blessed($user) &&
$user->isa('NGCP::Panel::AuthenticationStore::SystemRole')) {
$user->isa('NGCP::Panel::Authentication::Store::SystemRole')) {
return $user;
} else {
Catalyst::Exception->throw(
"The user '$username' must be a hash reference or an " .
"object of class NGCP::Panel::AuthenticationStore::SystemRole");
"object of class NGCP::Panel::Authentication::Store::SystemRole");
}
return;

@ -1,4 +1,4 @@
package NGCP::Panel::AuthenticationStore::SystemRole;
package NGCP::Panel::Authentication::Store::SystemRole;
use Sipwise::Base;
use parent 'Catalyst::Authentication::User::Hash';

@ -11,6 +11,7 @@ use NGCP::Panel::Utils::Admin;
use DateTime qw();
use Time::HiRes qw();
use DateTime::Format::RFC3339 qw();
use HTTP::Status qw(:constants);
#
# Sets the actions in this controller to be registered with no prefix
@ -117,6 +118,19 @@ sub auto :Private {
$c->log->warn("invalid api system login from '".$c->req->address."'");
}
$self->api_apply_fake_time($c);
return 1;
} 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);
unless ($c->user_exists) {
$c->log->debug("+++++ invalid api subscriber JWT login");
# $c->log->warn("invalid api system login from '".$c->req->address."'");
}
$self->api_apply_fake_time($c);
return 1;
} elsif ($ngcp_api_realm eq "subscriber") {
@ -344,6 +358,87 @@ sub emptyajax :Chained('/') :PathPart('emptyajax') :Args(0) {
$c->detach( $c->view("JSON") );
}
sub login_jwt :Chained('/') :PathPart('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_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};
$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 ($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,
webpassword => $pass,
'voip_subscriber.status' => 'active',
'domain.domain' => $d,
'contract.status' => 'active',
}, {
join => ['domain', 'contract', 'voip_subscriber'],
});
my $auth_user = $authrs->first;
my $result = {};
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 => $raw_key,
alg => $alg,
$relative_exp ? (relative_exp => $relative_exp) : (),
);
$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 api_apply_fake_time :Private {
my ($self, $c) = @_;
my $allow_fake_client_time = 0;

@ -26,6 +26,15 @@ log4perl.appender.Default.layout.ConversionPattern=%d{ISO8601} [%p] [%F +%L] %m{
schema_class NGCP::InterceptSchema
</Model::InterceptDB>
<Plugin::Authentication>
<api_subscriber_jwt>
<credential>
jwt_key ""
relative_exp 36000
</credential>
</api_subscriber_jwt>
</Plugin::Authentication>
<contact>
email postmaster@domain.invalid
</contact>

Loading…
Cancel
Save