Allow to revoke tokens

Change-Id: I2e55935441b108740a164385923c5b7b994a24ed
diff --git a/Changes b/Changes
index f79dfe9..706386a 100755
--- a/Changes
+++ b/Changes
@@ -21,6 +21,7 @@
         - Use AutoSecrets plugin to improve security.
         - Fixed bug where missing documentation pages raise
           exceptions in Mojo >= 9.0.
+        - Support revocation of tokens.
 
         WARNING: Upgrading to Mojolicious 9.19 will
           invalidate all sessions. This is a security update.
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 458a290..66e4d2f 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -62,10 +62,13 @@
           tokenInvalid => 'Zugriffstoken ungültig',
           refreshFail => 'Fehlerhafter Refresh-Token',
           responseError => 'Unbekannter Autorisierungsfehler',
+          revokeFail => 'Der Token kann nicht widerrufen werden',
+          revokeSuccess => 'Der Token wurde erfolgreich widerrufen',
           paramError => 'Einige Eingaben sind fehlerhaft',
           redirectUri => 'Weiterleitungs-Adresse',
           homepage => 'Webseite',
           desc => 'Kurzbeschreibung',
+          revoke => 'Widerrufen',
           clientCredentials => 'Client Daten',
           clientType => 'Art der Client-Applikation',
           clientName => 'Name der Client-Applikation',
@@ -79,6 +82,7 @@
           loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
           oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $client_name %></span>',
           accessToken => 'Access Token',
+          oauthRevokeToken => 'Widerrufe einen Token für <span class="client-name"><%= $client_name %></span>',
         },
         -en => {
           loginSuccess => 'Login successful',
@@ -91,10 +95,13 @@
           tokenInvalid => 'Access token invalid',
           refreshFail => 'Bad refresh token',
           responseError => 'Unknown authorization error',
+          revokeFail => 'Token can\'t be revoked',
+          revokeSuccess => 'Token was revoked successfully',
           paramError => 'Some fields are invalid',
           redirectUri => 'Redirect URI',
           homepage => 'Homepage',
           desc => 'Short description',
+          revoke => 'Revoke',
           clientCredentials => 'Client Credentials',
           clientType => 'Type of the client application',
           clientName => 'Name of the client application',
@@ -106,8 +113,9 @@
           oauthSettings => 'OAuth',
           oauthUnregister => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
           loginHint => 'Maybe you need to log in first?',
-          oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $client_name %></span>',
+          oauthIssueToken => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
           accessToken => 'Access Token',
+          oauthRevokeToken => 'Revoka a token for <span class="client-name"><%= $client_name %></span>',
         }
       }
     }
@@ -775,7 +783,7 @@
               $c->notify(error => $c->loc('Auth_csrfFail'));
             }
             else {
-              $c->notify(warn => $c->loc('Auth_paramError'));
+              $c->notify(error => $c->loc('Auth_paramError'));
             };
             # return $c->redirect_to('oauth-settings');
             return $c->render(template => 'auth/clients');
@@ -870,7 +878,7 @@
               $c->notify(error => $c->loc('Auth_csrfFail'));
             }
             else {
-              $c->notify(warn => $c->loc('Auth_paramError'));
+              $c->notify(error => $c->loc('Auth_paramError'));
             };
             return $c->redirect_to('oauth-settings');
           };
@@ -968,14 +976,23 @@
     };
 
 
-    # Show information of a client
-    $r->get('/settings/oauth/client/:client_id/token')->to(
+    # Ask if new token should be issued
+    $r->get('/settings/oauth/client/:client_id/token/issue')->to(
       cb => sub {
         shift->render(template => 'auth/issue-token');
       }
     )->name('oauth-issue-token');
 
 
+    # Ask if a token should be revoked
+    $r->post('/settings/oauth/client/:client_id/token/revoke')->to(
+      cb => sub {
+        shift->render(template => 'auth/revoke-token');
+      }
+    )->name('oauth-revoke-token');
+
+
+    # Issue new token
     $r->post('/settings/oauth/client/:client_id/token')->to(
       cb => sub {
         my $c = shift;
@@ -990,7 +1007,6 @@
         };
 
         $v->csrf_protect;
-        # $v->required('client-id', 'trim')->size(3, 255);
         $v->optional('client-secret');
         $v->required('name', 'trim');
 
@@ -1000,7 +1016,7 @@
             $c->notify(error => $c->loc('Auth_csrfFail'));
           }
           else {
-            $c->notify(warn => $c->loc('Auth_paramError'));
+            $c->notify(error => $c->loc('Auth_paramError'));
           };
           return $c->redirect_to('oauth-settings')
         };
