Added support for OAuth2 client unregistration

Change-Id: Ib67c63ffd8398b733a2633ca3ac1865a1feb66ef
diff --git a/Changes b/Changes
index f7559e1..f71d471 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.38 2020-03-31
+0.38 2020-04-14
         - Support X-Forwarded-Host name for proxy.
         - Document API URI.
         - Improve redirect handling in proxy.
@@ -9,6 +9,7 @@
         - Improve error status codes.
         - Support HTML responses for match information.
         - Reuse failure template.
+        - Added support for OAuth2 client unregistration.
 
 0.37 2020-01-16
         - Removed deprecated 'kalamar_test_port' helper.
diff --git a/dev/scss/base/form.scss b/dev/scss/base/form.scss
index b2ae781..d5c9aba 100644
--- a/dev/scss/base/form.scss
+++ b/dev/scss/base/form.scss
@@ -31,11 +31,11 @@
     text-align: left;
   }
   
-  label[for], input[type=submit] {
+  label[for], input[type=submit], a.form-button {
     margin-top: 2em;
   }
 
-  input, textarea, button {
+  input, textarea, button, a.form-button {
     border-radius: $standard-border-radius;
   }
   
@@ -45,11 +45,11 @@
     border-style: solid;
   }
   
-  input, textarea, select {
+  input, textarea, select, a.form-button {
+    border-style: solid;
     display: inline-block;
     width: 20%;
     min-width: 20em;
-    // margin: 0 20pt 0 20pt;
     padding: $base-padding;
   }
 
@@ -69,19 +69,39 @@
     border-color: $ids-pink-1;
   }
 
-  input:not([type=radio]), button {
+  /*
+  input:not([type=radio]),
+  button,
+  a.form-button {
      height: 3em;
   }
+*/
 
-  input[type=submit], button {
-     display: inline-block;
-     text-align: center;
-     background-color: $middle-green;
-     border-color: $dark-green;
+  input[type=submit],
+  button,
+  a.form-button {
+    display: inline-block;
+    cursor: pointer;
+    border-width: thin;
+    text-align: center;
+    background-color: $middle-green;
+    border-color: $dark-green;
+    font-size: 8pt;
+    color: $dark-green;
   }
 
+  a.form-button:hover {
+    color: default !important;
+  }
+  
   label.field-required::after {
     color: $ids-blue-1;
     content:'*';
   }
-}
\ No newline at end of file
+}
+
+.button-abort {
+  background-color: $middle-orange !important;
+  color: $darkest-orange !important;
+  border-color: $darkest-orange !important;
+}
diff --git a/dev/scss/main/buttongroup.scss b/dev/scss/main/buttongroup.scss
index 756ad58..55bd6b9 100644
--- a/dev/scss/main/buttongroup.scss
+++ b/dev/scss/main/buttongroup.scss
@@ -9,7 +9,6 @@
     cursor: pointer;
   }
 
