Merge "Extended Guided Tour"
diff --git a/Changes b/Changes
index ee8858a..ef8de58 100755
--- a/Changes
+++ b/Changes
@@ -1,9 +1,16 @@
-0.36 2019-07-25
+0.36 2019-08-29
         - Rename all cookies to be independent
           for different instance (#94).
         - Enable https only via
           configuration option 'https_only'.
         - Make VC replaceable via KorAP.vc.fromJson().
+        - Emit 'after_render' in proxy responses
+          to make it accessible to post processing
+          (such as the Piwik plugin).
+        - Fix treatment of legacy "collection" parameter.
+        - Fix pagination by not repeating page value in URL.
+        - Added auto-refresh of OAuth tokens.
+        - Added token revocation on logout.
 
         WARNING: This requires relogin for all users!
 
diff --git a/Makefile.PL b/Makefile.PL
index 92c8b68..19284c1 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -14,7 +14,7 @@
   },
   LICENSE      => 'freebsd',
   PREREQ_PM => {
-    'Mojolicious' => '8.18',
+    'Mojolicious' => '8.22',
     'Mojolicious::Plugin::TagHelpers::Pagination' => 0.07,
     'Mojolicious::Plugin::TagHelpers::MailToChiffre' => 0.10,
     'Mojolicious::Plugin::ClosedRedirect' => 0.14,
@@ -26,6 +26,8 @@
     'Cache::FastMmap' => 1.47,
     'Data::Serializer' => 0.60,
     'Mojo::JWT' => 0.05,
+    'Test::Mojo::Session' => 1.05,
+    'Test::Mojo::WithRoles' => 0.02,
 
     # Required for Data::Serializer at the moment
     'JSON' => 4.02,
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/Proxy.pm b/lib/Kalamar/Controller/Proxy.pm
index 4bb4116..aff9d8a 100644
--- a/lib/Kalamar/Controller/Proxy.pm
+++ b/lib/Kalamar/Controller/Proxy.pm
@@ -49,6 +49,8 @@
       # another proxy, e.g. Apache, manages multiple
       # connections
       $headers->connection('close');
+
+      $c->app->plugins->emit_hook(after_render => $c);
     }
   );
 };
diff --git a/lib/Kalamar/Controller/Search.pm b/lib/Kalamar/Controller/Search.pm
index 331f78d..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
 #
@@ -38,14 +41,16 @@
   # $v->optional('action'); # action 'inspect' is no longer valid
   # $v->optional('snippet');
 
-  my $cutoff = 0;
+  my $cutoff;
   if ($v->param('cutoff') && $v->param('cutoff') =~ /^1|true$/i) {
-    $cutoff = 1;
+    $cutoff = 'true';
   };
 
   # Deal with legacy collection
