Authentication in Slim: is a combined middleware, singleton and hook approach smart?

Wilbo Baggins picture Wilbo Baggins · Sep 29, 2014 · Viewed 15.1k times · Source

I have written my own authentication controller to perform user authentication in my Slim application. Although it works, I am unsure if this is the way Slim is intended to work.

My authentication controller $auth has methods like $auth->login($user, $password) and $auth->logout() that change the state of the session and methods that report status, such as $auth->userIsLoggedIn(). Also, given a request, it can determine if a user has access to the requested route.

Currently, I am using a single instance of $auth in my Slim application in two different ways: as a singleton registered to $app->auth, and as route middleware that is applied to all routes. So, the Slim application is bootstrapped like this:

// Create singleton instance of MyAuthWrapper
$app->auth = new MyAuthenticationWrapper( array() );

// Attach the same instance as middleware to all routes
$app->add( $app->auth );

I am using the singleton instance from within my routes, for example, in the login route:

$app->post( '/login', function() use ($app)
{
    // ...
    $user = $app->auth->authenticate( $app->request()->post('username'), $app->request()->post('password') );
    // ...
}

And I am using the middleware version in all routes by attaching a method to the slim.before.dispatch hook that verifies that the user is authenticated and redirects to the login page otherwise. In order to do that, the authentication wrapper extends \Slim\Middleware and thus implements the call method, like this (simplified):

class MyAuthenticationWrapper extends \Slim\Middleware
{
    // ... Implementation of methods such as authenticate(), isLoggedIn(), logout(), etc.

    public function call()
    {
        $app = $this->app;

        $isAuthorized = function () use ($app) {

            $hasIdentity = $this->userIsLoggedIn(); // Assume this to work
            $isAllowed = $this->userHasAccessToRequestedRoute(); // Assume this to work

            if ($hasIdentity && !$isAllowed) 
            {
                throw new Exception("You have no access to this route");
            }

            if (!$hasIdentity && !$isAllowed) 
            {
                return $app->redirect( $loginPath );
            }
        };

        $app->hook('slim.before.dispatch', $isAuthorized);

        $this->next->call();
    }
}

Using a singleton is a slight code smell to me, but then adding the singleton instance as middleware with $app->add( $app->auth ) feels plain dirty. And finally using the middleware to register a closure to the dispatch hook makes me wonder if this whole strategy isn't too convoluted for a framework called Slim. But I can't figure out if there is an easier or more elegant way to accomplish what I want.

The question: Am I on the right track, or am I missing something about how Slim works that would allow me to accomplish this in a less complex way?

Answer

Jeremy Kendall picture Jeremy Kendall · Sep 30, 2014

You're absolutely on the right track using Middleware to register a hook for authentication. That's the approach I take and the approach I implemented in my own library, Slim Auth.

Using a Singleton can definitely be a code smell, but isn't always. If you feel you need to refactor MyAuthenticationWrapper, that's entirely up to you. The way you're using Middleware and Hooks with your custom class is, IMHO, 100% on target.

Side note: One of my mottos is "Make it work, then refactor." It looks like you're on track there too, so kudos.

Finally, authentication and authorization are complex topics requiring complex solutions. Complex does not mean convoluted, hard-to-maintain spaghetti, but getting it right can result in more code than I'd hoped to write (or more dependencies than I'd hoped to pull in via Composer).

UPDATE

If $app->auth is Middleware, then yes, you're off track a bit. Your instinct to create Middleware to register a hook is dead on, but Middleware is Middleware and shouldn't be used outside of that context. Ideally you'd create (or better yet find a package on Packagist) an auth class you can use both in your routes and in your Middleware. Pseudo-code would look something like:

$auth = new Auth(); // This is *not* middleware
$app->auth = $auth;

// Login route looks the same

// Middleware
class MyAuthenticationWrapper extends \Slim\Middleware
{
    public function call()
    {
        $app = $this->app;
        $auth = $app->auth;

        // Register your hook using appropriate methods from Auth class ...

        $this->next->call();
    }
}

Here's an example Middleware from Slim Auth. I've put together a sample implementation you can look at to see how I put it all together.