Access to Route from middleware


#1

Hi!

I’m writing an authentication layer around my application and would like to know if I can somehow get access to the matched route or the invokable, from within the middleware. This way I can set a constant in my controller defining whether or not this request requires authentication.

I have yet to find a way to do this though, any pointers?


#2

Why is it that you always find the answer right after posting a question? Figured it out:

<?php
declare(strict_types = 1);

namespace Application\Infrastructure\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class AuthenticationMiddleware
{
    /**
     * @param ServerRequestInterface $request
     * @param ResponseInterface $response
     * @param callable $next
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
    {
        list($controller) = explode(':', $request->getAttribute('route')->getCallable());

        $requiresAuthentication = true;

        if (defined($controller . '::REQUIRES_AUTH')) {
            $requiresAuthentication = (bool)$controller::REQUIRES_AUTH;
        }

        if (!$requiresAuthentication) {
            return $next($request, $response);
        }

        // Perform authorization here and act accordingly
        return $next($request, $response);
    }
}

#3

@ErikBooij I’ve recently implemented something similar, using a public routes array in my validation middleware. The way I access the route in the middleware is using this:

$routes = explode('/',$request->getUri()->getPath());
$currentRoute = $routes[1];

How I use it in my app:

$c = $app->getContainer();

$c['validateUser'] = function() {
	$publicRoutes = array('login','register');
	return new ValidateUser($publicRoutes,$_SESSION['user']);
};

$app->add($c->validateUser);

Middleware class:

class ValidateUser {

	public $publicRoutes;
	public $portalUser;
	
	public function __construct(array $publicRoutes,$user) {
		$this->publicRoutes = $publicRoutes;
		$this->user = $user;
	}

	public function __invoke($request,$response,$next) {
		$routes = explode('/',$request->getUri()->getPath());
		$currentRoute = $routes[1];
		
		// if the current route isn't in the publicRoutes array, validate the user
		if (!in_array($currentRoute,$this->publicRoutes)) {

			// invalid user
			if (empty($this->user)) {
				return $response->withStatus(401);
			}
		}

		// valid user or public route
		return $next($request,$response);
	}
}

This, however, only evaluates the first route.


#4

@robrothedev Thanks for the reply, seems like an alright solution. I just figured out a way to do this on a controller level though, which suits my needs perfectly. I used

$controller = $request->getAttribute('route')->getCallable();

And having the Slim setting determineRouteBeforeAppMiddleware set to true.


#5

Never thought about handling it at the controller level or using the getCallable method. I might have to tinker around with this.


#6

I have a public route check in my Middleware that now uses setArgument.

$app->group('/admin', function() use ($app) {
    $app->get('', 'App\Controller\AdminController:actionDashboard')->setName('admin.dashboard');
    $app->map(['GET', 'POST'], '/login', 'App\Controller\AdminController:actionLogin')->setName('admin.login')->setArgument('auth', true);
    $app->get('/logout', 'App\Controller\AdminController:actionLogout')->setName('admin.logout')->setArgument('auth', false);
    $app->get('/reset', 'App\Controller\AdminController:actionReset')->setName('admin.reset')->setArgument('auth', false);
    $app->map(['GET', 'POST'], '/auth', 'App\Controller\AdminController:actionAuth')->setName('admin.auth')->setArgument('auth', false);
})->add(new App\Middleware\Auth($app->getContainer()->get('router')));

Then in AuthMiddleware:

class Auth {

    protected $router;
    protected $auth;

    public function __construct($router) {
        $this->router = $router;
        $this->is_authenticated = self::is_authenticated();
    }

    public function __invoke($request, $response, $next) {

        $auth = $request->getAttribute('route')->getArgument('auth');

        if ((isset($auth)) || $this->is_authenticated) {
            $response = $next($request, $response);
        } else {
            $response = $response->withRedirect($this->router->pathFor('admin.login'), 403);
        }
        return $response;
    }

    public function is_authenticated() {
        return (isset($_SESSION['user'])) ? true : false;
    }

}

#7

Thanks for replying! I don’t really want to define the authentication requirements anywhere else than in my controller, but I can imagine if that’s not an issue to you, this is a fair solution.

I find my own solution (which I’ve shown in the second post in this thread) to be be less error prone and easier to maintain. That’s largely personal preference though.


#8

I suppose both methods are equally fine, the ->setArgument(‘auth’, false); isn’t even needed but the true is.
There are usually not a lot of admin routes that would need a “no authentication” method anyway.
Before using setArgument I was doing it allmost the same way you did though.

This is how I did it before:

public function __invoke($request, $response, $next) {
    if (in_array($request->getAttribute('route')->getName(), ['admin.login', 'admin.reset']) || $this->is_authenticated) {
        $response = $next($request, $response);
    } else {
        $response = $response->withRedirect($this->router->pathFor('admin.login'), 403);
    }
    return $response;
}

#9

Another way would be to get the current path and match it against a array:

if (in_array(ltrim($request->getUri()->getPath(), "/"), ['admin/login', 'admin/reset']) || $this->is_authenticated()) { .. }