Laravel 8

Laravel Fortify et Paper CSS

Au début de l’année j’ai rédigé un article concernant l’authentification de Laravel 6 avec Paper CSS. C’est un framework CSS léger, original et esthétique qui mérite d’être mieux connu. Depuis cet article l’authentification dans Laravel a été quelque peu malmenée jusqu’au tsunami de Laravel 8. J’ai écrit un article récent sur le sujet pour faire le point. J’y montre comment utiliser Fortify avec Bootstrap pour créer l’authentification. Je n’y ai pas développé totalement le sujet pour ne pas rendre l’article trop volumineux.

J’ai trouvé judicieux de compléter tout ça avec cet article résolument plus complet qui montre la mise en place intégrale des fonctionnalités de Fortify  avec un framework CSS autre que Tailwind qui est plébiscité par la communauté mais qui personnellement ne me convient pas. J’ai donc choisi Paper CSS mais ça aurait pu être n’importe quel autre framework CSS. L’intérêt est essentiellement de montrer comment utiliser Fortify.

Plutôt que de montrer toute la démarche au niveau du code, ce qui aurait donné un article très long et assez laborieux, j’ai préféré créer un dépôt Github avec le projet complet. Il suffit donc d’utiliser ce dépôt pour créer une application locale. Pour suivre cet article il vaut mieux le faire avec une installation locale avec une base de données et une gestion des mails comme Mailtrap pour les essais.

Fortify

Fortify est une sur-couche pour Laravel qui établit la relation entre backend et frontend sans rien préjuger quant à la nature de ce frontend. Voici une illustration que j’ai déjà utilisée précédemment qui montre bien la place de Fortify dans l’architecture de Laravel :

On n’est évidemment pas obligé d’utiliser Fortify, (il existe les packages laravel/ui et laravel/breeze) mais je trouve judicieux de le faire puisqu’il existe !

Fortify ajoute à Laravel des actions :

Les actions sont à la mode, vous pouvez lire cet article sur le sujet. C’est une nouvelle façon d’organiser le code. Ici on voit qu’on hérite de 5 actions :

  • pour créer un nouvel utilisateur
  • pour définir les règles de validation pour les mots de passe
  • pour la réinitialisation du mot de passe
  • pour le changement du mot de passe
  • pour le changement des informations de l’utilisateur

Le fait d’avoir le code dans des actions nous donne la liberté d’apporter des modifications si nécessaire, je ne m’en suis d’ailleurs pas privé.

Fortify ajoute de multiples routes à Laravel (celles que j’ai surlignées) :

On voit aussi qu’on dispose de tous les contrôleurs nécessaires, il nous suffit donc d’ajouter les vues et ça doit fonctionner !

J’ai toutefois dû ajouter quelques routes pour l’application :

use Laravel\Fortify\Features as Fortify;

Route::middleware('auth', 'verified')->group(function () {
    Route::view('home', 'home')->name('home');
    Route::prefix('user')->group(function () {
        if((Fortify::enabled(Fortify::updateProfileInformation()))) {
            Route::view('profile', 'auth.informations')->name('profile.edit');
        }
        if((Fortify::enabled(Fortify::updatePasswords()))) {
            Route::view('password', 'auth.passwords.edit')->name('password.edit');
        }        
        Route::middleware('password.confirm')->group(function () {
            if(Fortify::canManageTwoFactorAuthentication()) {
                Route::view('twofactors', 'auth.twofactors')->name('twofactors');
            }
            if(config('app.settings.deletaccount')) { 
                Route::view('deleteaccount', 'auth.delete')->name('deleteAccount.view');
                Route::put('deleteaccount', App\Http\Controllers\DeleteAccount::class)->name('deleteAccount.delete');
            }
        });
    });
});

Essentiellement ce sont des routes pour accéder aux formualires du profil et en plus à la fonctionnalité de suppression de compte qui n’est pas géré par Fortify mais que j’ai trouvé judicieux d’ajouter ici.

On peut choisir les fonctionnalités qu’on veut, Fortify ajoute ce fichier de configuration :

On y trouve en particulier ce tableau :

'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication([
        'confirmPassword' => true,
    ]),
],

Il suffit de commenter la fonctionnalité qu’on ne veut pas pour qu’elle disparaisse. Dans mon dépôt j’ai tout activé.

Les assets

