Merge "Multipage Tour (with session-cookie to next page)"
diff --git a/Changes b/Changes
index 7948840..6efd47c 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.34 2019-06-24
+0.34 2019-06-26
         - Introduced guided tour (hebasta, #19).
         - Updated dependency on M::P::Notifications to
           be compatible with recent versions of Mojolicious.
@@ -6,6 +6,7 @@
         - Improve QueryCreator to single-quote-escape special
           characters in orth-line and include more symbols.
         - Remove deprecated auth_support support.
+        - Add OAuth2 password grand flow.
 
 0.33 2019-03-28
         - Fix problem with serialization and deserialization
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 1add8f5..8f770a7 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -5,6 +5,8 @@
 # TODO:
 #   CSRF-protect logout!
 
+our $EXPECTED_EXPIRATION_IN = 259200;
+
 # Register the plugin
 sub register {
   my ($plugin, $app, $param) = @_;
@@ -24,11 +26,10 @@
     });
   };
 
-
-  # unless ($param->{client_id} && $param->{client_secret}) {
-  #   $mojo->log->error('client_id or client_secret not defined');
-  #   return;
-  # };
+  # Get the client id and the client_secret as a requirement
+  unless ($param->{client_id} && $param->{client_secret}) {
+    $app->log->error('client_id or client_secret not defined');
+  };
 
   # TODO:
   #   Define user CHI cache
@@ -43,7 +44,8 @@
           logoutSuccess => 'Abmeldung erfolgreich',
           logoutFail => 'Abmeldung fehlgeschlagen',
           csrfFail => 'Fehlerhafter CSRF Token',
-          openRedirectFail => 'Weiterleitungsfehler'
+          openRedirectFail => 'Weiterleitungsfehler',
+          refreshFail => 'Fehlerhafter Refresh-Token'
         },
         -en => {
           loginSuccess => 'Login successful',
@@ -51,7 +53,8 @@
           logoutSuccess => 'Logout successful',
           logoutFail => 'Logout failed',
           csrfFail => 'Bad CSRF token',
-          openRedirectFail => 'Redirect failure'
+          openRedirectFail => 'Redirect failure',
+          refreshFail => 'Bad refresh token'
         }
       }
     }
@@ -80,6 +83,10 @@
       my $auth_token = $c->auth->token or return;
       my $h = $tx->req->headers;
       $h->header('Authorization' => $auth_token);
+
+      # TODO:
+      #   When a request fails because the access token timed out,
+      #   rerequest with the refresh token.
     }
   );
 
@@ -107,134 +114,280 @@
 
   # Log in to the system
   my $r = $app->routes;
