• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 06/02/2020

L'authentification

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

L'authentification constitue une tâche fréquente. En effet il y a souvent des parties d'un site qui ne doivent être accessibles qu'à certains utilisateurs, ne serait-ce que l'administration. La solution proposée par Laravel est d'une grande simplicité parce que tout est déjà préparé comme nous allons le voir dans ce chapitre.

Un package

La version 5.0 de Laravel était équipée des vues, assets et routes pour l'authentification. L'arrivée de la version 5.1 a vu la disparition de ces éléments suite à de nombreuses et animées discussions. Je ne vais pas développer toutes ces motivations et m'en tenir juste au fait que maintenant tout ça n'y est plus. Du coup lorsqu'on installe Laravel on dispose juste de la page d'accueil.

Je me suis dit qu'il serait intéressant, au moins pour des raisons didactiques de pouvoir retrouver tout ce qui correspondait à l'authentification dans la version 5.0 dans la nouvelle version de Laravel. En conséquence j'ai créé un package qui permet de les restaurer. Je me suis d'ailleurs rendu compte qu'il est plutôt bien accueilli dans la communauté et correspond donc à un réel besoin.

Pour le présent chapitre vous allez donc mettre en place ce package en suivant les indications concernant son installation. Ce n'est pas bien compliqué et c'est un bon exercice. Si tout se passe bien vous devriez avoir ces vues :

Les vues de l'authentification

Vous êtes maintenant prêt pour ce chapitre !

Les tables

La table users

Nous allons utiliser la table users que nous avons créée au chapitre précédent. Par défaut Laravel considère que cette table existe et il s'en sert comme référence pour l'authentification.

Par défaut également c'est Eloquent qui est utilisé comme driver, il est aussi possible de changer ce fonctionnement.

La table password_reset

Lors de l'installation il existe deux migrations présentes :

Les 2 migrations présentes à l'installation
Les 2 migrations présentes à l'installation

Lors des précédents chapitres je vous ai fait supprimer la seconde parce qu'on avait juste besoin de la table des utilisateurs. Pour ce chapitre on va également avoir besoin de la seconde table qui va nous servir pour la réinitialisation des mots de passe.

On va faire un peu le ménage en lançant une commande d'artisan pour supprimer toutes les tables que vous avez créées :

php artisan migrate:reset

Il ne devrait alors plus vous rester que la table migrations vide. Si ce n'est pas le cas faites le nécessaire dans votre base.

Avec les deux migrations présentes lancez alors la commande d'Artisan pour créer les tables :

php artisan migrate

Vous devriez normalement obtenir ceci :

Les 3 tables
Les 3 tables

Maintenant que les tables sont prêtes passons à la suite.

Les middlewares

C'est quoi un middleware ?

 

Voici un petit schéma pour illustrer cela :

Middleware
Positionnement fonctionnel des middlewares

Un middleware effectue un traitement à l'arrivée de la requête ou à son départ. Par exemple la gestion des sessions ou des cookies dans Laravel se fait dans un middleware, ainsi que l'authentification. On a en fait plusieurs middleware en pelures d'oignon, chacun effectue son traitement et transmet la requête ou la réponse au suivant.

Laravel s'installe avec deux fichiers middlewares qui concernent tous les deux l'authentification :

Les middlewares pour l'authentification
Authenticate

Ce middleware permet de savoir si un utilisateur est authentifié. Voici son code :

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('auth/login');
}
}
return $next($request);
}
}

La méthode intéressante est handle qui reçoit la requête en paramètre. On regarde si l'utilisateur est juste un invité (guest). Si c'est le cas on teste si la requête est en Ajax, auquel cas on renvoie Unauthorized. Si elle n'est pas en Ajax on fait une redirection vers la route auth/login pour que l'invité puisse s'authentifier. Si ce n'est pas un invité on laisse la requête suivre normalement son cours. Tout cela n'est évidemment pas figé dans le marbre et on peut changer tout ce qu'on veut. Pour ce cours je vais me contenter de prendre le code par défaut.

RedirectIfAuthenticated

Ce filtre permet de savoir si l'utilisateur n'est pas authentifié. C'est donc l'exact inverse du précédent. Voici son code :

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class RedirectIfAuthenticated
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
return redirect('/home');
}
return $next($request);
}
}

Ici aussi on va encore s'intéresser à la méthode handle. Si l'utilisateur est authentifié, ce qui nous est indiqué par la méthode check, alors on renvoie à la page d'accueil du site (home). Sinon on laisse la requête suivre normalement son cours.

Routes et contrôleurs

Les routes

Les routes pour l'authentification se trouvent dans le package :

Les routes pour l'authentification
Les routes pour l'authentification

Avec ce code :

