Plugin API


1Présentation

Temma peut facilement être utilisé pour créer des API web (aussi appelés webservices) de type REST.

Fondamentalement, les API web sont gérées par des contrôleurs, qui reçoivent des données envoyées en paramètres GET ou POST, et qui retournent des données au format JSON.

Ce plugin sert à :

  • gérer l'authentification en utilisant des paires de clés publique/privée (voir plus bas) ;
  • définir la vue JSON (pour ne pas avoir besoin de la définir dans chaque action) ;
  • gérer différentes versions de l'API, qui peuvent éventuellement être utilisable simultanément.

2Authentification

Certaines API peuvent être librement accessibles, sans vérification des droits d'accès. Mais la plupart du temps, l'utilisateur qui se connecte à l'API doit s'authentifier, pour vérifier qu'il a bien le droit d'accéder aux fonctionnalités.

Le pluggin API utilise une authentification HTTP Basic, qui repose sur deux clés, une clé "publique" et une clé "privée", qui sont envoyées respectivement comme identifiant et mot de passe à chaque requête sur l'API.

Il est important que le site soit sécurisé par SSL, pour que les clés ne soient pas visibles par quelqu'un qui écouterait les échanges réseau. Les certificats SSL sont aujourd'hui faciles à mettre en place (et gratuits) grâce à la solution Let's Encrypt.

Le choix a été fait de ne pas se baser sur des jetons d'authentification (tels que JWT) pour deux raisons :

  • L'authentification HTTP Basic est la plus simple à implémenter (autant côté client que serveur). Si elle a une mauvaise réputation du point de vue de la sécurité des échanges, cela date de l'époque où la plupart des communications web n'étaient pas sécurisées par SSL. Avec des sites en HTTPS, il n'y a pas de souci de sécurité.
    Les jetons, quant à eux, nécessitent au minimum une connexion supplémentaire pour la génération du token, lors de laquelle les identifiants (clé publique/privée ou login/mot de passe) sont envoyés au serveur. Cette cinématique n'est pas plus sécurisée, car les identifiants circulent quand même sur le réseau : un hacker qui écouterait les verrait passer (directement ou sous une forme "digest" qui resterait exploitable), même s'ils ne circulent pas à chaque requête. De plus, cela impose pour le client une gestion complexe de la durée d'expiration des jetons, avec des mécanismes de réessais.
  • Les jetons sont utilisés pour ne pas avoir besoin de revérifier les droits d'accès de l'utilisateur à chaque requête, limitant ainsi les accès à la base de données. Si cela semblait initialement être une bonne idée, de nombreuses applications nécessitent une gestion précise des droits d'accès ; lorsqu'un utilisateur se voit retiré un accès, il n'est pas acceptable qu'il puisse continuer à utiliser l'API jusqu'à ce que son jeton expire. Face à ce genre de situation, la durée de vie des jetons est souvent réduite jusqu'à ce que l'authentification soit revérifiée quasiment à chaque requête. On se retrouve ainsi avec le pire de chaque monde : des accès fréquent à la base de données, mais aussi une cinématique complexe de connexion.

3URLs et contrôleurs

Ce plugin est prévu pour gérer des URLs du type /v[version]/[contrôleur]/[action].
Exemples :

  • /v1/ : appellera l'action par défaut du contrôleur par défaut
  • /v1/articles : appellera l'action par défaut du contrôleur "\v1\Articles"
  • /v2/user/list : appellera l'action "list()" du contrôleur "\v2\User"
  • /v3/user/remove/123 : appellera l'action "remove()" du contrôleur "\v3\User", en lui fournissant "123" en paramètre

Les contrôleurs doivent donc être placés dans le namespace correspondant au numéro de version de l'API.


4Base de données

Ce helper nécessite deux tables en base de données, l'une nommée User (contenant les informations sur les utilisateurs), l'autre nommée ApiKey (contenant les couples de clés publique/privée).

