Implement token refresh mechanism

Change-Id: Id58e14f663ebdd86f3f2206d4bfb9ad5d87a35fa
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index a72ead5..54fbe09 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -185,7 +185,9 @@
     $self->plugin('MailException' => $self->config('MailException'));
   };
 
-  # Load further plugins
+  # Load further plugins,
+  # that can override core functions,
+  # therefore order may be of importance
   if (exists $conf->{'plugins'}) {
     foreach (@{$conf->{'plugins'}}) {
       $self->plugin('Kalamar::Plugin::' . $_);
diff --git a/lib/Kalamar/Controller/Search.pm b/lib/Kalamar/Controller/Search.pm
index 3a7b380..e69f8f0 100644
--- a/lib/Kalamar/Controller/Search.pm
+++ b/lib/Kalamar/Controller/Search.pm
@@ -9,7 +9,10 @@
 
 # TODO:
 #   Support server timing API
-
+#
+# TODO:
+#   use "token" instead of "user"
+#
 # TODO:
 #   Add match_info template for HTML
 #
@@ -154,6 +157,8 @@
     sub {
       my $json = shift;
 
+      $c->app->log->debug("Receiving cached promised results");
+
       #######################
       # Cache total results #
       #######################
@@ -249,10 +254,12 @@
 
       # Only raised in case of connection errors
       if ($err_msg) {
-        $c->stash('err_msg' => 'backendNotAvailable');
+        # $c->stash('err_msg' => 'backendNotAvailable');
         $c->notify(error => { src => 'Backend' } => $err_msg)
       };
 
+      $c->app->log->debug("Receiving cached promised failure");
+
       # $c->_notify_on_errors(shift);
       return $c->render(
         template => 'failure'
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index fc2cd8b..6c6a043 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -2,9 +2,25 @@
 use Mojo::Base 'Mojolicious::Plugin';
 use Mojo::ByteStream 'b';
 
+# This is a plugin to deal with the Kustvakt OAuth server.
+# It establishes both the JWT as well as the OAuth password
+# flow for login.
+# All tokens are stored in the session. Access tokens are short-lived,
+# which limits the effects of misuse.
+# Refresh tokens are bound to client id and client secret,
+# which again limits the effects of misuse.
+
+# TODO:
+#   Establish a plugin 'OAuth' that works independent of 'Auth'.
+
 # TODO:
 #   CSRF-protect logout!
 
+# TODO:
+#   Remove the Bearer prefix from auth.
+
+# In case no expiration time is returned by the server,
+# take this time.
 our $EXPECTED_EXPIRATION_IN = 259200;
 
 # Register the plugin
@@ -31,9 +47,6 @@
     $app->log->error('client_id or client_secret not defined');
   };
 
-  # TODO:
-  #   Define user CHI cache
-
   $app->plugin('Localize' => {
     dict => {
       Auth => {
@@ -45,7 +58,10 @@
           logoutFail => 'Abmeldung fehlgeschlagen',
           csrfFail => 'Fehlerhafter CSRF Token',
           openRedirectFail => 'Weiterleitungsfehler',
-          refreshFail => 'Fehlerhafter Refresh-Token'
+          tokenExpired => 'Zugriffstoken abgelaufen',
+          tokenInvalid => 'Zugriffstoken ungültig',
+          refreshFail => 'Fehlerhafter Refresh-Token',
+          responseError => 'Unbekannter Autorisierungsfehler'
         },
         -en => {
           loginSuccess => 'Login successful',
@@ -54,7 +70,10 @@
           logoutFail => 'Logout failed',
           csrfFail => 'Bad CSRF token',
           openRedirectFail => 'Redirect failure',
-          refreshFail => 'Bad refresh token'
+          tokenExpired => 'Access token expired',
+          tokenInvalid => 'Access token invalid',
+          refreshFail => 'Bad refresh token',
+          responseError => 'Unknown authorization error'
         }
       }
     }
@@ -76,54 +95,31 @@
     }
   );
 
-  # Inject authorization to all korap requests
-  $app->hook(
-    before_korap_request => sub {
-      my ($c, $tx) = @_;
-      my $h = $tx->req->headers;
-
-      # If the request already has an Authorization
-      # header, respect it
-      unless ($h->authorization) {
-        my $auth_token = $c->auth->token or return;
-        $h->authorization($auth_token);
-
-      }
-
-      # TODO:
-      #   When a request fails because the access token timed out,
-      #   rerequest with the refresh token.
-
-      # TODO:
-      #   Check if the auth_token is timed out
-
-    }
-  );
-
 
   # Get or set the user token necessary for authorization
   $app->helper(
     'auth.token' => sub {
-      my ($c, $token) = @_;
+      my ($c, $token, $expires_in) = @_;
 
-      unless ($token) {
-        # Get token from stash
-        $token = $c->stash('auth');
-
-        return $token if $token;
-
-        # Get auth from session
-        $token = $c->session('auth') or return;
-
-        # Set token to stash
+      if ($token) {
+        # Set auth token
         $c->stash(auth => $token);
-
-        return $token;
+        $c->session(auth => $token);
+        $c->session(auth_exp => time + $expires_in);
+        return 1;
       };
 
-      # Set auth token
-      $c->stash('auth' => $token);
-      $c->session('auth' => $token);
+      # Get token from stash
+      $token = $c->stash('auth');
+
+      return $token if $token;
+
+      # Get auth from session
+      $token = $c->session('auth') or return;
+      $c->stash(auth => $token);
+
+      # Return stashed value
+      return $token;
     }
   );
 
@@ -136,38 +132,281 @@
     my $client_id = $param->{client_id};
     my $client_secret = $param->{client_secret};
 
-    # This refreshes an oauth2 token and
-    # returns a promise
+
+    # Sets a requested token and returns
+    # an error, if it didn't work
     $app->helper(
-      'auth.refresh_token' => sub {
+      'auth.set_tokens_p' => sub {
+        my ($c, $json) = @_;
+        my $promise = Mojo::Promise->new;
+
+        # No json object
+        unless ($json) {
+          return $promise->reject({
+            message => 'Response is no valid JSON object (remote)'
+          });
+        };
+
+        # There is an error here
+        # Dealing with errors here
+        if ($json->{error} && ref $json->{error} ne 'ARRAY') {
+          return $promise->reject(
+            {
+              message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
+            }
+          );
+        }
+
+        # There is an array of errors
+        elsif (my $error = $json->{errors} // $json->{error}) {
+          if (ref $error eq 'ARRAY') {
+            my @errors = ();
+            foreach (@{$error}) {
+              if ($_->[1]) {
+                push @errors, { code => $_->[0], message => $_->[1]}
+              }
+            }
+            return $promise->reject(@errors);
+          }
+
+          return $promise->reject({message => $error});
+        };
+
+        # Everything is fine
+        my $access_token  = $json->{access_token};
+        my $token_type    =  $json->{token_type};
+        my $refresh_token = $json->{refresh_token};
+        my $expires_in    = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
+        my $auth          = $token_type . ' ' . $access_token;
+        # my $scope       = $json->{scope};
+
+        # Set session info
+        $c->session(auth => $auth);
+
+        # Expiration of the token minus tolerance
+        $c->session(auth_exp => time + $expires_in - 60);
+
+        # Set session info for refresh token
+        # This can be stored in the session, as it is useless
+        # unless the client secret is stolen
+        $c->session(auth_r => $refresh_token) if $refresh_token;
+
+        # Set stash info
+        $c->stash(auth => $auth);
+
+        return $promise->resolve;
+      }
+    );
+
+
+    # Refresh tokens and return a promise
+    $app->helper(
+      'auth.refresh_p' => sub {
         my $c = shift;
         my $refresh_token = shift;
 
-        unless ($refresh_token) {
-          return Mojo::Promise->reject({message => 'Missing refresh token'})
-        };
-
         # Get OAuth access token
-        my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+        state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
 
-        return $c->korap_request('POST', $url, {} => form => {
+        $c->app->log->debug("Refresh at " . $r_url);
+
+        return $c->kalamar_ua->post_p($r_url, {} => form => {
           grant_type => 'refresh_token',
           client_id => $client_id,
           client_secret => $client_secret,
           refresh_token => $refresh_token
         })->then(
           sub {
-            # Set the tokens and return a promise
-            return $plugin->set_tokens(
-              $c,
-              shift->result->json
-            )
+            my $tx = shift;
+            my $json = $tx->result->json;
+
+            # Response is fine
+            if ($tx->res->is_success) {
+
+              $c->app->log->info("Refresh was successful");
+
+              # Set the tokens and return a promise
+              return $c->auth->set_tokens_p($json);
+            };
+
+            # There is a client error - refresh fails
+            if ($tx->res->is_client_error && $json) {
+
+              $c->stash(auth => undef);
+              $c->stash(auth_exp => undef);
+              delete $c->session->{user};
+              delete $c->session->{auth};
+              delete $c->session->{auth_r};
+              delete $c->session->{auth_exp};
+
+              # Response is 400
+              return Mojo::Promise->reject(
+                $json->{error_description} // $c->loc('Auth_refreshFail')
+              );
+            };
+
+            $c->notify(error => $c->loc('Auth_responseError'));
+            return Mojo::Promise->reject;
+          }
+        )
+      }
+    );
+
+
+    # Issue a korap request with "oauth"orization
+    # This will override the core request helper
+    $app->helper(
+      korap_request => sub {
+        my $c      = shift;
+        my $method = shift;
+        my $path   = shift;
+        my @param = @_;
+
+        # TODO:
+        #   Check if $tx is not leaked!
+
+        # Get plugin user agent
+        my $ua = $c->kalamar_ua;
+
+        my $url = Mojo::URL->new($path);
+        my $tx = $ua->build_tx(uc($method), $url->clone, @param);
+
+        # Set X-Forwarded for
+        $tx->req->headers->header(
+          'X-Forwarded-For' => $c->client_ip
+        );
+
+        # Emit Hook to alter request
+        $c->app->plugins->emit_hook(
+          before_korap_request => ($c, $tx)
+        );
+
+        my $h = $tx->req->headers;
+
+        # If the request already has an Authorization
+        # header, respect it!
+        if ($h->authorization) {
+          return $ua->start_p($tx);
+        };
+
+        # Get auth token
+        if (my $auth_token = $c->auth->token) {
+
+          # The token is already expired!
+          my $exp = $c->session('auth_exp');
+          if (defined $exp && $exp < time) {
+
+            # Remove auth ...
+            $c->stash(auth => undef);
+
+            # And get refresh token from session
+            if (my $refresh_token = $c->session('auth_r')) {
+
+              $c->app->log->debug("Refresh is required");
+
+              # Refresh
+              return $c->auth->refresh_p($refresh_token)->then(
+                sub {
+                  $c->app->log->debug("Search with refreshed tokens");
+
+                  # Tokens were set - now send the request the first time!
+                  $tx->req->headers->authorization($c->stash('auth'));
+                  return $ua->start_p($tx);
+                }
+              );
+            }
+
+            # The token is expired and no refresh token is
+            # available - issue an unauthorized request!
+            else {
+              $c->stash(auth => undef);
+              $c->stash(auth_exp => undef);
+              delete $c->session->{user};
+              delete $c->session->{auth};
+              delete $c->session->{auth_r};
+              delete $c->session->{auth_exp};
+
+              # Warn on Error!
+              $c->notify(warn => $c->loc('Auth_tokenExpired'));
+              return $ua->start_p($tx);
+            };
+          }
+
+          # Auth token is fine
+          else {
+
+            # Set auth
+            $h->authorization($auth_token);
+          }
+        }
+
+        # No token set
+        else {
+
+          # Return unauthorized request
+          return $ua->start_p($tx);
+        };
+
+        # Issue an authorized request and automatically
+        # refresh the token on expiration!
+        return $ua->start_p($tx)->then(
+          sub {
+            my $tx = shift;
+
+            # Response is fine
+            if ($tx->res->is_success) {
+              return Mojo::Promise->resolve($tx);
+            }
+
+            # There is a client error - maybe refresh!
+            elsif ($tx->res->is_client_error) {
+
+              # Check the error
+              my $json = $tx->res->json('/errors/0/1');
+              if ($json && ($json =~ /expired|invalid/)) {
+                $c->stash(auth => undef);
+                $c->stash(auth_exp => undef);
+                delete $c->session->{user};
+                delete $c->session->{auth};
+
+                # And get refresh token from session
+                if (my $refresh_token = $c->session('auth_r')) {
+
+                  # Refresh
+                  return $c->auth->refresh_p($refresh_token)->then(
+                    sub {
+                      $c->app->log->debug("Search with refreshed tokens");
+
+                      my $tx = $ua->build_tx(uc($method), $url->clone, @param);
+
+                      # Set X-Forwarded for
+                      $tx->req->headers->header(
+                        'X-Forwarded-For' => $c->client_ip
+                      );
+
+                      # Tokens were set - now send the request the first time!
+                      $tx->req->headers->authorization($c->stash('auth'));
+                      return $ua->start_p($tx);
+                    }
+                  )
+                };
+
+                # Reject the invalid token
+                $c->notify(error => $c->loc('Auth_tokenInvalid'));
+                return Mojo::Promise->reject;
+              };
+
+              return Mojo::Promise->resolve($tx);
+            };
+
+            $c->notify(error => $c->loc('Auth_responseError'));
+            return Mojo::Promise->reject;
           }
         );
       }
     );
 
-    # Password flow
+    # Password flow for OAuth
     $r->post('/user/login')->to(
       cb => sub {
         my $c = shift;
@@ -201,7 +440,7 @@
 
         my $pwd = $v->param('pwd');
 
-        $c->app->log->debug("Login from user $user:XXXX");
+        $c->app->log->debug("Login from user $user");
 
         # <specific>
 
@@ -218,10 +457,7 @@
         })->then(
           sub {
             # Set the tokens and return a promise
-            return $plugin->set_tokens(
-              $c,
-              shift->result->json
-            )
+            return $c->auth->set_tokens_p(shift->result->json)
           }
         )->catch(
           sub {
@@ -271,10 +507,41 @@
         return 1;
       }
     )->name('login');
+
+
+    # Log out of the session
+    # $r->get('/user/logout')->to(
+    #  cb => sub {
+    #
+    #    # TODO!
+    #    return shift->redirect_to('index');
+    #  }
+    #)->name('logout');
   }
+
   # Use JWT login
+  # (should be deprecated)
   else {
 
+    # Inject authorization to all korap requests
+    $app->hook(
+      before_korap_request => sub {
+        my ($c, $tx) = @_;
+        my $h = $tx->req->headers;
+
+        # If the request already has an Authorization
+        # header, respect it
+        unless ($h->authorization) {
+
+          # Get valid auth token and set as header
+          if (my $auth_token = $c->auth->token) {
+            $h->authorization($auth_token);
+          };
+        };
+      }
+    );
+
+    # Password flow with JWT
     $r->post('/user/login')->to(
       cb => sub {
         my $c = shift;
@@ -308,7 +575,7 @@
 
         my $pwd = $v->param('pwd');
 
-        $c->app->log->debug("Login from user $user:XXXX");
+        $c->app->log->debug("Login from user $user");
 
         my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
 
@@ -353,7 +620,7 @@
             # TODO: Deal with user return values.
             my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
 
-            $c->app->log->debug(qq!Login successful: "$user" with "$auth"!);
+            $c->app->log->debug(qq!Login successful: "$user"!);
 
             $user = $jwt->{username} ? $jwt->{username} : $user;
 
@@ -365,8 +632,6 @@
             $c->stash(user => $user);
             $c->stash(auth => $auth);
 
-            # Set cache
-            $c->chi('user')->set($auth => $user);
             $c->notify(success => $c->loc('Auth_loginSuccess'));
           }
         )->catch(
@@ -463,61 +728,6 @@
   )->name('logout');
 };
 
-# Sets a requested token and returns
-# an error, if it didn't work
-sub set_tokens {
-  my ($plugin, $c, $json) = @_;
-
-  my $promise = Mojo::Promise->new;
-
-  # No json object
-  unless ($json) {
-    return $promise->reject({message => 'Response is no valid Json object (remote)'});
-  };
-
-  # There is an error here
-  # Dealing with errors here
-  if ($json->{error} && ref $json->{error} ne 'ARRAY') {
-    return $promise->reject(
-      {
-        message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
-      }
-    );
-  } elsif (my $error = $json->{errors} // $json->{error}) {
-    if (ref $error eq 'ARRAY') {
-      my @errors = ();
-      foreach (@{$error}) {
-        if ($_->[1]) {
-          push @errors, { code => $_->[0], message => $_->[1]}
-        }
-      }
-      return $promise->reject(@errors);
-    }
-
-    return $promise->reject({message => $error});
-  };
-
-  my $access_token = $json->{access_token};
-  my $token_type =  $json->{token_type};
-  my $refresh_token = $json->{refresh_token};
-  # my $scope = $json->{scope};
-  my $expires_in = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
-  my $auth = $token_type . ' ' . $access_token;
-
-  # Set session info
-  $c->session(auth => $auth);
-
-  # Set stash info
-  $c->stash(auth => $auth);
-
-  # Remember refresh token in cache
-  $c->chi('user')->set(
-    "refr_" . $auth => $refresh_token,
-    $expires_in
-  );
-
-  return $promise->resolve;
-}
 
 1;
 
diff --git a/lib/Kalamar/Plugin/KalamarHelpers.pm b/lib/Kalamar/Plugin/KalamarHelpers.pm
index f97f453..4731e4d 100644
--- a/lib/Kalamar/Plugin/KalamarHelpers.pm
+++ b/lib/Kalamar/Plugin/KalamarHelpers.pm
@@ -338,7 +338,7 @@
             my $tx = shift;
             # Catch errors and warnings
             return ($c->catch_errors_and_warnings($tx) ||
-              Mojo::Promise->new->reject);
+              Mojo::Promise->reject);
           }
         );
       };
@@ -380,6 +380,9 @@
         # Cache on success
         sub {
           my $json = shift;
+
+          $c->app->log->debug("Receiving promised results");
+
           $c->chi->set($cache_str => $json);
           return $json;
         }
diff --git a/lib/Kalamar/Plugin/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index c1a6b0c..fbda902 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -8,10 +8,6 @@
 has 'ua';
 
 # TODO:
-#   This Plugin will be removed in favour of
-#   Kalamar::Plugin::Auth!
-
-# TODO:
 #   Replace plugin-api with korap->api!
 
 sub register {