3

I've been testing the new Slim 4 framework and redirects work fine for me in normal classes, but I cannot seem to get them working in middleware, where a response is dynamically generated (apparently?) by the Request Handler. When I try to redirect with a Location header, it simply fails to redirect, and my route continues to the original location.

Here’s a basic version of my authentication middleware for testing:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class AuthMiddleware extends Middleware {

    public function __invoke(Request $request, RequestHandler $handler): Response {
        $response = $handler->handle($request);
        $loggedInTest = false;
        if ($loggedInTest) {
            echo "User authorized.";
            return $response;
        } else {
            echo "User NOT authorized.";
            return $response->withHeader('Location', '/users/login')->withStatus(302);
        }
    }
}

Has anybody got this to work? And if so, how did you accomplish it? Thanks in advance.

ConleeC
  • 337
  • 6
  • 13
  • Try creating and using a new _response_ instance for redirecting - maybe by using a [HTTP factory](https://www.php-fig.org/psr/psr-17/#22-responsefactoryinterface) for this: `echo 'User NOT authorized.'; $response = $this->responseFactory->createResponse(); return $response->withHeader('Location', '/users/login')->withStatus(302);`. – PajuranCodes Oct 29 '19 at 10:34
  • @dakis another user on another forum suggested similar. Life has gotten in the way of my coding the past few weeks, but when I get back to testing I will definitely try this. Thank you! – ConleeC Oct 29 '19 at 17:08

4 Answers4

7

I think I see the problem with this code.

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class AuthMiddleware extends Middleware {

    public function __invoke(Request $request, RequestHandler $handler): Response {
        $response = $handler->handle($request);
        $loggedInTest = false;
        if ($loggedInTest) {
            echo "User authorized.";
            return $response;
        } else {
            echo "User NOT authorized.";
            return $response->withHeader('Location', '/users/login')->withStatus(302);
        }
    }
}

When you call $handler->handle($request), that processes the request normally and calls whatever closure is supposed to handle the route. The response hasn't been completed yet, you can still append stuff to it, but the headers are already set, so you can't do a redirect, because the headers are done.

Maybe try this:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;

class AuthMiddleware extends Middleware {

    public function __invoke(Request $request, RequestHandler $handler): ResponseInterface {            
        $loggedInTest = false;
        if ($loggedInTest) {
            $response = $handler->handle($request);
            echo "User authorized.";
            return $response;
        } else {
            $response = new Response();
            // echo "User NOT authorized.";
            return $response->withHeader('Location', '/users/login')->withStatus(302);
        }
    }
}

If the login test fails, we never call $handler->handle(), so the normal response doesn't get generated. Meanwhile, we create a new response.

Note that the ResponseInterface and Response can't both be called Response in the same file, so I had to remove that alias, and just call the ResponseInterface by its true name. You could give it a different alias, but I think that would only create more confusion.

Also, I commented out the echo before the redirect. I think this echo will force headers to be sent automatically, which will break the redirect. Unless Slim 4 is doing output buffering, in which case you're still not going to see it, because the redirect will immediately send you to a different page. Anyway, I commented it out to give the code the best chance of working but left it in place for reference.

Anyway, I think if you make that little change, everything will work. Of course, this post is almost a year old, so you've probably solved this on your own, switched to F3, or abandoned the project by now. But hopefully, this will be helpful to someone else. That's the whole point of StackOverflow, right?

eimajenthat
  • 1,338
  • 3
  • 15
  • 33
  • @ eimajenthat thanks for this detailed answer. I did in fact complete the project using F3, but I have ideas for feature enhancements so some day I will revisit. I'm not certain of the state of F3, so I might go back to Slim. Anyway, I appreciate the detailed answer. I don't actually have the ability to test it right now, but as you said, maybe somebody else has the same problem and will be able to confirm it. – ConleeC Aug 07 '20 at 23:21
  • Glad to help. I looked at F3 many years ago, and really liked what they were doing. But the GPL license wouldn't work for my use case, so I ended up using something else. I have no idea what the current state of F3 is. I hope they're going strong. It was a very nice tool as I recall. – eimajenthat Aug 11 '20 at 15:32
  • As I recall, just FYI, they did make a change to their license, or at least clarified it, because of user concerns. – ConleeC Aug 11 '20 at 21:22
  • I had a similar issue with Slim 4 where `$response = $handler->handle($request);` seems to immediately return from the function. If I placed a `die('HERE');` immediately after, it wouldn't die, and would just continue as if I'd done a `return`. I resolved this issue by making the before middleware to `$request->setAttribute()` with an error status and details, and test if `$request` bore that attribute in the core layer. – Fabien Haddadi Sep 23 '21 at 16:25
2

eimajenthat is right, except that you cannot create an instance of interface.

Try this instead:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;

class AuthMiddleware extends Middleware {

public function __invoke(Request $request, RequestHandler $handler): Response {            
    global $app; // Assuming $app is your global object

    $loggedInTest = false;
    if ($loggedInTest) {
        $response = $handler->handle($request);
        echo "User authorized.";
        return $response;
    } else {
        $response = $app->getResponseFactory()->createResponse();
        // echo "User NOT authorized.";
        return $response->withHeader('Location', '/users/login')->withStatus(302);
    }
}

}

0

I was growing so frustrated by Slim 4 and redirect issues that I took a look at FatFreeFramework and had the exact same problem. So I knew it was something I was doing. My code was putting the app into a never-ending redirect loop. I can make it work by validating the redirect URL like so in FatFreeFramework:

class Controller {

    protected $f3;

    public function __construct() {
        $isLoggedIn = false;
        $this->f3 = Base::instance();
        if ($isLoggedIn == false && $_SERVER['REQUEST_URI'] != '/login') {
            $this->f3->reroute('/login');
            exit();
        }
    }
}

Therefore, although I haven't actually taken the time to test it, I'm assuming I could fix it in Slim 4 by doing something like:

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;

class AuthMiddleware extends Middleware {

    public function __invoke(Request $request, RequestHandler $handler): Response {
        $response = $handler->handle($request);
        $loggedInTest = false;
        if (!$loggedInTest && $_SERVER['REQUEST_URI'] != '/user/login') {
            return return $response->withHeader('Location', '/users/login')->withStatus(302);
        } else {
            return $response;
        }
    }
}

Does anybody have another idea for how to break a continuous redirect loop? Or is the $_SERVER variable the best option?

Thanks in advance.

reformed
  • 4,505
  • 11
  • 62
  • 88
ConleeC
  • 337
  • 6
  • 13
  • Spoke too soon. Long story short, in my FatFreeFramework testing, I was definitely putting my test app in an infinite redirect loop, so I assumed it was the same issue here. But sadly it is not. My 'login' method is in a completely different class, and it obviously is NOT behind authentication. And sadly, the redirect still falls thru to the original route. Anybody have an idea on this? Has anybody else made redirect work in middleware, and if so, can you post a snippet showing how? – ConleeC Oct 18 '19 at 16:37
  • Usually for detecting redirect loops, particularly ones that loop through several pages, I'll buffer up to 20 request targets with a timestamp of when they hit the server into the user session and check for three things where I redirect; a looping pattern that repeats at least twice, a timespan of less than one second per link (indicating it's redirection and not arbitrary user navigation, network lag notwithstanding), and the current link I am going to redirect to existing within the loop. I then kick it to a 500 error page if both of those are true, and `array_unshift` the buffer after 20. – mopsyd Aug 31 '21 at 05:22
  • You can be more concise about it if you also explicitly check for redirect headers, but the above will also capture issues with frontend bugs that do `window.location.href` – mopsyd Aug 31 '21 at 05:25
0

Use 2 response

namespace App\middleware;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response as Response7;
use Psr\Http\Message\ResponseInterface as Response;

final class OtorisasiAdmin {
     public function __invoke(Request $request, RequestHandler $handler): Response {
        $session = new \Classes\session();
        $session->start();
        $isAdmin=($session->has("login","admin"))?true:false;
        if(!$isAdmin){
            $response = new Response7();
            $error = file_get_contents(__dir__."/../../src/error/404.html");    
            $response->getBody()->write($error);
            return $response->withStatus(404);
        }
        $response=$handler->handle($request);
        return $response;
    }
}