Decoupled authentication from core and introduced as a plugin
Change-Id: I149e5f7f5ab2d833d812e6e381da8ad4b45c1ed7
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 232e96b..8cf581f 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -4,7 +4,7 @@
use Mojo::URL;
use Mojo::File;
use Mojo::JSON 'decode_json';
-use Mojo::Util qw/url_escape/;
+use Mojo::Util qw/url_escape deprecated/;
use List::Util 'none';
# Minor version - may be patched from package.json
@@ -109,11 +109,6 @@
push @{$self->static->paths}, 'dev';
};
- # Check search configuration
-
- # Set endpoint
- $self->config('Search')->{api} //= $kalamar_conf->{api};
-
# Client notifications
$self->plugin(Notifications => {
'Kalamar::Plugin::Notifications' => 1,
@@ -178,14 +173,22 @@
};
};
- # Deprecated Legacy code -
- # TODO: Remove 2019-02
+ # Deprecated Legacy code
if ($self->config('Piwik') &&
none { $_ eq 'Piwik' } @{$conf->{plugins} // []}) {
- use Data::Dumper;
- warn Dumper $self->config('Piwik');
- $self->log->error('Piwik is no longer considered a mandatory plugin');
- $self->plugin('Piwik');
+
+ # 2018-11-12
+ deprecated 'Piwik is no longer considered a mandatory plugin';
+ $self->plugin('Kalamar::Plugin::Piwik');
+ };
+
+ # Deprecated Legacy code
+ if ($self->config('Kalamar')->{auth_support} &&
+ none { $_ eq 'Auth' } @{$conf->{plugins} // []}) {
+
+ # 2018-11-16
+ deprecated 'auth_support configuration is deprecated in favor of Plugin loading';
+ $self->plugin('Kalamar::Plugin::Auth')
};
# Configure documentation navigation
@@ -197,26 +200,6 @@
# Establish routes with authentification
my $r = $self->routes;
- # Check for auth support
- $self->defaults(
- auth_support => $self->config('Kalamar')->{auth_support}
- );
-
- # Support auth
- if ($self->stash('auth_support')) {
- $r = $r->under(
- '/' => sub {
- my $c = shift;
-
- if ($c->session('auth')) {
- $c->stash(auth => $c->session('auth'));
- $c->stash(user => $c->session('user'));
- };
- return 1;
- }
- );
- };
-
# Set footer value
$self->content_block(footer => {
inline => '<%= doc_link_to "V ' . $Kalamar::VERSION . '", "korap", "kalamar" %>',
@@ -241,13 +224,6 @@
my $doc = $r->route('/corpus/:corpus_id/:doc_id');
my $text = $doc->get('/:text_id')->to('search#text_info')->name('text');
my $match = $doc->get('/:text_id/:match_id')->to('search#match_info')->name('match');
-
- # User Management
- my $user = $r->any('/user')->to(controller => 'User');
- $user->post('/login')->to(action => 'login')->name('login');
- $user->get('/logout')->to(action => 'logout')->name('logout');
- # $r->any('/register')->to(action => 'register')->name('register');
- # $r->any('/forgotten')->to(action => 'pwdforgotten')->name('pwdforgotten');
};
diff --git a/lib/Kalamar/Controller/Search.pm b/lib/Kalamar/Controller/Search.pm
index 4fba8f8..ff11115 100644
--- a/lib/Kalamar/Controller/Search.pm
+++ b/lib/Kalamar/Controller/Search.pm
@@ -112,7 +112,7 @@
if (!$cutoff && !$c->no_cache) {
# Create cache string
- my $user = $c->user->handle;
+ my $user = $c->user_handle;
my $cache_url = $url->clone;
$cache_url->query->remove('context')->remove('count')->remove('cutoff')->remove('offset');
$total_cache_str = "total-$user-" . $cache_url->to_string;
diff --git a/lib/Kalamar/Controller/User.pm b/lib/Kalamar/Controller/User.pm
deleted file mode 100644
index af199a2..0000000
--- a/lib/Kalamar/Controller/User.pm
+++ /dev/null
@@ -1,82 +0,0 @@
-package Kalamar::Controller::User;
-use Mojo::Base 'Mojolicious::Controller';
-
-# Login action
-sub login {
- my $c = shift;
-
- # Validate input
- my $v = $c->validation;
- $v->required('handle_or_email', 'trim');
- $v->required('pwd', 'trim');
- $v->csrf_protect;
- $v->optional('fwd')->closed_redirect;
-
- if ($v->has_error) {
- if ($v->has_error('fwd')) {
- $c->notify(error => $c->loc('Auth_openRedirectFail'));
- }
- elsif ($v->has_error('csrf_token')) {
- $c->notify(error => $c->loc('Auth_csrfFail'));
- }
- else {
- $c->notify(error => $c->loc('Auth_loginFail'));
- };
- }
-
- # Login user
- elsif ($c->user->login(
- $v->param('handle_or_email'),
- $v->param('pwd')
- )) {
- $c->notify(success => $c->loc('Auth_loginSuccess'));
- }
-
- else {
- $c->notify(error => $c->loc('Auth_loginFail'));
- };
-
- # Set flash for redirect
- $c->flash(handle_or_email => $v->param('handle_or_email'));
-
- # Redirect to slash
- return $c->relative_redirect_to($v->param('fwd') // 'index');
-};
-
-
-# Logout of the session
-sub logout {
- my $c = shift;
-
- # Log out of the system
- if ($c->user->logout) {
- $c->notify(success => $c->loc('Auth_logoutSuccess'));
- }
-
- # Something went wrong
- else {
- $c->notify('error', $c->loc('Auth_logoutFail'));
- };
-
- return $c->redirect_to('index');
-};
-
-
-# Currently not in used
-sub register {
- my $c = shift;
- $c->render(json => {
- response => 'register'
- });
-};
-
-
-# Currently not in use
-sub pwdforgotten {
- my $c = shift;
- $c->render(json => {
- response => 'pwdforgotten'
- });
-};
-
-1;
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
new file mode 100644
index 0000000..d61d77f
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -0,0 +1,271 @@
+package Kalamar::Plugin::Auth;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::ByteStream 'b';
+
+# TODO:
+# Get rid of auth_support for templates!
+# TODO:
+# Make all authentification parts in templates
+# content_block aware!
+
+# Register the plugin
+sub register {
+ my ($plugin, $app, $param) = @_;
+
+
+ # Load parameter from config file
+ if (my $config_param = $app->config('Kalamar-Auth')) {
+ $param = { %$param, %$config_param };
+ };
+
+
+ # Temp
+ $app->defaults(auth_support => 1);
+
+
+ # Load 'notifications' plugin
+ unless (exists $app->renderer->helpers->{notify}) {
+ $app->plugin(Notifications => {
+ HTML => 1
+ });
+ };
+
+
+ # unless ($param->{client_id} && $param->{client_secret}) {
+ # $mojo->log->error('client_id or client_secret not defined');
+ # return;
+ # };
+
+ # TODO:
+ # Define user CHI cache
+
+ $app->plugin('Localize' => {
+ dict => {
+ Auth => {
+ _ => sub { $_->locale },
+ de => {
+ loginSuccess => 'Anmeldung erfolgreich',
+ loginFail => 'Anmeldung fehlgeschlagen',
+ logoutSuccess => 'Abmeldung erfolgreich',
+ logoutFail => 'Abmeldung fehlgeschlagen',
+ csrfFail => 'Fehlerhafter CSRF Token',
+ openRedirectFail => 'Weiterleitungsfehler'
+ },
+ -en => {
+ loginSuccess => 'Login successful',
+ loginFail => 'Access denied',
+ logoutSuccess => 'Logout successful',
+ logoutFail => 'Logout failed',
+ csrfFail => 'Bad CSRF token',
+ openRedirectFail => 'Redirect failure'
+ }
+ }
+ }
+ });
+
+
+ # Inject authorization to all korap requests
+ $app->hook(
+ before_korap_request => sub {
+ my ($c, $tx) = @_;
+ my $auth_token = $c->auth->token or return;
+ my $h = $tx->req->headers;
+ $h->header('Authorization' => $auth_token);
+ }
+ );
+
+
+ # Get the user token necessary for authorization
+ $app->helper(
+ 'auth.token' => sub {
+ my $c = shift;
+
+ # Get token from stash
+ my $token = $c->stash('auth');
+
+ return $token if $token;
+
+ # Get auth from session
+ my $auth = $c->session('auth') or return;
+
+ # Set token to stash
+ $c->stash(auth => $auth);
+
+ return $auth;
+ }
+ );
+
+
+ # Log in to the system
+ my $r = $app->routes;
+ $r->post('/user/login')->to(
+ cb => sub {
+ my $c = shift;
+
+ # Validate input
+ my $v = $c->validation;
+ $v->required('handle_or_email', 'trim');
+ $v->required('pwd', 'trim');
+ $v->csrf_protect;
+ $v->optional('fwd')->closed_redirect;
+
+ my $user = $v->param('handle_or_email');
+ my $fwd = $v->param('fwd');
+
+ # Set flash for redirect
+ $c->flash(handle_or_email => $user);
+
+ if ($v->has_error || index($user, ':') >= 0) {
+ if ($v->has_error('fwd')) {
+ $c->notify(error => $c->loc('Auth_openRedirectFail'));
+ }
+ elsif ($v->has_error('csrf_token')) {
+ $c->notify(error => $c->loc('Auth_csrfFail'));
+ }
+ else {
+ $c->notify(error => $c->loc('Auth_loginFail'));
+ };
+
+ return $c->relative_redirect_to($fwd // 'index');
+ }
+
+ my $pwd = $v->param('pwd');
+
+ $c->app->log->debug("Login from user $user:$pwd");
+
+ my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
+
+ # Korap request for login
+ $c->korap_request('get', $url, {
+
+ # Set authorization header
+ Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
+
+ })->then(
+ sub {
+ my $tx = shift;
+
+ # Get the java token
+ my $jwt = $tx->result->json;
+
+ # No java web token
+ unless ($jwt) {
+ $c->notify(error => 'Response is no valid JWT (remote)');
+ return;
+ };
+
+ # There is an error here
+ # Dealing with errors here
+ if (my $error = $jwt->{error}) {
+ if (ref $error eq 'ARRAY') {
+ $c->notify(error => $c->dumper($_));
+ }
+ else {
+ $c->notify(error => 'There is an unknown JWT error');
+ };
+ return;
+ };
+
+ # TODO: Deal with user return values.
+ my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
+
+ $c->app->log->debug(qq!Login successful: "$user" with "$auth"!);
+
+ $user = $jwt->{username} ? $jwt->{username} : $user;
+
+ # Set session info
+ $c->session(user => $user);
+ $c->session(auth => $auth);
+
+ # Set stash info
+ $c->stash(user => $user);
+ $c->stash(auth => $auth);
+
+ # Set cache
+ $c->chi('user')->set($auth => $user);
+ $c->notify(success => $c->loc('Auth_loginSuccess'));
+ }
+ )->catch(
+ sub {
+ my $e = shift;
+
+ # Notify the user
+ $c->notify(
+ error =>
+ ($e->{code} ? $e->{code} . ': ' : '') .
+ $e->{message} . ' for Login (remote)'
+ );
+
+ # Log failure
+ $c->app->log->debug(
+ ($e->{code} ? $e->{code} . ' - ' : '') .
+ $e->{message}
+ );
+
+ $c->app->log->debug(qq!Login fail: "$user"!);
+ $c->notify(error => $c->loc('Auth_loginFail'));
+ }
+ )->finally(
+ sub {
+
+ # Redirect to slash
+ return $c->relative_redirect_to($fwd // 'index');
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+
+ return 1;
+ }
+ )->name('login');
+
+
+ # Log out of the session
+ $r->get('/user/logout')->to(
+ cb => sub {
+ my $c = shift;
+
+ # TODO: csrf-protection!
+
+ # Log out of the system
+ my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
+
+ $c->korap_request(
+ 'get', $url
+ )->then(
+ # Logged out
+ sub {
+ my $tx = shift;
+ # Clear cache
+ $c->chi('user')->remove($c->auth->token);
+
+ # Expire session
+ $c->session(user => undef);
+ $c->session(auth => undef);
+ $c->notify(success => $c->loc('Auth_logoutSuccess'));
+ }
+
+ )->catch(
+ # Something went wrong
+ sub {
+ # my $err_msg = shift;
+ $c->notify('error', $c->loc('Auth_logoutFail'));
+ }
+
+ )->finally(
+ # Redirect
+ sub {
+ return $c->redirect_to('index');
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+
+ return 1;
+ }
+ )->name('logout');
+};
+
+1;
diff --git a/lib/Kalamar/Plugin/KalamarHelpers.pm b/lib/Kalamar/Plugin/KalamarHelpers.pm
index 295a04f..f5e995b 100644
--- a/lib/Kalamar/Plugin/KalamarHelpers.pm
+++ b/lib/Kalamar/Plugin/KalamarHelpers.pm
@@ -2,7 +2,7 @@
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::JSON qw/decode_json true false/;
use Mojo::ByteStream 'b';
-use Mojo::Util qw/xml_escape/;
+use Mojo::Util qw/xml_escape deprecated/;
sub register {
my ($plugin, $mojo) = @_;
@@ -223,6 +223,9 @@
kalamar_test_port => sub {
my $c = shift;
+ # 2018-11-15
+ deprecated 'kalamar_test_port is deprecated and will be removed';
+
# Test port is defined in the stash
if (defined $c->stash('kalamar.test_port')) {
return $c->stash('kalamar.test_port');
@@ -240,6 +243,7 @@
return 0;
});
+
# Establish 'search_results' taghelper
# This is based on Mojolicious::Plugin::Search
$mojo->helper(
@@ -270,9 +274,11 @@
}
);
+
+ # Get the KorAP API endpoint
$mojo->helper(
'korap.api' => sub {
- return shift->config('Search')->{api};
+ return shift->config('Kalamar')->{api};
}
);
@@ -284,14 +290,18 @@
# In case the user is not known, it is assumed,
# the user is not logged in
- my $user = $c->user->handle;
+ # TODO:
+ # Make this more general
+ my $user = $c->user_handle;
# Set api request for debugging
my $cache_str = "$method-$user-" . $url->to_string;
$c->stash(api_request => $url->to_string);
+ # No cache request
if ($c->no_cache) {
- return $c->user->auth_request_p($method => $url)->then(
+
+ return $c->korap_request($method => $url)->then(
sub {
my $tx = shift;
# Catch errors and warnings
@@ -306,6 +316,9 @@
my $promise;
+ # TODO:
+ # emit_hook(after_koral_fetch => $c)
+
# Cache was found
if ($koral) {
@@ -324,7 +337,8 @@
};
# Resolve request
- return $c->user->auth_request_p($method => $url)->then(
+ # Before: user->auth_request_p
+ return $c->korap_request($method => $url)->then(
sub {
my $tx = shift;
return ($c->catch_errors_and_warnings($tx) ||
@@ -340,7 +354,6 @@
);
}
);
-
};
diff --git a/lib/Kalamar/Plugin/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index 4cf7642..d0340e8 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -1,12 +1,15 @@
package Kalamar::Plugin::KalamarUser;
use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Util qw/deprecated/;
use Mojo::Promise;
use Mojo::ByteStream 'b';
has 'api';
has 'ua';
-# TODO: Merge with meta-button
+# TODO:
+# This Plugin will be removed in favour of
+# Kalamar::Plugin::Auth!
sub register {
my ($plugin, $mojo, $param) = @_;
@@ -34,11 +37,79 @@
# Set app to server
$plugin->ua->server->app($mojo);
+ # Get user handle
+ $mojo->helper(
+ 'user_handle' => sub {
+ my $c = shift;
+
+ # Get from stash
+ my $user = $c->stash('user');
+ return $user if $user;
+
+ # Get from session
+ $user = $c->session('user');
+
+ # Set in stash
+ if ($user) {
+ $c->stash(user => $user);
+ return $user;
+ };
+
+ return 'not_logged_in';
+ }
+ );
+
+ # This is a new general korap_request helper,
+ # that can trigger some hooks for, e.g., authentication
+ # or analysis. It returns a promise.
+ $mojo->helper(
+ 'korap_request' => sub {
+ my $c = shift;
+ my $method = shift;
+ my $path = shift;
+
+ # Get plugin user agent
+ my $ua = $plugin->ua;
+
+ my $url = Mojo::URL->new($path);
+ my $tx = $ua->build_tx(uc($method), $url, @_);
+
+ # Set X-Forwarded for
+ $tx->req->headers->header(
+ 'X-Forwarded-For' => $c->client_ip
+ );
+
+
+ # Emit Hook to alter request
+ $c->app->plugins->emit_hook(
+ before_korap_request => ($c, $tx)
+ );
+
+ return $ua->start_p($tx);
+ }
+ );
+
+ #############################################
+ # WARNING! #
+ # The following helpers are all deprecated: #
+ #############################################
+
+ $mojo->helper(
+ 'user.handle' => sub {
+
+ # 2018-11-16
+ deprecated 'user.handle is deprecated in favour of user_handle!';
+ return shift->user_handle;
+ });
+
# Get the user token necessary for authorization
$mojo->helper(
'user_auth' => sub {
my $c = shift;
+ # 2018-11-16
+ deprecated 'user_auth is deprecated in favour of auth->token!';
+
# Get token from stash
my $token = $c->stash('auth');
@@ -56,6 +127,10 @@
$mojo->helper(
'user.ua' => sub {
+
+ # 2018-11-15
+ deprecated 'user->ua is deprecated!';
+
my $c = shift;
my $auth = $c->user_auth;
@@ -86,33 +161,15 @@
}
);
- # Get user handle
- $mojo->helper(
- 'user.handle' => sub {
- my $c = shift;
-
- # Get from stash
- my $user = $c->stash('user');
- return $user if $user;
-
- # Get from session
- $user = $c->session('user');
-
- # Set in stash
- if ($user) {
- $c->stash(user => $user);
- return $user;
- };
-
- return 'not_logged_in';
- }
- );
-
# Request with authorization header
$mojo->helper(
'user.auth_request' => sub {
my $c = shift;
+
+ # 2018-11-15
+ deprecated 'user->auth_request is deprecated!';
+
my $method = shift;
my $path = shift;
@@ -144,10 +201,14 @@
# return a promise
$mojo->helper(
'user.auth_request_p' => sub {
- my $c = shift;
+ my $c = shift;
my $method = shift;
- my $path = shift;
+ my $path = shift;
+ # 2018-11-16
+ deprecated 'user->auth_request_p is deprecated!';
+
+ # Get plugin user agent
my $ua = $plugin->ua;
my $tx;
@@ -175,6 +236,9 @@
my $c = shift;
my ($user, $pwd) = @_;
+ # 2018-11-16
+ deprecated 'user->login is deprecated!';
+
return if (index($user, ':') >= 0);
$c->app->log->debug("Login from user $user:$pwd");
@@ -350,6 +414,9 @@
'user.logout' => sub {
my $c = shift;
+ # 2018-11-16
+ deprecated 'user->logout is deprecated!';
+
# TODO: csrf-protection!
my $url = Mojo::URL->new($plugin->api)->path('auth/logout');