<?php
Route::get('home', '\Bestmomo\Scafold\Http\Controllers\HomeController@index');
// Authentication routes...
Route::get('auth/login', 'Auth\AuthController@getLogin');
Route::post('auth/login', 'Auth\AuthController@postLogin');
Route::get('auth/logout', 'Auth\AuthController@getLogout');
// Registration routes...
Route::get('auth/register', 'Auth\AuthController@getRegister');
Route::post('auth/register', 'Auth\AuthController@postRegister');
// Password reset link request routes...
Route::get('password/email', 'Auth\PasswordController@getEmail');
Route::post('password/email', 'Auth\PasswordController@postEmail');
// Password reset routes...
Route::get('password/reset/{token}', 'Auth\PasswordController@getReset');
Route::post('password/reset', 'Auth\PasswordController@postReset');

On a vu la méthode controller plusieurs fois pour créer les routes vers un contrôleur implicite. La méthode controllers permet d'en déclarer plusieurs d'un coup.

Voyons un peu les routes ainsi créées :

Les routes créées
Les routes créées

Nous allons analyser tout ça dans ce chapitre.

Les contrôleurs

Il est fait référence à deux contrôleurs que l'on trouve ici :

Les deux contrôleurs de l'authentification
Les deux contrôleurs de l'authentification
AuthController

Ce contrôleur est destiné à gérer :

  1. l'enregistrement des utilisateurs

  2. la connexion

  3. la déconnexion

Si on regarde son code :