Pour le fonctionnement de Paper CSS dans package.json on charge la librairie (dans le déroulement du projet on ne rentre pas directement cette information ici mais on charge avec npm) :

"devDependencies": {
    ...
    "papercss": "^1.8.2",
    ...
}

On crée un fichier sass :

Là on importe la librairie et on ajoute quelques règles :

@import "~papercss/src/styles.scss";

main {
  margin-top: 100px;
}
input.is-invalid {
  border: 2px solid $danger;
}
.red {
  color: $danger;
}
.right {
  float: right;
}
.wide {
  width: 100%;
}

Dans webpack.mix.js :on prévoit ce code :

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

 

Il suffit ensuite de générer pour le développement avec :

npm run dev

On dispose alors d’un fichier CSS ici :

Il suffit ensuite de le charger dans le layout :

<link href="{{ asset('css/app.css') }}" rel="stylesheet">

Je précise que je n’ai utilisé aucune librairie Javascript. Paper CSS n’en a pas besoin et je m’en suis aussi passé pour toutes les actions. C’est d’ailleurs devenu pour moi une habitude après avoir passé de nombreuses années en compagnie de JQuery et ensuite avec Vue.js. Mais les choses évoluent rapidement et les API des navigateurs sont suffisamment performantes pour pouvoir écrire du Javascript léger, d’autant que désormais ES6 est bien supporté.

Pour une application qui réside essentiellement côté serveur on peut s’en sortir efficacement avec du Vanilla Javascript. Et franchement on n’a pas besoin de Liveware (j’en ai fait une courte présentation ici) qui fait un mélange des genres qui ne va pas du tout vers la simplicité malgré les apparences qui sont souvent trompeuses. Mais chacun ses goûts…

Pour une application qui réside essentiellement côté client alors une librairie Javascript se justifie amplement et Laravel beaucoup moins et autant utiliser Lumen qui est une version allégée parfaite pour créer des API. Mais si vous avez envie de plonger dans Inertia pourquoi pas…

Les langues

À la base Laravel est seulement en anglais. J’ai donc complété avec les fichiers du package laravel/lang. d’autre part j’ai complété les traductions avec un fichier JSON :

Ainsi tous les textes sont en français, dans le fichier config.app j’ai localisé :

'locale' => 'fr',

Vous pouvez évidemment revenir en anglais en changeant la valeur ici si Molière vous indispose.

La connexion

Nous allons voir toutes les fonctionnalités en commençant par la connexion. Il faut déclarer la vue à Fortify, ça se passe dans le provider FortifyServiceProvider :

public function boot()
{
    ...

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

    ...
}

J’ai bien prévu cette vue ici :

Le code est léger parce que j’utilise un layout et des vues partielles :

@extends('layouts.card')

@section('card-content')
    <div class="card-header">@lang('Login')</div>

    <div class="card-body">
        <form method="POST" action="{{ route('login') }}">
            @csrf

            @include('partials.forms.email')

            @include('partials.forms.password')

            <div class="form-group">
                <label for="remember" class="paper-check">
                    <input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}> 
                    <span>@lang('Remember Me')</span>
                </label>
            </div>

            @include('partials.forms.submit', [ 'text' => 'Login', ])

            @if (Route::has('password.request'))
                <a class="paper-btn right" href="{{ route('password.request') }}">
                    @lang('Forgot Your Password?')
                </a>
            @endif

        </form>
    </div>
@endsection

On fait appel à deux routes de Fortify :

  • login pour la soumission du formulaire
  • password.request pour l’oubli du mot de passe

On obtient ce formulaire à la mode Paper CSS :

Évidemment la validation est totalement fonctionnelle :

Pour la redirection en cas de succès on a cette valeur dans config.fortify :

'home' => RouteServiceProvider::HOME,

Ce qui nous renvoie dans la classe RouteServiceProvider :

public const HOME = '/home';

Ce sont les valeurs par défaut que je n’ai pas modifiées.

Par défaut Fortify utilise l’email et le mot de passe pour l’authentification mais on peut changer ce comportement, c’est expliqué dans la documentation.

L’inscription

Il faut déclarer la vue à Fortify, ça se passe encore dans le provider FortifyServiceProvider :

public function boot()
{
    ...

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

    ...
}

J’ai créé la vue :

Là aussi le code est léger avec l’utilisation de vue partielles :

@extends('layouts.card')

