Autenticação em APIs REST com PHP

Introdução

Neste artigo, comentarei sobre alguns métodos de autenticação que normalmente são usados para proteger APIs REST. Também apresentarei uma parte prática, implementando um simples API REST com autenticação usando Json Web Token (JWT) com PHP e o micro-framework Silex.

Métodos de Autenticação

Veja abaixo alguns métodos de autenticação mais conhecidos:

  • Basic

  • É a abordagem mais simples para proteger qualquer página ou recurso na web. Esta tecnologia esta disponível em qualquer navegador ou servidor web.

    A autenticação é feita enviando usuário e senha pelo header HTTP Authorization. O envio das credenciais não são criptografados. Portanto tenha cuidado ao utilizar este método. Garanta que esteja usando SSL no seu site.

    A grande vantagem deste método é que por ser simples é bem mais veloz do que as outras opções.

  • Digest

  • A autenticação Digest é um pouco mais complexa que a Basic. A principal diferença é que os dados trafegados com Digest são protegidos por criptografia enquanto o Basic utiliza apenas texto puro codificado com Base64.

    Caso não seja possível usar HTTPS em sua API, Digest é uma opção mais segura que Basic.

    No caso de estar trabalhando com SSL, a autenticação Basic se torna uma melhor opção. A Digest é vulnerável a ataques man-in-the-middle enquanto Basic não.

  • OAuth2

  • OAuth2 é um framework de autorização que permite aplicações de terceiros obterem acesso limitado à contas dos usuário através de serviços HTTP. Normalmente são APIs REST. Muitas plataformas conhecidas utilizam esse esquema de autenticação, por exemplo Facebook, GitHub, Google, etc.

    OAuth2 não engloba apenas autenticação, mas também autorização. Outra vantagem significante é que trabalha de forma stateless, ou seja, não mantém estado/sessão no lado do servidor.

    OAuth2 utiliza tokens de acesso com controles de expiração.

  • JWT

  • Json Web Token (JWT) é uma forma elegante e segura de tratar autenticação em APIs. JWT não é um simples token gerado a partir de dados randômicos. JWT contém informações e metadados que descrevem a entidade do usuário, dados de autorização, validade do token, domínio válido, etc.

    Funciona de forma stateless, ou seja, sem manter estado no lado do servidor. Dados podem ser inspecionados, é compatível com OAuth2, possui controles de expiração, segurança e possui implementações em diferentes linguagens de programação.

    Devido a sua gama de vantagens e popularidade, escolhi o JWT para fazer uma demonstração de autenticação em APIs REST usando o micro-framework Silex, como veremos a diante.

JSON Web Token – JWT

Como citei anteriormente vamos demonstrar um mecanismo de autenticação em API usando JWT.
Utilizarei a biblioteca php-jwt do firebase disponível em https://github.com/firebase/php-jwt que irá auxiliar nossa implementação.

O foco deste artigo é mais prático, mostrando a implementação de autenticação com JWT. Caso queira se aprofundar na anatomia do JWT sugiro ler o excelente artigo The Anatomy of a JSON Web Token.

Preparando o Ambiente

Realize os procedimentos abaixo dentro do diretório raiz da aplicação.

Vamos usar o composer para instalar o silex e o php-jwt. Baixe a última versão do composer através do link https://getcomposer.org/composer.phar.

Instalando Silex:

$ php composer.phar require silex/silex "~1.3"

Instalando php-jwt:

$ php composer.phar require firebase/php-jwt

Será criado o diretório vendor/ e os arquivos composer.json e composer.lock no diretório corrente de sua aplicação.

Criando um Wrapper para a classe JWT

Vamos criar uma classe em nosso projeto para isolar os comandos para geração e decodificação do JWT coma finalidade de deixar o código mais coeso. Vamos chamar a classe de JWTWrapper e deve estar definida no caminho src/JWTWrapper.php.

<?php
use \Firebase\JWT\JWT;

/**
 * Gerenciamento de tokens JWT
 */
class JWTWrapper
{
    const KEY = '7Fsxc2A865V6'; // chave

    /**
     * Geracao de um novo token jwt
     */
    public static function encode(array $options)
    {
        $issuedAt = time();
        $expire = $issuedAt + $options['expiration_sec']; // tempo de expiracao do token

        $tokenParam = [
            'iat'  => $issuedAt,            // timestamp de geracao do token
            'iss'  => $options['iss'],      // dominio, pode ser usado para descartar tokens de outros dominios
            'exp'  => $expire,              // expiracao do token
            'nbf'  => $issuedAt - 1,        // token nao eh valido Antes de
            'data' => $options['userdata'], // Dados do usuario logado
        ];

        return JWT::encode($tokenParam, self::KEY);
    }