<?php
namespace App\Http\Controllers\Auth;
use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class AuthController extends Controller
{
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
|
*/
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
/**
* Create a new authentication controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'getLogout']);
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
}

On ne retrouve aucune des méthodes dont il est fait référence dans les routes. Vous pouvez remarquer l'utilisation du trait Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers. Donc toutes les méthodes se trouvent dans le framework lui-même. Si nous avons besoin de les personnaliser la seule solution est donc de les surcharger.

PasswordController

Ce contrôleur est destiné uniquement à permettre la réinitialisation du mot de passe en cas d'oubli par l'utilisateur. Si on regarde aussi son code :

<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Create a new password controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
}

On se rend compte que lui aussi utilise un trait (Illuminate\Foundation\Auth\ResetsPasswords).

Les vues

Vous allez trouver les vues ici :

Les vues de l'authentification
Les vues de l'authentification

Vous n'êtes évidemment pas obligé d'utiliser ces vues si vous voulez les intégrer visuellement dans un site mais pour ce chapitre on va les utiliser directement. Elles sont toutes conçues de la même manière. Voici par exemple la vue pour la connexion (resources/views/auth/login.blade.php):

@extends('app')
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form class="form-horizontal" role="form" method="POST" action="{{ url('/auth/login') }}">
{!! csrf_field() !!}
<div class="form-group">
<label class="col-md-4 control-label">E-Mail Address</label>
<div class="col-md-6">
<input type="email" class="form-control" name="email" value="{{ old('email') }}">
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label">Password</label>
<div class="col-md-6">
<input type="password" class="form-control" name="password">
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>
<input type="checkbox" name="remember"> Remember Me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<button type="submit" class="btn btn-primary">Login</button>
<a class="btn btn-link" href="{{ url('/password/email') }}">Forgot Your Password?</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

Évidemment tout est en anglais ! D'autre part on voit qu'on utilise un template (app) que l'on peut trouver ici :

Le template

L'enregistrement d'un utilisateur

La validation

Dans le contrôleur AuthController vous trouvez ce code :

<?php
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
}

On a vu jusqu'à présent la validation se faire à partir d'une requête de formulaire et là c'est réalisé différemment. Sans doute parce qu'il était difficile pour le framework de gérer correctement les espaces de noms dans ce cas. On trouve aussi dans cette classe une méthode pour créer l'utilisateur :

<?php
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}

Comme on a ajouté une colonne admin il faut un peu modifier le code pour aussi l'enregistrer :

<?php
public function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'admin' => isset($data['admin'])
]);
}

Le contrôleur

Avant d'envisager une authentification un visiteur doit pouvoir s'enregistrer.

Si on regarde le contenu du trait AuthenticatesAndRegistersUsers on se rend compte qu'il fait appel à deux autres traits :

<?php
namespace Illuminate\Foundation\Auth;
trait AuthenticatesAndRegistersUsers
{
use AuthenticatesUsers, RegistersUsers {
AuthenticatesUsers::redirectPath insteadof RegistersUsers;
}
}

Il y a deux méthodes dans le trait RegistersUsers pour l'enregistrement des utilisateurs :

<?php
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function getRegister()
{
return view('auth.register');
}
/**
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postRegister(Request $request)
{
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
Auth::login($this->create($request->all()));
return redirect($this->redirectPath());
}

Voyons de plus près ces deux méthodes :

  • getRegister : ici on renvoie la vue auth.register qui doit contenir le formulaire pour l'enregistrement.

  • postRegister : ici on traite la soumission du formulaire, la validation est assurée par la méthode validator qu'on a vue ci-dessus, et si la validation est correcte on crée dans la base cet utilisateur avec la méthode create, on connecte le nouvel utilisateur avec la méthode login, enfin on renvoie à l'url définie dans la méthode redirectPath.

Comme tout ce code se situe dans le framework nous ne devons pas directement le modifier. On peut toutefois modifier l'url de redirection. Regardez la méthode redirectPath placée dans le trait RedirectUsers :

<?php
public function redirectPath()
{
if (property_exists($this, 'redirectPath'))
{
return $this->redirectPath;
}
return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
}

On teste la présence éventuelle d'une propriété redirectPath. Si elle est présente on l'utilise pour la redirection. On teste aussi la présence d'une propriété redirectTo, sinon on redirige vers home. Donc si on veut une redirection spécifique il suffit de créer une propriété redirectPath ou redirectTo dans le contrôleur. Pour toute autre modification on devra surcharger les méthodes.

Etant donné qu'on a un contrôleur implicite l'URL est .../auth/register.

La vue auth.register

Il nous faut une vue pour l'enregistrement, on a vue que Laravel nous en propose une. Voici l'aspect normalement obtenu :

Le formulaire pour l'enregistrement

Lorsqu'un utilisateur est créé et connecté il est renvoyé sur la route définie par la fonction redirectPath qu'on a vue ci-dessus. Comme la route, le contrôleur et la vue sont prévues avec le package on obtient :

Utilisateur enregistré et connecté
Utilisateur enregistré et connecté

La connexion et la déconnexion

Maintenant que les utilisateurs peuvent s'enregistrer passons à la connexion. Par défaut Laravel prévoit de le faire à partir de l'adresse email. On va conserver ce comportement.

La connexion

On a deux méthodes concernées dans le trait AuthenticatesUsers :

<?php
namespace Illuminate\Foundation\Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Lang;
trait AuthenticatesUsers
{
use RedirectsUsers;
/**
* Show the application login form.
*
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
if (view()->exists('auth.authenticate')) {
return view('auth.authenticate');
}
return view('auth.login');
}
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postLogin(Request $request)
{
$this->validate($request, [
$this->loginUsername() => 'required', 'password' => 'required',
]);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
$throttles = $this->isUsingThrottlesLoginsTrait();
if ($throttles && $this->hasTooManyLoginAttempts($request)) {
return $this->sendLockoutResponse($request);
}
$credentials = $this->getCredentials($request);
if (Auth::attempt($credentials, $request->has('remember'))) {
return $this->handleUserWasAuthenticated($request, $throttles);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
if ($throttles) {
$this->incrementLoginAttempts($request);
}
return redirect($this->loginPath())
->withInput($request->only($this->loginUsername(), 'remember'))
->withErrors([
$this->loginUsername() => $this->getFailedLoginMessage(),
]);
}
...
}

Voyons de plus près ces deux méthodes :

  • getLogin : ici on renvoie la vue auth.login (sauf s'il existe éventuellement une vue auth.authenticate) qui doit contenir le formulaire pour la connexion.

  • postLogin : ici on traite la soumission du formulaire, la validation est assurée directement dans la méthode avec la puissante méthode validate qui est une alternative intéressante aux requêtes de formulaires. Si la validation est correcte on vérifie qu'on a pas d'attaque (throttle) puis les données sont vérifiées dans la table avec la méthode attempt. Si tout va bien on renvoie comme défini par la méthode handleUserWasAuthenticated, sinon on redirige sur le formulaire avec un message d'erreur défini par la fonction getFailedLoginMesssage.

Ici encore si vous voulez modifier quelque chose (comme par exemple permettre la connexion avec le nom d'utilisateur). Au fil des évolutions de nombreuse fonctions ont été mises en place pour éviter de surcharger dans le contrôleur, pour chaque cas il convient de bien regarder le code pour déterminer la meilleure stratégie à adopter.

Etant donné qu'on a un contrôleur implicite l'URL est .../auth/login.

La vue auth.login

Cette vue est aussi prévue avec le package. Voici l'aspect du formulaire de connexion :

Le formulaire de connexion

La déconnexion

Dans la barre de menu est prévu la déconnexion :

La commande de déconnexion
La commande de déconnexion

Voici la méthode concernée dans le trait AuthenticatesUsers :

<?php
public function getLogout()
{
Auth::logout();
return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
}

L'utilisateur est déconnecté avec la méthode logout et il est ensuite redirigé à la racine du site ou à la route définie par la propriété redirectAfterLogout.

L'oubli du mot de passe

Il y a la table password_resets dans notre base :

La table password_reminders
La table password_resets

On voit qu'on va mémoriser ici l'adresse email, le jeton (token) et le timestamp (par défaut les jetons sont valables pendant une heure).

On a aussi un contrôleur :

Le contrôleur PasswordController

Les URL auront la forme .../password/...

Dans le formulaire de connexion il est prévu un lien en cas d'oubli du mot de passe :

Le lien pour l'oubli du mot de passe
Le lien pour l'oubli du mot de passe

L'url correspondant est .../password/email

Voici l'aspect du formulaire :

Le formulaire pour l'oubli du mot de passe

On demande à l'utilisateur de saisir son adresse email pour pouvoir le retrouver dans la table des utilisateurs.

Voici le code dans le trait ResetsPasswords :

<?php
/**
* Display the form to request a password reset link.
*
* @return \Illuminate\Http\Response
*/
public function getEmail()
{
return view('auth.password');
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$response = Password::sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});
switch ($response) {
case Password::RESET_LINK_SENT:
return redirect()->back()->with('status', trans($response));
case Password::INVALID_USER:
return redirect()->back()->withErrors(['email' => trans($response)]);
}
}

