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;