    /**
     * Decodifica token jwt
     */
    public static function decode($jwt)
    {
        return JWT::decode($jwt, self::KEY, ['HS256']);
    }
}

Veja que definimos a chave para o JWT como uma constante na linha 9. Modifique o valor da constante com que achar mais apropriado.

O método encode(), definido na linha 14, deve receber um array de parâmetros com as seguintes opções:

  • expiration_sec – tempo em segundos para a expiração do jwt
  • iss – domínio que pode ser usado para validar a precedencia do jwt
  • userdata – dados de aplicação que deseja armazenar no jwt. Ex: id do usuário, permissões de acesso, etc.

Usaremos esses métodos mais adiante na implementação das rotas no silex.

Preparando o Silex

Utilizaremos a ajuda do Silex para criarmos as rotas de autenticação e acesso logado de nossa aplicação. Antes de continuar vamos preparar o arquivo web/index.php que servirá como front-controller da aplicação.

Por enquanto o script não tem nenhuma funcionalidade, é apenas uma estrutura mínima para o Silex funcionar. Para o silex funcionar corretamente em seu servidor web (apache, nginx, etc.) siga as instruções descritas no link http://silex.sensiolabs.org/doc/master/web_servers.html

// web/index.php

<?php
// timezone
date_default_timezone_set('America/Sao_Paulo');

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../src/JWTWrapper.php';

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Silex\Application;

$app = new Silex\Application();
$app->run();

Rota de Autenticação

Nossa primeira rota será responsável por autenticar o usuário. Após a validação de usuário e senha iremos gerar o JWT e retorná-lo para o cliente em uma estrutura JSON.

Segue a implementação da rota:

// Autenticacao
$app->post('/auth', function (Request $request) use ($app) {
    $dados = json_decode($request->getContent(), true);

    if($dados['user'] == 'foo' && $dados['pass'] == 'bar') {
        // autenticacao valida, gerar token
        $jwt = JWTWrapper::encode([
            'expiration_sec' => 3600,
            'iss' => 'douglaspasqua.com',
            'userdata' => [
                'id' => 1,
                'name' => 'Douglas Pasqua'
            ]
        ]);

        return $app->json([
            'login' => 'true',
            'access_token' => $jwt
        ]);
    }

    return $app->json([
        'login' => 'false',
        'message' => 'Login Inválido',
    ]);
});

Veja que a comparação do usuário e senha logado esta em hardcode (linha 5). Basta modificar de acordo com sua necessidade. Não esqueça de parametrizar também os dados do token, linhas 8-14, de acordo com o seu ambiente. O mais importante aqui é demonstrar a geração do JWT.

O comando curl abaixo consome a rota implementada anteriormente: (Transcreva o curl no aplicativo cliente que consumirá a API)

$ curl -X POST -H "Content-type: application/json" \
       -d '{"user": "foo", "pass": "bar"}' \
       http://localhost/auth

Exemplo de retorno da requisição:

{  
 "login":"true",
 "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzA2MDczMDAsImlzcyI6ImRvdWdsYXNwYXNxdWEuY29tIiwiZXhwIjoxNDcwNjEwOTAwLCJuYmYiOjE0NzA2MDcyOTksImRhdGEiOnsiaWQiOjEsIm5hbWUiOiJEb3VnbGFzIFBhc3F1YSJ9fQ.WuT3TRLqUkzOgDdEr1YiQdXhz0OvwMDTzYpeKDDFDAY"
}

Após recebido o jwt, o cliente deverá usá-lo nas requisições subsequentes caso queira acessar recursos protegidos por autenticação. Veremos mais adiante como proceder.

Controle de Acesso

Vamos usar o middleware before() do silex para controlar acesso de rotas que podem ser acessadas somente caso o usuário esteja autenticado, ou seja, que ele esteja informando o jwt:

// verificar autenticacao
$app->before(function(Request $request, Application $app) {
    $route = $request->get('_route');

    if($route != 'POST_auth') {
        $authorization = $request->headers->get("Authorization");
        list($jwt) = sscanf($authorization, 'Bearer %s');

        if($jwt) {
            try {
                $app['jwt'] = JWTWrapper::decode($jwt);
            } catch(Exception $ex) {
                // nao foi possivel decodificar o token jwt
                return new Response('Acesso nao autorizado', 400);
            }

        } else {
            // nao foi possivel extrair token do header Authorization
            return new Response('Token nao informado', 400);
        }
    }
});

Como podemos ver na linha 5, qualquer rota diferente de POST em /auth será verificado se o cliente esta informando o jwt. Caso queira deixar alguma outra rota em aberto é neste ponto que deve modificar.

O middleware garante que o jwt foi informado, que seja válido e que não tenha expirado.