La soumission du formulaire est prévue dans la méthode postEmail

On a deux cas :

  • L'utilisateur n'est pas valide (l'adresse email n'existe pas) : on redirige sur le formulaire avec le message d'erreur dans la variable error, avec cet aspect :

Les message d'erreur pour une adresse inconnue
Le message d'erreur pour une adresse inconnue
  • l'utilisateur est valide (on lui a envoyé un email) : on redirige sur le formulaire avec le message dans la variable status.

L'email a bien été envoyé.
L'email a bien été envoyé.

C'est quoi la méthode trans ?

C'est un helper  qui permet d'adapter le texte linguistiquement à partir  de la valeur de la clé locale dans config/app.php. Nous verrons cela en détail dans le chapitre sur la localisation.

Pour que l'email soit effectivement envoyé il faut que tout soit bien configuré dans le fichier .env :

MAIL_DRIVER=smtp
MAIL_HOST=smtp.free.fr
MAIL_PORT=25
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=""

Et dans config/mail.php :

'from' => array('address' => 'moi@free.fr', 'name' => 'Administrateur'),

La configuration ci-dessus correspond à ma situation en local avec le prestataire free. Votre cas doit être sans doute différent.

A quoi ressemble l'email envoyé ? Vous trouvez la vue correspondante ici :

La vue pour l'email

Avec ce code :

Click here to reset your password: {{ url('password/reset/'.$token) }}

C'est clair et concis. Voyons ce qu'on obtient en réception :

Click here to reset your password: http://.../password/reset/6e8ad7609e1b7cc5a483d81524ca8443f0138b73

Voyons ce qu'il s'est passé dans la table password_resets :

La table password_reminders

On a la mémorisation de l'adresse email, du jeton et le moment de la création.

Si on utilise l'url de l'email on est dirigé sur la route password/reset et donc sur la méthode getReset du trait ResetsPasswords :

<?php
/**
* Display the password reset view for the given token.
*
* @param string $token
* @return Response
*/
public function getReset($token = null)
{
if (is_null($token))
{
throw new NotFoundHttpException;
}
return view('auth.reset')->with('token', $token);
}

Le jeton est transmis dans la variable $token. On vérifie sa présence sinon on envoie une erreur. S'il est présent on retourne le formulaire de saisi du nouveau mot de passe avec la vue reset dans le dossier auth :

La vue de reset

Avec cet aspect :

Le formulaire pour le nouveau mot de passe

A la soumission on tombe sur la méthode postReset du trait :

<?php
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postReset(Request $request)
{
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed',
]);
$credentials = $request->only(
'email', 'password', 'password_confirmation', 'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
switch ($response) {
case Password::PASSWORD_RESET:
return redirect($this->redirectPath());
default:
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}

On trouve une validation pour les entrées. Si tout se passe bien le nouveau mot de passe est mémorisé et l'utilisateur connecté. Pour la redirection la méthode redirectPath va vérifier la présence éventuelle d'une propriété, sinon elle renvoie home.

En cas d'erreur on renvoie le formulaire avec un message indiquant le souci de validation.

En résumé

  • L'authentification est totalement et simplement prise en charge par Laravel.

  • Un middleware permet d'effectuer un traitement à l'arrivée ou au départ de la requête.

  • On peut utiliser les middlewares Authenticate et RedirectIfAuthenticated pour autoriser ou interdire un accès.

  • Un système complet de renouvellement du mot de passe est déjà en place dans une installation fraîche de Laravel.

Exemple de certificat de réussite
Exemple de certificat de réussite