-  $r->post('/user/login')->to(
-    cb => sub {
-      my $c = shift;
 
-      # Validate input
-      my $v = $c->validation;
-      $v->required('handle_or_email', 'trim');
-      $v->required('pwd', 'trim');
-      $v->csrf_protect;
-      $v->optional('fwd')->closed_redirect;
+  if ($param->{oauth2}) {
 
-      my $user = $v->param('handle_or_email');
-      my $fwd = $v->param('fwd');
+    my $client_id = $param->{client_id};
+    my $client_secret = $param->{client_secret};
 
-      # Set flash for redirect
-      $c->flash(handle_or_email => $user);
+    # This refreshes an oauth2 token and
+    # returns a promise
+    $app->helper(
+      'auth.refresh_token' => sub {
+        my $c = shift;
+        my $refresh_token = shift;
 
-      if ($v->has_error || index($user, ':') >= 0) {
-        if ($v->has_error('fwd')) {
-          $c->notify(error => $c->loc('Auth_openRedirectFail'));
-        }
-        elsif ($v->has_error('csrf_token')) {
-          $c->notify(error => $c->loc('Auth_csrfFail'));
-        }
-        else {
-          $c->notify(error => $c->loc('Auth_loginFail'));
+        unless ($refresh_token) {
+          return Mojo::Promise->reject({message => 'Missing refresh token'})
         };
 
-        return $c->relative_redirect_to($fwd // 'index');
+        # Get OAuth access token
+        my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+
+        return $c->korap_request('POST', $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 $pwd = $v->param('pwd');
+    # Password flow
+    $r->post('/user/login')->to(
+      cb => sub {
+        my $c = shift;
 
-      $c->app->log->debug("Login from user $user:$pwd");
+        # Validate input
+        my $v = $c->validation;
+        $v->required('handle_or_email', 'trim');
+        $v->required('pwd', 'trim');
+        $v->csrf_protect;
+        $v->optional('fwd')->closed_redirect;
 
-      my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
+        my $user = $v->param('handle_or_email');
+        my $fwd = $v->param('fwd');
 
-      # Korap request for login
-      $c->korap_request('get', $url, {
+        # Set flash for redirect
+        $c->flash(handle_or_email => $user);
 
-        # Set authorization header
-        Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
-
-      })->then(
-        sub {
-          my $tx = shift;
-
-          # Get the java token
-          my $jwt = $tx->result->json;
-
-          # No java web token
-          unless ($jwt) {
-            $c->notify(error => 'Response is no valid JWT (remote)');
-            return;
+        if ($v->has_error || index($user, ':') >= 0) {
+          if ($v->has_error('fwd')) {
+            $c->notify(error => $c->loc('Auth_openRedirectFail'));
+          }
+          elsif ($v->has_error('csrf_token')) {
+            $c->notify(error => $c->loc('Auth_csrfFail'));
+          }
+          else {
+            $c->notify(error => $c->loc('Auth_loginFail'));
           };
 
-          # There is an error here
-          # Dealing with errors here
-          if (my $error = $jwt->{error} // $jwt->{errors}) {
-            if (ref $error eq 'ARRAY') {
-              foreach (@$error) {
-                unless ($_->[1]) {
-                  $c->notify(error => $c->loc('Auth_loginFail'));
-                }
-                else {
-                  $c->notify(error => $_->[0] . ($_->[1] ? ': ' . $_->[1] : ''));
-                };
-              };
-            }
-            else {
-              $c->notify(error => 'There is an unknown JWT error');
-            };
-            return;
-          };
-
-          # TODO: Deal with user return values.
-          my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
-
-          $c->app->log->debug(qq!Login successful: "$user" with "$auth"!);
-
-          $user = $jwt->{username} ? $jwt->{username} : $user;
-
-          # Set session info
-          $c->session(user => $user);
-          $c->session(auth => $auth);
-
-          # Set stash info
-          $c->stash(user => $user);
-          $c->stash(auth => $auth);
-
-          # Set cache
-          $c->chi('user')->set($auth => $user);
-          $c->notify(success => $c->loc('Auth_loginSuccess'));
-        }
-      )->catch(
-        sub {
-          my $e = shift;
-
-          # Notify the user
-          $c->notify(
-            error =>
-              ($e->{code} ? $e->{code} . ': ' : '') .
-              $e->{message} . ' for Login (remote)'
-            );
-
-          # Log failure
-          $c->app->log->debug(
-            ($e->{code} ? $e->{code} . ' - ' : '') .
-              $e->{message}
-            );
-
-          $c->app->log->debug(qq!Login fail: "$user"!);
-          $c->notify(error => $c->loc('Auth_loginFail'));
-        }
-      )->finally(
-        sub {
-
-          # Redirect to slash
           return $c->relative_redirect_to($fwd // 'index');
         }
-      )
 
-      # Start IOLoop
-      ->wait;
+        my $pwd = $v->param('pwd');
 
-      return 1;
-    }
-  )->name('login');
+        $c->app->log->debug("Login from user $user:XXXX");
+
+        # <specific>
+
+        # Get OAuth access token
+        my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+
+        # Korap request for login
+        $c->korap_request('post', $url, {}, form => {
+          grant_type => 'password',
+          username => $user,
+          password => $pwd,
+          client_id => $client_id,
+          client_secret => $client_secret
+        })->then(
+          sub {
+            # Set the tokens and return a promise
+            return $plugin->set_tokens(
+              $c,
+              shift->result->json
+            )
+          }
+        )->catch(
+          sub {
+
+            # Notify the user on login failure
+            unless (@_) {
+              $c->notify(error => $c->loc('Auth_loginFail'));
+            }
+
+            # There are known errors
+            foreach (@_) {
+              if (ref $_ eq 'HASH') {
+                my $err = ($_->{code} ? $_->{code} . ': ' : '') .
+                  $_->{message};
+                $c->notify(error => $err);
+                # Log failure
+                $c->app->log->debug($err);
+              }
+              else {
+                $c->notify(error => $_);
+                $c->app->log->debug($_);
+              };
+            };
+
+            $c->app->log->debug(qq!Login fail: "$user"!);
+          }
+        )->then(
+          sub {
+            # Set user info
+            $c->session(user => $user);
+            $c->stash(user => $user);
+
+            # Notify on success
+            $c->app->log->debug(qq!Login successful: "$user"!);
+            $c->notify(success => $c->loc('Auth_loginSuccess'));
+          }
+        )->finally(
+          sub {
+            # Redirect to slash
+            return $c->relative_redirect_to($fwd // 'index');
+          }
+        )
+
+        # Start IOLoop
+        ->wait;
+
+        return 1;
+      }
+    )->name('login');
+  }
+  # Use JWT login
+  else {
+
+    $r->post('/user/login')->to(
+      cb => sub {
+        my $c = shift;
+
+        # Validate input
+        my $v = $c->validation;
+        $v->required('handle_or_email', 'trim');
+        $v->required('pwd', 'trim');
+        $v->csrf_protect;
+        $v->optional('fwd')->closed_redirect;
+
+        my $user = $v->param('handle_or_email');
+        my $fwd = $v->param('fwd');
+
+        # Set flash for redirect
+        $c->flash(handle_or_email => $user);
+
+        if ($v->has_error || index($user, ':') >= 0) {
+          if ($v->has_error('fwd')) {
+            $c->notify(error => $c->loc('Auth_openRedirectFail'));
+          }
+          elsif ($v->has_error('csrf_token')) {
+            $c->notify(error => $c->loc('Auth_csrfFail'));
+          }
+          else {
+            $c->notify(error => $c->loc('Auth_loginFail'));
+          };
+
+          return $c->relative_redirect_to($fwd // 'index');
+        }
+
+        my $pwd = $v->param('pwd');
+
+        $c->app->log->debug("Login from user $user:XXXX");
+
+        my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
+
+        # Korap request for login
+        $c->korap_request('get', $url, {
+
+          # Set authorization header
+          Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
+
+        })->then(
+          sub {
+            my $tx = shift;
+
+            # Get the java token
+            my $jwt = $tx->result->json;
+
+            # No java web token
+            unless ($jwt) {
+              $c->notify(error => 'Response is no valid JWT (remote)');
+              return;
+            };
+
+            # There is an error here
+            # Dealing with errors here
+            if (my $error = $jwt->{error} // $jwt->{errors}) {
+              if (ref $error eq 'ARRAY') {
+                foreach (@$error) {
+                  unless ($_->[1]) {
+                    $c->notify(error => $c->loc('Auth_loginFail'));
+                  }
+                  else {
+                    $c->notify(error => $_->[0] . ($_->[1] ? ': ' . $_->[1] : ''));
+                  };
+                };
+              }
+              else {
+                $c->notify(error => 'There is an unknown JWT error');
+              };
+              return;
+            };
+
+            # TODO: Deal with user return values.
+            my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
+
+            $c->app->log->debug(qq!Login successful: "$user" with "$auth"!);
+
+            $user = $jwt->{username} ? $jwt->{username} : $user;
+
+            # Set session info
+            $c->session(user => $user);
+            $c->session(auth => $auth);
+
+            # Set stash info
+            $c->stash(user => $user);
+            $c->stash(auth => $auth);
+
+            # Set cache
+            $c->chi('user')->set($auth => $user);
+            $c->notify(success => $c->loc('Auth_loginSuccess'));
+          }
+        )->catch(
+          sub {
+            my $e = shift;
+
+            # Notify the user
+            $c->notify(
+              error =>
+                ($e->{code} ? $e->{code} . ': ' : '') .
+                $e->{message} . ' for Login (remote)'
+              );
+
+            # Log failure
+            $c->app->log->debug(
+              ($e->{code} ? $e->{code} . ' - ' : '') .
+                $e->{message}
+              );
+
+            $c->app->log->debug(qq!Login fail: "$user"!);
+            $c->notify(error => $c->loc('Auth_loginFail'));
+          }
+        )->finally(
+          sub {
+
+            # Redirect to slash
+            return $c->relative_redirect_to($fwd // 'index');
+          }
+        )
+
+        # Start IOLoop
+        ->wait;
+
+        return 1;
+      }
+    )->name('login');
+  };
 
 
   # Log out of the session
@@ -244,6 +397,9 @@
 
       # TODO: csrf-protection!
 
+      # TODO:
+      #   Revoke refresh token!
+
       # Log out of the system
       my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
 
@@ -254,7 +410,14 @@
         sub {
           my $tx = shift;
           # Clear cache
-          $c->chi('user')->remove($c->auth->token);
+          # ?? 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);
@@ -284,6 +447,62 @@
   )->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/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index 83c5fe2..60fadfe 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -37,6 +37,13 @@
   # Set app to server
   $plugin->ua->server->app($mojo);
 
+  # Get a user agent object for Kalamar
+  $mojo->helper(
+    'kalamar_ua' => sub {
+      return $plugin->ua;
+    }
+  );
+
   # Get user handle
   $mojo->helper(
     'user_handle' => sub {
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
new file mode 100644
index 0000000..d67837f
--- /dev/null
+++ b/t/plugin/auth-oauth.t
@@ -0,0 +1,268 @@
+use Mojo::Base -strict;
+use Test::More;
+use Test::Mojo;
+use Mojo::File qw/path/;
+use Data::Dumper;
+
+
+#####################
+# Start Fake server #
+#####################
+my $mount_point = '/api/';
+$ENV{KALAMAR_API} = $mount_point;
+
+my $t = Test::Mojo->new('Kalamar' => {
+  Kalamar => {
+    plugins => ['Auth']
+  },
+  'Kalamar-Auth' => {
+    client_id => 2,
+    client_secret => 'k414m4r-s3cr3t',
+    oauth2 => 1
+  }
+});
+
+# Mount fake backend
+# Get the fixture path
+my $fixtures_path = path(Mojo::File->new(__FILE__)->dirname, '..', 'server');
+my $fake_backend = $t->app->plugin(
+  Mount => {
+    $mount_point =>
+      $fixtures_path->child('mock.pl')
+  }
+);
+# Configure fake backend
+$fake_backend->pattern->defaults->{app}->log($t->app->log);
+
+$t->get_ok('/api')
+  ->status_is(200)
+  ->content_is('Fake server available');
+
+$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\"\:null/)
+  ->element_exists_not('div.button.top a')
+  ->element_exists_not('aside.active')
+  ->element_exists_not('aside.off')
+  ;
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('form[action=/user/login] input[name=handle_or_email]')
+  ->element_exists('aside.active')
+  ->element_exists_not('aside.off')
+  ;
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'fail' })
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', 'Bad CSRF token')
+  ->element_exists('input[name=handle_or_email][value=test]')
+  ->element_exists_not('div.button.top a')
+  ;
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'pass' })
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+my $csrf = $t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', 'Bad CSRF token')
+  ->element_exists_not('div.button.top a')
+  ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
+  ;
+
+$t->post_ok('/user/login' => form => {
+  handle_or_email => 'test',
+  pwd => 'ldaperr',
+  csrf_token => $csrf
+})
+  ->status_is(302)
+  ->content_is('')
+  ->header_is('Location' => '/');
+
+$csrf = $t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', '2022: LDAP Authentication failed due to unknown user or password!')
+  ->element_exists('input[name=handle_or_email][value=test]')
+  ->element_exists_not('div.button.top a')
+  ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
+  ;
+
+$t->post_ok('/user/login' => form => {
+  handle_or_email => 'test',
+  pwd => 'unknown',
+  csrf_token => $csrf
+})
+  ->status_is(302)
+  ->content_is('')
+  ->header_is('Location' => '/');
+
+$csrf = $t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', '2022: LDAP Authentication failed due to unknown user or password!')
+  ->element_exists('input[name=handle_or_email][value=test]')
+  ->element_exists_not('div.button.top a')
+  ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
+  ;
+
+$t->post_ok('/user/login' => form => {
+  handle_or_email => 'test',
+  pwd => 'pass',
+  csrf_token => $csrf
+})
+  ->status_is(302)
+  ->content_is('')
+  ->header_is('Location' => '/');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ->element_exists('div.notify-success')
+  ->text_is('div.notify-success', 'Login successful')
+  ->element_exists('aside.off')
+  ->element_exists_not('aside.active')
+  ;
+
+# Now the user is logged in and should be able to
+# search with authorization
+$t->get_ok('/?q=Baum')
+  ->status_is(200)
+  ->text_like('h1 span', qr/KorAP: Find .Baum./i)
+  ->text_like('#total-results', qr/\d+$/)
+  ->element_exists_not('div.notify-error')
+  ->content_like(qr/\"authorized\"\:\"yes\"/)
+  ->element_exists('div.button.top a')
+  ->element_exists('div.button.top a.logout[title~="test"]')
+  ;
+
+# Logout
+$t->get_ok('/user/logout')
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ->element_exists('div.notify-success')
+  ->text_is('div.notify-success', 'Logout successful')
+  ;
+
+$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\"\:null/)
+  ;
+
+# Get redirect
+my $fwd = $t->get_ok('/?q=Baum&ql=poliqarp')
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ->tx->res->dom->at('input[name=fwd]')->attr('value')
+  ;
+
+is($fwd, '/?q=Baum&ql=poliqarp', 'Redirect is valid');
+
+$t->post_ok('/user/login' => form => {
+  handle_or_email => 'test',
+  pwd => 'pass',
+  csrf_token => $csrf,
+  fwd => 'http://bad.example.com/test'
+})
+  ->status_is(302)
+  ->header_is('Location' => '/');
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->element_exists_not('div.notify-success')
+  ->text_is('div.notify-error', 'Redirect failure')
+  ;
+
+$t->post_ok('/user/login' => form => {
+  handle_or_email => 'test',
+  pwd => 'pass',
+  csrf_token => $csrf,
+  fwd => $fwd
+})
+  ->status_is(302)
+  ->header_is('Location' => '/?q=Baum&ql=poliqarp');
+
+$t->get_ok('/?q=Baum&ql=poliqarp')
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ->element_exists('div.notify-success')
+  ->text_is('div.notify-success', 'Login successful')
+  ;
+
+$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('/')
+  ->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,}!)
+  ;
+
+
+done_testing;
+__END__
+
+
+
+# Login mit falschem Nutzernamen:
+# 400 und:
+{"errors":[[2022,"LDAP Authentication failed due to unknown user or password!"]]}
+
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 31cfaca..359f78c 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -14,6 +14,7 @@
 my $secret = 's3cr3t';
 my $fixture_path = path(Mojo::File->new(__FILE__)->dirname)->child('..', 'fixtures');
 
+# Legacy:
 helper jwt_encode => sub {
   shift;
   return Mojo::JWT->new(
@@ -24,6 +25,7 @@
   );
 };
 
+# Legacy;
 helper jwt_decode => sub {
   my ($c, $auth) = @_;
   $auth =~ s/\s*api_token\s+//;
@@ -106,7 +108,13 @@
 
   # Check authentification
   if (my $auth = $c->req->headers->header('Authorization')) {
-    if (my $jwt = $c->jwt_decode($auth)) {
+
+    my $jwt;
+    if ($auth =~ /^Bearer/) {
+      # Username unknown in OAuth2
+      $response->{json}->{meta}->{authorized} = 'yes';
+    }
+    elsif ($jwt = $c->jwt_decode($auth)) {
       $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
     };
   };
@@ -265,6 +273,107 @@
   );
 };
 
+
+# Request API token
+post '/oauth2/token' => sub {
+  my $c = shift;
+
+  if ($c->param('grant_type') eq 'password') {
+
+    # Check for wrong client id
+    if ($c->param('client_id') ne '2') {
+      return $c->render(
+        json => {
+          "error_description" => "Unknown client with " . $_->{client_id},
+          "error" => "invalid_client"
+        },
+        status => 401
+      );
+    }
+
+    # Check for wrong client secret
+    elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
+      return $c->render(
+        json => {
+          "error_description" => "Invalid client credentials",
+          "error" => "invalid_client"
+        },
+        status => 401
+      );
+    }
+
+    # Check for wrong user name
+    elsif ($c->param('username') ne 'test') {
+      return $c->render(json => {
+        error => [[2004, undef]]
+      });
+    }
+
+    # Check for ldap error
+    elsif ($c->param('password') eq 'ldaperr') {
+      return $c->render(
+        format => 'html',
+        status => 401,
+        json => {
+          "errors" => [
+            [
+              2022,
+              "LDAP Authentication failed due to unknown user or password!"
+            ]
+          ]
+        }
+      );
+    }
+
+    # Check for wrong password
+    elsif ($c->param('password') ne 'pass') {
+      return $c->render(json => {
+        format => 'html',
+        status => 401,
+        "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
+      });
+    }
+
+    # Return fine access
+    return $c->render(
+      json => {
+        "access_token" => "4dcf8784ccfd26fac9bdb82778fe60e2",
+        "refresh_token" => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
+        "scope" => "all",
+        "token_type" => "Bearer",
+        "expires_in" => 86400
+      });
+  }
+
+  # Refresh token
+  elsif ($c->param('grant_type') eq 'refresh_token') {
+    return $c->render(
+      status => 200,
+      json => {
+        "access_token" => "abcde",
+        "refresh_token" => "fghijk",
+        "token_type" => "Bearer",
+        "expires_in" => 86400
+      }
+    );
+  }
+
+  # Unknown token grant
+  else {
+    return $c->render(
+      json => {
+        "errors" => [
+          [
+            0, "Grant Type unknown", $c->param("grant_type")
+          ]
+        ]
+      }
+    )
+  }
+};
+
+
+
 app->start;