Connecter le front-end, VitePress et le back-end, Laravel

Fait partie de la série Améliorer votre blog VitePress avec une API Laravel

La synchronisation des articles est en place. L'étape suivante consiste à connecter le site VitePress à l'API Laravel.

Comme les deux applications seront déployées séparément, il ne suffit pas d'envoyer une requête vers une URL. Nous devons configurer CORS (Cross-Origin Resource Sharing) et la protection contre les CSRF (Cross-Site Request Forgery), puis faire en sorte que le navigateur envoie les identifiants dont Laravel a besoin pour l'authentification par session.

La configuration peut sembler intimidante au début, mais nous allons traiter chaque élément séparément. Laravel fournit déjà la plupart des briques nécessaires.

Tip

Je vous recommande de lire les articles liés si vous souhaitez approfondir ces concepts. La configuration de cet article sera plus facile à comprendre.

Installer Laravel Sanctum

L'écosystème Laravel fournit Sanctum, qui prend en charge les jetons d'API et l'authentification par cookie pour les applications monopages (SPA).

Pour commencer, installons Sanctum :

bash
php artisan install:api

Publions ensuite le fichier de configuration de CORS :

bash
php artisan config:publish cors

CORS, ou Cross-Origin Resource Sharing, est un mécanisme de sécurité du navigateur. Par défaut, les navigateurs empêchent une page d'effectuer des requêtes vers une autre origine. Cela protège les utilisateurs contre les sites malveillants qui tenteraient d'effectuer des actions en leur nom.

Notre front-end doit communiquer avec une API hébergée sur une autre origine. Nous devons donc autoriser explicitement cette origine dans la configuration de l'API.

Configurons CORS pour autoriser les requêtes provenant de notre front-end :

php
<?php

declare(strict_types=1);

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    'allowed_origins' => [
        env('APP_FRONTEND_URL'), 
    ],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true, 

];

Nous parlerons du déploiement plus tard, mais retenez que nous créons deux applications : le blog et l'API qui l'enrichit. Elles seront déployées séparément, c'est pourquoi l'API doit autoriser les requêtes provenant du blog.

Note

Nous pourrions déployer les deux applications derrière le même domaine avec un reverse proxy, mais cela ajouterait une complexité dont nous n'avons pas besoin ici.

Configurons ensuite Sanctum pour autoriser les requêtes de notre front-end :

php
return [
    'stateful' => explode(',', (string) env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost:4173,localhost:5173', 
        Sanctum::currentApplicationUrlWithPort()
    ))),

  // ...
]

La modification importante consiste à ajouter les origines de développement du front-end aux domaines stateful de Sanctum.

Une origine de navigateur combine le protocole, le nom d'hôte et le port. En développement, localhost:5173 et localhost:8000 sont des origines différentes, même si elles partagent le même nom d'hôte.

Ajoutons ensuite un middleware :

php
return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->statefulApi(); 
    })
     ->create();

Ce middleware indique à Sanctum d'authentifier les requêtes avec le cookie de session Laravel plutôt qu'avec un jeton d'API. C'est l'approche que nous utiliserons depuis le front-end.

Enfin, modifions le middleware d'authentification dans routes/api.php. En réalité, nous utilisons le middleware Laravel par défaut, qui garantit que les requêtes respectent la configuration de Sanctum, notamment les domaines stateful.

php
Route::middleware('auth:sanctum')->group(function () { 
    // Handle authenticated requests...
});

Notre back-end est maintenant prêt à accepter les requêtes du front-end.

Avant de passer au front-end, terminons quelques modifications du back-end.

  1. Mettez à jour store dans RegisteredViaGitHubController afin qu'elle redirige l'utilisateur vers le front-end :
php
final class RegisteredViaGitHubController extends Controller
{
    public function store(): RedirectResponse
    {
        // ...

        return redirect(Config::get('app.frontend_url'));
    }
}

Mettez également son test à jour :

php
// ...

test('redirects to the frontend', function () {
    fakeGitHubUser();

    $this->get(route('auth.github.callback'))
        ->assertRedirect(config('app.frontend_url')); 
});

// ...
  1. Ajoutez un seeder qui génère des commentaires d'exemple :

Tip

Supprimez le contenu existant de la méthode run du seeder avant d'ajouter ce code.

php
use App\Models\Post;

final class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        Post::factory()
            ->count(10)
            ->hasComments(5)
            ->create();
    }
}

Cela crée 10 articles avec cinq commentaires chacun, ce qui nous donne des données pour tester le front-end.

  1. Recréez la base de données et exécutez les migrations :
bash
rm database/database.sqlite && touch database/database.sqlite && php artisan migrate --seed

