How to redirect to a different request type?

During the process of logging in my user needs to be redirected to the “dashboard”. My problem is the request type when logging in is a POST but I’m trying to redirect to a GET route. hmmmm, not sure how to do this.

I’m attempting to call the redirect like this inside my Authorization class:

return $response->withRedirect($this->router->pathFor('dashboard'), 200);

And my route looks like this, of course it’s a GET route because that’s how the page will be called %99 percent of the time:

$app->get('/dashboard', function ($request,$response,$args) {
    return $this->view->render($response, "dashboard.html", []);
})->add('Header_footer')->add('Auth')->setName('dashboard');

What kind of error or issue are you having? You should be able to issue a redirect regardless of the previous or next request type. Remember, you are telling the browser to make a new request.

I have a /login route which is a GET request to show the login form. That form then performs a POST to /login which goes to a Login controller. The login controller then verifies the credentials from the post and redirects to the user’s dashboard. That is at /dashboard which is a GET.

The /dashboard route has my Auth middleware applied, but the login (both get and post) routes do not. The Auth middleware itself just looks to see if the user is not logged in. If true, it redirects to the login page. If false (meaning the user is logged in) then it just passes the request to the next callable.

I’m away from the code now I’ll check it out tomorrow, thanks!

I figured out that I had some logic that wasn’t making the get routes available. I had:

switch ($_SERVER['REQUEST_METHOD']) {
    case 'PUT':
        include BASE_PATH_CONFIG .'rest/put.php'; 
       break;
    case 'POST':
        include BASE_PATH_CONFIG .'rest/post.php';
        break;
    case 'GET': 
        include BASE_PATH_CONFIG .'rest/get.php';
       break;
    case 'DELETE':
        include BASE_PATH_CONFIG .'rest/delete.php';  
       break;
    default:
        handle_error($request);  
        break;
}

$app->run();

This had worked a long time ago, maybe version 2? I guess all the redirecting happens internally inside the app and doesn’t call this controller (index.php) again.

So now it’s error free when I redirect, however it is still not working as expected.

As soon as the user’s posted data is approved the url still shows mysite/login and the page does not contain the html from “dashboard”

When I manually type in mysite/dashboard things work just fine however.

My post route looks like this:
> $app->post("/login", 'Auth:login')->add('Header_footer');

My method for verification inside my verification class looks like this:

public function dologin(Request $request, Response $response, $next){
    $data = $request->getParams();
    $u=$this->db->user
        ->where('admin', 1)
        ->where('first_name', trim($data['first_name']))
        ->where('password', trim(sha1($data['password'])))
        ->fetch();
    if($u){
        $_SESSION['u'] = $u['id'];
        return $response->withRedirect($this->router->pathFor('dashboard'), 200);
    }else{
        return $this->view->render($response, "login.html", ['msg'=>'Wrong password and/or user name.']);
    }
}

And my get route for “dashboard” looks like this:

$app->get('/dashboard', function ($request,$response,$args) {
    return $this->view->render($response, "dashboard.html", []);
})->add('Header_footer')->add('Auth')->setName('dashboard');

I think it has to do with my __invoke function() ?

public function __invoke(Request $request, Response $response, $next){
    $response = $next($request, $response);
   if(isset($_SESSION['u'])){
       return $response;
    }else{
        return $response->withRedirect($this->router->pathFor('login'), 403);
    }
    return $response;
}

I have a feeling __invoke is only being called once and not again during the redirection… can anyone confirm this? And if so how do I check for validation each time from everywhere? Maybe use __construct() ? Again I’m not sure. When does Slim 3 call constructors and invoke?

I don’t think I can put the login check logic in __construct() because the constructor doesn’t have any knowledge of $request, $response, $next

I haven’t reviewed all of the code, but the first thing that jumps out at me is the __invoke() method. The first line within that method is passing along the request to the next middleware (or the app if this is the last middleware) so none of the other lines are run until the response comes back.

I suspect you want something more like this, but again, I haven’t looked in detail and need to step out for a little bit.

public function __invoke(Request $request, Response $response, $next){
    // this part runs before it hits the app
   if(isset($_SESSION['u'])){
        // we're good here, pass along to the next middleware or the app
        $response = $next($request, $response);
    }else{
        // nope, stop executing any future middeware and the app and just redirect the user here
        return $response->withRedirect($this->router->pathFor('login'), 403);
    }

   // now you are sending along the request to the next middleware, or app if this is the last middleware
   $response = $next($request, $response);

    // Now this is after the middleware chain has hit the app and is on its way back out through the middleware
   return $response;
}

Everything seems to fire off fine up until the redirect. I went in and used print_r(), echo(), exit(), what ever I could to trace the path of the application. it fails at:

> return $response->withRedirect($this->router->pathFor('dashboard'), 200);

Remove the status code? What do you see in the browser? Are you passing the router to the constructor?

Tried it, still the same. The redirect is not occurring. A page gets rendered with my header and footer (provided by middleware) but the url doesn’t reflect the redirection and the “dashboard” content is not displayed. - I can confirm this by looking directly at the html source -it’s a header / footer and nothing else