La table User doit contenir les champs suivants :

  • id (string) : Clé primaire.
  • date_creation (datetime) : Date de création de l'utilisateur.
  • date_last_login (datetime) : Date de dernière authentification de l'utilisateur.
  • date_last_access (datetime) : Date de dernier accès de l'utilisateur.
  • email (string) : Adresse mail de l'utilisateur.
  • name (string) : Nom de l'utilisateur (champ obligatoire, même si vous ne l'utilisez pas).
  • roles (set) : Rôles assignés à l'utilisateur.
  • services (set) : Services auxquels l'utilisateur a accès.

La table ApiKey doit contenir les champs suivants :

  • public_key (string) : Clé publique de l'utilisateur.
  • private_key (string) : Empreinte numérique (algorithme SHA-256) de la clé privée de l'utilisateur.
  • name (string) : Nom de la paire de clés (éventuellement défini par l'utilisateur).
  • user_id (int) : Clé étrangère vers l'utilisateur.

Voici un exemple de requête de création de ces tables, dont vous devez personnaliser les champs roles et services de la table 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('admin', 'writer', 'reviewer'), -- à personnaliser
    services         SET('articles', 'news', 'images'), -- à personnaliser
    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   CHAR(64) 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;

5Configuration

5.1Configuration du plugin

Ce plugin doit être l'un des tous premiers pré-plugins dans la chaîne d'exécution.

Il faudra donc ajouter une directive dans le fichier etc/temma.php (voir la documentation de la configuration) :

[
    'plugins' => [
        // liste des pré-plugins
        '_pre' => [
            '\Temma\Plugins\Api'
        ]
    ]
]

5.2Configuration de la base de données

Par défaut, les noms des tables et des champs sont ceux indiqué plus haut, et ces tables doivent être placées dans la base ouverte par la connexion nommée db dans la directive de configuration dataSources. Vous avez la possibilité d'indiquer le nom de la base, les nom des tables, ainsi que les noms des champs, s'ils sont différents des noms par défaut :

[
    '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'
            ]
        ]
    ]
]
  • Lignes 4 à 12 : Configuration de la table des utilisateurs.
    • Ligne 5 : Nom de la base de données qui contient la table.
    • Ligne 6 : Nom de la table.
    • Ligne 7 : Nom du champ contenant la clé primaire.
    • Ligne 8 : Nom du champ contenant l'adresse mail de l'utilisateur.
    • Ligne 9 : Nom du champ contenant le nom de l'utilisateur.
    • Ligne 10 : Nom du champ contenant les rôles de l'utilisateur.
    • Ligne 11 : Nom du champ contenant les services auxquels l'utilisateur a accès.
  • Lignes 13 à 19 : Configuration de la table contenant les clés publique/privée.
    • Ligne 14 : Nom de la base de données qui contient la table.
    • Ligne 15 : Nom de la table.
    • Ligne 16 : Nom du champ contenant l'empreinte de la clé publique.
    • Ligne 17 : Nom du champ contenant la clé privée.
    • Ligne 18 : Nom du champ contenant l'identifiant de l'utilisateur.

Si la table des utilisateurs contient d'autres champs que vous souhaitez récupérer, vous pouvez les ajouter à la liste. Si les champs ne doivent pas être renommés, mettez le même nom en clé et en valeur :

[
    'x-security' => [
        'auth' => [
            'userData' => [
                'email'    => 'user_mail',
                'name'     => 'user_name',
                'org'      => 'user_organization',
                'birthday' => 'birthday'
            ]
        ]
    ]
]
  • Ligne 5 : Définition du nom du champ (user_mail) qui contient l'adresse mail de l'utilisateur.
  • Ligne 6 : Définition du nom du champ (user_name) qui contient le nom de l'utilisateur.
  • Ligne 7 : Ajout d'un champ organization, renommé en org.
  • Ligne 8 : Ajout d'un champ birthday, qui n'est pas renommé.

5.3Configuration de la base de données par DAO

Plutôt que d'utiliser le fichier de configuration (etc/temma.php) pour définir les paramètres concernant la base de données, il est possible d'utiliser des DAO personnalisées.

