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
2.1Principes
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 plugin API utilise une authentification HTTP Basic, qui repose sur deux clés, une clé "publique" et une clé "privée", qui sont envoyées à chaque requête sur l'API, respectivement comme identifiant et mot de passe.
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.
2.2Pourquoi une authentification HTTP Basic ?
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 blowfish) 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 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;
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) :
<?php
return [
'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 :
<?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'
]
]
]
];
-
Lignes 6 à 14 : Configuration de la table des utilisateurs.
- Ligne 7 : Nom de la base de données qui contient la table.
- Ligne 8 : Nom de la table.
- Ligne 9 : Nom du champ contenant la clé primaire.
- Ligne 10 : Nom du champ contenant l'adresse mail de l'utilisateur.
- Ligne 11 : Nom du champ contenant le nom de l'utilisateur.
- Ligne 12 : Nom du champ contenant les rôles de l'utilisateur.
- Ligne 13 : Nom du champ contenant les services auxquels l'utilisateur a accès.
-
Lignes 15 à 21 : Configuration de la table contenant les clés publique/privée.
- Ligne 16 : Nom de la base de données qui contient la table.
- Ligne 17 : Nom de la table.
- Ligne 18 : Nom du champ contenant l'empreinte de la clé publique.
- Ligne 19 : 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 :
<?php
return [
'x-security' => [
'auth' => [
'userData' => [
'email' => 'user_mail',
'name' => 'user_name',
'org' => 'user_organization',
'birthday' => 'birthday',
]
]
]
];
- Ligne 7 : Définition du nom du champ (user_mail) qui contient l'adresse mail de l'utilisateur.
- Ligne 8 : Définition du nom du champ (user_name) qui contient le nom de l'utilisateur.
- Ligne 9 : Ajout d'un champ organization, renommé en org.
- Ligne 10 : 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 :
<?php
return [
'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 la fonction PHP
password_hash()) 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 = password_hash($privateKey, PASSWORD_BCRYPT);
// 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,
]);