-
   span.button-icon {
     font-family: 'FontAwesome';
     > span {
@@ -18,9 +17,10 @@
   }
   
   &.button-panel, &.operators {
-    > span {
+    > span, a {
       box-shadow: $choose-box-shadow;
       font-size: 9pt;
+      font-weight: normal;
       line-height: 1.5em;
       padding: 0 4px;
       display: inline-block;
@@ -48,19 +48,21 @@
   }
 
   &.button-panel {
-    > span > span.check {
-      font-family: 'FontAwesome';
-      width: 1.2em;
-      display: inline-block;
-      text-align: left;
-      &:not(.checked)::after {
-        content: $fa-check;
-      }
-      &.checked::after {
-        content: $fa-checked;
-      }
-      > span {
-        @include blind;
+    > span, a {
+      > span.check {
+        font-family: 'FontAwesome';
+        width: 1.2em;
+        display: inline-block;
+        text-align: left;
+        &:not(.checked)::after {
+          content: $fa-check;
+        }
+        &.checked::after {
+          content: $fa-checked;
+        }
+        > span {
+          @include blind;
+        }
       }
     }
   }
diff --git a/dev/scss/main/oauth.scss b/dev/scss/main/oauth.scss
index fc41fcb..0b37285 100644
--- a/dev/scss/main/oauth.scss
+++ b/dev/scss/main/oauth.scss
@@ -1,23 +1,28 @@
 ul.client-list {
   padding-left: 1.5em;
-  li {
+  li.client {
     list-style-type: none;
     span.client-name {
       &::before {
-        display: inline-block;
-        width: 1.5em;
         margin-left: -1.5em;
-        content: $fa-plugin;
-        font-family: 'FontAwesome';
-        color: $ids-blue-1;
-        font-size: 100%;
       }
       font-weight: bold;
-      display: block;
     }
     span.client-desc {
       font-size: 70%;
       display: block;
     }
   }
+}
+
+span.client-name {
+  &::before {
+    display: inline-block;
+    width: 1.5em;
+    content: $fa-plugin;
+    font-family: 'FontAwesome';
+    color: $ids-blue-1;
+    font-size: 100%;
+  }
+  font-weight: bold;
 }
\ No newline at end of file
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index f038638..a419882 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -75,6 +75,7 @@
           registerSuccess => 'Registrierung erfolgreich',
           registerFail => 'Registrierung fehlgeschlagen',
           oauthSettings => 'OAuth',
+          oauthUnregister => 'Möchten sie <span class="client-name"><%= $clientName %></span> wirklich löschen?'
         },
         -en => {
           loginSuccess => 'Login successful',
@@ -100,6 +101,7 @@
           registerSuccess => 'Registration successful',
           registerFail => 'Registration denied',
           oauthSettings => 'OAuth',
+          oauthUnregister => 'Do you really want to unregister <span class="client-name"><%= $clientName %></span>?',
         }
       }
     }
@@ -291,6 +293,7 @@
         # Get list of registered clients
         state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/list');
 
+        # Get the list of all clients
         return $c->korap_request(post => $r_url, {} => form => {
           client_id => $client_id,
           client_secret => $client_secret,
@@ -315,6 +318,7 @@
       }
     );
 
+
     # Issue a korap request with "oauth"orization
     # This will override the core request helper
     $app->helper(
@@ -678,7 +682,7 @@
             }
           );
         }
-      );
+      )->name('oauth-settings');
 
       # Route to oauth client registration
       $r->post('/settings/oauth/register')->to(
@@ -767,6 +771,85 @@
           );
         }
       )->name('oauth-register');
+
+
+      $r->get('/settings/oauth/unregister/:client_id')->to(
+        cb => sub {
+          shift->render(template => 'auth/unregister');
+        }
+      )->name('oauth-unregister');
+
+      # Unregister client
+      $r->post('/settings/oauth/unregister')->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('client-name', 'trim')->size(3, 255);
+          $v->required('client-id', 'trim')->size(3, 255);
+          $v->optional('client-secret');
+
+          # 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')
+          };
+
+          my $client_id =     $v->param('client-id');
+          my $client_name =   $v->param('client-name');
+          my $client_secret = $v->param('client-secret');
+
+          # Get list of registered clients
+          my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/deregister/')->path(
+            $client_id
+          );
+
+          my $send = {};
+
+          if ($client_secret) {
+            $send->{client_secret} = $client_secret;
+          };
+
+          # Get the list of all clients
+          return $c->korap_request(delete => $r_url, {} => form => $send)->then(
+            sub {
+              my $tx = shift;
+
+              # Response is fine
+              if ($tx->res->is_success) {
+                # Okay
+                $c->notify(success => 'Successfully deleted ' . $client_name);
+              }
+              else {
+
+                # Failure
+                my $json = $tx->result->json;
+                if ($json && $json->{error_description}) {
+                  $c->notify(error => $json->{error_description});
+                } else {
+                  $c->notify(error => $c->loc('Auth_responseError'));
+                };
+              };
+
+              return $c->redirect_to('oauth-settings');
+            }
+          );
+        }
+      )->name('oauth-unregister-post');
     };
   }
 
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep
index 2218236..527f370 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/register-success.html.ep
@@ -5,15 +5,17 @@
 <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>
