Issue a new token for a public client

Change-Id: Id44501d46aff4fd540339c0b2901879ab8a77734
diff --git a/Changes b/Changes
index b5a4db7..2b2bfa5 100755
--- a/Changes
+++ b/Changes
@@ -1,8 +1,9 @@
-0.42 2021-03-08
+0.42 2021-03-15
         - Added GitHub based CI for perl.
         - Added further methods for communicating JSON Files
           with the server to the APIs (lerepp).
         - Remove ruby-sass requirement (fixes #123).
+        - Added support to issue new OAuth2 tokens.
 
 0.41 2021-03-01
         - Introduce CORS headers to the proxy.
diff --git a/dev/scss/base/base.scss b/dev/scss/base/base.scss
index be195e3..c245cd9 100644
--- a/dev/scss/base/base.scss
+++ b/dev/scss/base/base.scss
@@ -44,6 +44,10 @@
 }
 
 a {
+  &:visited {
+    color: $darkest-orange;
+  }
+  
   &:link {
     text-decoration: none;
     color:           $dark-orange;
diff --git a/dev/scss/main/buttongroup.scss b/dev/scss/main/buttongroup.scss
index db3462a..6a89297 100644
--- a/dev/scss/main/buttongroup.scss
+++ b/dev/scss/main/buttongroup.scss
@@ -5,6 +5,7 @@
  * Define the base layout of horizontal button groups
  */
 .button-group {
+  font-size: 0;
   > span {
     cursor: pointer;
   }
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 885be07..380593d 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -76,7 +76,8 @@
           registerFail => 'Registrierung fehlgeschlagen',
           oauthSettings => 'OAuth',
           oauthUnregister => 'Möchten sie <span class="client-name"><%= $clientName %></span> wirklich löschen?',
-          loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.'
+          loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
+          oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $clientName %></span>',
         },
         -en => {
           loginSuccess => 'Login successful',
@@ -103,7 +104,8 @@
           registerFail => 'Registration denied',
           oauthSettings => 'OAuth',
           oauthUnregister => 'Do you really want to unregister <span class="client-name"><%= $clientName %></span>?',
-          loginHint => 'Maybe you need to log in first?'
+          loginHint => 'Maybe you need to log in first?',
+          oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $clientName %></span>',
         }
       }
     }
@@ -732,7 +734,8 @@
             else {
               $c->notify(warn => $c->loc('Auth_paramError'));
             };
-            return $c->render(template => 'auth/clients')
+            # return $c->redirect_to('oauth-settings');
+            return $c->render(template => 'auth/clients');
           };
 
           # Wait for async result
@@ -826,7 +829,7 @@
             else {
               $c->notify(warn => $c->loc('Auth_paramError'));
             };
-            return $c->render(template => 'auth/clients')
+            return $c->redirect_to('oauth-settings');
           };
 
           my $client_id =     $v->param('client-id');
@@ -906,7 +909,9 @@
             sub {
               # return $c->render(text => 'hui');
 
-
+              # TODO:
+              #   This would better be a redirect to the client page, but in case there is a client_secret
+              # this wouldn't work (unless we flash that).
               return $c->render(template => 'auth/client')
             }
           );
@@ -915,6 +920,158 @@
         }
       )->name('oauth-tokens');
     };
