TP REST AUTH
Objectifs de ce TP : Implémenter l'authentification dans notre API REST.
Documentation : https://restfulapi.net/security-essentials/
Il existe différents mécanismes pour l'authentification basées sur HTTP Authentication Schemes : Basic, Bearer, Digest ou encore OAuth. Pour une API RESTful, il faut :
utiliser https
que l'authentification soit sans état i.e. l'authentication ne doit pas reposer sur les cookies ou les sessions par exemple.
Dans la suite de ce TP, nous allons modifier notre API REST pour utiliser le standard JWT.
Ce TP s'inspire du tutoriel JWT suivant :
https://www.techiediaries.com/php-jwt-authentication-tutorial/
Principe
Certaines requêtes de l'API nécessitent d'être authentifiées pour que le backend exécute ce qui est attendu. Par exemple, une requête de modification d'un utilisateur (PUT /user/{id}
) ne réussira que si l'utilisateur qui envoie cette requête est bien authentifié soit en tant que l'utilisateur id
, soit en tant qu'admin. Si ce n'est pas le cas, une erreur HTTP 401 est retournée par le backend.

Stockage des hashs des mots de passe
Commençons par ajouter un champs USER_PASSWORD
dans la table USER
de la base de données :
Ajouter le champs
USER_PASSWORD
dans la tableusers
de la base de donnéeVous devez maintenir à jour le fichier
sql/createDB.sql
par rapport à votre code PHP. Ce fichier contient le code SQL de création de la base :
SET foreign_key_checks = 0;
drop table if exists USER;
create table USER
(
USER_ID smallint not null AUTO_INCREMENT,
USER_LOGIN varchar(25) not null,
USER_EMAIL varchar(255) not null,
USER_PASSWORD varchar(255),
USER_FIRSTNAME varchar(50) not null,
USER_LASTNAME varchar(50) not null,
USER_ROLE smallint not null,
primary key (USER_ID)
) DEFAULT CHARSET=utf8;
Ajouter des utilisateurs dans la base. Attention, il ne faut jamais stocker des mots de passe en clair dans une base de données. Le champs
USER_PASSWORD
contiendra donc le hash du mot de passe de l'utilisateur. Un algorithme de hachage est asymétrique c'est-à-dire que l'on peut calculer lehash
d'une chaîne de caractères mais on ne peut pas (ou difficilement) retrouver la chaîne initiale à partir d'unhash
. Dans ce projet, lehash
d'un mot de passe sera calculé en utilisant la méthodeBCRYPT
. Vous pouvez calculer le hash d'une chaîne de caractères :en utilisant un site tel que : https://bcrypt-generator.com/
ou directement avec du code PHP :
$password_hash = password_hash($password, PASSWORD_BCRYPT);
Installation d'une bibliothèque PHP JWT
Il existe de nombreuses implémentations de JWT. Attention, toutes ne sont pas équivalentes en terme de fonctionnalités, de respect du standard ou encore de bugs. Dans ce projet vous utiliserez (ce n'est pas une assurance que cette bibliothèque soit la meilleure) : https://github.com/firebase/php-jwt
Téléchager le zip du dépôt et décompressez l'archive dans un nouveau répertoire nommé libs
. Renommez les répertoires afin d'obtenir la hiérarchie suivante :
.
├── api.php
├── libs
│ └── php-jwt
│ ├── composer.json
│ ├── LICENSE
│ ├── README.md
│ └── src
│ ├── BeforeValidException.php
│ ├── ExpiredException.php
│ ├── JWT.php
│ └── SignatureInvalidException.php
...
Implémentation du endpoint login
et génération d'un token JWT
login
et génération d'un token JWTNous allons ajouter un nouvel endpoint POST /login
dans l'API Web :
POST /login
{
"login" : "<login>",
"password" : "<hashed_password>"
}
Ce endpoint nécessite 2 paramètres passés en JSON :
le
<login>
de l'utlisateur qui se connecte<hashed_password>
qui est le hash du mot de passe (la même méthode de hashage doit être utilisée par le back et le front,BCRYPT
dans ce projet).
Si la base contient bien un utilisateur avec l'email <email>
dont le hash du mot de passe est bien <hashed_password>
alors cette requête retournera un code de succès avec un token JWT ou une erreur HTTP. Les hashs doivent être comparés avec la fonction PHP hash_equals
et non pas ==
ou ===
pour éviter des attaques basées sur le temps de comparaison.
Travail à faire :
Ajouter les définitions de 2 nouvelles constantes dans
config.php
:
define( 'JWT_BACKEND_KEY', '6d8HbcZndVGNAbo4Ih1TGaKcuA1y2BKs-I5CmP' );
define( 'JWT_ISSUER', $_SERVER['HTTP_HOST'] . $_SERVER['CONTEXT_PREFIX']);
Ajouter
controller/LoginController.php
avec le code suivant :
<?php
include_once __ROOT_DIR . '/libs/php-jwt/src/BeforeValidException.php';
include_once __ROOT_DIR . '/libs/php-jwt/src/ExpiredException.php';
include_once __ROOT_DIR . '/libs/php-jwt/src/SignatureInvalidException.php';
include_once __ROOT_DIR . '/libs/php-jwt/src/JWT.php';
use \Firebase\JWT\JWT;
class LoginController extends Controller {
public function __construct($name, $request) {
parent::__construct($name, $request);
}
public function processRequest() {
if($this->request->getHttpMethod() !== 'POST')
return Response::errorResponse('{ "message" : "Unsupported endpoint" }' );
$json = $this->request->jsonContent();
if(!isset($json->pwd) || !isset($json->login)) {
$r = new Response(422,"login and pwd fields are mandatory");
$r->send();
}
$user = User::tryLogin($json->login);
if(empty($user) || !hash_equals($json->pwd,$user->password())) {
$r = new Response(422,"wrong credentials");
$r->sendWithLog();
}
// generate json web token
$issued_at = time();
$expiration_time = $issued_at + (60 * 60); // valid for 1 hour
$token = array(
"iat" => $issued_at,
"exp" => $expiration_time,
"iss" => JWT_ISSUER,
"data" => array(
"id" => $user->id(),
"firstname" => $user->firstname(),
"lastname" => $user->lastname(),
"email" => $user->email()
)
);
$jwt = JWT::encode( $token, JWT_BACKEND_KEY );
$jsonResult = json_encode(
array(
"jwt_token" => $jwt
)
);
return Response::okResponse($jsonResult);
}
}
Testez une requête
POST /login
avec des identifiants valides afin d'obtenir un token JWT :
{
"jwt_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MDU2NDMyNDYsImV4cCI6MTYwNTY0Njg0NiwiaXNzIjoiaW8iLCJkYXRhIjp7ImlkIjpudWxsLCJmaXJzdG5hbWUiOm51bGwsImxhc3RuYW1lIjpudWxsLCJlbWFpbCI6bnVsbH19.X4PMcAyT30FtX-dkua3Jlyiv3UwEMmuta3ogoKZdBlQ"
}
Émettre une requête authentifiée
Que ce soit pour tester avec Postman ou via le front-end, il faut émettre des requêtes avec le token JWT récupéré en réponse d'une authentification réussie. Ce token doit être passé dans toutes les requêtes nécessitant de l'authentification. Plusieurs solutions possibles pour passer le token JWT dans une requête. Dans ce TP nous utliserons le champs Authorization de l'en-tête de la requête et la méthode Bearer
. Exemple :
Authorization: Bearer <YOUR_JWT_TOKEN_HERE>
Recevoir une requête authentifiée
Pour tester nous allons implémenter un endpoint temporaire GET /validatetoken
pour tester le décodage du token JWT.
Ajouter la méthode
getJwtToken
dansRequest
:
// returns JWT token in Authorization header or throw an exception
public function getJwtToken() {
$headers = array_change_key_case(getallheaders());
$autorization = $headers['authorization'];
$arr = explode(" ", $autorization);
if(count($arr)<2)
throw new Exception("Missing JWT token");
$jwt_token = $arr[1];
return $jwt_token;
}
Ajouter un controller nommé
ValidatetokenController
:
<?php
include_once __ROOT_DIR . '/libs/php-jwt/src/BeforeValidException.php';
include_once __ROOT_DIR . '/libs/php-jwt/src/ExpiredException.php';
include_once __ROOT_DIR . '/libs/php-jwt/src/SignatureInvalidException.php';
include_once __ROOT_DIR . '/libs/php-jwt/src/JWT.php';
use \Firebase\JWT\JWT;
class ValidatetokenController extends Controller {
public function __construct($name, $request) {
parent::__construct($name, $request);
}
public function processRequest() {
try {
$jwt_token = $this->request->getJwtToken();
$decodedJWT = JWT::decode($jwt_token, JWT_BACKEND_KEY, array('HS256'));
$jsonResult = json_encode(array(
"message" => "Access granted.",
"data" => $decodedJWT
));
} catch (Exception $e){
header('WWW-Authenticate: Bearer realm="'.JWT_ISSUER.'"');
$jsonResult = json_encode(array(
"message" => "Access denied.",
"error" => $e->getMessage()
));
return Response::unauthorizedResponse($jsonResult);
}
$response = Response::okResponse($jsonResult);
return $response;
}
}
La méthode JWT::decode
décode le contenu du token mais teste également si le token n'a pas expiré. En envoyant une requête correcte avec un token JWT dans le header Authorization
vous devez obtenir les informations stockée dans le token JWT ainsi :
{
"message": "Access granted.",
"data": {
"iat": 1605645134,
"exp": 1605648734,
"iss": "io",
"data": {
"id": "3",
"firstname": "Luke",
"lastname": "Skywalker",
"email": "luke@luke.com"
}
}
}
Si vous envoyez une requête sans token JWT ou avec un token erroné, vous devez obtenir :
{
"message": "Access denied.",
"error": "Missing JWT token"
}
Modifiez PUT /user/{id}
PUT /user/{id}
Implémenter le endpoint
PUT /user/{id}
qui permet de modifier l'email de l'utilisateur{id}
En vous inspirant du code précédent de
ValidatetokenController
, modifier ce endpoint pour que seul l'utilisateur authentifié{id}
puisse modifier son email
Documentation
Mettez à jour la documentation de votre API Web en indiquant les requêtes qui nécessitent d'être authentifié et donc d'envoyer un token JWT.
Last updated
Was this helpful?