Plugin API


1Presentación

Temma puede usarse fácilmente para crear APIs web (también conocidas como servicios web) de tipo REST.

Básicamente, las APIs web son gestionadas por controladores, que reciben datos enviados como parámetros GET o POST, y devuelven datos en formato JSON.

Este plugin se usa para:

  • gestionar la autenticación mediante pares de claves pública/privada (ver más abajo);
  • definir la vista JSON (para no tener que definirla en cada acción);
  • gestionar diferentes versiones de la API, que pueden usarse simultáneamente.

2Autenticación

2.1Principios

Algunas APIs pueden ser de acceso libre, sin ninguna verificación de los derechos de acceso. Pero la mayoría de las veces, el usuario que se conecta a la API debe autenticarse, para verificar que tiene derecho a acceder a las funcionalidades.

El plugin API usa la autenticación HTTP Basic, basada en dos claves, una "pública" y otra "privada", que se envían cada vez que se solicita la API, respectivamente como usuario y contraseña.

Es importante que el sitio esté protegido con SSL, para que las claves no sean visibles a quien esté espiando los intercambios de red. Hoy en día, los certificados SSL son fáciles de configurar (y gratuitos), gracias a la solución Let's Encrypt.


2.2¿Por qué una autenticación HTTP Basic?

Se optó por no depender de tokens de autenticación (como JWT) por dos razones.

La autenticación HTTP Basic es la más simple de implementar (tanto en el lado del cliente como en el del servidor). Si tiene mala reputación desde el punto de vista de la seguridad de los intercambios, esto se remonta a la época en que la mayoría de las comunicaciones web no estaban protegidas por SSL. Con sitios HTTPS, no hay problemas de seguridad.
Los tokens, por su parte, requieren al menos una conexión adicional para generar el token, durante la cual los identificadores (clave pública/privada o usuario/contraseña) se envían al servidor. Esta cinemática no es más segura, ya que los identificadores siguen circulando por la red: un pirata informático que espíe los vería pasar (directamente o en una forma de "digest" que aún podría explotarse), aunque no circulen en cada petición. Además, esto impone al cliente una gestión compleja de los plazos de expiración de los tokens, con mecanismos de reintento.

Los tokens se usan para evitar la necesidad de comprobar de nuevo los derechos de acceso del usuario en cada petición, limitando así el acceso a la base de datos. Aunque en un principio parecía una buena idea, muchas aplicaciones requieren una gestión precisa de los derechos de acceso; cuando se retira el acceso a un usuario, no es aceptable que este siga usando la API hasta que expire su token. En tales situaciones, la vida útil de los tokens se reduce a menudo hasta el punto de que la autenticación debe volver a comprobarse casi en cada petición. El resultado es lo peor de ambos mundos: acceso frecuente a la base de datos, pero también una cinemática de conexión compleja.


3URLs y controladores

Este plugin está diseñado para gestionar URLs del tipo /v[version]/[controller]/[action].
Ejemplos:

  • /v1/: llamará a la acción por defecto del controlador por defecto
  • /v1/articles: llamará a la acción por defecto del controlador "\v1\Articles"
  • /v2/user/list: llama a la acción "list()" del controlador "\v2\User"
  • /v3/user/remove/123: llama a la acción "remove()" del controlador "\v3\User", suministrando "123" como parámetro

Los controladores deben, por lo tanto, colocarse en el namespace correspondiente al número de versión de la API.


4Base de datos

Este helper requiere dos tablas de base de datos, una llamada User (que contiene la información del usuario), y la otra llamada ApiKey (que contiene los pares de claves pública/privada).

La tabla User debe contener los siguientes campos:

  • id (int): clave primaria.
  • date_creation (datetime): fecha de creación del usuario.
  • date_last_login (datetime): fecha de la última autenticación del usuario.
  • date_last_access (datetime): fecha del último acceso del usuario.
  • email (string): dirección de correo electrónico del usuario.
  • name (string): nombre del usuario (campo obligatorio, aunque no lo uses).
  • roles (set): roles asignados al usuario.
  • services (set): servicios a los que tiene acceso el usuario.

La tabla ApiKey debe contener los siguientes campos:

  • public_key (string): clave pública del usuario.
  • private_key (string): hash (algoritmo blowfish) de la clave privada del usuario.
  • name (string): nombre del par de claves (definido por el usuario, si es necesario).
  • user_id (int): clave foránea hacia el usuario.

A continuación, un ejemplo de consulta para crear estas tablas, en la que debes personalizar los campos roles y services de la tabla 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'), -- debe personalizarse
    services         SET('articles', 'news', 'images'), -- debe personalizarse
    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;

5Configuración

5.1Configuración del plugin

Este plugin debe ser uno de los primerísimos pre-plugins en la cadena de ejecución.

Por lo tanto, hay que añadir una directiva en el archivo etc/temma.php (ver la documentación de configuración):

<?php

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

5.2Configuración de la base de datos

Por defecto, los nombres de tablas y campos son los indicados más arriba, y estas tablas deben ubicarse en la base de datos abierta por la conexión llamada db en la directiva de configuración dataSources. Tienes la opción de especificar el nombre de la base de datos, los nombres de las tablas y los nombres de los campos, si son distintos de los valores por defecto:

