Laravel 8

Le bazar de l’authentification

Laravel est un framework qui connait un grand succès pour des raisons évidentes de solidité et de simplicité. Il propose une architecture MVC éprouvée, et on apprécie tous des outils comme Eloquent ou Blender. On utilise Artisan avec un grand plaisir et les commandes deviennent de plus en plus nombreuses. Les applications sont aussi faciles à tester. Mais si la gestion et l’organisation du code côté serveur est limpide il n’en est pas de même côté client.

Dans cet article je vais me pencher particulièrement sur le cas de l’authentification. Si on se contente de construire des API le problème ne se pose évidemment pas mais pour une application traditionnelle avec persistance on a besoin d’authentifier le client et de le conserver en session. Au fil des versions cet aspect a connu de nombreuses évolutions et on en arrive désormais à une certain confusion à même de décourager les nouveaux venus.

Alors quel est le problème ? Au départ l’authentification faisait partie intégrante de Laravel et arrivait en même temps que l’installation. Puis c’est devenu un package distinct pour ce qui concerne la partie frontend (laravel/ui). Après tout Laravel n’a pas à se soucier de l’aspect que prennent les pages côté client et cette séparation semble très saine. Mais avec Laravel 8 est arrivé un changement technologique un peu brutal. Quand on lit la documentation au chapitre de l’authentification on tombe sur cette phrase :

Want to get started fast? Install Laravel Jetstream in a fresh Laravel application.

On est donc fortement incité à utiliser le package Jetstream qui lui même charge un autre package : Fortify. De quoi s’agit-t-il ? Pour résumé la situation j’ai créé cette illustration dans mon cours sur Laravel 8 :

En gros on a :

  • Laravel offre des « guards » (comment on authentifie un utilisateur, par exemple avec des sessions) et des « providers » (comment on retrouve un utilisateur authentifié, par exemple avec Eloquent)
  • Fortify est une sur-couche pour Laravel qui établie la relation entre backend et frontend sans rien préjuger quant à la nature de ce frontend
  • Jetstream est un scaffolding complet pour gérer l’interface client avec le choix entre deux technologies : Livewire ou Inertia.

Vous y voyez un peu plus clair ? Non ? c’est normal..

Pour ceux qui comprennent l’horrible anglais des américains Taylor Otwell (le créateur de Laravel) a clarifié son choix technilogique dans une longue vidéo.

Au départ le package laravel/ui était déclaré obsolète et ne devait plus être utilisé, puis la discours a changé et désormais il sera maintenu en vie, mais pour combien de temps ? Pourtant pour un débutant c’est le bon choix sinon il risque d’être rapidement rebuté par Jetstream qui fait appel à des technologies nouvelles et par forcément faciles à comprendre.

En plus franchement on peut longuement discuter de l’intérêt par exemple de Livewire et Tailwind. J’ai d’ailleurs lancé ce sujet sur le forum de Laracasts et on peut constater que le débat est loin d’être clos. Après chacun fait ce qu’il veut avec son code !

Dans le présent blog je me suis toujours attaché à demeurer accessible aux débutants, ça possède une grande vertu : celle d’obliger à rester simple et à avoir une claire vision des principes de base.

Alors je le dis clairement : le débutant ne doit pas se lancer dans Jetstream mais utiliser le package laravel/ui.

laravel/ui

Avec ce package tout est simple. On part d’un Laravel tout fraîchement installé :

composer create-project laravel/laravel laravel8 --prefer-dist

Et ensuite on installe le package :

composer require laravel/ui

On crée ensuite le scaffolding. On a le choix entre 3 version :

  1. Bootstrap
  2. Vue
  3. React

On va choisir par exemple la première si on n’a pas besoin d’un framework Javascript élaboré (ce qui est le cas dans 99% des cas)  :

php artisan ui bootstrap --auth

Il ne reste plus qu’à générer les fichiers avec npm :

npm install
npm run dev

On crée la base et on renseigne le fichier .env :

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel8
DB_USERNAME=root
DB_PASSWORD=

On crée les tables dans la base en lançant les migrations :

php artisan migrate

Et c’est terminé, on a une authentification complète !

On a une page pour s’enregistrer :