@@ -1114,6 +1130,79 @@
         return 1;
       }
     )->name('oauth-issue-token-post');
+
+
+    # Revoke token
+    $r->delete('/settings/oauth/client/:client_id/token')->to(
+      cb => sub {
+        my $c = shift;
+
+        my $v = $c->validation;
+
+        unless ($c->auth->token) {
+          return $c->render(
+            content => 'Unauthorized',
+            status => 401
+          );
+        };
+
+        $v->csrf_protect;
+        $v->required('token', 'trim');
+        $v->optional('name', 'trim');
+        my $private_client_id = $c->stash('client_id');
+
+        # Render with error
+        if ($v->has_error) {
+          if ($v->has_error('csrf_token')) {
+            $c->notify(error => $c->loc('Auth_csrfFail'));
+          }
+          else {
+            $c->notify(error => $c->loc('Auth_paramError'));
+          };
+          return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
+        };
+
+        # Revoke token using super client privileges
+        state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke/super');
+
+        my $token = $v->param('token');
+
+        return $c->korap_request(post => $r_url, {} => form => {
+          super_client_id => $client_id,
+          super_client_secret => $client_secret,
+          token => $token
+        })->then(
+          sub {
+            my $tx = shift;
+
+            # Response is fine
+            if ($tx->res->is_success) {
+              $c->notify(success => $c->loc('Auth_revokeSuccess'));
+              return Mojo::Promise->resolve;
+            };
+
+            return Mojo::Promise->reject;
+          }
+        )->catch(
+          sub {
+            my $err_msg = shift;
+            if ($err_msg) {
+              $c->notify(error => { src => 'Backend' } => $err_msg );
+            }
+            else {
+              $c->notify(error => $c->loc('Auth_revokeFail'));
+            };
+          }
+        )->finally(
+          sub {
+            return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
+          }
+        )
+
+        # Start IOLoop
+        ->wait;
+      }
+    )->name('oauth-revoke-token-delete');
   }
 
   # Use JWT login
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
index 60f7870..2fbbd80 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
@@ -2,7 +2,7 @@
 
 %= page_title
 
-<form class="form-table">
+<div class="form-table">
   <fieldset>
     <legend><%= loc 'Auth_clientCredentials' %></legend>
     <ul class="client-list">
@@ -46,10 +46,18 @@
       % if ($_->{scope}) {
       <p name="scope">Scope: <tt><%= join ',', @{$_->{scope}} %></tt></p>
       % };
+      <span class="button-group button-panel">
+        %= form_for 'oauth-revoke-token' => {} => ( class => 'token-revoke' ), begin
+          %= csrf_field
+          <input type="hidden" name="name" value="<%= stash('client_name') %>" />
+          <input type="hidden" name="token" value="<%= $_->{token} %>" />
+          <input type="submit" value="<%= loc 'Auth_revoke' %>" />
+        % end
+      </span>
       </li>
       % }
     </ul>
     % };
 
   </fieldset>
-</form>
+</div>
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
new file mode 100644
index 0000000..5c53c83
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
@@ -0,0 +1,13 @@
+% extends 'settings', title => 'KorAP: ' . loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+<p><%== loc('Auth_oauthRevokeToken', client_name => param('name')) %></p>
+
+%= form_for url_for('oauth-revoke-token-delete', client_id => stash('client_id'))->query('_method' => 'DELETE'), id => 'revoke-token', class => 'form-table', method => "POST", begin
+   %= csrf_field
+   %= hidden_field 'name' => param('name')
+   %= hidden_field 'token' => param('token')
+   <input type="submit" value="<%= loc 'Auth_revoke' %>" />
+   %= link_to 'Abort' => url_for('oauth-tokens', client_id => stash('client_id')) => {} => (class => 'form-button button-abort')
+% end
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 4f0d67a..4bdfe54 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -467,8 +467,8 @@
   ;
 
 $t->get_ok('/settings/oauth')
-  ->text_is('form.form-table legend', 'Register new client application')
-  ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+  ->text_is('.form-table legend', 'Register new client application')
+  ->attr_is('.oauth-register','action', '/settings/oauth/register')
   ->text_is('ul.client-list > li > span.client-name a', 'MyApp')
   ->text_is('ul.client-list > li > span.client-desc', 'This is my application')
   ->text_is('ul.client-list > li > span.client-url a', '')
@@ -476,16 +476,16 @@
 
 $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
   ->status_is(200)