@section('card-content')
    <div class="card-header">@lang('Register')</div>

    <div class="card-body">
        <form method="POST" action="{{ route('register') }}">
            @csrf

            @include('partials.forms.name')

            @include('partials.forms.email')

            @include('partials.forms.password')

            @include('partials.forms.password-confirm')

            @include('partials.forms.submit', [ 'text' => 'Register', ])
                   
        </form>
    </div>
@endsection

Le formulaire se présente ainsi :

Pour le mot de passe on peut fixer un certain nombre de règles, on dispose pour le faire de l’action PasswordValidationRules :

J’ai prévu la totale :

protected function passwordRules()
{
    return [
      'required', 
      'string', 
      (new Password)->requireUppercase()
                    ->requireNumeric()
                    ->requireSpecialCharacter()
                    ->length(10), 
      'confirmed'
    ];
}

La validation va avec :

La vérification de l’email

Comme j’ai activé la vérification de l’email lorsque quelqu’un s’inscrit il tombe ici :

Pour mémoire ça fonctionne si dans le modèle User on prévoit cette implémentation :

class User extends Authenticatable implements MustVerifyEmail

Et évidemment il faut déclarer la vue à Fortify, ça se passe encore dans le provider FortifyServiceProvider :

public function boot()
{
    ...

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

    ...
}

Si tout va bien l’utilisateur a reçu cet email :

Quand l’utilisateur clique sur le bouton ou sur le lien il est validé et il tombe directement sur son profil :

L’oubli du mot de passe

Il faut déclarer les vues à Fortify, ça se passe encore dans le provider FortifyServiceProvider :

public function boot()
{
    ...

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

    ...
}

Les deux vues concernées sont ici :

Dans la vue de connexion on a un bouton pour l’oubli du mot de passe :

Si on l’utilise on arrive à ce formulaire :

Là encore la validation fonctionne :

Si le mail est reconnu on a une alerte :

L’utilisateur reçoit le mail :

Le bouton et le lien revoient sur le formulaire de réinitialisation :

On a donc le processus classique poru cette réinitialisation.

Les informations personnelles

Sur la page du profil on a un bouton pour accéder au formulaire de modification des données personnelles :

Avec Fortify on dispose d’une route pour la mise à jour des informations personnelles :

J’ai donc juste ajouté la route pour accéder au formulaire (cette route étant désactivée si la fonctionnalité a été désactivée dans Fortify) :

if((Fortify::enabled(Fortify::updateProfileInformation()))) {
    Route::view('profile', 'auth.informations')->name('profile.edit');
}

La vue est ici :

La validation est assurée par Fortify (le traitement se passe dans l’action UpdateUserProfileInformation) :

Notez que par défaut on a un messageBag :

Validator::make($input, [
   ...
])->validateWithBag('updateProfileInformation');

Ce qui se justifie si on a plusieurs formulaires sur une même page, mais comme ce n’est pas le cas je suis revenu à une validation simple :

])->validate();

La modification du mot de passe

Sur la page du profil on a un bouton pour accéder au formulaire de modification du mot de passe :

Avec Fortify on dispose d’une route pour la mise à jour le mot de passe :

J’ai donc juste ajouté la route pour accéder au formulaire (cette route étant désactivée si la fonctionnalité a été désactivée dans Fortify) :

if((Fortify::enabled(Fortify::updatePasswords()))) {
    Route::view('password', 'auth.passwords.edit')->name('password.edit');
}

La vue est ici :

La validation est assurée par Fortify (le traitement se passe dans l’action UpdateUserPassword) :

La confirmation du mot de passe

