Do not follow redirects on authorization requests (fixes #195)

Change-Id: Idba831398cfa7c51fb8f56e55c5265234a0d4652
diff --git a/Changes b/Changes
index c0fab50..271d991 100755
--- a/Changes
+++ b/Changes
@@ -1,6 +1,8 @@
-0.49 2023-02-14
+0.49 2023-02-23
         - Introduce conllu2korapxml command via plugin. (diewald)
         - Introduce korapxml2conllu command via plugin. (diewald)
+        - Do not follow redirect links on OAuth authorization
+          requests. (diewald)
 
 0.48 2023-01-12
         - Added support for NKJP tagset in annotation
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 5df0568..5fd3578 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -525,16 +525,23 @@
   $app->helper(
     korap_request => sub {
       my $c      = shift;
+
+      # Get plugin user agent
+      my $ua = $c->kalamar_ua;
+
+      # Override if UA is granted
+      if (ref $_[0] eq 'Mojo::UserAgent') {
+        $ua = 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);
 
@@ -622,7 +629,7 @@
           my $tx = shift;
 
           # Response is fine
-          if ($tx->res->is_success) {
+          if ($tx->res->is_success || $tx->res->is_redirect) {
             return Mojo::Promise->resolve($tx);
           }
 
@@ -1420,7 +1427,15 @@
         state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
         $c->stash(redirect_uri => Mojo::URL->new($v->param('redirect_uri')));
 
-        return $c->korap_request(post => $r_url, {} => form => {
+        my $ua = Mojo::UserAgent->new(
+          connect_timeout => 30,
+          inactivity_timeout => 30,
+          max_redirects => 0
+        );
+
+        $ua->server->app($app);
+
+        return $c->korap_request($ua, post => $r_url, { } => form => {
           response_type => 'code',
           client_id => $v->param('client_id'),
           redirect_uri => $c->stash('redirect_uri'),
@@ -1436,6 +1451,20 @@
 
             # Check for location header with code in redirects
             my $loc;
+
+            # Look for code in location URL
+            if ($loc = $tx->res->headers->header('Location')) {
+              my $url = Mojo::URL->new($loc);
+
+              if ($url->query->param('code')) {
+                return Mojo::Promise->resolve($loc);
+              } elsif (my $err = $url->query->param('error_description')) {
+                return Mojo::Promise->reject($err);
+              }
+            };
+
+            # Check for location header with code in redirects
+            # This should be dead code tbh
             foreach (@{$tx->redirects}) {
               $loc = $_->res->headers->header('Location');
 
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 9deec0e..dfe9fc0 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -1019,16 +1019,27 @@
   ->header_is('location', '/settings/oauth?error_description=Bad+CSRF+token')
   ;
 
+
+my $local_port = $t->get_ok('/')->tx->local_port;
+my $remote_port = $t->get_ok('/')->tx->remote_port;
+
+like($local_port, qr!^\d+$!);
+like($remote_port, qr!^\d+$!);
+
+my $port = $remote_port;
+
+my $redirect_url_fakeapi = $t->app->close_redirect_to(Mojo::URL->new('http://localhost:' . $port)->path($fake_backend_app->url_for('return_uri'))->to_abs->to_string);
+
 $fwd = $t->post_ok(Mojo::URL->new('/settings/oauth/authorize')->query({
   client_id => 'xyz',
   state => 'abcde',
   scope => 'search match',
-  redirect_uri_server => 'http://example.com/',
-  redirect_uri => $fake_backend_app->url_for('return_uri')->to_abs,
+  redirect_uri_server => 'http://localhost:'.$port,
+  redirect_uri => "$redirect_url_fakeapi",
   csrf_token => $csrf,
 }))
   ->status_is(302)
-  ->header_like('location', qr!/realapi/fakeclient/return!)
+  ->header_like('location', qr!^http://localhost:\d+/realapi/fakeclient/return\?code=.+$!)
   ->tx->res->headers->header('location')
   ;
 
@@ -1037,6 +1048,28 @@
   ->content_like(qr'welcome back! \[(.+?)\]')
   ;
 
+my $fake_port = $port;
+
+while ($fake_port == $remote_port || $fake_port == $local_port) {
+  $fake_port++;
+};
+
+$redirect_url_fakeapi = $t->app->close_redirect_to(Mojo::URL->new('http://localhost:' . $fake_port)->path($fake_backend_app->url_for('return_uri'))->to_abs->to_string);
+
+$fwd = $t->post_ok(Mojo::URL->new('/settings/oauth/authorize')->query({
+  client_id => 'xyz',
+  state => 'abcde',
+  scope => 'search match',
+  redirect_uri_server => 'http://localhost:'.$port,
+  redirect_uri => "$redirect_url_fakeapi",
+  csrf_token => $csrf,
+}))
+  ->status_is(302)
+  ->header_unlike('location', qr!^http://localhost:\d+/realapi/fakeclient/return\?error_description=Connection\+refused$!)
+  ->header_like('location', qr!^http://localhost:\d+/realapi/fakeclient/return\?code=.+?$!)
+  ->tx->res->headers->header('location')
+  ;
+
 $t->post_ok(Mojo::URL->new('/settings/oauth/authorize')->query({
   client_id => 'xyz',
   state => 'abcde',