+
+
+    # Show information of a client
+    $r->get('/settings/oauth/client/:client_id/token')->to(
+      cb => sub {
+        shift->render(template => 'auth/issue-token');
+      }
+    )->name('oauth-issue-token');
+
+
+    $r->post('/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('client-id', 'trim')->size(3, 255);
+        $v->optional('client-secret');
+        $v->required('name', 'trim');
+
+        # 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->redirect_to('oauth-settings')
+        };
+
+        # Get authorization token
+        state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
+        my $client_id = $c->stash('client_id');
+        my $name = $v->param('name');
+        my $redirect_url = $c->url_for->query({name => $name});
+
+        return $c->korap_request(post => $r_url, {} => form => {
+          response_type => 'code',
+          client_id => $client_id,
+          redirect_uri => $redirect_url,
+          # TODO: State
+        })->then(
+          sub {
+            my $tx = shift;
+
+            # Strip the token from the location header of the fake redirect
+            # TODO: Alternatively redirect!
+            my ($code, $scope, $loc, $name);
+            foreach (@{$tx->redirects}) {
+              $loc = $_->res->headers->header('Location');
+              if (index($loc, 'code') > 0) {
+                my $q = Mojo::URL->new($loc)->query;
+                $code  = $q->param('code');
+                $scope = $q->param('scope');
+                $name  = $q->param('name');
+                last;
+              };
+            };
+
+            # Fine!
+            if ($code) {
+              return Mojo::Promise->resolve(
+                $client_id,
+                $redirect_url,
+                $code,
+                $scope,
+                $name
+              );
+            };
+            return Mojo::Promise->reject;
+          }
+        )->then(
+          sub {
+            my ($client_id, $redirect_url, $code, $scope, $name) = @_;
+
+            # Get OAuth access token
+            state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+            return $c->kalamar_ua->post_p($r_url, {} => form => {
+              client_id => $client_id,
+              # NO CLIENT_SECRET YET SUPPORTED
+              grant_type => 'authorization_code',
+              code => $code,
+              redirect_uri => $redirect_url
+            })->then(
+              sub {
+                my $tx = shift;
+                my $json = $tx->res->json;
+
+                if ($tx->res->is_error) {
+                  $c->notify(error => 'Unable to fetch new token');
+                  return Mojo::Promise->reject;
+                };
+
+                $c->notify(success => 'New access token created');
+
+                return $c->render(
+                  template => 'auth/client',
+                  client_name => $name,
+                  %$json
+                );
+              }
+            )->catch(
+              sub {
+                my $err_msg = shift;
+
+                # Only raised in case of connection errors
+                if ($err_msg) {
+                  $c->notify(error => { src => 'Backend' } => $err_msg)
+                };
+
+                $c->render(
+                  status => 400,
+                  template => 'failure'
+                );
+              }
+            )
+
+            # Start IOLoop
+            ->wait;
+
+          }
+        )->catch(
+          sub {
+            my $err_msg = shift;
+
+            # Only raised in case of connection errors
+            if ($err_msg) {
+              $c->notify(error => { src => 'Backend' } => $err_msg)
+            };
+
+            return $c->render(
+              status => 400,
+              template => 'failure'
+            );
+          }
+        )
+
+        # Start IOLoop
+        ->wait;
+
+        return 1;
+      }
+    )->name('oauth-issue-token-post');
   }
 
   # 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 38fa13d..8900777 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
@@ -16,15 +16,26 @@
         % };
       </li>
     </ul>