Estando tudo certo com o JWT ele é decodificado e seus dados estarão disponíveis para a rota sendo processada através de $app[‘jwt’].

Para quem não conhece, o método before() é uma trigger disparada antes do processamento de cada requisição enviada para o silex permitindo-nos modificar seu comportamento. Caso queira aprender mais sobre middleware no silex, acesse http://silex.sensiolabs.org/doc/master/middlewares.html

Testando rotas protegidas

Vamos criar uma rota simples no silex somente para ilustrar que o acesso deverá ser possível somente caso o usuário tenha informando o JWT.

// rota deve ser acessada somente por usuario autorizado com jwt
$app->get('/home', function(Application $app) {
    return new Response ('Olá '. $app['jwt']->data->name);
});

A rota é bem simples, ela exibe na tela o nome do usuário “logado”. O nome do usuário é obtido a partir do JWT decodificado e presente em $app[‘jwt’] como vimos anteriormente.

O JWT deve ser informado no header HTTP Authorization da requisição. Segue o exemplo do comando curl que faz uma requisição válida na rota protegida /home usando o JWT que geramos anteriormente no momento da autenticação:

curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzA2MDczMDAsImlzcyI6ImRvdWdsYXNwYXNxdWEuY29tIiwiZXhwIjoxNDcwNjEwOTAwLCJuYmYiOjE0NzA2MDcyOTksImRhdGEiOnsiaWQiOjEsIm5hbWUiOiJEb3VnbGFzIFBhc3F1YSJ9fQ.WuT3TRLqUkzOgDdEr1YiQdXhz0OvwMDTzYpeKDDFDAY" \
     http://localhost/home

Retorno esperado:

Olá Douglas Pasqua

Juntando Tudo

Finalizamos nossa mini-aplicação por aqui. Para facilitar a sua implementação desta solução segue abaixo a listagem completa e final do arquivo web/index.php:

<?php
// timezone
date_default_timezone_set('America/Sao_Paulo');

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../src/JWTWrapper.php';

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Silex\Application;

$app = new Silex\Application();

$app->get('/teste', function() use ($app) {

    $jwt = JWTWrapper::encode([
        'expiration_sec' => 3600,
        'iss' => 'douglaspasqua.com',        
        'userdata' => [
            'id' => 1,
            'name' => 'Douglas Pasqua'
        ]
    ]);

    $data = JWTWrapper::decode($jwt);
    print_r($data);

    return $jwt;
});

// Autenticacao
$app->post('/auth', function (Request $request) use ($app) {
    $dados = json_decode($request->getContent(), true);

    if($dados['user'] == 'foo' && $dados['pass'] == 'bar') {
        // autenticacao valida, gerar token
        $jwt = JWTWrapper::encode([
            'expiration_sec' => 3600,
            'iss' => 'douglaspasqua.com',        
            'userdata' => [
                'id' => 1,
                'name' => 'Douglas Pasqua'
            ]
        ]);

        return $app->json([
            'login' => 'true',
            'access_token' => $jwt
        ]);
    }

    return $app->json([
        'login' => 'false',
        'message' => 'Login Inválido',
    ]);
});

// verificar autenticacao
$app->before(function(Request $request, Application $app) {
    $route = $request->get('_route');

    if($route != 'POST_auth') {
        $authorization = $request->headers->get("Authorization");
        list($jwt) = sscanf($authorization, 'Bearer %s');

        if($jwt) {
            try {
                $app['jwt'] = JWTWrapper::decode($jwt);
            } catch(Exception $ex) {
                // nao foi possivel decodificar o token jwt
                return new Response('Acesso nao autorizado', 400);
            }
    
        } else {
            // nao foi possivel extrair token do header Authorization
            return new Response('Token nao informado', 400);
        }
    }
});

// rota deve ser acessada somente por usuario autorizado com jwt
$app->get('/home', function(Application $app) {
    return new Response ('Olá '. $app['jwt']->data->name);
});

$app->run();

A estrutura final deste mini-projeto ficou da seguintes forma:

├── composer.json
├── composer.lock
├── vendor
│   └── ...
└── src
│   └── JWTWrapper.php
└── web
    └── index.php

Considerações Finais

O projeto construído neste artigo pode servir como um “esqueleto” para um projeto de API REST seu que precisa de autenticação e autorização. Você pode melhora-lo de várias formas, incluíndo Namespace, validando o usuário, consultando banco de dados, colocando os controllers em arquivos separados, etc.
Espero que lhe seja útil. Até a próxima.

Comments

  1. Reply

  2. Reply

  3. By Diego

    Reply

Deixe um comentário

Follow

Get every new post on this blog delivered to your Inbox.

Join other followers: