Back-End

8 jul, 2014

Autenticação baseada em tokens com aplicativos Silex

Publicidade

Imagine este simples aplicativo Silex:

use Silex\Application;
 
$app = new Application();
 
$app->get('/api/info', function (Application $app) {
    return $app->json([
        'status' => true,
        'info'   => [
            'name'    => 'Gonzalo',
            'surname' => 'Ayuso'
        ]]);
});
 
$app->run();

O que acontece se quisermos utilizar uma camada de segurança? Podemos utilizar sessões. Elas são a forma “padrão” para realizar a autenticação em aplicativos da Web, mas quando nosso aplicativo é um aplicativo PhoneGap/Cordova que usa um servidor Silex como servidor de API, elas não são o melhor caminho. A melhor maneira é uma autenticação baseada em token. A ideia é simples. Primeiro precisamos de um token válido. Nosso servidor de API nos dará um se enviamos credenciais válidas em um formulário de login. Então precisamos enviar o token com cada solicitação (da mesma forma que enviamos o cookie de sessão com cada pedido).

Com o Silex, podemos verificar esse token e validar.

use Silex\Application;
 
$app = new Application();
 
$app->get('/api/info', function (Application $app) {
    $token = $app->get('_token');
     
    // here we need to validate the token ...
 
    return $app->json([
        'status' => true,
        'info'   => [
            'name'    => 'Gonzalo',
            'surname' => 'Ayuso'
        ]]);
});
 
$app->run();

Não é uma solução elegante. Precisamos validar o token dentro de todas as vias, e isso é um saco. Podemos também usar middlewares e validar o token com $app->before(). Vamos construir algo parecido, mas com algumas variações. Primeiro quero manter o aplicativo principal o mais limpo possível. A lógica de validação deve estar separada da lógica do aplicativo, por isso vamos estender Silex\Application. Nosso aplicativo principal será assim:

use G\Silex\Application;
 
$app = new Application();
 
$app->get('/api/info', function (Application $app) {
    return $app->json([
        'status' => true,
        'info'   => [
            'name'    => 'Gonzalo',
            'surname' => 'Ayuso'
        ]]);
});
 
$app->run();

No lugar do Silex\Application, vamos usar G\Silex\Application.

namespace G\Silex;
 
use Silex\Application as SilexApplication;
use G\Silex\Provider\Login\LoginBuilder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
 
class Application extends SilexApplication
{
    public function __construct(array $values = [])
    {
        parent::__construct($values);
 
        LoginBuilder::mountProviderIntoApplication('/auth', $this);
 
        $this->after(function (Request $request, Response $response) {
            $response->headers->set('Access-Control-Allow-Origin', '*');
        });
    }
}

O nosso novo G\Silex\Application é um Silex\Application habilitando CORS. A gente também montou um provedor de serviço.

A responsabilidade do nosso servidor de API será verificar o token de todos os pedidos e fornecer uma forma de obter um novo token. Para isso, vamos criar uma rota “/auth/validateCredentials”.. Se forem dadas credenciais válidas, um novo token será enviado para o cliente.

Nosso provedor de serviço possui duas partes: um provedor de serviço e um provedor de controller.

Usaremos uma classe LoginBuilder para montar os dois provedores:

namespace G\Silex\Provider\Login;
 
use Silex\Application;
 
class LoginBuilder
{
    public static function mountProviderIntoApplication($route, Application $app)
    {
        $app->register(new LoginServiceProvider());
        $app->mount($route, (new LoginControllerProvider())->setBaseRoute($route));
    }
}

Nosso provedor de controller:

namespace G\Silex\Provider\Login;
 
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpFoundation\Request;
use Silex\ControllerProviderInterface;
use Silex\Application;
 
class LoginControllerProvider implements ControllerProviderInterface
{
    const VALIDATE_CREDENTIALS = '/validateCredentials';
    const TOKEN_HEADER_KEY     = 'X-Token';
    const TOKEN_REQUEST_KEY    = '_token';
 
