Implement token refresh mechanism

Change-Id: Id58e14f663ebdd86f3f2206d4bfb9ad5d87a35fa
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 17c3cda..4ce999a 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)
@@ -203,91 +266,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/server/mock.pl b/t/server/mock.pl
index 961c6ea..f8e6e62 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
       }