Nous recréons la base de données car, jusqu'à présent, les tests utilisaient une base SQLite en mémoire. La base locale contient encore la table users créée par le scaffold initial, et non la version adaptée à l'authentification GitHub.

  1. Définissez APP_FRONTEND_URL dans .env et .env.example avec l'URL de développement du front-end :
ini
APP_FRONTEND_URL=http://localhost:5173

Jusqu'ici, elle pointait vers http://localhost:4173, le serveur de prévisualisation VitePress.

Se connecter au back-end depuis le front-end

Dans cette section, nous effectuerons les premières requêtes depuis le front-end et vérifierons que la connexion fonctionne.

Warning

Ce n'est pas la version finale du code. Nous l'améliorerons dans l'article suivant.

Authentification sociale

La première fonctionnalité que nous pouvons tester est l'authentification sociale. Nous ajouterons un bouton dans l'en-tête qui pointe vers /auth/github/redirect sur le back-end. Comme l'URL du back-end sera différente en développement et en production, nous stockerons son URL de base dans une variable d'environnement. L'utilisateur sera ensuite redirigé vers GitHub, puis vers le front-end après avoir autorisé l'application.

Pour cette variable d'environnement, nous pouvons utiliser le préfixe VITE_ afin de l'exposer au front-end :

ini
VITE_API_URL=http://localhost:8000

Tip

Vous pouvez ajouter cette variable sans risque au fichier .env.example.

Ajoutez maintenant le bouton à l'en-tête :

vue
<script lang="ts" setup>
// ...

const loginUrl = `${import.meta.env.VITE_API_URL}/auth/github/redirect`
</script>

<template>
  <header class="border-b-4 border-black bg-white p-8">
    <!-- ... -->

    <div class="flex items-center gap-6">
      <!-- ... -->

      <Button :href="loginUrl" label="Connexion" /> 
    </div>
  </header>
</template>

Nous utilisons VITE_API_URL pour construire l'URL de connexion. Lorsque Vite démarre, il charge src/.env et expose les variables préfixées par VITE_ via import.meta.env.

Nous pouvons également ajouter les types de cet objet afin d'améliorer l'expérience de développement.

  1. Créez un fichier vite-env.d.ts à la racine du projet :
ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
  1. Installez Vite comme dépendance de développement :
bash
npm install -D vite

Note

Le gestionnaire de paquets pnpm ne rend accessibles que les packages installés directement dans le projet. Même si VitePress utilise Vite, nous devons donc l'installer pour accéder à ses types.

  1. Ajoutez le fichier vite-env.d.ts à tsconfig.json :
json
{
  "include": [
    "./.vitepress/**/*",
    "vite-env.d.ts"
  ]
}

import.meta.env est maintenant typé et TypeScript connaît VITE_API_URL.

En cliquant sur le bouton, nous devons être redirigés vers GitHub pour l'autorisation, puis vers le front-end. La première connexion fonctionne.

Créer un commentaire

La deuxième fonctionnalité que nous pouvons tester est la création d'un commentaire. Si elle fonctionne, nous pourrons passer à l'article suivant en sachant que le back-end et le front-end communiquent correctement.

Pour effectuer les requêtes, nous utiliserons ofetch. Il encapsule l'API native fetch et ajoute la sérialisation des corps JSON, les tentatives automatiques, une URL de base et des intercepteurs.

bash
pnpm add ofetch

Nous effectuerons la première requête depuis Layout.vue comme test temporaire.

Commençons par une requête simple :

vue
<script lang="ts" setup>
import { ofetch } from 'ofetch'

ofetch('/api/posts/1/comments', {
  baseURL: import.meta.env.VITE_API_URL,
  method: 'POST',
  body: {
    content: 'Hello world!',
  },
  headers: {
    Accept: 'application/json',
  }
})
</script>

Cette requête semble suffisante, mais l'onglet réseau du navigateur montre une réponse 419. L'aperçu de la réponse contient :

json
{
  "message": "CSRF token mismatch."
  // ...
}

L'erreur est explicite : la requête a besoin d'un jeton CSRF.

Qu'est-ce qu'un jeton CSRF et comment le gérer dans une SPA ?

Un jeton CSRF contribue à empêcher les attaques Cross-Site Request Forgery. Il permet au serveur de vérifier qu'une requête qui modifie l'état vient bien de notre application et non d'un site malveillant agissant au nom d'un utilisateur authentifié. Dans une application rendue côté serveur, le jeton est intégré au HTML. Dans une SPA, nous devons le récupérer et l'envoyer séparément.

