Support confidential clients

Change-Id: I907592587ae296bef592c2f731a0302c6e9e8c8b
diff --git a/Changes b/Changes
index 00aad71..3131657 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,7 @@
-0.44 2022-02-24
+0.45 2022-04-06
+        - Added confidential client support to OAuth. (diewald)
+
+0.44 2022-02-31
         - Fixed autosecrets migration. (diewald)
         - Format page numbers in pagination. (diewald)
         - Introduce tei2korapxml command via plugin. (diewald)
diff --git a/dev/scss/main/oauth.scss b/dev/scss/main/oauth.scss
index db8e09d..c1e38c1 100644
--- a/dev/scss/main/oauth.scss
+++ b/dev/scss/main/oauth.scss
@@ -9,14 +9,17 @@
 
   li.client {
     list-style-type: none;
+    margin-bottom: 1.5em;
 
     span.client-name::before {
       margin-left: -1.5em;
     }
 
-    span.client-desc {
-      font-size: 70%;
-      display:   block;
+    p.client-desc, p.client-url {
+      font-weight: normal;
+      font-size: 80%;
+      margin-top: .2em;
+      margin-bottom: .2em;
     }
   }
 
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 97bb173..00f8242 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -8,7 +8,7 @@
 use List::Util 'none';
 
 # Minor version - may be patched from package.json
-our $VERSION = '0.44';
+our $VERSION = '0.45';
 
 # Supported version of Backend API
 our $API_VERSION = '1.0';
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 011dae1..963300a 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -873,6 +873,14 @@
               my $result = $tx->result;
 
               if ($result->is_error) {
+                my $json = $result->json;
+                if ($json && $json->{error}) {
+                  $c->notify(
+                    error => $json->{error} .
+                      ($json->{error_description} ? ': ' . $json->{error_description} : '')
+                  )
+                };
+
                 return Mojo::Promise->reject;
               };
 
@@ -898,10 +906,7 @@
             }
           )->catch(
             sub {
-              # Server may be irresponsible
-              my $err = shift;
               $c->notify('error' => $c->loc('Auth_en_registerFail'));
-              return Mojo::Promise->reject($err);
             }
           )->finally(
             sub {
@@ -1018,7 +1023,7 @@
               $c->stash(client_name => $item->{client_name});
               $c->stash(client_desc => $item->{client_description});
               $c->stash(client_url  => $item->{client_url});
-              $c->stash(client_type => 'PUBLIC');
+              $c->stash(client_type => ($item->{client_type} // 'PUBLIC'));
 
               $c->auth->token_list_p($c->stash('client_id'));
             }
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
index 28074ea..de9e3a6 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
@@ -9,19 +9,23 @@
       <li class="client">
         <span class="client-name"><%= stash 'client_name' %></span>
         % if (stash('client_desc')) {
-        <span class="client-desc"><%= stash 'client_desc' %></span>
+        <p class="client-desc"><%= stash 'client_desc' %></p>
         % };
         % if (stash('client_url')) {
-        <span class="client-url"><a href="<%= stash('client_url') %>"><%= stash('client_url') %></a></span>
+        <p class="client-url"><a href="<%= stash('client_url') %>"><%= stash('client_url') %></a></p>
+        % };
+      
+        % if (stash('client_redirect_uri')) {
+        <p class="client-redirect-uri"><%= loc 'Auth_redirectUri' %>: <tt><%= stash('client_redirect_uri') %></tt></p>
         % };
 
-        <p><%= loc 'Auth_clientType' %>: <tt><%= stash 'client_type' %></tt></p>
+        <p class="client-type"><%= 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', class => 'copy-to-clipboard'
-        % if (stash('client_type') && stash('client_type') ne 'PUBLIC') {
+        % if (stash('client_type') && stash('client_type') ne 'PUBLIC' && stash('client_secret')) {
         <div>
           %= label_for 'client_secret' => loc('Auth_clientSecret')
-          %= password_field 'client_secret', value => stash('client_secret'), readonly => 'readonly'
+          %= password_field 'client_secret', value => stash('client_secret'), readonly => 'readonly', class => 'show-pwd copy-to-clipboard'
         </div>
         % };
 
@@ -29,7 +33,9 @@
 
         <span class="button-group button-panel">
           %= link_to loc('Auth_oauthUnregister_short') => url_for('oauth-unregister', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-unregister' )
-          %= link_to loc('Auth_oauthIssueToken_short') => url_for('oauth-issue-token', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-issue-token' )
+          % if (stash('client_type') && stash('client_type') eq 'PUBLIC') {
+            %= link_to loc('Auth_oauthIssueToken_short') => url_for('oauth-issue-token', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-issue-token' )
+          % };
         </span>        
       </li>
     </ul>    
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep
index 928e4c7..cee1abd 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep
@@ -8,10 +8,12 @@
 %   foreach (@$list) {
   <li class="client">
     <span class="client-name"><%= link_to $_->{client_name} => url_for('oauth-tokens', client_id => $_->{client_id}) %></span>
-    <span class="client-desc"><%= $_->{client_description} %></span>
-% if ($_->{client_url}) {
-    <span class="client-url"><a href="<%= $_->{client_url} %>"><%= $_->{client_url} %></a></span>
-% }
+    % if ($_->{client_description}) {
+      <p class="client-desc"><%= $_->{client_description} %></p>
+    % };
+    % if ($_->{client_url}) {
+    <p class="client-url"><a href="<%= $_->{client_url} %>"><%= $_->{client_url} %></a></p>
+    % }
   </li>
 %   };
 </ul>
@@ -32,9 +34,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>
@@ -47,10 +49,10 @@
       %= url_field 'url', placeholder => 'https://...'
     </div>
 
-%#    <div>
-%#      %= label_for name => loc('Auth_redirectUri')
-%#      %= url_field 'redirect_uri', placeholder => 'https://...'
-%#    </div>
+    <div>
+      %= label_for name => loc('Auth_redirectUri')
+      %= url_field 'redirect_uri', placeholder => 'https://...'
+    </div>
 
     %= submit_button loc('Auth_clientRegister')
   </fieldset>
diff --git a/package.json b/package.json
index cdba928..1297967 100755
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "Kalamar",
   "description": "Mojolicious-based Frontend for KorAP",
   "license": "BSD-2-Clause",
-  "version": "0.44.1",
+  "version": "0.45.0",
   "pluginVersion": "0.2.2",
   "engines": {
     "node": ">=6.0.0"
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 635df8c..e8ac795 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -516,18 +516,18 @@
   ->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 > p.client-desc', 'This is my application')
   ->header_is('Cache-Control','max-age=0, no-cache, no-store, must-revalidate')
   ->header_is('Expires','Thu, 01 Jan 1970 00:00:00 GMT')
   ->header_is('Pragma','no-cache')
-  ->tx->res->dom->at('ul.client-list > li > span.client-url a')
+  ->tx->res->dom->at('ul.client-list > li > p.client-url a')
   ;
 is(defined $anchor ? $anchor->text : '', '');
 
 $t->get_ok('/settings/oauth/fCBbQkA2NDA3MzM1Yw==')
   ->status_is(200)
   ->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('ul.client-list > li.client > p.client-desc', 'This is my application')
   ->text_is('a.client-unregister', 'Unregister')
   ->attr_is('a.client-unregister', 'href', '/settings/oauth/fCBbQkA2NDA3MzM1Yw==/unregister?name=MyApp')
   ;
@@ -592,6 +592,7 @@
   ->header_is('Cache-Control','max-age=0, no-cache, no-store, must-revalidate')
   ->header_is('Expires','Thu, 01 Jan 1970 00:00:00 GMT')
   ->header_is('Pragma','no-cache')
+  ->element_exists('.client-issue-token')
   ;
 
 $t->get_ok('/settings/oauth/fCBbQkA2NDA3MzM1Yw==')
@@ -605,6 +606,7 @@
   ->text_is('ul.token-list label[for=token]', 'Access Token')
   ->text_is('p[name=created]', 'Created at ')
   ->text_is('p[name=expires]', 'Expires in 31533851 seconds.')
+  ->element_exists('.client-issue-token')
   ;
 
 $csrf = $t->get_ok('/settings/oauth/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
@@ -698,11 +700,58 @@
   ->header_is('Location','/settings/oauth/fCBbQkA2NDA3MzM1Yw==')
   ;
 
-
 $t->get_ok('/settings/oauth/fCBbQkA2NDA3MzM1Yw==')
   ->element_exists_not('div.notify-error')
   ->text_is('div.notify-success', 'Token was revoked successfully')
   ;
 
+$t->app->routes->get('/x/redirect-target')->to(
+  cb => sub {
+    my $c = shift;
+    return $c->render(text => 'redirected');
+  }
+);
+
+$csrf = $t->post_ok('/settings/oauth/register' => form => {
+  name => 'MyConfApp',
+  type => 'CONFIDENTIAL',
+  desc => 'This is my application',
+})
+  ->text_is('div.notify-error', 'Bad CSRF token')
+  ->tx->res->dom->at('input[name="csrf_token"]')
+  ->attr('value')
+  ;
+
+$t->post_ok('/settings/oauth/register' => form => {
+  name => 'MyConfApp',
+  type => 'CONFIDENTIAL',
+  desc => 'This is my confidential application',
+  csrf_token => $csrf,
+  redirect_uri => 'http://localhost/redirect-target'
+})
+  ->text_is('div.notify-error', undef)
+  ->text_is('li.client span.client-name', 'MyConfApp')
+  ->text_is('li.client p.client-desc', 'This is my confidential application')
+  ->text_is('li.client .client-redirect-uri tt', 'http://localhost/redirect-target')
+  ->text_is('li.client .client-type tt', 'CONFIDENTIAL')
+  ->element_exists_not('.client-issue-token')
+  ;
+
+$t->post_ok('/settings/oauth/register' => form => {
+  name => 'MyConfApp2',
+  type => 'CONFIDENTIAL',
+  desc => 'This is my second confidential application',
+  csrf_token => $csrf,
+  redirect_uri => 'http://localhost/FAIL'
+})
+  ->status_is(302)
+  ->header_is('location','/settings/oauth/')
+  ;
+
+$t->get_ok('/settings/oauth/')
+  ->text_is('div.notify-error', 'invalid_request: http://localhost/FAIL is invalid.')
+  ;
+
+
 done_testing;
 __END__
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 59a6766..d7ec2ff 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -21,6 +21,7 @@
   'access_token_3' => 'jvgjbvjgzucgdwuiKHJK',
   'refresh_token_2' => "fghijk",
   'new_client_id' => 'fCBbQkA2NDA3MzM1Yw==',
+  'new_client_id_2' => 'hghGHhjhFRz_gJhjrd==',
   'new_client_secret' => 'KUMaFxs6R1WGud4HM22w3HbmYKHMnNHIiLJ2ihaWtB4N5JxGzZgyqs5GTLutrORj',
   'auth_token_1'    => 'mscajfdghnjdfshtkjcuynxahgz5il'
 );
@@ -543,7 +544,7 @@
   my $desc = $json->{description};
   my $type = $json->{type};
   my $url  = $json->{url};
-  my $redirect_url = $json->{redirect_uri};
+  my $redirect_uri = $json->{redirect_uri};
 
   my $list = $c->app->defaults('oauth.client_list');
 
@@ -551,13 +552,25 @@
     "client_id" => $tokens{new_client_id},
     "client_name" => $name,
     "client_description" => $desc,
-    "client_url" => $url
+    "client_url" => $url,
+    "client_redirect_uri" => $redirect_uri
+  };
+
+  if ($redirect_uri && $redirect_uri =~ /FAIL$/) {
+    return $c->render(
+      status => 400,
+      json => {
+        "error_description" => $redirect_uri . " is invalid.",
+        "error" => "invalid_request"
+      }
+    )
   };
 
   # Confidential server application
   if ($type eq 'CONFIDENTIAL') {
+
     return $c->render(json => {
-      client_id => $tokens{new_client_id},
+      client_id => $tokens{new_client_id_2},
       client_secret => $tokens{new_client_secret}
     });
   };