    private $baseRoute;
 
    public function setBaseRoute($baseRoute)
    {
        $this->baseRoute = $baseRoute;
 
        return $this;
    }
 
    public function connect(Application $app)
    {
        $this->setUpMiddlewares($app);
 
        return $this->extractControllers($app);
    }
 
    private function extractControllers(Application $app)
    {
        $controllers = $app['controllers_factory'];
 
        $controllers->get(self::VALIDATE_CREDENTIALS, function (Request $request) use ($app) {
            $user   = $request->get('user');
            $pass   = $request->get('pass');
            $status = $app[LoginServiceProvider::AUTH_VALIDATE_CREDENTIALS]($user, $pass);
 
            return $app->json([
                'status' => $status,
                'info'   => $status ? ['token' => $app[LoginServiceProvider::AUTH_NEW_TOKEN]($user)] : []
            ]);
        });
 
        return $controllers;
    }
 
    private function setUpMiddlewares(Application $app)
    {
        $app->before(function (Request $request) use ($app) {
            if (!$this->isAuthRequiredForPath($request->getPathInfo())) {
                if (!$this->isValidTokenForApplication($app, $this->getTokenFromRequest($request))) {
                    throw new AccessDeniedHttpException('Access Denied');
                }
            }
        });
    }
 
    private function getTokenFromRequest(Request $request)
    {
        return $request->headers->get(self::TOKEN_HEADER_KEY, $request->get(self::TOKEN_REQUEST_KEY));
    }
 
    private function isAuthRequiredForPath($path)
    {
        return in_array($path, [$this->baseRoute . self::VALIDATE_CREDENTIALS]);
    }
 
    private function isValidTokenForApplication(Application $app, $token)
    {
        return $app[LoginServiceProvider::AUTH_VALIDATE_TOKEN]($token);
    }
}

E o nosso provedor de serviço:

namespace G\Silex\Provider\Login;
 
use Silex\Application;
use Silex\ServiceProviderInterface;
 
class LoginServiceProvider implements ServiceProviderInterface
{
    const AUTH_VALIDATE_CREDENTIALS = 'auth.validate.credentials';
    const AUTH_VALIDATE_TOKEN       = 'auth.validate.token';
    const AUTH_NEW_TOKEN            = 'auth.new.token';
 
    public function register(Application $app)
    {
        $app[self::AUTH_VALIDATE_CREDENTIALS] = $app->protect(function ($user, $pass) {
            return $this->validateCredentials($user, $pass);
        });
 
        $app[self::AUTH_VALIDATE_TOKEN] = $app->protect(function ($token) {
            return $this->validateToken($token);
        });
 
        $app[self::AUTH_NEW_TOKEN] = $app->protect(function ($user) {
            return $this->getNewTokenForUser($user);
        });
    }
 
    public function boot(Application $app)
    {
    }
 
    private function validateCredentials($user, $pass)
    {
        return $user == $pass;
    }
 
    private function validateToken($token)
    {
        return $token == 'a';
    }
 
    private function getNewTokenForUser($user)
    {
        return 'a';
    }
}

Nosso provedor de serviço terá a lógica para validar as credenciais e token, e deve ser capaz de gerar um novo token:

private function validateCredentials($user, $pass)
{
    return $user == $pass;
}
 
private function validateToken($token)
{
    return $token == 'a';
}
 
private function getNewTokenForUser($user)
{
    return 'a';
}

Como podemos observar, a lógica do exemplo é muito simples. É só um exemplo, e aqui temos que realizar a nossa lógica. É provável que seja preciso verificar as credenciais com nosso banco de dados e que o nosso token deva estar armazenado em algum lugar para ser validado depois.

Você pode dar uma olhada no exemplo na minha conta do GitHub. Em outro artigo, vamos ver como construir um aplicativo de cliente com angularJs para usar esse servidor de API.

***

Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em http://gonzalo123.com/2014/05/05/token-based-authentication-with-silex-applications/