-  ->text_is('form ul.client-list > li.client > span.client-name', 'MyApp')
-  ->text_is('form ul.client-list > li.client > span.client-desc', 'This is my application')
+  ->text_is('ul.client-list > li.client > span.client-name', 'MyApp')
+  ->text_is('ul.client-list > li.client > span.client-desc', 'This is my application')
   ->text_is('a.client-unregister', 'Unregister')
   ->attr_is('a.client-unregister', 'href', '/settings/oauth/unregister/fCBbQkA2NDA3MzM1Yw==?name=MyApp')
   ;
 
 $csrf = $t->get_ok('/settings/oauth/unregister/fCBbQkA2NDA3MzM1Yw==?name=MyApp')
   ->content_like(qr!Do you really want to unregister \<span class="client-name"\>MyApp\<\/span\>?!)
-  ->attr_is('form.form-table input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
-  ->attr_is('form.form-table input[name=client-name]', 'value', 'MyApp')
+  ->attr_is('.form-table input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
+  ->attr_is('.form-table input[name=client-name]', 'value', 'MyApp')
   ->tx->res->dom->at('input[name="csrf_token"]')
   ->attr('value')
   ;
@@ -500,8 +500,8 @@
   ;
 
 $t->get_ok('/settings/oauth')
-  ->text_is('form.form-table legend', 'Register new client application')
-  ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+  ->text_is('.form-table legend', 'Register new client application')
+  ->attr_is('.oauth-register','action', '/settings/oauth/register')
   ->element_exists('ul.client-list > li')
   ->text_is('div.notify', 'Unknown client with xxxx==.')
   ;
@@ -516,8 +516,8 @@
   ;
 
 $t->get_ok('/settings/oauth')
-  ->text_is('form.form-table legend', 'Register new client application')
-  ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+  ->text_is('.form-table legend', 'Register new client application')
+  ->attr_is('.oauth-register','action', '/settings/oauth/register')
   ->element_exists_not('ul.client-list > li')
   ->text_is('div.notify-success', 'Successfully deleted MyApp')
   ;
@@ -539,10 +539,10 @@
   ->text_is('.client-name', 'MyApp2')
   ->text_is('.client-desc', 'This is my application')
   ->text_is('.client-issue-token', 'IssueToken')
-  ->attr_is('.client-issue-token', 'href', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
+  ->attr_is('.client-issue-token', 'href', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/issue?name=MyApp2')
   ;
 
-$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
+$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/issue?name=MyApp2')
   ->status_is(200)
   ->attr_is('#issue-token','action', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token')
   ->attr_is('input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
@@ -560,10 +560,71 @@
   ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
   ;
 
+
 $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
   ->text_is('div.notify-success', 'New access token created')
   ;
 
+$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ->status_is(200)
+  ->attr_is('form.token-revoke', 'action', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/revoke')
+  ->attr_is('form.token-revoke input[name=token]', 'value', 'jhkhkjhk_hjgjsfz67i')
+  ->attr_is('form.token-revoke input[name=name]', 'value', 'MyApp2')
+  ->tx->res->dom->at('input[name="csrf_token"]')
+  ->attr('value')
+  ;
+
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/revoke' => form => {
+  csrf_token => $csrf,
+  name => 'MyApp2',
+  token => 'jhkhkjhk_hjgjsfz67i'
+})
+  ->status_is(200)
+  ->attr_is('form#revoke-token','action','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE')
+  ->attr_is('form#revoke-token','method','POST')
+  ->attr_is('form#revoke-token input[name=token]','value','jhkhkjhk_hjgjsfz67i')
+;
+
+
+# CSRF missing
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE' => form => {
+  name => 'MyApp2',
+  token => 'jhkhkjhk_hjgjsfz67i'
+})->status_is(302)
+  ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ;
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ->element_exists_not('div.notify-success')
+  ->text_is('div.notify-error', 'Bad CSRF token')
+  ;
+
+# Token missing
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE' => form => {
+  name => 'MyApp2',
+  csrf_token => $csrf,
+})->status_is(302)
+  ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ;
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ->element_exists_not('div.notify-success')
+  ->text_is('div.notify-error', 'Some fields are invalid')
+  ;
+
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE' => form => {
+  name => 'MyApp2',
+  csrf_token => $csrf,
+  token => 'jhkhkjhk_hjgjsfz67i'
+})->status_is(302)
+  ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ;
+
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ->element_exists_not('div.notify-error')
+  ->text_is('div.notify-success', 'Token was revoked successfully')
+  ;
 
 done_testing;
 __END__
diff --git a/t/server/mock.pl b/t/server/mock.pl
index c3808f6..6c9318f 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -771,6 +771,16 @@
   );
 };
 
+post '/v1.0/oauth2/revoke/super' => sub {
+  my $c = shift;
+
+  my $s_client_id = $c->param('super_client_id');
+  my $s_client_secret = $c->param('super_client_secret');
+  my $token = $c->param('token');
+
+  return $c->render(text => 'SUCCESS');
+};
+
 
 app->start;