Optionally replace JWT request flow with OAuth2 user credential flow

Change-Id: I6fb675182d3b9f95152f6746ba205869d3364eaa
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
new file mode 100644
index 0000000..2769515
--- /dev/null
+++ b/t/plugin/auth-oauth.t
@@ -0,0 +1,210 @@
+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', 'Access denied')
+  ->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');
+
+
+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..e32365e 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,71 @@
   );
 };
 
+
+# Request API token
+post '/oauth2/token' => sub {
+  my $c = shift;
+
+  # 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 => {
+      error => [[2004, undef]]
+    });
+  }
+
+  # Return fine access
+  return $c->render(
+    json => {
+      "access_token" => "4dcf8784ccfd26fac9bdb82778fe60e2",
+      "refresh_token" => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
+      "scope" => "all",
+      "token_type" => "Bearer",
+      "expires_in" => 86400
+    });
+};
+
+
+
 app->start;