-  if ($v->param('collection')) {
+  my $cq = $v->param('cq');
+  if ($v->param('collection') && !defined $cq) {
     $c->param(cq => $v->param('collection'));
+    $cq = $v->param('collection');
   };
 
   # No query (Check ignoring validation)
@@ -75,9 +80,9 @@
 
 
   $query{count}   = $v->param('count') // $c->items_per_page;
-  $query{cq}      = $v->param('cq');
 
-  $query{cutoff}  = $v->param('cutoff');
+  $query{cq}      = $cq;
+  $query{cutoff}  = $cutoff;
   # Before: 'base/s:p'/'paragraph'
   $query{context} = $v->param('context') // '40-t,40-t';
 
@@ -152,6 +157,8 @@
     sub {
       my $json = shift;
 
+      $c->app->log->debug("Receiving cached promised results");
+
       #######################
       # Cache total results #
       #######################
@@ -163,6 +170,7 @@
 
         # There are results to remember
         if (!$cutoff &&
+              !$c->stash('no_cache') &&
               $json->{meta}->{totalResults} >= 0) {
 
           # Remove cutoff requirement again
@@ -246,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..7072f9f 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,101 @@
         return 1;
       }
     )->name('login');
+
+
+    # Log out of the session
+    $r->get('/user/logout')->to(
+      cb => sub {
+        my $c = shift;
+
+        # TODO: csrf-protection!
+
+        my $refresh_token = $c->session('auth_r');
+
+        # Revoke the token
+        state $url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke');
+
+        $c->kalamar_ua->post_p($url => {} => form => {
+          client_id => $client_id,
+          client_secret => $client_secret,
+          token => $refresh_token,
+          token_type => 'refresh_token'
+        })->then(
+          sub {
+            my $tx = shift;
+            my $json = $tx->result->json;
+
+            my $promise;
+
+            # Response is fine
+            if ($tx->res->is_success) {
+              $c->app->log->info("Revocation was successful");
+              $c->notify(success => $c->loc('Auth_logoutSuccess'));
+
+              $c->stash(auth => undef);
+              $c->stash(auth_exp => undef);
+              $c->flash(handle_or_email => delete $c->session->{user});
+              delete $c->session->{auth};
+              delete $c->session->{auth_r};
+              delete $c->session->{auth_exp};
+              return Mojo::Promise->resolve;
+            }
+
+            # Token may be invalid
+            $c->notify('error', $c->loc('Auth_logoutFail'));
+
+            # There is a client error - refresh fails
+            if ($tx->res->is_client_error && $json) {
+
+              return Mojo::Promise->reject(
+                $json->{error_description}
+              );
+            };
+
+            # Resource may not be found (404)
+            return Mojo::Promise->reject
+
+          }
+        )->catch(
+          sub {
+            my $err = shift;
+
+            # Server may be irresponsible
+            $c->notify('error', $c->loc('Auth_logoutFail'));
+            return Mojo::Promise->reject($err);
+          }
+        )->finally(
+          sub {
+            return $c->redirect_to('index');
+          }
+        )->wait;
+      }
+    )->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 +635,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 +680,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 +692,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(
@@ -403,122 +728,63 @@
         return 1;
       }
     )->name('login');
+
+
+    # Log out of the session
+    $r->get('/user/logout')->to(
+      cb => sub {
+        my $c = shift;
+
+        # TODO: csrf-protection!
+
+        # Log out of the system
+        my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
+
+        $c->korap_request(
+          'get', $url
+        )->then(
+          # Logged out
+          sub {
+            my $tx = shift;
+            # Clear cache
+            # ?? Necesseary
+            # $c->chi('user')->remove($c->auth->token);
+
+            # TODO:
+            #   Revoke refresh token!
+            #   based on auth token!
+            # my $refresh_token = $c->chi('user')->get('refr_' . $c->auth->token);
+            # $c->auth->revoke_token($refresh_token)
+
+            # Expire session
+            $c->session(user => undef);
+            $c->session(auth => undef);
+            $c->notify(success => $c->loc('Auth_logoutSuccess'));
+          }
+
+        )->catch(
+          # Something went wrong
+          sub {
+            # my $err_msg = shift;
+            $c->notify('error', $c->loc('Auth_logoutFail'));
+          }
+
+        )->finally(
+          # Redirect
+          sub {
+            return $c->redirect_to('index');
+          }
+        )
+
+        # Start IOLoop
+        ->wait;
+
+        return 1;
+      }
+    )->name('logout');
   };