Une autre pour s’authentifier :

Une autre encore pour le renouvellement du mot de passe :

Simple et efficace…

On trouve facilement les contrôleurs pour toute modification nécessaire :

Ainsi que les vues si on veut apporter un changement :

Par exemple si Bootstrap ne vous convient pas vous pouvez utiliser n’importe quel autre framework. J’avais montré comment faire dans un article de février avec Paper.css. La version de Laravel était encore la 6 mais ça ne change rien dans ce domaine. Dans un article plus récent concernant la réalisation d’une boutique en ligne je montre comment utiliser Materialize pour l’authentification.

Donc ce package laravel/ui est simple à mettre en œuvre et à adapter à ses convenances et offre comme fonctionnalités :

  • Enregistrement
  • Connexion
  • Renouvellement du mot de passe
  • Vérification de l’email
  • Confirmation du mot de passe

Si vous avez besoin d’autre chose il faut évidemment le coder en complément.

Si vous préférez Tailwind à Bootstrap pas de problème, il existe désormais laravel/breeze qui fait ça très bien et constitue donc une alternative à laravel/ui.

Jetstream

En ce qui concerne Jetstream j’ai bien détaillé tout ça dans mon cours. A partir d’un Laravel tout neuf on installe le package :

composer require laravel/jetstream

Ensuite il faut choisir entre Livewire et Inertia. Tout dépend si on a une application plutôt serveur ou plutôt client. Mon but dans cet article n’est pas de vous parler spécifiquement de ces deux technologies. Personnellement je ne les aime pas mais tous les goûts sont dans la nature. Je pourrais me justifier sur le sujet mais je ne le ferai pas ici. Si vous aimez ces approches et qu’en plus vous appréciez Tailwind alors faites vous plaisir et utilisez Jetstream !

Pour l’installation avec Livewire c’est tout simple :

php artisan jetstream:install livewire
npm install
npm run dev
php artisan migrate

On se retrouve avec beaucoup de choses :

  • L’authentification complète avec deux facteurs possibles
  • La gestion du mot de passe avec la possibilité de le changer
  • La confirmation du mot de passe
  • La vérification de l’email
  • La gestion du profil utilisateur avec éventuellement sa photo
  • L’abandon des sessions
  • La gestion des clés d’API
  • La suppression du compte
  • La gestion éventuelle des équipes avec rôles et permissions

Ça fait effectivement beaucoup de choses !

J’ai montré dans un dépôt sur Github comment reproduire les fonctionnalités essentielles de Jetstream avec juste Fortify et du Vanilla Javascript.

Une approche intermédiaire

J’ai évoqué dans mon cours comment se passer de Jetstream tout en conservant les possibilités de Fortify. On va creuser cette piste en allant un peu plus loin. On commence par créer une nouvelle instance de Laravel :

composer create-project laravel/laravel laravel8 --prefer-dist

Fortify

Et cette fois on installe Fortify :

composer require laravel/fortify

Et on publie le provider :

php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"

Il faut déclarer ce provider dans config/app.php :

App\Providers\FortifyServiceProvider::class,

Comme précédemment on crée la base et on renseigne le fichier .env :

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel8
DB_USERNAME=root
DB_PASSWORD=

On crée les tables dans la base en lançant les migrations :

php artisan migrate

Maintenant Fortify est bien installé, toute l’intendance de l’authentification est présente côté serveur mais évidemment pour le client on n’a pour le moment rien du tout. Il faut donc qu’on choisisse comment on veut construire le frontend et là le choix est très vaste ! Il serait dommage de se passer d’un framework CSS parce qu’il en existe d’excellents qui font gagner un temps précieux tout en offrant à la fois du code éprouvé et  une cohérence visuelle. Par contre au niveau du Javascript je pense vraiment que l’utilisation d’un framework ne se justifie que pour une application SPA, sinon les API des navigateurs sont largement suffisantes pour les tâches courantes.

Les assets

On va déjà initialiser npm :

npm i

Pour changer un peu je vous propose d’utiliser UIKit. On va l’installer :

npm i -D uikit

On va ensuite utiliser Mix pour créer nos assets. remplacez le code de resources/js/app.js avec :

