Plugin API


1Apresentação

O Temma pode ser facilmente usado para criar APIs web (também conhecidas como webservices) do tipo REST.

Basicamente, as APIs web são gerenciadas por controladores, que recebem dados enviados como parâmetros GET ou POST, e retornam dados no formato JSON.

Este plugin é usado para:

  • gerenciar a autenticação usando pares de chaves pública/privada (veja abaixo);
  • definir a visão JSON (para que você não precise defini-la em cada ação);
  • gerenciar diferentes versões da API, que podem ser usadas simultaneamente.

2Autenticação

2.1Princípios

Algumas APIs podem ser livremente acessíveis, sem nenhuma verificação de direitos de acesso. Mas, na maioria das vezes, o usuário que se conecta à API deve se autenticar, para verificar se tem o direito de acessar as funcionalidades.

O plugin da API usa a autenticação HTTP Basic, baseada em duas chaves, uma "pública" e uma "privada", que são enviadas a cada requisição à API, respectivamente como login e senha.

É importante que o site esteja protegido por SSL, para que as chaves não fiquem visíveis a quem estiver espionando as trocas de rede. Os certificados SSL agora são fáceis de configurar (e gratuitos), graças à solução Let's Encrypt.


2.2Por que uma autenticação HTTP Basic?

A escolha foi não depender de tokens de autenticação (como o JWT), por dois motivos.

A autenticação HTTP Basic é a mais simples de implementar (tanto no lado do cliente quanto no do servidor). Se ela tem uma má reputação do ponto de vista da segurança das trocas, isso remonta à época em que a maioria das comunicações web não era protegida por SSL. Com sites HTTPS, não há problemas de segurança.
Os tokens, por outro lado, exigem pelo menos uma conexão adicional para gerar o token, durante a qual os identificadores (chave pública/privada ou login/senha) são enviados ao servidor. Essa dinâmica não é mais segura, já que os identificadores continuam circulando na rede: um hacker que esteja espionando os veria passar (diretamente ou em uma forma de "digest" que ainda poderia ser explorada), mesmo que não circulem a cada requisição. Além disso, isso impõe ao cliente uma gestão complexa dos prazos de expiração dos tokens, com mecanismos de nova tentativa.

Os tokens são usados para evitar a necessidade de reverificar os direitos de acesso do usuário a cada requisição, limitando assim o acesso ao banco de dados. Embora isso inicialmente parecesse uma boa ideia, muitas aplicações exigem uma gestão precisa dos direitos de acesso; quando o acesso de um usuário é revogado, não é aceitável que ele continue usando a API até que seu token expire. Nessas situações, a vida útil dos tokens costuma ser reduzida a tal ponto que a autenticação precisa ser reverificada em quase todas as requisições. O resultado é o pior dos dois mundos: acesso frequente ao banco de dados, mas também uma dinâmica de conexão complexa.


3URLs e controladores

Este plugin foi projetado para lidar com URLs como /v[version]/[controller]/[action].
Exemplos:

  • /v1/: chamará a ação padrão do controlador padrão
  • /v1/articles: chamará a ação padrão do controlador "\v1\Articles"
  • /v2/user/list: chama a ação "list()" do controlador "\v2\User"
  • /v3/user/remove/123: chama a ação "remove()" do controlador "\v3\User", fornecendo "123" como parâmetro

Os controladores devem, portanto, ser colocados no namespace correspondente ao número de versão da API.


4Banco de dados

Este helper requer duas tabelas no banco de dados, uma chamada User (contendo informações do usuário), a outra chamada ApiKey (contendo os pares de chaves pública/privada).

A tabela User deve conter os seguintes campos:

  • id (int): chave primária.
  • date_creation (datetime): data de criação do usuário.
  • date_last_login (datetime): data da última autenticação do usuário.
  • date_last_access (datetime): data do último acesso do usuário.
  • email (string): endereço de e-mail do usuário.
  • name (string): nome do usuário (campo obrigatório, mesmo que você não o utilize).
  • roles (set): papéis atribuídos ao usuário.
  • services (set): serviços aos quais o usuário tem acesso.