Laravel stocke le jeton dans la session et place également une valeur chiffrée dans un cookie que le JavaScript de notre site peut lire. Nous envoyons cette valeur dans l'en-tête X-XSRF-TOKEN, et Laravel la compare à celle de la session.

Note

Nous ne pouvons pas faire confiance au cookie seul, car le navigateur l'envoie automatiquement avec les requêtes. C'est l'en-tête explicite qui prouve que notre JavaScript a volontairement inclus le jeton.

Ajoutons le jeton CSRF à la requête :

vue
<script lang="ts" setup>
import { ofetch } from 'ofetch'

function getCookie(name: string) {
  const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`))
  return match ? match[2] : null
}

ofetch('/api/posts/1/comments', {
  // ...
  onRequest: async ({ options }) => {
    const method = options.method?.toUpperCase() || 'GET'

    if (['GET', 'HEAD', 'OPTIONS'].includes(method)) {
      return
    }

    const xsrfToken = getCookie('XSRF-TOKEN')

    if (!xsrfToken) {
      await ofetch('/sanctum/csrf-cookie', {
        baseURL: import.meta.env.VITE_API_URL,
        credentials: 'include',
      })
    }

    options.headers.set('X-Xsrf-Token', decodeURIComponent(getCookie('XSRF-TOKEN') || ''))
  },
})
</script>

Cette implémentation utilise un intercepteur ofetch pour ajouter automatiquement le jeton CSRF. Un intercepteur est appelé à une étape précise du cycle de vie de la requête. Ici, onRequest modifie la requête sortante.

Nous avons besoin du jeton uniquement pour les requêtes qui modifient l'état, nous ignorons donc les méthodes sûres. L'ajouter à ces méthodes ne serait pas forcément dangereux, mais cela déclencherait un travail inutile.

Nous lisons ensuite le jeton dans le cookie. S'il n'existe pas, nous appelons l'endpoint /sanctum/csrf-cookie de Sanctum pour le créer.

Enfin, nous ajoutons le jeton aux en-têtes de la requête. Grâce à cette aide, le front-end peut effectuer des requêtes qui modifient l'état sans répéter la configuration CSRF à chaque fois.

Nous rendons cette aide complète dès maintenant, car nous la réutiliserons dans l'article suivant.

Actualisez la page. La requête échoue toujours avec le statut 419. Inspectez-la :

http
POST /api/posts/1/comments HTTP/1.1
Origin: http://localhost:5173
accept: application/json
x-xsrf-token: eyJpdiI6IktwK1I0UXA3K3F6Y3BvMEdKWHhGblE9PSIsInZhbHVlIjoidlgyMXlSbmlRMjhuNFNhODBLMHdUSkFJSDlQZzhObjJ2VDBIOWd1WjBrMzRZVWdqZldMZWxKcFlDMHB5ajZxZTY5bWR2amJLNTdhTFhtRHJqb3kxZlEvOFptNzdMSmZHU1FmeUw0d0pIRE1DWHRCSmZ1eFJ4aDBhN21qMW5KcDEiLCJtYWMiOiJmYjgyN2E1ZDAxMjMxZWZiZDk4MjU4MzAxOGE4OTYwOWY4ZmM1OGE3ZWMyYzM2Y2FjMTM4YWIwNGVmMDU1M2NhIiwidGFnIjoiIn0=

La requête contient l'en-tête x-xsrf-token, mais pas d'en-tête cookie. C'est un problème, car l'utilisateur est authentifié avec un cookie de session. Sans le cookie, le back-end ne peut ni authentifier l'utilisateur ni comparer le jeton CSRF à la session.

Pour corriger cela, ajoutez credentials: 'include' à la requête. Le navigateur inclura ainsi les cookies dans une requête cross-origin, exactement ce dont nous avons besoin.

vue
<script lang="ts" setup>
ofetch('/api/posts/1/comments', {
  // ...
  credentials: 'include', 
})
</script>

La requête fonctionne maintenant. 🎉

Dernières réflexions

La connexion fonctionne maintenant de bout en bout. Le front-end peut appeler Laravel entre deux origines, obtenir un jeton CSRF si nécessaire et inclure le cookie de session requis pour les requêtes authentifiées.

Cette configuration nous fournit une fondation API réutilisable plutôt qu'une requête isolée. Ensuite, nous construirons une couche d'authentification partagée avec Pinia Colada afin que les composants puissent réagir à l'utilisateur courant et le déconnecter sans dupliquer la logique des requêtes.

Pd

Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.

Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.

Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.

J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !

Continuer la lectureAuthentifier les utilisateurs du front-end au back-end

Réactions

Discussions

Ajouter un commentaire

Vous devez être connecté pour accéder à cette fonctionnalité.