window.UIkit = require("uikit");
window.Icons = require("uikit/dist/js/uikit-icons");

UIkit.use(window.Icons);

Vous pouvez supprimer resources/js/bootstrap.js.

On va créer resources/sass/app.scss avec ce code :

@import "~uikit/src/scss/variables-theme.scss";
@import "~uikit/src/scss/mixins-theme.scss";
@import "~uikit/src/scss/uikit-theme.scss";

Et pour finir on modifie webpack.mix.js :

const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

Maintenant on peut compiler les assets :

npm run dev

Maintenant on a généré les fichiers css/app.css et js/app.js dans le public.

Les vues

Pour nous simplifier la vie on va récupérer les vues dans ce package (il y a d’ailleurs un petit raté dans deux vues concernant les alertes, j’ai envoyé un PR pour corriger ça). On va récupérer les vues de l’authentification ainsi que le layout et les copier dans notre projet, on ajoute aussi les démos pour avoir quelque chose qui fonctionne tout de suite :

Dans le layout supprimez laravel-uikit:: aux trois emplacements sinon vous allez avoir une erreur puisque les vues ne sont plus dans un package.

On ajoute aussi la route pour la démo :

Route::get('demo', function () { return view('demo'); })->name('demo');

Pour terminer dans FortifyServiceprovider on déclare ces vues :

public function boot()
{
    Fortify::createUsersUsing(CreateNewUser::class);
    Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
    Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
    Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

    Fortify::registerView(function () {
      return view('auth.register');
    });

    Fortify::loginView(function () {
        return view('auth.login');
    });

    Fortify::requestPasswordResetLinkView(function () {
        return view('auth.forgot-password');
    });
    Fortify::resetPasswordView(function ($request) {
        return view('auth.reset-password', ['token' => $request->token]);
    });

    Fortify::verifyEmailView(function () {
        return view('auth.verify-email');
    });
}

On va aussi changer la redirection dans RouteServiceProvider parce qu’on n’a pas de route HOME :

public const HOME = '/demo';

Et ça devrait fonctionner !

On a une vue pour le login :

Une autre pour l’enregistrement :

Et aussi les autres pour le renouvellement du mot de passe et la vérification de l’email.

Le changement du mot de passe

Maintenant on va aller un peu plus loin pour mieux exploiter les possibilités de Fortify. On ajoute une route :

Route::middleware('auth')->group(function () {
  Route::prefix('profile')->group(function () {
    Route::view('updatepassword', 'profile.updatepassword')->name('profile.updatepassword');
  });
});

On crée la vue :

@extends('layouts.app')

@section('content')
    <div class="uk-section uk-section-small uk-section-muted uk-flex uk-flex-center">
        <div class="uk-card uk-card-default uk-card-body uk-width-large">
            <h2 class="uk-card-title">@lang('Update Password')</h2>
            <form method="POST" action="{{ route("user-password.update")  }}" class="uk-form-stacked">
                @csrf
                @method('PUT')
                @if (session('status'))
                    <div uk-alert class="uk-alert-success">
                        {{ session('status') }}
                    </div>
                @endif
                <div class="uk-margin">
                    <label for="current_password" class="uk-form-label">
                        {{ __('Current Password') }}
                    </label>
                    <div class="uk-form-control">
                        <input id="current_password" type="password"
                               class="uk-input @error('current_password') uk-form-danger @enderror" name="current_password" required>
                        @error('current_password')
                        <span class="uk-text-danger">{{ $message }}</span>
                        @enderror
                    </div>
                </div>
                <div class="uk-margin">
                    <label for="password" class="uk-form-label">
                        {{ __('New Password') }}
                    </label>
                    <div class="uk-form-control">
                        <input id="password" type="password"
                              class="uk-input @error('password') uk-form-danger @enderror" name="password" required>
                        @error('password')
                        <span class="uk-text-danger">{{ $message }}</span>
                        @enderror
                    </div>
                </div>
                <div class="uk-margin">
                    <label for="password_confirmation" class="uk-form-label">
                        {{ __('Confirm Password') }}
                    </label>
                    <div class="uk-form-control">
                        <input id="password_confirmation" type="password"
                              class="uk-input" name="password_confirmation" required>
                    </div>
                </div>
                <div class="uk-margin">
                    <div class="uk-form-control">
                        <button type="submit" class="uk-button uk-button-primary">
                            {{ __('Save') }}
                        </button>
                    </div>
                </div>
            </form>
        </div>
    </div>