I moved all my code into one index file which I’m going to paste here, along with the two middleware classes I’m using to verify a logged in user and to add a header/footer to each page. Hopefully this helps someone see my errors.

my controller: index.php:

<?php
//load application settings
require 'config.php';
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\PhpRenderer;
$config['displayErrorDetails'] = true;
$config['addContentLengthHeader'] = false;
$config['db']['host']   = "127.0.0.1";
$config['db']['user']   = "root";
$config['db']['pass']   = "";
$config['db']['dbname'] = "assessment";
// Create the Slim application using our container.
$app = new \Slim\App(["settings" => $config]);
$container = $app->getContainer();
//database
$container['db'] = function($c){ 
    //setup NOTORM
    $db = $c['settings']['db'];
    $pdo = new PDO("mysql:host=" . $db['host'] . ";dbname=" . $db['dbname'], $db['user'], $db['pass']);
    $db = new NotORM($pdo);
    $db->debug = true;
    return $db;
};
$container['pdo'] = function ($c) {
    //setup PDO
    $db = $c['settings']['db'];
    $pdo = new PDO("mysql:host=" . $db['host'] . ";dbname=" . $db['dbname'], $db['user'], $db['pass']);
    $pdo->setAttribute(s::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    return $pdo;
};
$container['view'] = new PhpRenderer(BASE_PATH . "/views/page/");
$container['elem'] = new PhpRenderer(BASE_PATH . "/views/elem/");
//middleware
require 'models/Header_footer.php';
$container['Header_footer'] = function ($c) { ;
    $header_footer = new \App\Middleware\Header_footer($c['elem']);
    return $header_footer;
};
require 'models/Auth.php';
$container['Auth'] = function ($c) {
    $auth = new \App\Middleware\Auth;
    $auth->set($c);
    return $auth;
};
//POST routes
$app->post("/login", 'Auth:login')->add('Header_footer');
//GET routes
$app->get('/', function ($request,$response,$args) {
    return $this->view->render($response, "login.html", ['msg'=>'please login']);
})->add('Header_footer');
$app->get("/login", 'Auth:show_login')->add('Header_footer')->setName('login');
$app->get('/logout', function ($request,$response,$args) {
    // remove all session variables
    session_unset(); 
    // destroy the session 
    session_destroy();
    $_SESSION=[];
    return $this->view->render($response, '/login.html', ['msg'=>'You are logged out.']);
})->add('Header_footer');
$app->get('/dashboard', function ($request,$response,$args) {
    return $this->view->render($response, "dashboard.html", []);
})->add('Header_footer')->add('Auth')->setName('dashboard');
$app->run();

My header/footer class (middleware):
<?php

namespace App\Middleware;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

class Header_footer{
    /**
     * Authentication middleware invokable class
     *
     * @param  \Psr\Http\Message\ServerRequestInterface $request  PSR7 request
     * @param  \Psr\Http\Message\ResponseInterface      $response PSR7 response
     * @param  callable                                 $next     Next middleware
     *
     * @return \Psr\Http\Message\ResponseInterface
    **/
    public function __construct($elem)
    {
        $this->elem = $elem;
    }
    public function __invoke(Request $request, Response $response, $next){
        $renderer = $this->elem; 
        $renderer->render($response, 'header.php');
        $next($request, $response);
        $renderer->render($response, 'footer.php');
        return $response;
    }
}

My Verification class (middleware):
class Auth{
protected $view;
protected $elem;
protected $db;
protected $router;

    public function __invoke(Request $request, Response $response, $next){
        // this part runs before it hits the app
        if(isset($_SESSION['tid'])){
            // we're good here, pass along to the next middleware or the app
            $response = $next($request, $response);
        }else{
            // nope, stop executing any future middeware and the app and just redirect the user here
            return $response->withRedirect($this->router->pathFor('login'), 403);
        }
        // now you are sending along the request to the next middleware, or app if this is the last middleware

// ! //$response = $next($request, $response);   <-- this line doubles up my webpage   ??? wierd
       
        // Now this is after the middleware chain has hit the app and is on its way back out through the middleware
        return $response;
    }    

    public function set($c){
    //set container objects
        $this->view = $c['view'];
        $this->elem = $c['elem'];
        $this->db = $c['db'];
        $this->router = $c['router'];
    }
    public function login(Request $request, Response $response, $next){
    //handle login request and redirect if ok, render msg if not ok
        $data = $request->getParams();
        $teacher=$this->db->teacher
            ->where('admin', 1)
            ->where('first_name', trim($data['first_name']))
            ->where('last_name', trim($data['last_name']))
            ->where('password', sha1($data['password']))
            ->fetch();
        if($teacher){
            //set session hash-id
            $_SESSION['tid'] = $teacher['tid']; 
            //load teacher_obj :NOTE: admins are teachers with special permissions
            //$response = $next($request, $response);
            return $response->withRedirect($this->router->pathFor('dashboard'));
        }else{
           // $response = $next($request, $response);
            return $this->view->render($response, "login.html", ['msg'=>'Wrong password and/or user name.']);
        }
    }
    public function show_login(Request $request, Response $response, $next){
    //display login page
         return $this->view->render($response, "login.html", ['msg'=>'']);
    }
}

Hmmm… I wouldn’t think putting those additional methods in the Auth class itself would work, but maybe it will. That is different than the approach I take. Here is what I do.

My Auth middleware doesn’t have any methods other than __construct() and __invoke(). This middleware doesn’t actually log the user in nor log the user out. All it does is check to see if the user is logged in our out.

My logic to log the user in/out is in an Auth controller. I’ll try to break down the flow for users in my app. It is a bit simplified, but generally matches what I’m doing.

  • User performs a GET request on /login, and the request goes to AuthController:create() where the login form is shown.
  • User POSTs their login information to /login and the request goes to AuthController:store()` where the credentials are checked. If there is a match, we login the user, set the session, etc then redirect to the dashboard.
  • The dashboard page has an AuthMiddleware applied as middleware. This middleware has two methods, the construct and invoke magic methods. For my purposes, I pass in my session object and the router object to the constructor. In the invoke method I’m asking the session object (and some other, not relevant stuff) if the user is logged in. If the user is not, then I return (early) a redirect to the login page. If the user is logged in then nothing really happens except the request gets passed along down the middleware chain. So my AuthMiddleware’s invoke method looks something like this.
    public function __invoke(Request $request, Response $response, $next)
    {
        // Before
        $userInfo = $this->session->getUser();
        if (!isset($userInfo)) {
            return $response->withRedirect($this->router->pathFor('login'), 403);
        }

        $response = $next($request, $response);

        // After
        return $response;
    }

I’m not sure, but I think you might be trying to do too much inside the Middleware. I’d think your Auth middleware shouldn’t actually be performing any login/logout tasks. Those might be better placed in a controller. Then all the middleware needs to do is check if the user is logged in or not.

It looks like you have a method: getUser() in your AuthMiddleware. ? or are you passing a class called “session”

$userInfo = $this->session->getUser();

And why pass a session? aren’t they globally accessible?

I’m going to rewrite my app based off your logic and see what happens!

I’m passing in an object/class session. That session class has a method called getUser that just grabs the appropriate session parameter for me.

$_SESSION is globally accessible, yes, however I wrap it into a class and then use the class instead of using the session directly. For examples of this, if you’re interested, see RKA Slim Session and/or Aura.Session.

Cool, thanks for the info… I noticed this which is super interesting…

When I handle the posted login like this it does not redirect:
$app->post("/login", 'Auth:login')->add('Header_footer');

When I remove the ->add(‘Header_footer’) middleware it does redirect.
???
This works as expected, logging in a user and rerouting them.
$app->post("/login", 'Auth:login');

Now this perplexes me because now I’m wondering if there is something wrong in my code or if the chaining is somehow busted with the slim library.

What does your Header_footer middleware look like? There could be something in there that is preventing it from continuing down the middleware chain.

it is posted above tim.

Might be getting a bit out of my league, but I wonder if this has something to do with the request object being immutable, but you might be changing it with your renderer rather than creating a new response. See Filtering the PSR-7 body in middleware and Writing PSR-7 MIddleware. Although, if that was the case I’d expect that middleware to not work altogether or otherwise truncate the response.

good tip, I won’t be able to test anything else out until after the weekend (here in U.S.)… the code is at my work.
Thanks again Tim

I’m at my wits’ end here. I’ve been working on a login script for 3 weeks now. I’ve stripped down my code to just simply do a redirect from inside middleware and that doesn’t even work. I’m simply attempting to go from a post request to a redirect, just a simple version without all the fancy sessions/db/etc…

I’ve tried routing this way:

$app->post("/login", function ($request,$response,$args) {
    return $response;
})->add('Auth');

I’ve tried this way:
$app->post("/login", 'Auth');

My Auth Middleware is about as stripped-down as it can get:

    class Auth{

       function __construct($c){
              return $c->response->withRedirect($c->router->pathFor('dashboard'));
       }
        public function __invoke(Request $request, Response $response, $next=null){
            if (!$next) {
                return $response;
            }
            $response = $next($request, $response);;
            return $response;
        }
}

Here’s me setting up the middleware (not sure of the buzzword for this piece):
require ‘models/Auth.php’;

$container['Auth'] = function ($c) {
    $auth = new \App\Middleware\Auth($c);
    return $auth;
};

I’m not the only one troubled by this:

That is a bit different from your Auth middleware previously. I don’t believe you can return anything from a constructor in PHP. Your redirect should happen during __invoke.

namespace App\Middleware;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\router;

class Auth{
       protected $router;

       public function __construct(Router $router){
              $this->router = $router;
       }
        public function __invoke(Request $request, Response $response, $next){
            return $response->withRedirect($this->router->pathFor('dashboard'));
            
            $response = $next($request, $response);;

            return $response;
        }
}

If I can grab a few minutes tonight I’ll try to post a full example app.