Initial token management
Change-Id: I6177b46961b7a0e53b9fa1fa9430a4d5562ae2da
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 2ef30df..2f1632b 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -272,9 +272,18 @@
$r->get('/doc')->to('documentation#page', page => 'korap')->name('doc_start');
$r->get('/doc/:scope/:page')->to('documentation#page', scope => undef)->name('doc');
- # Settings routes (deactivated)
- # $r->get('/settings')->to(cb => sub { shift->render('settings') })->name('settings_start');
- # $r->get('/settings/:scope/:page')->to(scope => undef, page => undef)->name('settings');
+ # Settings routes
+ if ($self->navi->exists('settings')) {
+ $r->get('/settings')->to(
+ cb => sub {
+ return shift->render('settings')
+ }
+ )->name('settings_start');
+ $r->get('/settings/:scope/:page')->to(
+ scope => undef,
+ page => undef
+ )->name('settings');
+ };
# Contact route
$r->get('/contact')->to('documentation#contact');
@@ -318,7 +327,7 @@
=head2 COPYRIGHT AND LICENSE
-Copyright (C) 2015-2019, L<IDS Mannheim|http://www.ids-mannheim.de/>
+Copyright (C) 2015-2020, L<IDS Mannheim|http://www.ids-mannheim.de/>
Author: L<Nils Diewald|http://nils-diewald.de/>
Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 7072f9f..920eb46 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -1,5 +1,7 @@
package Kalamar::Plugin::Auth;
use Mojo::Base 'Mojolicious::Plugin';
+use File::Basename 'dirname';
+use File::Spec::Functions qw/catdir/;
use Mojo::ByteStream 'b';
# This is a plugin to deal with the Kustvakt OAuth server.
@@ -47,6 +49,7 @@
$app->log->error('client_id or client_secret not defined');
};
+ # Load localize
$app->plugin('Localize' => {
dict => {
Auth => {
@@ -61,7 +64,20 @@
tokenExpired => 'Zugriffstoken abgelaufen',
tokenInvalid => 'Zugriffstoken ungültig',
refreshFail => 'Fehlerhafter Refresh-Token',
- responseError => 'Unbekannter Autorisierungsfehler'
+ responseError => 'Unbekannter Autorisierungsfehler',
+ paramError => 'Einige Eingaben sind fehlerhaft',
+ redirectUri => 'Weiterleitungs-Adresse',
+ homepage => 'Webseite',
+ desc => 'Kurzbeschreibung',
+ clientCredentials => 'Client Daten',
+ clientType => 'Art der Client-Applikation',
+ clientName => 'Name der Client-Applikation',
+ clientID => 'ID der Client-Applikation',
+ clientSecret => 'Client-Secret',
+ clientRegister => 'Neue Client-Applikation registrieren',
+ registerSuccess => 'Registrierung erfolgreich',
+ registerFail => 'Registrierung fehlgeschlagen',
+ oauthSettings => 'OAuth',
},
-en => {
loginSuccess => 'Login successful',
@@ -73,7 +89,20 @@
tokenExpired => 'Access token expired',
tokenInvalid => 'Access token invalid',
refreshFail => 'Bad refresh token',
- responseError => 'Unknown authorization error'
+ responseError => 'Unknown authorization error',
+ paramError => 'Some fields are invalid',
+ redirectUri => 'Redirect URI',
+ homepage => 'Homepage',
+ desc => 'Short description',
+ clientCredentials => 'Client Credentials',
+ clientType => 'Type of the client application',
+ clientName => 'Name of the client application',
+ clientID => 'ID of the client application',
+ clientSecret => 'Client secret',
+ clientRegister => 'Register new client application',
+ registerSuccess => 'Registration successful',
+ registerFail => 'Registration denied',
+ oauthSettings => 'OAuth',
}
}
}
@@ -95,6 +124,11 @@
}
);
+ # The plugin path
+ my $path = catdir(dirname(__FILE__), 'Auth');
+
+ # Append "templates"
+ push @{$app->renderer->paths}, catdir($path, 'templates');
# Get or set the user token necessary for authorization
$app->helper(
@@ -577,6 +611,110 @@
)->wait;
}
)->name('logout');
+
+ # If "experimental_registration" is set, open
+ # OAuth registration dialogues.
+ if ($param->{experimental_client_registration}) {
+
+ # Add settings
+ $app->navi->add(settings => (
+ $app->loc('Auth_oauthSettings'), 'oauth'
+ ));
+
+ # Route to oauth settings
+ $r->get('/settings/oauth')->to(
+ cb => sub {
+ return shift->render(template => 'auth/tokens')
+ }
+ );
+
+ # Route to oauth client registration
+ $r->post('/settings/oauth/register')->to(
+ cb => sub {
+ my $c = shift;
+ my $v = $c->validation;
+
+ unless ($c->auth->token) {
+
+ # TODO: not allowed
+ return $c->reply->not_found;
+ };
+
+ $v->csrf_protect;
+ $v->required('name', 'trim')->size(3, 255);
+ $v->required('type')->in('PUBLIC', 'CONFIDENTIAL');
+ $v->required('desc', 'trim')->size(3, 255);
+ $v->optional('url', 'trim')->like(qr/^(http|$)/i);
+ $v->optional('redirectUri', 'trim')->like(qr/^(http|$)/i);
+
+ # Render with error
+ if ($v->has_error) {
+ if ($v->has_error('csrf_token')) {
+ $c->notify(error => $c->loc('Auth_csrfFail'));
+ }
+ else {
+ $c->notify(warn => $c->loc('Auth_paramError'));
+ };
+ return $c->render(template => 'auth/tokens')
+ };
+
+ # Wait for async result
+ $c->render_later;
+
+ # Register on server
+ state $url = Mojo::URL->new($c->korap->api)->path('oauth2/client/register');
+ $c->korap_request('POST', $url => {} => json => {
+ name => $v->param('name'),
+ type => $v->param('type'),
+ description => $v->param('desc'),
+ url => $v->param('url'),
+ redirectURI => $v->param('redirectURI')
+ })->then(
+ sub {
+ my $tx = shift;
+ my $result = $tx->result;
+
+ if ($result->is_error) {
+ return Mojo::Promise->reject;
+ };
+
+ my $json = $result->json;
+
+ # TODO:
+ # Respond in template
+ my $client_id = $json->{client_id};
+ my $client_secret = $json->{client_secret};
+
+ $c->stash('client_name' => $v->param('name'));
+ $c->stash('client_desc' => $v->param('desc'));
+ $c->stash('client_type' => $v->param('type'));
+ $c->stash('client_url' => $v->param('url'));
+ $c->stash('client_redirect_uri' => $v->param('redirectURI'));
+ $c->stash('client_id' => $client_id);
+
+ if ($client_secret) {
+ $c->stash('client_secret' => $client_secret);
+ };
+
+ $c->notify(success => $c->loc('Auth_en_registerSuccess'));
+
+ return $c->render(template => 'auth/register-success');
+ }
+ )->catch(
+ sub {
+ # Server may be irresponsible
+ my $err = shift;
+ $c->notify('error' => $c->loc('Auth_en_registerFail'));
+ return Mojo::Promise->reject($err);
+ }
+ )->finally(
+ sub {
+ return $c->redirect_to('settings' => { scope => 'oauth' });
+ }
+ );
+ }
+ )->name('oauth-register');
+ };
}
# Use JWT login
@@ -783,6 +921,8 @@
}
)->name('logout');
};
+
+ $app->log->info('Successfully registered Auth plugin');
};
1;
@@ -821,3 +961,68 @@
__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+Kalamar::Plugin::Auth - OAuth-2.0-based authorization plugin
+
+=head1 DESCRIPTION
+
+L<Kalamar::Plugin::Auth> is an OAuth-2.0-based authorization
+plugin for L<Kalamar>. It requires a C<Kustvakt> full server
+with OAuth 2.0 capabilities.
+It is activated by loading C<Auth> as a plugin in the C<Kalamar.plugins>
+parameter in the Kalamar configuration.
+
+=head1 CONFIGURATION
+
+L<Kalamar::Plugin::Auth> supports the following parameter for the
+C<Kalamar-Auth> configuration section in the Kalamar configuration:
+
+=over 2
+
+=item B<client_id>
+
+The client identifier of Kalamar to be send with every OAuth 2.0
+management request.
+
+=item B<client_secret>
+
+The client secret of Kalamar to be send with every OAuth 2.0
+management request.
+
+=item B<oauth2>
+
+Initially L<Kalamar-Plugin-Auth> was based on JWT. This parameter
+is historically used to switch between oauth2 and jwt. It is expected
+to be deprecated in the future, but for the moment it is required
+to be set to a true value.
+
+=item B<experimental_client_registration>
+
+Activates the oauth client registration flow.
+
+=back
+
+=head2 COPYRIGHT AND LICENSE
+
+Copyright (C) 2015-2020, L<IDS Mannheim|http://www.ids-mannheim.de/>
+Author: L<Nils Diewald|http://nils-diewald.de/>
+
+Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
+Corpus Analysis Platform at the
+L<Leibniz Institute for the German Language (IDS)|http://ids-mannheim.de/>,
+member of the
+L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de>
+and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
+funded by the
+L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
+
+Kalamar is free software published under the
+L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
+
+=cut
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep
new file mode 100644
index 0000000..2218236
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep
@@ -0,0 +1,24 @@
+% extends 'settings', title => 'KorAP: '.loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+<form class="form-table">
+ <fieldset>
+ <legend><%= loc 'Auth_clientCredentials' %></legend>
+ <p><strong><%= stash 'client_name' %></strong></p>
+ % if (stash('client_desc')) {
+ <p><%= stash 'client_desc' %></p>
+ % };
+ <p><%= loc 'Auth_clientType' %>: <%= stash 'client_type' %></p>
+ <div>
+ %= label_for 'client_id' => loc('Auth_clientID')
+ %= text_field 'client_id', stash('client_id'), readonly => 'readonly'
+ </div>
+ % if (stash('client_type') ne 'PUBLIC') {
+ <div>
+ %= label_for 'client_secret' => loc('Auth_clientSecret')
+ %= password_field 'client_secret', value => stash('client_secret'), readonly => 'readonly'
+ </div>
+ % };
+ </fieldset>
+</form>
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep
new file mode 100644
index 0000000..d5fe095
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep
@@ -0,0 +1,41 @@
+% extends 'settings', title => 'KorAP: '.loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+%= form_for 'oauth-register', class => 'form-table oauth-register', begin
+ <fieldset>
+ %= csrf_field
+ <legend><%= loc('Auth_clientRegister') %></legend>
+
+ <div>
+ %= label_for name => loc('Auth_clientName'), class => 'field-required', maxlength => 255
+ %= text_field 'name'
+ </div>
+
+ <div>
+ %= label_for type => loc('Auth_clientType'), class => 'field-required'
+ <%= radio_button type => 'PUBLIC', checked => 'checked' %>
+ <label>Public</label>
+ <br />
+ <%= radio_button type => 'CONFIDENTIAL' %>
+ <label>Confidential</label>
+ </div>
+
+ <div>
+ %= label_for 'desc' => loc('Auth_desc'), class => 'field-required'
+ %= text_field 'desc'
+ </div>
+
+ <div>
+ %= label_for name => loc('Auth_homepage')
+ %= url_field 'url', placeholder => 'https://...'
+ </div>
+
+ <div>
+ %= label_for name => loc('Auth_redirectUri')
+ %= url_field 'redirectURI', placeholder => 'https://...'
+ </div>
+
+ %= submit_button loc('Auth_clientRegister')
+ </fieldset>
+% end
diff --git a/lib/Kalamar/Plugin/KalamarPages.pm b/lib/Kalamar/Plugin/KalamarPages.pm
index c7694e4..eba3dbd 100644
--- a/lib/Kalamar/Plugin/KalamarPages.pm
+++ b/lib/Kalamar/Plugin/KalamarPages.pm
@@ -152,6 +152,14 @@
# Take items from central list
unless ($items) {
$items = $navi->{$realm};
+
+ # Realm has no entries
+ return '' unless $items;
+ }
+
+ # Set realm
+ else {
+ $navi->{$realm} = $items;
};
# Create unordered list
@@ -270,6 +278,7 @@
}
);
+ # Add an item to the realm
$mojo->helper(
'navi.add' => sub {
my $c = shift;
@@ -284,6 +293,19 @@
}
}
);
+
+ # Check for existence
+ $mojo->helper(
+ 'navi.exists' => sub {
+ my $c = shift;
+ my $realm = shift;
+ unless (exists $navi->{$realm}) {
+ return 0 ;
+ };
+ return 0 unless ref $navi->{$realm} && @{$navi->{$realm}} > 0;
+ return 1;
+ }
+ );
}
1;