+    <ul class="client-list">
+      <li class="client">
+        <span class="client-name"><%= stash 'client_name' %></span>
+        % if (stash('client_desc')) {
+        <span class="client-desc"><%= stash 'client_desc' %></span>
+        % };
+      </li>
+    </ul>
+    <p><%= loc 'Auth_clientType' %>: <tt><%= stash 'client_type' %></tt></p>
+    %= label_for 'client_id' => loc('Auth_clientID')
+    %= text_field 'client_id', stash('client_id'), readonly => 'readonly'
     % if (stash('client_type') ne 'PUBLIC') {
     <div>
       %= label_for 'client_secret' => loc('Auth_clientSecret')
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep
index 74f59ef..d504daa 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/tokens.html.ep
@@ -6,9 +6,13 @@
 % if ($list) {
 <ul class="client-list">
 %   foreach (@$list) {
-  <li>
+  <li class="client">
     <span class="client-name"><%= $_->{clientName} %></span>
     <span class="client-desc"><%= $_->{description} %></span>
+    <span class="button-group button-panel"><%= link_to Unregister => url_for('oauth-unregister', client_id => $_->{clientId})->query('name' => $_->{clientName}) => {} => ( class => 'client-unregister' ) %></span>
+% if ($_->{url}) {
+    <span class="client-url"><a href="<%= $_->{url} %>"><%= $_->{url} %></a></span>
+% }
   </li>
 %   };
 </ul>
@@ -29,9 +33,9 @@
       %= 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>
+%#      <br />
+%#      <%= radio_button type => 'CONFIDENTIAL' %>
+%#      <label>Confidential</label>
     </div>
 
     <div>
@@ -44,10 +48,10 @@
       %= url_field 'url', placeholder => 'https://...'
     </div>
 
-    <div>
-      %= label_for name => loc('Auth_redirectUri')
-      %= url_field 'redirectURI', placeholder => 'https://...'
-    </div>
+%#    <div>
+%#      %= label_for name => loc('Auth_redirectUri')
+%#      %= url_field 'redirectURI', placeholder => 'https://...'
+%#    </div>
 
     %= submit_button loc('Auth_clientRegister')
   </fieldset>
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep
new file mode 100644
index 0000000..b04f1e2
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep
@@ -0,0 +1,14 @@
+% extends 'settings', title => 'KorAP: ' . loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+<p><%== loc('Auth_oauthUnregister', clientName => param('name')) %></p>
+
+%= form_for 'oauth-unregister-post', class => 'form-table', begin
+   %= csrf_field
+   %= hidden_field 'client-id' => stash('client_id')
+   %= hidden_field 'client-name' => param('name')
+   %#= hidden_field 'client-secret' 
+   <input type="submit" value="Unregister" />
+   %= link_to 'Abort' => 'oauth-settings' => {} => (class => 'form-button button-abort')
+% end
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 8556319..cc52f9e 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -407,8 +407,13 @@
 $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('ul.client-list > li > span.client-name', 'R statistical computing tool')
-  ->text_is('ul.client-list > li > span.client-desc', 'R is a free software environment for statistical computing and graphics.')
+  ->element_exists('ul.client-list')
+  ->element_exists_not('ul.client-list > li')
+#  ->text_is('ul.client-list > li > span.client-name', 'R statistical computing tool ')
+#  ->text_is('ul.client-list > li > span.client-desc', 'R is a free software environment for statistical computing and graphics.')
+#  ->text_is('ul.client-list > li > span.client-url a', 'https://www.r-project.org/')
+#  ->text_is('ul.client-list > li a.client-unregister', 'Unregister')
+#  ->attr_is('ul.client-list > li a.client-unregister', 'href', '/settings/oauth/unregister/9aHsGW6QflV13ixNpez?name=R+statistical+computing+tool')
   ;
 
 $csrf = $t->post_ok('/settings/oauth/register' => form => {
@@ -435,6 +440,55 @@
   ->element_exists('input[name=client_secret][readonly][value]')
   ;
 
+$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('ul.client-list > li > span.client-name', 'MyApp')
+  ->text_is('ul.client-list > li > span.client-desc', 'This is my application')
+  ->text_is('ul.client-list > li > span.client-url a', '')
+  ->text_is('ul.client-list > li a.client-unregister', 'Unregister')
+  ->attr_is('ul.client-list > li 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')
+  ->tx->res->dom->at('input[name="csrf_token"]')
+  ->attr('value')
+  ;
+
+$t->post_ok('/settings/oauth/unregister' => form => {
+  'client-name' => 'MyApp',
+  'client-id' => 'xxxx==',
+  'csrf_token' => $csrf
+})->status_is(302)
+  ->content_is('')
+  ->header_is('Location' => '/settings/oauth')
+  ;
+
+$t->get_ok('/settings/oauth')
+  ->text_is('form.form-table legend', 'Register new client application')
+  ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+  ->element_exists('ul.client-list > li')
+  ->text_is('div.notify', 'Unknown client with xxxx==.')
+  ;
+
+$t->post_ok('/settings/oauth/unregister' => form => {
+  'client-name' => 'MyApp',
+  'client-id' => 'fCBbQkA2NDA3MzM1Yw==',
+  'csrf_token' => $csrf
+})->status_is(302)
+  ->content_is('')
+  ->header_is('Location' => '/settings/oauth')
+  ;
+
+$t->get_ok('/settings/oauth')
+  ->text_is('form.form-table legend', 'Register new client application')
+  ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+  ->element_exists_not('ul.client-list > li')
+  ->text_is('div.notify-success', 'Successfully deleted MyApp')
+  ;
+
 done_testing;
 __END__
-
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 4073d8d..38a3300 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -95,6 +95,8 @@
   return $decode;
 };
 
+app->defaults('oauth.client_list' => []);
+
 
 # Base page
 get '/v1.0/' => sub {
@@ -495,11 +497,20 @@
   my $json = $c->req->json;
 
   my $name = $json->{name};
-  my $desc = $json->{desc};
+  my $desc = $json->{description};
   my $type = $json->{type};
   my $url  = $json->{url};
   my $redirect_url = $json->{redirectURI};
 
+  my $list = $c->app->defaults('oauth.client_list');
+
+  push @$list, {
+    "clientId" => $tokens{new_client_id},
+    "clientName" => $name,
+    "description" => $desc,
+    "url" => $url
+  };
+
   # Confidential server application
   if ($type eq 'CONFIDENTIAL') {
     return $c->render(json => {
@@ -520,21 +531,45 @@
   my $c = shift;
 
   # $c->param('client_secret');
+
+  # Is empty [] when nothing registered
+
   return $c->render(
-    json => [
-      {
-        "clientId" => "9aHsGW6QflV13ixNpez",
-        "clientName" => "R statistical computing tool",
-        "description" => "R is a free software environment for statistical computing and graphics.",
-        "url" => "https://www.r-project.org/"
+    json => $c->stash('oauth.client_list'),
+    status => 200
+  );
+};
+
+del '/v1.0/oauth2/client/deregister/:client_id' => sub {
+  my $c = shift;
+  my $client_id = $c->stash('client_id');
+
+  my $list = $c->app->defaults('oauth.client_list');
+
+  my $break = -1;
+  for (my $i = 0; $i < @$list; $i++) {
+    if ($list->[$i]->{clientId} eq $client_id) {
+      $break = $i;
+      last;
+    };
+  };
+
+  if ($break != -1) {
+    splice @$list, $break, 1;
+  }
+
+  else {
+    return $c->render(
+      json => {
+        error_description => "Unknown client with $client_id.",
+        error => "invalid_client"
       },
-      {
-        "clientId" => "8bIDtZnH6NvRkW2Fq",
-        "clientName" => "EasyPDF Exporter",
-        "description" => "EasyPDF is a tool for exporting data to PDF.",
-        "url" => "https://www.easypdf.org/"
-      }
-    ],
+      status => 401
+    );
+  };
+
+  return $c->render(
+    json => $c->stash('oauth.client_list'),
     status => 200
   );
 };