Les autres éléments du profil (authentification à deux facteurs et suppression du compte sont protégés par une confirmation du mot de passe. J’ai ajouté le middleware password.confirm dans les routes concernées :

Route::middleware('password.confirm')->group(function () {
    if(Fortify::canManageTwoFactorAuthentication()) {
        Route::view('twofactors', 'auth.twofactors')->name('twofactors');
    }
    if(config('app.settings.deletaccount')) { 
        Route::view('deleteaccount', 'auth.delete')->name('deleteAccount.view');
        Route::put('deleteaccount', App\Http\Controllers\DeleteAccount::class)->name('deleteAccount.delete');
    }
});

Il faut préciser la vue à Fortify (FortifyServiceProvider) :

public function boot()
{
    ...

    Fortify::confirmPasswordView(function () {
        return view('auth.passwords.confirm');
    });

    ...
}

La vue concernée est ici :

Les deux routes sont ajoutées par Fortify :

L’authentification à deux facteurs

Activation

On en arrive maintenant à la partie la plus délicate avec l’authentification à deux facteurs. Fortify ajoute 5 routes (la derbière est celle que j’ai ajouté pour accéder au formualire) :

Voyons ça de plus près, dans l’ordre :

  • activation de l’authentification à deux facteurs
  • désactivation de l’authentification à deux facteurs
  • récupération du QR Code
  • récupération des codes de récupération
  • régénération des codes de récupération

Sur la page du profil j’ai prévu un bouton pour accéder au formulaire :

On passe par la confirmation du mot de passe qu’on a vue ci-dessus pour accéder à ce formulaire :

La vue correspondante est ici :

Cette vue comporte pas mal de code conditionnel selon la situation, on a en effet plusieurs états possibles :

  1. si l’authentification à deux facteurs n’est pas activée on a la vue ci-dessus avec juste un bouton pour l’activation.
  2. si l’authentification à deux facteurs vient d’être activée on montre :
    • le QR code
    • les codes de récupération
    • un bouton pour régénérer les codes
    • un bouton de désactivation
  3. si l’authentification à deux facteurs est activée mais qu’on a rechargé la page on ne voit plus le QR code ni les codes de récupération (mais on a un bouton pour les faire réapparaître)

Donc quand on active cette authentification on a cet aspect :

Mais quand on recharge la page :

Mais on peut remontrer les codes de récupération et les régénérer :

Pour la régénération des codes plutôt que de recharger toute la page je suis passé en Ajax avec un peu de Vanilla Javascript :

const sendRequest = async (method) => {
    
    const response = await fetch('{{ url('user/two-factor-recovery-codes') }}', { 
        method: method,
        headers: { 
            'X-CSRF-TOKEN': '{{ csrf_token() }}', 
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
    });

    if(response.ok) {
        return response.json();
    }
}    

const regenerateCodes = async () => {

    await sendRequest('POST');
    const data = await sendRequest('GET');
    
    let dom = '';
    for(element of data) {
        dom += '<div>' + element + '</div>';
    }
    document.querySelector('#codeList').innerHTML = dom;         
}

window.addEventListener('DOMContentLoaded', () => {
    document.querySelector('#recoveryButton').addEventListener('click', regenerateCodes);
})

Avec Fetch on utilise deux réquêtes :

  1. on commence par demander la régénération des codes
  2. ensuite on les récupère pour les afficher

L’authentification

Une fois que l’authentification à deux facteurs est activée on a un formulaire complémentaire quand on se connecte :

Il faut expliquer à Fortify où est la vue :

public function boot()
{
    ...

    Fortify::twoFactorChallengeView(function () {
        return view('auth.challenge');
    });
}

Dans cette vue on a un peu de Javascript pour basculer de la saisie du code d’authentification à cette d’un des codes de récupération (on voit là encore que les librairies Javascript n’apporteraient pas grand chose) :

window.addEventListener('DOMContentLoaded', () => {
    document.querySelector('#command').addEventListener('click', e => {
        e.preventDefault();            
        document.querySelectorAll('.toggle').forEach((element) => {
            element.toggleAttribute('hidden');
        });              
    });
})

Suppression du compte

On en arrive enfin à la dernière fonctionnalité qui n’a rien à voir avec Fortify et sur laquelle je vais passer rapidement : la suppression du compte.

Dans le profil on a ce bouton :

J’ai prévu ces deux routes :

if(config('app.settings.deletaccount')) { 
    Route::view('deleteaccount', 'auth.delete')->name('deleteAccount.view');
    Route::put('deleteaccount', App\Http\Controllers\DeleteAccount::class)->name('deleteAccount.delete');
}

On voit qu’on peut désactiver cette option dans la configuration :

'settings' => [
  'deletaccount' => true,
],

On a un contrôleur pour le traitement :

Evidemment l’accès est protégé par la confirmation du mot de passe. On a ce formulaire tout simple :

Conclusion

On a vue dans cet article comment mettre en oeuvre toutes les possibilités de Fortify avec comme frontend la librairie Paper CSS. On pourrait évidemment faire la même chose avec n’importe quelle autre librairie ou même aucune et du simple CSS.

Print Friendly, PDF & Email

4 commentaires

Laisser un commentaire