Dans ce cours, nous allons apprendre à utiliser JavaScript afin de communiquer avec une API de manière sécurisée (API Platform Symfony) que l’on va paramétrer. (Ce cours n’a pas pour but d’apprendre à installer et utiliser de 0 API Platform mais juste de savoir et d’apprendre à utiliser des Tokens)
Dans un premier temps, nous allons créer une base de données avec uniquement des users et la paramétrer pour l’utiliser avec notre API. Ensuite, nous créerons 5 fonctions JavaScript :
- une fonction pour s’authentifier
- une fonction POST
- une fonction Get et GetCollection
- une fonction PATCH
- une fonction DELETE
Toutes ces fonctions seront seulement utilisables à l’aide d’un Token pour que ce soit sécurisé, sauf pour les fonctions Get et GetCollection auxquelles nous limiterons les données qu’elles peuvent accéder ainsi que la fonction Post.
Table des matières
Toggle- Paramétrage et sécurisation de l'API
Voici le paramétrage initial de l’API sur l’entité User de manière « non sécurisée ».
–> Pour le moment toutes les fonctions sont accessibles par tout le monde, la seule restriction est que pour le Get et GetCollection la seule chose qui est renvoyée est l’email.
Pour sécuriser les fonctions Patch, Post et Delete nous allons utiliser des Tokens. Pour ce faire, on va installer la bundle jwt voici la commande :
composer req lexik/jwt-authentication-bundle
Une fois installé, il faut juste créer les clés publiques et privées à l’aide de la commande :
php bin/console lexik:jwt:generate-keypair
<– Une fois fait, dans le .env vous trouverez la configuration de vos clés comme le montre la photo à gauche
<– Aussi, vous trouverez dans le répertoire « config/jwt » la clé publique et la clé privée générées par la commande de manière identique à la photo.
Maintenant, nous allons configurer le système de jeton. Dans un premier temps, nous allons attribuer une durée de vie d’une heure au « token » en modifiant le fichier « config/packages/lexik_jwt_authentication.yaml ».
Code :
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600
Nous allons maintenant déclarer la route pour l’authentification. Allez dans le fichier « config/routes.yaml » et ajoutez les lignes suivantes :
Code :
authentication_token:
path: /api/authentication_token
methods: ['POST']
(Cela doit ressembler à la capture d’écran à droite)
En réalisant la commande ci-dessous, nous allons vérifier si la route que nous avons ajoutée a bien été prise en compte. Vous devriez obtenir un résultat similaire à celui de la commande ci-dessous.
php bin/console debug:router
Il nous reste quelques modifications à faire pour que tout soit sécurisé et fonctionnel.
Dans le fichier « config/packages/security.yaml », nous allons mettre en place le pare-feu. Il va falloir ajouter les lignes de code suivantes dans le fichier, à l’endroit approprié :
Voici le code :
<– Le code devra être ajouté en dessous de la ligne « provider », comme le montre la capture d’écran ci-dessus.
Et pour finir, il ne reste plus qu’à modifier le fichier « config/packages/api_platform.yaml » afin de pouvoir se connecter avec son token sur API Platform et à ajouter une certaine ligne dans l’entité User.
Voici le code :
swagger:
api_keys:
apiKey:
name: Authorization
type: header
Dans l’entité User, on ajoute la ligne security: « is_granted(‘ROLE_ADMIN’) or object==user » dans les opérations Patch et Delete, comme ceci :
Voici le résultat sur API Platform. Nous pouvons maintenant créer un Token pour un utilisateur grâce à authentification_token, mais aussi les opérations Delete et Patch ne sont utilisables que si un admin les utilise ou si on veut modifier ou supprimer son propre compte de la base de données.
Nous allons maintenant créer un utilisateur afin de tester tout ce que nous avons fait. Pour ce faire, on utilise l’opération POST comme ceci :
Ici, on peut voir que je crée un utilisateur qui a pour adresse e-mail thoma@gmail.com, qui possède le rôle User, et dont le mot de passe est test.
Après exécution, le serveur nous renvoie le code réponse 201, ce qui signifie que tout a bien été exécuté et que l’utilisateur a été créé. Allons vérifier dans la base de données.
Comme on peut le voir sur la capture de la base de données, l’utilisateur a bien été créé, mais il y a un problème. On peut voir que le mot de passe est clairement lisible, il n’est pas hashé. Nous allons résoudre ce problème.
- chifrer le mot de passe lors du Post
Afin de hasher le mot de passe, nous allons créer un state-processor grâce à la commande ci-dessous que nous allons nommer UserStateProcessor.
php bin/console make:state-processor
Ensuite, dans le fichier « src/State/UserStateProcessor.php », il suffira de remplacer tout le code du fichier par le code suivant en faisant un copier-coller, c’est lui qui s’occupera de hasher le mot de passe lors de la création de l’utilisateur.
<?php namespace App\State;
// https://api-platform.com/docs/core/state-processors/
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\User;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserStateProcessor implements ProcessorInterface {
private $passwordHasher;
private $decorated;
public function __construct(private ProcessorInterface $persistProcessor,
UserPasswordHasherInterface $passwordHasher) {
$this->passwordHasher = $passwordHasher;
}
public function process($data, Operation $operation, array $uriVariables = [], array $context =
[])
{
// call your persistence layer to save $data
if ($data->getPassword()) {
$data->setPassword(
$this->passwordHasher->hashPassword($data, $data->getPassword())
);
}
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
return $result;
}
}
Une fois cela fait, il nous reste plus que deux choses à faire :
- Ajouter une ligne de code dans « config/services.yaml » pour utiliser le state-processor.
- Importer dans l’entité User le state-processor pour hasher le mot de passe à la création d’un compte.
Dans « config/services.yaml » on ajoute le code suivant :
App\State\UserStateProcessor:
bind:
$persistProcessor: ‘@api_platform.doctrine.orm.state.persist_processor’
Ce qui doit donner ceci dans le fichier :
Et enfin, dans l’entité User, on ajoute la ligne use App\State\UserStateProcessor; ainsi que la ligne processor: UserStateProcessor::class dans l’opération POST. Cela doit donner comme sur la capture à droite.
Voilà, nous avons fini de tout paramétrer. Nous allons refaire un test pour voir si tout fonctionne.
Cette fois-ci, on crée un utilisateur test@test.fr qui a aussi pour mot de passe test.
Encore une fois, nous avons un code réponse 201, ce qui indique que l’utilisateur a bien été créé. On voit déjà que le hachage du mot de passe s’est bien effectué sur la capture du dessus, mais pour être sûr, regardons dans la base de données.
Comme on peut le voir, tout fonctionne correctement désormais. Il ne nous reste plus qu’à créer les fonctions en JavaScript qui vont nous permettre d’utiliser notre API.
- Fonction javascript
NB : Toutes les fonctions utilisent une variable API_URL qui contient l’URL de l’API. Vous devez créer cette variable dans votre projet pour que tout fonctionne.
- fonction Get :
Cette fonction permet de récupérer les informations d’un utilisateur en particulier en fonction de son ID
async function getUser(id) {
try {
const reponse = await fetch(`${API_URL}/${id}`);
if (!reponse.ok) {
throw new Error('Erreur : '+reponse.statusText);
}
const data = await reponse.json();
return data;
} catch (error) {
console.error('Erreur lors de la reception : ',error);
throw error;
}
}
- fonction GetCollection :
Cette fonction permet de récupérer les informations de tous les utilisateurs
async function getUsers() {
try {
const reponse = await fetch(`${API_URL}`);
if (!reponse.ok) {
throw new Error('Erreur : '+reponse.statusText);
}
const data = await reponse.json();
return data["hydra:member"];
} catch (error) {
console.error('Erreur lors de la reception : ',error);
throw error;
}
}
- fonction authentification :
Cette fonction permet de récupérer le Token d’un utilisateur grâce à son email et son mot de passe
async function authentifier(email, password){
try{
const data = {
email: `${email}`,
password: `${password}`
};
//création des options de la requête
const options={
method: 'POST', //Méthode HTTP
headers: {
'Content-Type':'application/json',
},
body: JSON.stringify(data)
};
const response = await fetch(`https://s3-4672.nuage-peda.fr/coursAPIToken/public/api/authentication_token`,options);
if(!response.ok){
throw new Error('Erreur :'+ response.statusText);
//on va nous afficher l'erreur qu'on nous a retourner
}
const r = await response.json();
//console.log(r);
return r.token;
}
catch(erreur){
console.error('Erreur lors de lauthentification: ',erreur);
throw erreur;
//throw erreur -> on va franchir l'erreur sans que le reste sois perturber
}
}
- fonction Patch:
Cette fonction permet de modifier l’email d’un utilisateur grâce à son email et son mot de passe en raison du besoin du Token
async function patchUser(id,email,mdp,newEmail) {
try {
const token = await authentifier(email,mdp);
const data = {
"email": `${newEmail}`
};
// Création des options de la requête
const options = {
method: 'PATCH', // Méthode HTTP
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/merge-patch+json',
'Accept': 'application/ld+json',
'Authorization': `Bearer ${token}`,
},
};
const response = await fetch(`${API_URL}/${id}`, options);
if (!response.ok) {
throw new Error('Erreur :' + response.statusText);
}
const r = await response.json();
console.log(r);
return r;
} catch (erreur) {
console.error('Erreur lors de la modification : ', erreur);
throw erreur;
}
}
- fonction Post:
Cette fonction permet de créer un utilisateur
async function postUser(email,mdp) {
try {
const data = {
"email": `${email}`,
"roles": [],
"password": `${mdp}`
};
// Création des options de la requête
const options = {
method: 'POST', // Méthode HTTP
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/ld+json',
'Accept': 'application/ld+json',
},
};
const response = await fetch(`${API_URL}`, options);
if (!response.ok) {
throw new Error('Erreur :' + response.statusText);
}
const r = await response.json();
console.log(r);
return r;
} catch (erreur) {
console.error('Erreur lors de la modification : ', erreur);
throw erreur;
}
}
- fonction Delete:
Cette fonction permet de supprimer un utilisateur à l’aide de son ID et grâce à son email et son mot de passe en raison du besoin du Token
async function deleteUser(id,email,mdp) {
try {
const token = await authentifier(email,mdp);
// Création des options de la requête
const options = {
method: 'DELETE', // Méthode HTTP
headers: {
'Accept': '*/*',
'Authorization': `Bearer ${token}`,
},
};
const response = await fetch(`${API_URL}/${id}`, options);
if (!response.ok) {
throw new Error('Erreur :' + response.statusText);
}
return console.log("ok");
} catch (erreur) {
console.error('Erreur lors de la modification : ', erreur);
throw erreur;
}
}
- fonction recherche (USer) :
Cette fonction permet de rechercher un utilisateur à l’aide de son prénom et de son nom ( vous pouvez la modifier en fonction de ce que vous avez besoin) Attention vous aurez besoin de modifier votre entité en ajoutant le SearchFilter
async function getClientSearch(nom,prenom) {
try {
constreponse=awaitfetch(`${API_URL}?prenom=${prenom}&nom=${nom}`);
if (!reponse.ok) {
thrownewError('Erreur : '+reponse.statusText);
}
constdata=awaitreponse.json();
returndata;
} catch (error) {
console.error('Erreur lors de la reception : ',error);
throwerror;
}
}
async function afficherClientSearch() {
try {
varprenom=document.getElementById("prenom").value;
varnom=document.getElementById("nom").value;
constclients=awaitgetClientSearch(nom,prenom);
varlesCLients=clients["hydra:member"];
varolUsers=document.getElementById("olUsers");
olUsers.innerHTML="";
for(letclientoflesCLients){
varli=document.createElement('li');
li.classList.add("list-group-item" ,"d-flex" ,"justify-content-between","align-items-start")
vardiv=document.createElement('div');
div.classList.add("ms-2", "me-auto")
vardiv2=document.createElement('div');
div2.innerText=client.nom+" "+client.prenom
div2.classList.add("fw-bold")
li.appendChild(div);
div.appendChild(div2);
olUsers.appendChild(li);
}
} catch (error) {
console.log('Erreur :',error)
}
}
document.getElementById("nom").addEventListener("input",afficherClientSearch,false);
document.getElementById("prenom").addEventListener("input",afficherClientSearch,false);
<div class="container-fluid">
<div class="row">
<br class="col-md-4 d-md-block d-none">
<div class="d-flex">
<h1>Users</h1>
<input type="text" id="nom" placeholder="nom"><input type="text" id="prenom" placeholder="prenom">
</div>
<ul class="list-group" id="olUsers"></ul>
</div>
</div>