@endsection

On complète le menu dans la barre de navigation :

<div class="uk-navbar-dropdown">
    <ul class="uk-nav uk-navbar-dropdown-nav">
        <li>
            ...
        </li>
        <li class="uk-nav-divider"></li>
        <li class="uk-nav-header">@lang('Profile')</li>
        <li>
            <a href="{{ route('profile.updatepassword') }}">@lang('Update Password')</a>
        </li>
    </ul>
</div>

On a ainsi un lien :

Et le formulaire qui apparaît :

On va juste supprimer le message bag dans l’action UpdateUserPassword :

public function update($user, array $input)
{
    Validator::make($input, [
        ...
    })->validate();

Ce message bag n’est utile que s’il peut y avoir confusion entre deux formulaires sur une même page, ce qui n’est pas notre cas.

On peut maintenant changer facilement le mot de passe, la validation fonctionne et on a un message en cas de réussite. On a eu juste à créer la route et la vue, Fortify se charge du reste.

La gestion des informations

On va faire la même chose pour la gestion des informations. On ajoute une route :

Route::middleware('auth')->group(function () {
  Route::prefix('profile')->group(function () {
    Route::view('updatepassword', 'profile.updatepassword')->name('profile.updatepassword');
    Route::view('updateinfos', 'profile.updateinfos')->name('profile.updateinfos');
  });
});

On crée la vue :

@extends('layouts.app')

@section('content')
    <div class="uk-section uk-section-small uk-section-muted uk-flex uk-flex-center">
        <div class="uk-card uk-card-default uk-card-body uk-width-large">
            <h2 class="uk-card-title">@lang('Profile Information')</h2>
            <form method="POST" action="{{ route("user-profile-information.update") }}" class="uk-form-stacked">
                @csrf
                @method('PUT')
                @if (session('status'))
                    <div uk-alert class="uk-alert-success">
                        {{ session('status') }}
                    </div>
                @endif
                <div class="uk-margin">
                    <label for="name" class="uk-form-label">
                        {{ __('Name') }}
                    </label>
                    <div class="uk-form-control">
                        <input class="uk-input @error('name') uk-form-danger @enderror" id="name" name="name" type="text"
                              value="{{ old('name', auth()->user()->name) }}" required>
                        @error('name')
                            <span class="uk-text-danger">{{ $message }}</span>
                        @enderror
                    </div>
                </div>
                <div class="uk-margin">
                    <label for="email" class="uk-form-label">
                        {{ __('E-Mail Address') }}
                    </label>
                    <div class="uk-form-control">
                        <input class="uk-input @error('email') uk-form-danger @enderror" id="email" name="email" type="email"
                              value="{{ old('email', auth()->user()->email) }}" required >
                        @error('email')
                            <span class="uk-text-danger">{{ $message }}</span>
                        @enderror
                    </div>
                </div>
                <div class="uk-margin">
                    <div class="uk-form-control">
                        <button type="submit" class="uk-button uk-button-primary">
                            {{ __('Save') }}
                        </button>
                    </div>
                </div>
            </form>
        </div>
    </div>
@endsection

On complète le menu dans la barre de navigation :

<li class="uk-nav-header">@lang('Profile')</li>
<li><a href="{{ route('profile.updatepassword') }}">@lang('Update Password')</a></li>
<li><a href="{{ route('profile.updateinfos') }}">@lang('Profile informations')</a></li>

On a le lien dans le menu :

On obtient le formulaire :

On va aussi supprimer le message bag dans l’action UpdateUserProfileInformation :

Validator::make($input, [
    ...
])->validate();

On peut maintenant changer le nom ou le mot de passe, la validation fonctionne et on a un message en cas de réussite. On a eu juste à créer la route et la vue, Fortify se charge encore du reste.

Conclusion

On pourrait poursuivre ce codage en ajoutant l’authentification à double facteur ou la suppression du compte. Mais je ne veux pas trop alourdir cet article et je voulais juste montrer le principe d’utilisation de Fortify sans la complexité de Jetstream.

Vous pouvez télécharger le code ici.

Print Friendly, PDF & Email

5 commentaires

  • Lord Byron

    Merci pour cet article ! Mais j’ai une proposition ; une proposition que je voulais exposer directement sur laracast mais je préfère vous le confier, bien-sûr, ce sera à votre aise. J’ai constaté qu’il fallait pas mal de gymnastique pour créer des authentifications séparées (admins et utilisateurs par exemple). Alors je pense qu’il serait bien s’il y a avait une commande « Artisan » qui permette de créer un autre guard avec un scaffolding de base ou juste le système d’auth; bien-sûr, il est possible de le faire facilement avec l’ajout de rôles dans la base de donnée mais selon moi, ça ne convient pas à un projet d’envergure. J’espère avoir bien exposé les faits.

    Merci

    • bestmomo

      Bonjour,

      J’avoue n’avoir aucune expérience concernat les projets d’envergure et je n’y suis donc pas sensibilisé. Globalement est-il intéressant de créer plusieurs guards ? Il me semble qu’il faut bien distinguer ce qui relève de l’authentification (s’assurer que c’est la bonne personne qui se connecte) des rôles et permissions (cette personne peut faire ça mais pas ça). J’ai du mal à voir en quoi le fait d’avoirs plusieurs guards pourrait nous aider sur un point ou l’autre.

      • gil

        Bonjour,
        Pour répondre à ta question « Globalement est-il intéressant de créer plusieurs guards ? » : pas toujours, par exemple si les différences ne sont que des droits d’accés différents aux mêmes ressources (comme tu le dis, rôles et permissions suffisent). Mais sur de gros projets on peut être amenés à gérer des domaines distincts d’utilisateurs; par exemple des clients qui doivent s’authentifier sur un portail web (pas de rôles précis mais de l’authentification), des opérateurs back-office pour administrer le système (avec un ensemble lourd de rôles et permissions possibles), des agents de terrain qui n’accéderont pas en direct aux ressources du serveur mais à celles d’équipements locaux, mais qui néanmoins devront s’authentifier au serveur pour une question de sécurité dans son ensemble (donc pas de rôle mais authentification spécifiques), des API spécifiques pour accès par des applications externe, avec gestion de quelques rôles et permissions spécifiques. L’intérêt de ne pas mélanger le tout, c’est surtout la gestion de la sécurité (il y a un premier filtre au niveau du domaine, chacun ne donnant accès qu’à un sous-ensemble de ressources, donc moins de risques de donner par erreur un droit dédié « opérateur » à un usager d’un autre domaine), mais ça peut aussi faciliter l’administration des rôles et permissions pour chacun des domaines qui en ont besoin (il n’y a que la liste des personnes du domaines, et que la listes des ressources utiles pour ce domaine). Utile dans tous les cas ou des groupes de personnes (ou API, ou autre devant s’authentifier) sont disjoints. Ca offre en plus une souplesse d’évolution, de dire demain ce domaine là je ne le gère plus en base moi-même, mais j’en confie la gestion à un Active Directory ou un service externe.
        Si on prends l’exemple d’un forum classique par exemple, ce n’est pas utile et même contre-productif, parce qu’un utilisateur « normal » peut souvent être amené à devenir modérateur voire administrateur. Mais sur un site marchand assez gros, tu peux avoir besoin d’authentifier distinctivement deux domaines d’utilisateurs: les clients (qui peuvent être « rôlés » si tu veux gérer les clients « normaux » ou « premium » par exemple), et des opérateurs avec beaucoup plus de rôles et droits (celui qui ne fait qu’ajouter/retirer les produits, celui qui gère les plaintes, celui qui guère les stocks, celui qui manage prix et promotions, celui qui est administrateu, les « big boss » :)…). Scinder les deux domaines devrait normalement faciliter et la gestion des accés, et la sécurité globale du système.
        Sinon, toujours bravo pour ton site, J’ai du stopper les développements perso en laravel 5.2, tes articles et cours vont me permettre de me remettre dans le bain 🙂

Laisser un commentaire