-    <span class="button-group button-panel"><%= link_to Unregister => url_for('oauth-unregister', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-unregister' ) %></span>
+    <span class="button-group button-panel">
+      %= link_to Unregister => url_for('oauth-unregister', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-unregister' )
+      %= link_to IssueToken => url_for('oauth-issue-token', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-issue-token' )
+    </span>
     <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') {
+    % if (stash('client_type') && stash('client_type') ne 'PUBLIC') {
     <div>
       %= label_for 'client_secret' => loc('Auth_clientSecret')
       %= password_field 'client_secret', value => stash('client_secret'), readonly => 'readonly'
     </div>
     % };
+
+    % if (stash('access_token')) {
+    %= label_for 'access_token' => 'Access Token'
+    %= text_field 'access_token', stash('access_token'), readonly => 'readonly'
+    <p name="expires">Expires in: <tt><%= stash 'expires_in' %></tt></p>
+    <p name="scope">Scope: <tt><%= stash 'scope' %></tt></p>
+    <p name="type">Token-Type: <tt><%= stash 'token_type' %></tt></p>
+    % }
   </fieldset>
 </form>
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
new file mode 100644
index 0000000..ea1e66c
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
@@ -0,0 +1,14 @@
+% extends 'settings', title => 'KorAP: ' . loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+<p><%== loc('Auth_oauthIssueToken', clientName => param('name')) %></p>
+
+%= form_for 'oauth-issue-token-post', id => 'issue-token', class => 'form-table', begin
+   %= csrf_field
+   %= hidden_field 'client-id' => stash('client_id')
+   %= hidden_field 'name' => param('name')
+   %#= hidden_field 'client-secret' 
+   <input type="submit" value="issue" />
+   %= 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 22054d2..adb7378 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -522,5 +522,47 @@
   ->text_is('div.notify-success', 'Successfully deleted MyApp')
   ;
 
+$t->post_ok('/settings/oauth/register' => form => {
+  name => 'MyApp2',
+  type => 'PUBLIC',
+  desc => 'This is my application',
+  csrf_token => $csrf
+})->status_is(200)
+  ->element_exists('div.notify-success')
+  ->text_is('legend', 'Client Credentials')
+  ->text_is('label[for=client_id]', 'ID of the client application')
+  ->element_exists('input[name=client_id][readonly][value]')
+  ->element_exists_not('input[name=client_secret][readonly][value]')
+  ;
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+  ->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')
+  ;
+
+$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
+  ->status_is(200)
+  ->attr_is('#issue-token','action', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token')
+  ->attr_is('input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
+  ->attr_is('input[name=name]', 'value', 'MyApp2')
+  ->tx->res->dom->at('input[name="csrf_token"]')
+  ->attr('value')
+  ;
+
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token' => form => {
+  csrf_token => $csrf,
+  name => 'MyApp2',
+  'client-id' => 'fCBbQkA2NDA3MzM1Yw=='
+})
+  ->status_is(200)
+  ->attr_is('input[name=access_token]', 'value', 'jvgjbvjgzucgdwuiKHJK')
+  ->text_is('p[name=expires] tt', '31536000')
+  ->text_is('p[name=scope] tt', 'match_info search openid')
+  ->text_is('p[name=type] tt', 'Bearer')
+  ;
+
+
 done_testing;
 __END__
diff --git a/t/plugin/auth.t b/t/plugin/auth.t
index f5351de..bd9a3b1 100644
--- a/t/plugin/auth.t
+++ b/t/plugin/auth.t
@@ -196,8 +196,6 @@
   ->header_is('Location' => '/?q=Baum&ql=poliqarp');
 
 
-
-
 done_testing;
 __END__
 
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 31bd494..21b938f 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -18,9 +18,11 @@
   'access_token'    => "4dcf8784ccfd26fac9bdb82778fe60e2",
   'refresh_token'   => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
   'access_token_2'  => "abcde",
+  'access_token_3' => 'jvgjbvjgzucgdwuiKHJK',
   'refresh_token_2' => "fghijk",
   'new_client_id' => 'fCBbQkA2NDA3MzM1Yw==',
   'new_client_secret' => 'KUMaFxs6R1WGud4HM22w3HbmYKHMnNHIiLJ2ihaWtB4N5JxGzZgyqs5GTLutrORj',
+  'auth_token_1'    => 'mscajfdghnjdfshtkjcuynxahgz5il'
 );
 
 helper get_token => sub {
@@ -461,6 +463,21 @@
     );
   }
 
+  # Get auth_token_1
+  elsif ($grant_type eq 'authorization_code') {
+    if ($c->param('code') eq $tokens{auth_token_1}) {
+      return $c->render(
+        status => 200,
+        json => {
+          "access_token" => $tokens{access_token_3},
+          "expires_in" => 31536000,
+          "scope" => 'match_info search openid',
+          "token_type" => "Bearer"
+        }
+      );
+    };
+  }
+
   # Unknown token grant
   else {
     return $c->render(
@@ -580,6 +597,23 @@
   );
 };
 
+post '/v1.0/oauth2/authorize' => sub {
+  my $c = shift;
+  my $type = $c->param('response_type');
+  my $client_id = $c->param('client_id');
+  my $redirect_uri = $c->param('redirect_uri');
+
+  if ($type eq 'code') {
+
+    return $c->redirect_to(
+      Mojo::URL->new($redirect_uri)->query({
+        code => $tokens{auth_token_1},
+        scope => 'match_info search openid'
+      })
+      );
+  }
+};
+
 
 
 app->start;