Exemple de configuration :

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

Exemple de DAO :

namespace MyApp;

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

6Attribut Auth

Temma fournit l'attribut Auth, qui permet de spécifier si un contrôleur ou une action ne peut être accédé que par un utilisateur authentifié (ou non). Cet attribut se basant que la présence d'une variable de template currentUser (ainsi que sur ses clés roles et services), il est pleinement compatible avec ce plugin.

Par exemple, il est possible d'indiquer qu'un contrôleur d'API n'est accessible qu'aux utilisateurs authentifiés :

namespace v1;

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

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

Il est possible d'indiquer que certaines actions d'un contrôleur sont en accès libre, alors que d'autres nécessitent d'être authentifié, ou de ne pas être authentifié :

namespace v1;

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

class Media extends \Temma\Web\Controller {
    // action accessible à tous
    public function list() { }

    // action accessible uniquement aux
    // utilisateurs authentifiés
    #[TµAuth]
    public function get() { }

    // action accessible uniquement aux utilisateurs
    // non authentifiés
    #[TµAuth(authenticated: false)}
    public function remove() { }
}

Il est aussi possible de faire en sorte que des contrôleurs ou des actions ne soient accessibles qu'aux utilisateurs ayant un rôle spécifique ou ayant accès à un service particulier :

namespace v2;

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

class Article extends \Temma\Web\Controller {
    // accès autorisé uniquement aux utilisateurs
    // ayant le rôle "manager"
    #[TµAuth('manager')]
    public function remove($articleId) { }

    // autorisé uniquement aux utilisateurs
    // ayant accès au service "images" ou au
    // service "text"
    #[TµAuth(service: ['images', 'text'])]
    public function list() { }
}

Référez-vous à la documentation de l'attribut pour voir toutes ses capacités.


7Génération de clés

Vous devez créer les clés publiques et privées de vos utilisateurs pour qu'ils puissent se connecter à votre API. Habituellement, cela se fait via une interface web par laquelle les utilisateurs peuvent demander la génération d'un nouveau couple de clés, ainsi que révoquer les anciennes clés.

Le plugin offre la méthode statique generateKeys(), qui retourne un tableau associatif contenant les clés public et private. La clé publique est une chaîne de 32 caractères de long encodée en base 71 (voir le helper BaseConvert), alors que la clé privée est une chaîne de 64 caractères de long (elle aussi encodée en base 71).

Vous pouvez transmettre ces deux chaînes à l'utilisateur. En base de données, vous devez enregistrer la clé publique en clair ; par contre, c'est une empreinte numérique (utilisant l'algorithme SHA-256) de la clé privée qui doit être enregistrée.
Cela implique que l'utilisateur doit copier la clé privée au moment où elle lui est affichée, et qu'il ne pourra plus la récupérer par la suite (il faut donc lui donner la possibilité de regénérer de nouvelles clés si nécessaire).

Exemple de code :

// génération des clés
$keys = \Temma\Plugins\Api::generateKeys();

// récupération des clés qui seront affichées à l'utilisateur
$publicKey = $keys['public'];
$privateKey = $keys['private'];

// calcul de l'empreinte numérique de la clé privée
$hashedPrivateKey = hash('sha256', $privateKey);

// enregistrement des clés en base de données
// en utilisant une DAO préalablement créée
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
]);

Il est possible de donner un nom à chaque paire de clés, pour permettre aux utilisateurs de les gérer (par exemple générer des clés pour différents usages, et avoir la possiblité d'effacer certaines clés spécifiques). Si aucun nom n'est fourni, le nom par défaut sera utilisé.
Pour nommer la paire de clé, il suffit d'ajouter le nom au moment où la paire est enregistrée en base de données :

// enregistrement de la paire de clés, avec un nom associé
$apiKeyDao->create([
    'public_key'  => $publicKey,
    'private_key' => $hashedPrivateKey,
    'user_id'     => $userId,
    'name'        => $keyName,
]);