<?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',
            ]
        ]
    ]
];
  • Líneas 6 a 14: configuración de la tabla de usuario.
    • Línea 7: nombre de la base de datos que contiene la tabla.
    • Línea 8: nombre de la tabla.
    • Línea 9: nombre del campo que contiene la clave primaria.
    • Línea 10: nombre del campo que contiene la dirección de correo electrónico del usuario.
    • Línea 11: nombre del usuario.
    • Línea 12: nombre del campo que contiene los roles del usuario.
    • Línea 13: nombre del campo que contiene los servicios a los que tiene acceso el usuario.
  • Líneas 15 a 21: configuración de la tabla de claves de la API.
    • Línea 16: nombre de la base de datos que contiene la tabla.
    • Línea 17: nombre de la tabla
    • Línea 18: nombre del campo que contiene la clave pública.
    • Línea 19: nombre del campo que contiene el hash de la clave privada.
    • Línea 20: nombre del campo que contiene el identificador del usuario.

Si la tabla de usuarios contiene otros campos que deseas recuperar, puedes añadirlos a la lista. Si los campos no deben renombrarse, define la clave y el valor con el mismo nombre:

<?php

return [
    'x-security' => [
        'auth' => [
            'userData' => [
                'email'    => 'user_mail',
                'name'     => 'user_name',
                'org'      => 'user_organization',
                'birthday' => 'birthday',
            ]
        ]
    ]
];
  • Línea 7: nombre del campo (user_mail) que contiene la dirección de correo electrónico del usuario.
  • Línea 8: nombre del campo (user_name) que contiene el nombre del usuario.
  • Línea 9: campo user_organization añadido, renombrado a org.
  • Línea 10: campo birthday añadido, sin renombrar.

5.3Configuración de la base de datos usando DAO

En lugar de usar el archivo de configuración (etc/temma.php) para definir los parámetros de la base de datos, es posible usar DAO personalizados.

Ejemplo de configuración:

<?php

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

Ejemplo 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

Temma proporciona el atributo Auth, que permite especificar si un controlador o una acción solo puede ser accedido por un usuario autenticado (o no). Como este atributo se basa en la presencia de una variable de plantilla currentUser (así como en sus claves roles y services), es totalmente compatible con este plugin.

Por ejemplo, puedes especificar que un controlador solo sea accesible para usuarios autenticados:

namespace v1;

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

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

Es posible especificar que ciertas acciones de un controlador sean de acceso libre, mientras que otras requieran autenticación, y otras sean accesibles únicamente para usuarios no autenticados:

namespace v1;

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

class Media extends \Temma\Web\Controller {
    // acción accesible para todos
    public function list() { }

    // acción accesible solo para
    // usuarios autenticados
    #[TµAuth]
    public function get() { }

    // acción accesible solo para
    // usuarios no autenticados
    #[TµAuth(authenticated: false)]
    public function remove() { }
}

También es posible restringir el acceso a controladores o acciones a usuarios con un rol específico o con acceso a un servicio específico:

namespace v2;

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

class Article extends \Temma\Web\Controller {
    // acceso autorizado solo a usuarios con
    // el rol "manager"
    #[TµAuth('manager')]
    public function remove($articleId) { }

    // autorizado solo a usuarios con acceso
    // a los servicios "images" o "text"
    #[TµAuth(service: ['images', 'text'])]
    public function list() { }
}

Consulta la documentación del atributo para ver todas sus posibilidades.


7Generación de claves

Necesitas crear claves públicas y privadas para tus usuarios, de modo que puedan conectarse a tu API. Por lo general, esto se hace mediante una interfaz web a través de la cual los usuarios pueden solicitar la generación de un nuevo par de claves, así como revocar claves antiguas.

El plugin ofrece el método estático generateKeys(), que devuelve un array asociativo con las claves public y private. La clave pública es una cadena de 32 caracteres codificada en base 71 (ver el helper BaseConvert), mientras que la clave privada es una cadena de 64 caracteres (también codificada en base 71).

Ambas cadenas pueden transmitirse al usuario. En la base de datos, debes almacenar la clave pública en texto plano; para la clave privada, debe almacenarse una versión con hash (usando la función password_hash() de PHP) de la clave.
Esto significa que el usuario debe copiar la clave privada cuando esta se le muestre, y que no podrá recuperarla después (por lo que se le debe dar la opción de regenerar nuevas claves si es necesario).

Código de ejemplo:

// generación de las claves
$keys = \Temma\Plugins\Api::generateKeys();

// recupera las claves que se mostrarán al usuario
$publicKey = $keys['public'];
$privateKey = $keys['private'];

// calcula el hash de la clave privada
$hashedPrivateKey = password_hash($privateKey, PASSWORD_BCRYPT);

// almacena las claves en la base de datos usando un DAO
// creado previamente
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
]);

Se puede asignar un nombre a cada par de claves, para que los usuarios puedan gestionarlos (por ejemplo, generar claves para distintos usos, y tener la opción de eliminar claves específicas). Si no se proporciona ningún nombre, se usará el nombre por defecto.
Para nombrar el par de claves, basta con añadir el nombre al registrar el par en la base de datos:

// registro del par de claves, con un nombre asociado
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
    'name'        => $keyName,
]);