A tabela ApiKey deve conter os seguintes campos:

  • public_key (string): chave pública do usuário.
  • private_key (string): hash (algoritmo blowfish) da chave privada do usuário.
  • name (string): nome do par de chaves (definido pelo usuário, se necessário).
  • user_id (int): chave estrangeira para o usuário.

Aqui está um exemplo de consulta para criar essas tabelas, na qual você precisa personalizar os campos roles e services da tabela User:

CREATE TABLE User (
    id               INT UNSIGNED NOT NULL AUTO_INCREMENT,
    date_creation    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    date_last_login  DATETIME,
    date_last_access DATETIME,
    email            TINYTEXT CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
    name             TINYTEXT,
    roles            SET('writer', 'reviewer', 'validator'), -- deve ser personalizado
    services         SET('articles', 'news', 'images'), -- deve ser personalizado
    PRIMARY KEY (id),
    UNIQUE INDEX email (email(255))
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE TABLE ApiKey (
    public_key    CHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
    private_key   TINYTEXT CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
    name          TINYTEXT NOT NULL DEFAULT ('Default'),
    user_id       INT UNSIGNED NOT NULL,
    PRIMARY KEY (public_key),
    FOREIGN KEY (user_id) REFERENCES User (id) ON DELETE CASCADE
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

5Configuração

5.1Configuração do plugin

Este plugin deve ser um dos primeiríssimos pré-plugins na cadeia de execução.

Portanto, uma diretiva deve ser adicionada ao arquivo etc/temma.php (veja a documentação de configuração):

<?php

return [
    'plugins' => [
        // lista de pré-plugins
        '_pre' => [
            '\Temma\Plugins\Api'
        ]
    ]
];

5.2Configuração do banco de dados

Por padrão, os nomes de tabelas e campos são os indicados acima, e essas tabelas devem estar no banco de dados aberto pela conexão chamada db na diretiva de configuração dataSources. Você tem a opção de especificar o nome do banco de dados, os nomes das tabelas e os nomes dos campos, caso sejam diferentes do padrão:

<?php

return [
    'x-security' => [
        'auth' => [
            'userData' => [
                'base'     => 'auth_app',
                'table'    => 'tUser',
                'id'       => 'user_id',
                'email'    => 'user_mail',
                'name'     => 'user_name',
                'roles'    => 'user_roles',
                'services' => 'user_services',
            ],
            'apiKeyData' => [
                'base'        => 'auth_app',
                'table'       => 'tKeys',
                'public_key'  => 'public_string',
                'private_key' => 'secret_string',
                'user_id'     => 'identifier_user',
            ]
        ]
    ]
];
  • Linhas 6 a 14: configuração da tabela de usuário.
    • Linha 7: nome do banco de dados que contém a tabela.
    • Linha 8: nome da tabela.
    • Linha 9: nome do campo que contém a chave primária.
    • Linha 10: nome do campo que contém o endereço de e-mail do usuário.
    • Linha 11: nome do usuário.
    • Linha 12: nome do campo que contém os papéis do usuário.
    • Linha 13: nome do campo que contém os serviços aos quais o usuário tem acesso.
  • Linhas 15 a 21: configuração da tabela de chaves de API.
    • Linha 16: nome do banco de dados que contém a tabela.
    • Linha 17: nome da tabela
    • Linha 18: nome do campo que contém a chave pública.
    • Linha 19: nome do campo que contém o hash da chave privada.
    • Linha 20: nome do campo que contém o identificador do usuário.

Se a tabela de usuários contiver outros campos que você deseja recuperar, você pode adicioná-los à lista. Se os campos não precisarem ser renomeados, defina a chave e o valor com o mesmo nome:

<?php

return [
    'x-security' => [
        'auth' => [
            'userData' => [
                'email'    => 'user_mail',
                'name'     => 'user_name',
                'org'      => 'user_organization',
                'birthday' => 'birthday',
            ]
        ]
    ]
];
  • Linha 7: nome do campo (user_mail) que contém o endereço de e-mail do usuário.
  • Linha 8: nome do campo (user_name) que contém o nome do usuário.
  • Linha 9: campo user_organization adicionado, renomeado para org.
  • Linha 10: campo birthday adicionado, sem renomeação.

5.3Configuração do banco de dados usando DAO

Em vez de usar o arquivo de configuração (etc/temma.php) para definir os parâmetros do banco de dados, é possível usar DAOs personalizados.

Exemplo de configuração:

<?php

return [
    'x-security' => [
        'auth' => [
            'userDao'   => '\MyApp\UserDao',
            'apiKeyDao' => '\MyApp\ApiKeyDao',
        ]
    ]
];

Exemplo de DAO:

namespace MyApp;

class UserDao extends \Temma\Dao\Dao {
    protected $_tableName = 'tUser';
    protected $_idField = 'user_id';
    protected $_fields = [
        'user_id'       => 'id',
        'user_mail'     => 'email',
        'user_roles'    => 'roles',
        'user_services' => 'services',
        'user_name'     => 'name',
        'organization',
        'birthday',
    ];
}

6Atributo Auth

O Temma fornece o atributo Auth, que permite especificar se um controlador ou ação só pode ser acessado por um usuário autenticado (ou não). Como esse atributo se baseia na presença de uma variável de template currentUser (assim como suas chaves roles e services), ele é totalmente compatível com este plugin.

Por exemplo, você pode especificar que um controlador só é acessível a usuários autenticados:

namespace v1;

use \Temma\Attributes\Auth as TµAuth;

#[TµAuth]
class User extends \Temma\Web\Controller {
    // ...
}

É possível especificar que determinadas ações de um controlador sejam livremente acessíveis, enquanto outras exigem autenticação, e outras sejam acessíveis apenas a usuários não autenticados:

namespace v1;

use \Temma\Attributes\Auth as TµAuth;

class Media extends \Temma\Web\Controller {
    // ação acessível a todos
    public function list() { }

    // ação acessível apenas a
    // usuários autenticados
    #[TµAuth]
    public function get() { }

    // ação acessível apenas a
    // usuários não autenticados
    #[TµAuth(authenticated: false)]
    public function remove() { }
}

Também é possível restringir o acesso a controladores ou ações a usuários com um papel específico ou acesso a um serviço específico:

namespace v2;

use \Temma\Attributes\Auth as TµAuth;

class Article extends \Temma\Web\Controller {
    // acesso autorizado apenas a usuários com
    // o papel "manager"
    #[TµAuth('manager')]
    public function remove($articleId) { }

    // autorizado apenas a usuários com acesso
    // aos serviços "images" ou "text"
    #[TµAuth(service: ['images', 'text'])]
    public function list() { }
}

Consulte a documentação do atributo para ver todas as suas possibilidades.


7Geração de chaves

Você precisa criar chaves públicas e privadas para seus usuários, para que eles possam se conectar à sua API. Geralmente, isso é feito por meio de uma interface web, através da qual os usuários podem solicitar a geração de um novo par de chaves, bem como revogar chaves antigas.

O plugin oferece o método estático generateKeys(), que retorna um array associativo contendo as chaves public e private. A chave pública é uma string de 32 caracteres codificada em base 71 (veja o helper BaseConvert), enquanto a chave privada é uma string de 64 caracteres (também codificada em base 71).

Ambas as strings podem ser transmitidas ao usuário. No banco de dados, você deve armazenar a chave pública em texto simples; para a chave privada, uma versão com hash (usando a função password_hash() do PHP) da chave deve ser armazenada.
Isso significa que o usuário deve copiar a chave privada quando ela for exibida a ele, e que não poderá recuperá-la posteriormente (portanto, ele deve ter a opção de gerar novas chaves, se necessário).

Código de exemplo:

// geração das chaves
$keys = \Temma\Plugins\Api::generateKeys();

// recupera as chaves a serem exibidas ao usuário
$publicKey = $keys['public'];
$privateKey = $keys['private'];

// calcula o hash da chave privada
$hashedPrivateKey = password_hash($privateKey, PASSWORD_BCRYPT);

// armazena as chaves no banco de dados usando um DAO
// criado previamente
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
]);

É possível dar um nome a cada par de chaves, para que os usuários possam gerenciá-los (por exemplo, gerar chaves para usos diferentes, e ter a opção de excluir chaves específicas). Se nenhum nome for informado, o nome padrão será usado.
Para nomear o par de chaves, basta adicionar o nome ao registrar o par no banco de dados:

// registro do par de chaves, com um nome associado
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
    'name'        => $keyName,
]);