-
-
-  # Log out of the session
-  $r->get('/user/logout')->to(
-    cb => sub {
-      my $c = shift;
-
-      # TODO: csrf-protection!
-
-      # TODO:
-      #   Revoke refresh token!
-
-      # Log out of the system
-      my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
-
-      $c->korap_request(
-        'get', $url
-      )->then(
-        # Logged out
-        sub {
-          my $tx = shift;
-          # Clear cache
-          # ?? Necesseary
-          # $c->chi('user')->remove($c->auth->token);
-
-          # TODO:
-          #   Revoke refresh token!
-          #   based on auth token!
-          # my $refresh_token = $c->chi('user')->get('refr_' . $c->auth->token);
-          # $c->auth->revoke_token($refresh_token)
-
-          # Expire session
-          $c->session(user => undef);
-          $c->session(auth => undef);
-          $c->notify(success => $c->loc('Auth_logoutSuccess'));
-        }
-
-      )->catch(
-        # Something went wrong
-        sub {
-          # my $err_msg = shift;
-          $c->notify('error', $c->loc('Auth_logoutFail'));
-        }
-
-      )->finally(
-        # Redirect
-        sub {
-          return $c->redirect_to('index');
-        }
-      )
-
-      # Start IOLoop
-      ->wait;
-
-      return 1;
-    }
-  )->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;
 
 __DATA__
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 {
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 17c3cda..f03f61e 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -1,6 +1,6 @@
 use Mojo::Base -strict;
 use Test::More;
-use Test::Mojo;
+use Test::Mojo::WithRoles 'Session';
 use Mojo::File qw/path/;
 use Data::Dumper;
 
@@ -11,7 +11,7 @@
 my $mount_point = '/realapi/';
 $ENV{KALAMAR_API} = $mount_point;
 
-my $t = Test::Mojo->new('Kalamar' => {
+my $t = Test::Mojo::WithRoles->new('Kalamar' => {
   Kalamar => {
     plugins => ['Auth']
   },
@@ -32,7 +32,70 @@
   }
 );
 # Configure fake backend
-$fake_backend->pattern->defaults->{app}->log($t->app->log);
+my $fake_backend_app = $fake_backend->pattern->defaults->{app};
+
+# Set general app logger for simplicity
+$fake_backend_app->log($t->app->log);
+
+my $access_token = $fake_backend_app->get_token('access_token');
+my $refresh_token = $fake_backend_app->get_token('refresh_token');
+my $access_token_2 = $fake_backend_app->get_token('access_token_2');
+my $refresh_token_2 = $fake_backend_app->get_token('refresh_token_2');
+
+# Some routes to modify the session
+# This expires the session
+$t->app->routes->get('/x/expire')->to(
+  cb => sub {
+    my $c = shift;
+    $c->session(auth_exp => 0);
+    return $c->render(text => 'okay')
+  }
+);
+
+# This expires the session and removes the refresh token
+$t->app->routes->get('/x/expire-no-refresh')->to(
+  cb => sub {
+    my $c = shift;
+    $c->session(auth_exp => 0);
+    delete $c->session->{auth_r};
+    return $c->render(text => 'okay')
+  }
+);
+
+# This sets an invalid token
+$t->app->routes->get('/x/invalid')->to(
+  cb => sub {
+    my $c = shift;
+    $c->session(auth_exp => time + 1000);
+    $c->session(auth_r => $refresh_token_2);
+    $c->session(auth => 'Bearer inv4lid');
+    return $c->render(text => 'okay')
+  }
+);
+
+
+# This sets an invalid token
+$t->app->routes->get('/x/invalid-no-refresh')->to(
+  cb => sub {
+    my $c = shift;
+    $c->session(auth_exp => time + 1000);
+    delete $c->session->{auth_r};
+    $c->session(auth => 'Bearer inv4lid');
+    return $c->render(text => 'okay')
+  }
+);
+
+# This sets an invalid refresh token
+$t->app->routes->get('/x/expired-with-wrong-refresh')->to(
+  cb => sub {
+    my $c = shift;
+    $c->session(auth_exp => 0);
+    $c->session(auth => 'Bearer inv4lid');
+    $c->session(auth_r => 'inv4lid');
+    return $c->render(text => 'okay')
+  }
+);
+
 
 $t->get_ok('/realapi/v1.0')
   ->status_is(200)
@@ -137,6 +200,10 @@
 # search with authorization
 $t->get_ok('/?q=Baum')
   ->status_is(200)
+  ->session_has('/auth')
+  ->session_is('/auth', 'Bearer ' . $access_token)
+  ->session_is('/auth_r', $refresh_token)
+  ->session_is('/user', 'test')
   ->text_like('h1 span', qr/KorAP: Find .Baum./i)
   ->text_like('#total-results', qr/\d+$/)
   ->element_exists_not('div.notify-error')
@@ -148,6 +215,9 @@
 # Logout
 $t->get_ok('/user/logout')
   ->status_is(302)
+  ->session_hasnt('/auth')
+  ->session_hasnt('/auth_r')
+  ->session_hasnt('/user')
   ->header_is('Location' => '/');
 
 $t->get_ok('/')
@@ -155,6 +225,8 @@
   ->element_exists_not('div.notify-error')
   ->element_exists('div.notify-success')
   ->text_is('div.notify-success', 'Logout successful')
+  ->element_exists("input[name=handle_or_email]")
+  ->element_exists("input[name=handle_or_email][value=test]")
   ;
 
 $t->get_ok('/?q=Baum')
@@ -203,91 +275,113 @@
   ->element_exists_not('div.notify-error')
   ->element_exists('div.notify-success')
   ->text_is('div.notify-success', 'Login successful')
+  ->session_has('/auth')
+  ->session_is('/auth', 'Bearer ' . $access_token)
+  ->session_is('/auth_r', $refresh_token)
+  ->header_isnt('X-Kalamar-Cache', 'true')
   ;
 
-$t->app->routes->get(
-  '/user/refresh' => sub {
-    my $c = shift;
-
-    my $old_auth = $c->auth->token;
-    my $refresh = $c->chi('user')->get("refr_$old_auth");
-
-    $c->auth->refresh_token($refresh)->then(
-      sub {
-        my $new_auth = $c->auth->token;
-        $c->notify(success => $new_auth . ' vs. ' . $old_auth);
-      }
-    )->catch(
-      sub {
-
-        # Notify the user on login failure
-        unless (@_) {
-          $c->notify(error => $c->loc('Auth_refreshFail'));
-        }
-
-        # There are known errors
-        foreach (@_) {
-          if (ref $_ eq 'HASH') {
-            my $err = ($_->{code} ? $_->{code} . ': ' : '') .
-              $_->{message};
-            $c->notify(error => $err);
-          }
-          else {
-            $c->notify(error => $_);
-          }
-        };
-      }
-    )->finally(
-      sub {
-        return $c->redirect_to('index');
-      }
-    )->wait;
-  }
-);
-
-$t->get_ok('/user/refresh')
-  ->status_is(302)
-  ->header_is('Location' => '/');
-
-$t->get_ok('/')
+# Expire the session
+# (makes the token be marked as expired - though it isn't serverside)
+$t->get_ok('/x/expire')
   ->status_is(200)
-  ->element_exists_not('div.notify-error')
-  ->element_exists('div.notify-success')
-  ->text_like('div.notify-success', qr!Bearer abcde vs\. Bearer .{6,}!)
+  ->content_is('okay')
+  ;
+
+## It may be a problem, but the cache is still valid
+$t->get_ok('/?q=Baum')
+  ->status_is(200)
+  ->text_like('h1 span', qr/KorAP: Find .Baum./i)
+  ->text_like('#total-results', qr/\d+$/)
+  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->header_is('X-Kalamar-Cache', 'true')
+  ;
+
+# Query without partial cache (unfortunately) (but no total results)
+$t->get_ok('/?q=baum&cutoff=true')
+  ->status_is(200)
+  ->session_is('/auth', 'Bearer ' . $access_token_2)
+  ->session_is('/auth_r', $refresh_token_2)
+  ->text_is('#error','')
+  ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+  ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
+  ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->header_isnt('X-Kalamar-Cache', 'true')
+  ->content_like(qr!\"cutOff":true!)
+  ->element_exists_not('#total-results')
+  ;
+
+# Expire the session and remove the refresh option
+$t->get_ok('/x/expire-no-refresh')
+  ->status_is(200)
+  ->content_is('okay')
+  ;
+
+$t->app->defaults(no_cache => 1);
+
+
+$t->get_ok('/x/invalid-no-refresh')
+  ->status_is(200)
+  ->content_is('okay')
+  ;
+
+# Query without cache
+# The token is invalid and can't be refreshed!
+$t->get_ok('/?q=baum&cutoff=true')
+  ->status_is(200)
+  ->session_hasnt('/auth')
+  ->session_hasnt('/auth_r')
+  ->text_is('#error','')
+  ->text_is('div.notify-error','Access token invalid')
+  ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+  ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
+  ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+  ->content_unlike(qr/\"authorized\"\:\"yes\"/)
+  ->header_isnt('X-Kalamar-Cache', 'true')
+  ->element_exists('p.no-results')
+  ;
+
+$t->get_ok('/x/invalid')
+  ->status_is(200)
+  ->content_is('okay')
+  ;
+
+# Query without cache
+# The token is invalid and can't be refreshed!
+$t->get_ok('/?q=baum&cutoff=true')
+  ->status_is(200)
+  ->session_is('/auth', 'Bearer ' . $access_token_2)
+  ->session_is('/auth_r', $refresh_token_2)
+  ->text_is('#error','')
+  ->element_exists_not('div.notify-error','Access token invalid')
+  ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+  ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
+  ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->header_isnt('X-Kalamar-Cache', 'true')
+  ->element_exists_not('p.no-results')
   ;
 
 
-# Test before_korap_request_hook
-my $app = $t->app;
-my $c = $app->build_controller;
-my $tx = $app->build_tx('GET', 'https://korap.ids-mannheim.de/');
+$t->get_ok('/x/expired-with-wrong-refresh')
+  ->status_is(200)
+  ->content_is('okay')
+  ;
 
-# Emit Hook to alter request
-$app->plugins->emit_hook(
-  before_korap_request => ($c, $tx)
-);
 
-ok(!$tx->req->headers->authorization, 'No authorization');
+# The token is invalid and can't be refreshed!
+$t->get_ok('/?q=baum&cutoff=true')
+  ->status_is(200)
+  ->session_hasnt('/auth')
+  ->session_hasnt('/auth_r')
+  ->text_is('#error','')
+  ->text_is('div.notify-error','Refresh token is expired')
+  ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+  ->content_unlike(qr/\"authorized\"\:\"yes\"/)
+  ->element_exists('p.no-results')
+  ;
 
-# Set token
-$c->auth->token('abcd');
-
-# Emit Hook to alter request
-$app->plugins->emit_hook(
-  before_korap_request => ($c, $tx)
-);
-
-is($tx->req->headers->authorization, 'abcd', 'authorization');
-
-# Override authorization in header
-$tx->req->headers->authorization('xyz');
-
-# Emit Hook to alter request
-$app->plugins->emit_hook(
-  before_korap_request => ($c, $tx)
-);
-
-is($tx->req->headers->authorization, 'xyz', 'authorization');
 
 done_testing;
 __END__
diff --git a/t/proxy.t b/t/proxy.t
index f30e049..2e2f922 100644
--- a/t/proxy.t
+++ b/t/proxy.t
@@ -33,22 +33,44 @@
 # Globally set server
 $t->app->ua->server->app($t->app);
 
+my $rendered = 0;
+$t->app->hook(
+  after_render => sub {
+    $rendered++;
+  }
+);
+
+$t->get_ok('/doc')
+  ->status_is(200)
+  ->text_like('title', qr!KorAP!)
+  ;
+
+is($rendered, 1);
+
 $t->get_ok('/realapi/v1.0')
   ->status_is(200)
   ->content_is('Fake server available')
   ;
 
+is($rendered, 1);
+
 $t->get_ok('/api/v1.0/')
   ->status_is(200)
   ->content_is('Fake server available')
   ;
 
+# Proxy renders
+is($rendered, 2);
+
 $t->get_ok('/api/v1.0/search?ql=cosmas3')
   ->status_is(400)
   ->json_is('/errors/0/0','307')
   ->header_is('connection', 'close')
   ;
 
+# Proxy renders
+is($rendered, 3);
+
 $t->post_ok('/api/v1.0/oauth2/token' => {} => form => {
   username => 'test',
   password => 'pass'
diff --git a/t/query.t b/t/query.t
index 4532167..3252bac 100644
--- a/t/query.t
+++ b/t/query.t
@@ -161,6 +161,12 @@
   ->content_unlike(qr!\"cutOff":true!)
   ;
 
+# Check pagination repetion of page
+my $next_href = $t->get_ok('/?q=der&p=1&count=2')
+  ->tx->res->dom->at('#pagination a[rel=next]')->attr('href');
+like($next_href, qr/p=2/);
+unlike($next_href, qr/p=1/);
+
 # Query with page information - next page
 $t->get_ok('/?q=der&p=2&count=2')
   ->status_is(200)
@@ -225,10 +231,13 @@
   ->text_is('#total-results', '> 4,274,841');
   ;
 
+$t->app->defaults(no_cache => 1);
+
 # Query with collection
 $t->get_ok('/?q=baum&collection=availability+%3D+%2FCC-BY.*%2F')
   ->status_is(200)
   ->element_exists("input#cq[value='availability = /CC-BY.*/']")
+  ->content_like(qr!\"availability\"!)
   ->text_is('#error','')
   ;
 
@@ -236,8 +245,10 @@
 $t->get_ok('/?q=baum&cq=availability+%3D+%2FCC-BY.*%2F')
   ->status_is(200)
   ->element_exists("input#cq[value='availability = /CC-BY.*/']")
+  ->content_like(qr!\"availability\"!)
   ->text_is('#error','')
   ;
 
+
 done_testing;
 __END__
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 961c6ea..2e14035 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -14,6 +14,18 @@
 my $secret = 's3cr3t';
 my $fixture_path = path(Mojo::File->new(__FILE__)->dirname)->child('..', 'fixtures');
 
+our %tokens = (
+  "access_token"    => "4dcf8784ccfd26fac9bdb82778fe60e2",
+  "refresh_token"   => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
+  "access_token_2"  => "abcde",
+  "refresh_token_2" => "fghijk"
+);
+
+helper get_token => sub {
+  my ($c, $token) = @_;
+  return $tokens{$token}
+};
+
 # Legacy:
 helper jwt_encode => sub {
   shift;
@@ -32,6 +44,24 @@
   return Mojo::JWT->new(secret => $secret)->decode($auth);
 };
 
+# Expiration helper
+helper expired => sub {
+  my ($c, $auth, $set) = @_;
+
+
+  $auth =~ s/^[^ ]+? //;
+  if ($set) {
+    $c->app->log->debug("Set $auth for expiration");
+    $c->app->defaults('auth_' . $auth => 1);
+    return 1;
+  };
+
+  $c->app->log->debug("Check $auth for expiration: " . (
+    $c->app->defaults('auth_' . $auth) // '0'
+  ));
+
+  return $c->app->defaults('auth_' . $auth);
+};
 
 # Load fixture responses
 helper 'load_response' => sub {
@@ -111,14 +141,40 @@
   # Check authentification
   if (my $auth = $c->req->headers->header('Authorization')) {
 
+    $c->app->log->debug("There is an authorization header $auth");
     my $jwt;
     if ($auth =~ /^Bearer/) {
       # Username unknown in OAuth2
       $response->{json}->{meta}->{authorized} = 'yes';
     }
-    elsif ($jwt = $c->jwt_decode($auth)) {
+    elsif ($auth =~ /^api_token/ && ($jwt = $c->jwt_decode($auth))) {
       $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
     };
+
+    # Code is expired
+    if ($c->expired($auth)) {
+
+      $c->app->log->debug("The access token has expired");
+
+      return $c->render(
+        status => 401,
+        json => {
+          errors => [[2003,  'Access token is expired']]
+        }
+      );
+    }
+
+    # Auth token is invalid
+    if ($auth =~ /^Bearer inv4lid/) {
+      $c->app->log->debug("The access token is invalid");
+
+      return $c->render(
+        status => 401,
+        json => {
+          errors => [[2011,  'Access token is invalid']]
+        }
+      );
+    }
   };
 
   # Set page parameter
@@ -199,7 +255,13 @@
   my $c = shift;
 
   if (my $auth = $c->req->headers->header('Authorization')) {
-    if (my $jwt = $c->jwt_decode($auth)) {
+
+    if ($auth =~ /^Bearer/) {
+      $c->app->log->debug('Server-Logout: ' . $auth);
+      return $c->render(json => { msg => [[0, 'Fine!']]});
+    }
+
+    elsif (my $jwt = $c->jwt_decode($auth)) {
       my $user = $jwt->{username} if $jwt->{username};
 
       $c->app->log->debug('Server-Logout: ' . $user);
@@ -341,8 +403,8 @@
     # Return fine access
     return $c->render(
       json => {
-        "access_token" => "4dcf8784ccfd26fac9bdb82778fe60e2",
-        "refresh_token" => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
+        "access_token" => $c->get_token('access_token'),
+        "refresh_token" => $c->get_token('refresh_token'),
         "scope" => "all",
         "token_type" => "Bearer",
         "expires_in" => 86400
@@ -351,11 +413,24 @@
 
   # Refresh token
   elsif ($grant_type eq 'refresh_token') {
+
+    if ($c->param('refresh_token') eq 'inv4lid') {
+      return $c->render(
+        status => 400,
+        json => {
+          "error_description" => "Refresh token is expired",
+          "error" => "invalid_grant"
+        }
+      );
+    };
+
+    $c->app->log->debug("Refresh the token in the mock server!");
+
     return $c->render(
       status => 200,
       json => {
-        "access_token" => "abcde",
-        "refresh_token" => "fghijk",
+        "access_token" => $c->get_token("access_token_2"),
+        "refresh_token" => $c->get_token("refresh_token_2"),
         "token_type" => "Bearer",
         "expires_in" => 86400
       }
@@ -377,6 +452,26 @@
   }
 };
 
+# Revoke API token
+post '/v1.0/oauth2/revoke' => sub {
+  my $c = shift;
+
+  my $refresh_token = $c->param('token');
+
+  if ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
+    return $c->render(
+      json => {
+        "error_description" => "Invalid client credentials",
+        "error" => "invalid_client"
+      },
+      status => 401
+    );
+  };
+
+  return $c->render(
+    text => ''
+  )
+};
 
 
 app->start;
diff --git a/templates/failure.html.ep b/templates/failure.html.ep
index 5cb98a1..9155c31 100644
--- a/templates/failure.html.ep
+++ b/templates/failure.html.ep
@@ -8,5 +8,5 @@
 
 <p class="no-results"><%= loc('notIssued') %></p>
 % if (stash('err_msg')) {
-<p class="no-results"><%= loc(stash('err_msg'),stash('err_msg')) %></p>
+<p class="no-results"><%== loc(stash('err_msg'),stash('err_msg')) %></p>
 % }
diff --git a/templates/search.html.ep b/templates/search.html.ep
index be95990..a349401 100644
--- a/templates/search.html.ep
+++ b/templates/search.html.ep
@@ -1,7 +1,7 @@
 % layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
 
 <div id="resultinfo" <% if (stash('results')->size) { %> class="found"<%} %>>
-  <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query(['p' => '{page}'])) =%></div>
+  <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query({'p' => '{page}'})) =%></div>
 % my $found = stash('total_results') // 0;
   <p class="found">\
 % if ($found != -1) {