Maîtriser SvelteKit : Développement Web Réactif et Ultra-Performant
Maîtriser SvelteKit : Développement Web Réactif et Ultra-Performant

Gestion de l'Authentification et de l'Autorisation dans SvelteKit

Contexte du cours : Maîtriser SvelteKit : Développement Web Réactif et Ultra-Performant

Introduction

Dans le monde du développement web moderne, la sécurité est primordiale. Deux concepts fondamentaux garantissent cette sécurité en contrôlant l'accès aux ressources et aux fonctionnalités d'une application : l'Authentification et l'Autorisation. SvelteKit, avec ses capacités full-stack et son modèle de routage basé sur les fichiers, offre un cadre robuste pour implémenter ces mécanismes de manière efficace et sécurisée.

  • Authentification (Authentication) : Il s'agit du processus de vérification de l'identité d'un utilisateur. En d'autres termes, c'est la réponse à la question "Qui êtes-vous ?". Les méthodes courantes incluent la connexion par nom d'utilisateur/mot de passe, les fournisseurs d'identité tiers (Google, Facebook), les certificats, etc. Une fois authentifié, l'utilisateur est généralement associé à une session ou un jeton.
  • Autorisation (Authorization) : Une fois l'identité d'un utilisateur établie, l'autorisation détermine ce que cet utilisateur est autorisé à faire ou à accéder. C'est la réponse à la question "Avez-vous le droit de faire cela ?". Cela peut impliquer la vérification de rôles (administrateur, utilisateur régulier), de permissions spécifiques (lire, écrire, supprimer), ou d'accès basé sur des ressources.

SvelteKit excelle dans la gestion de ces deux aspects grâce à son approche hybride, permettant de mélanger code côté client (pour l'interface utilisateur) et code côté serveur (pour la logique métier sensible, la persistance des données et la sécurité).

Les Mécanismes Clés de SvelteKit pour l'Auth/Auth

SvelteKit fournit plusieurs primitives puissantes qui sont essentielles pour construire des systèmes d'authentification et d'autorisation sécurisés :

1. Les Hooks Serveur (src/hooks.server.js)

Les hooks serveur sont le cœur de la logique globale côté serveur dans SvelteKit. Le hook handle est particulièrement crucial pour l'authentification et l'autorisation car il s'exécute pour chaque requête entrante, avant même que la page ou l'endpoint API ne soit chargé. Cela en fait l'endroit idéal pour :

  • Analyser les cookies de session : Récupérer un jeton de session ou un ID utilisateur à partir des cookies.
  • Charger les données utilisateur : À partir du jeton ou de l'ID, interroger une base de données pour récupérer les informations complètes de l'utilisateur (rôles, permissions).
  • Enrichir l'objet event.locals : Attacher les informations de l'utilisateur authentifié (ou null si non authentifié) à l'objet event.locals. Ces informations seront ensuite accessibles dans les fonctions load des pages et les endpoints API.
  • Gérer les sessions : Mettre à jour, créer ou supprimer des cookies de session.

Exemple de src/hooks.server.js pour la gestion de session

// src/hooks.server.js
import { redirect } from '@sveltejs/kit';

// Simulate a user database
const users = [
    { id: '123', username: 'alice', password: 'password123', role: 'admin' },
    { id: '456', username: 'bob', password: 'password456', role: 'user' }
];

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
    // 1. Lire le cookie de session
    const sessionCookie = event.cookies.get('session_id');

    if (sessionCookie) {
        // En production, vous décoderiez un JWT ou interrogeriez une BDD de sessions
        // Pour cet exemple, nous simulons la récupération de l'utilisateur à partir d'un ID de session simple.
        const user = users.find(u => u.id === sessionCookie);

        if (user) {
            // 2. Attacher les informations de l'utilisateur à `event.locals`
            // Ces informations seront disponibles dans les fonctions `load` et les +server.js
            event.locals.user = {
                id: user.id,
                username: user.username,
                role: user.role
            };
        } else {
            // Le cookie de session est invalide, le supprimer
            event.cookies.delete('session_id', { path: '/' });
        }
    } else {
        // Aucune session trouvée
        event.locals.user = null;
    }

    // Si la route actuelle est protégée et l'utilisateur n'est pas authentifié
    // Note: Une approche plus robuste utiliserait une liste de routes protégées ou des marqueurs
    const protectedRoutes = ['/admin', '/profile'];
    const currentPath = event.url.pathname;

    if (protectedRoutes.some(route => currentPath.startsWith(route)) && !event.locals.user) {
        // Empêcher l'accès et rediriger vers la page de connexion
        // throw redirect(302, '/login'); // Décommenter pour une redirection forcée
    }

    // 3. Poursuivre le traitement de la requête
    const response = await resolve(event);
    return response;
}

Explication du code : Le hook handle intercepte chaque requête. Il tente de lire un cookie nommé session_id. Si ce cookie existe et correspond à un utilisateur (simulé ici), les informations de cet utilisateur sont stockées dans event.locals.user. event.locals est un objet éphémère qui est réinitialisé à chaque requête et est accessible par tous les autres modules côté serveur pour la requête en cours. C'est le moyen recommandé de passer des données entre les hooks, les fonctions load et les endpoints +server.js.

2. Les Fonctions load côté Serveur (+page.server.js)

Les fonctions load dans les fichiers +page.server.js sont exécutées sur le serveur avant le rendu d'une page. Elles sont parfaites pour :

  • Protéger des pages entières : Vérifier si l'utilisateur est authentifié et/ou autorisé. Si ce n'est pas le cas, vous pouvez lancer une redirection (throw redirect(...)) ou une erreur (throw error(...)).
  • Charger des données spécifiques à l'utilisateur : Accéder à event.locals.user pour récupérer des données personnalisées pour la page.

Exemple de +page.server.js protégeant une page

// src/routes/profile/+page.server.js
import { redirect } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load({ parent, locals }) {
    // Accéder aux informations utilisateur attachées par le hook `handle`
    const user = locals.user;

    // 1. Vérification de l'authentification
    if (!user) {
        throw redirect(302, '/login'); // Redirige vers la page de connexion si non authentifié
    }

    // 2. Vérification de l'autorisation (exemple : seul un administrateur peut accéder à cette page)
    if (user.role !== 'admin') {
        // Ou vous pouvez lancer une erreur 403 (Forbidden)
        // throw error(403, 'Accès non autorisé');
        throw redirect(302, '/dashboard'); // Redirige si l'utilisateur n'a pas le bon rôle
    }

    // Si l'utilisateur est authentifié et autorisé, charger des données spécifiques
    // Ici, on simule le chargement des préférences utilisateur
    const userPreferences = {
        theme: 'dark',
        notifications: true
    };

    return {
        // Les données retournées par `load` sont disponibles dans `+page.svelte`
        username: user.username,
        role: user.role,
        preferences: userPreferences
    };
}

Explication du code : Cette fonction load vérifie d'abord si locals.user existe. Si non, elle redirige l'utilisateur vers la page de connexion. Ensuite, elle effectue une vérification d'autorisation basée sur le rôle. Si toutes les vérifications passent, elle charge des données spécifiques à l'utilisateur et les rend disponibles pour le composant Svelte de la page (+page.svelte).

3. Les Endpoints API (+server.js)

Les fichiers +server.js vous permettent de créer des endpoints API RESTful. Ils sont essentiels pour gérer la logique d'authentification comme la connexion (POST), la déconnexion (DELETE) et l'inscription (POST).

  • Validation des informations d'identification : Recevoir le nom d'utilisateur et le mot de passe, les valider par rapport à votre base de données.
  • Gestion des sessions/tokens : Créer des cookies de session sécurisés (par exemple, HttpOnly, SameSite=Lax, Secure) après une connexion réussie.
  • Points d'entrée pour l'autorisation : Comme les fonctions load, les endpoints +server.js ont accès à event.locals.user pour déterminer si la requête est autorisée à effectuer une opération.

Exemple de +server.js pour la connexion et déconnexion

// src/routes/auth/+server.js
import { json, redirect } from '@sveltejs/kit';

// Simulation d'une base de données utilisateurs (en production, utilisez une vraie BDD et hashage de mots de passe)
const usersDb = [
    { id: 'user123', username: 'john.doe', hashedPassword: 'hashed_password_john', role: 'user' },
    { id: 'admin456', username: 'jane.admin', hashedPassword: 'hashed_password_jane', role: 'admin' }
];

/** @type {import('./$types').RequestHandler} */
export async function POST({ request, cookies }) {
    const { username, password } = await request.json();

    // 1. Authentification : Vérifier les identifiants
    const user = usersDb.find(u => u.username === username);

    // En production, vous utiliseriez bcrypt.compare(password, user.hashedPassword)
    if (!user || password !== 'test_password') { // Simplifié pour l'exemple
        return new Response(JSON.stringify({ message: 'Identifiants invalides' }), { status: 401 });
    }

    // 2. Création de session : Définir un cookie de session
    // En production, générez un ID de session unique et stockez-le en BDD avec une date d'expiration
    const sessionId = user.id; // Utilisation de l'ID utilisateur comme ID de session pour la simplicité

    cookies.set('session_id', sessionId, {
        path: '/',
        httpOnly: true, // Empêche l'accès via JavaScript côté client
        secure: process.env.NODE_ENV === 'production', // N'envoyer que via HTTPS en production
        sameSite: 'lax', // Protège contre les attaques CSRF
        maxAge: 60 * 60 * 24 * 7 // 1 semaine
    });

    return new Response(JSON.stringify({ message: 'Connexion réussie', user: { id: user.id, username: user.username, role: user.role } }), { status: 200 });
}

/** @type {import('./$types').RequestHandler} */
export async function DELETE({ cookies }) {
    // Déconnexion : Supprimer le cookie de session
    cookies.delete('session_id', { path: '/' });

    // Vous pouvez aussi rediriger l'utilisateur après déconnexion
    // throw redirect(302, '/');

    return new Response(JSON.stringify({ message: 'Déconnexion réussie' }), { status: 200 });
}

Explication du code : L'endpoint POST gère la connexion : il reçoit le nom d'utilisateur et le mot de passe, les valide et, en cas de succès, crée un cookie de session. L'endpoint DELETE gère la déconnexion en supprimant ce même cookie. Notez l'utilisation des options httpOnly, secure et sameSite pour renforcer la sécurité du cookie.

4. Les Form Actions (+page.server.js)

Les Form Actions sont une fonctionnalité élégante de SvelteKit qui permet de gérer les soumissions de formulaires directement dans +page.server.js, sans avoir besoin d'endpoints API séparés pour chaque formulaire. Elles sont parfaites pour les formulaires de connexion, d'inscription, de mise à jour de profil, etc.

Leur avantage majeur est qu'elles s'intègrent naturellement avec les mécanismes de rendu de SvelteKit, gérant automatiquement les redirections, les erreurs et la réinitialisation des formulaires.

Exemple de +page.server.js avec Form Actions pour la connexion/déconnexion

// src/routes/login/+page.server.js
import { redirect, fail } from '@sveltejs/kit';

// Simuler la base de données utilisateurs (identique à l'exemple +server.js)
const usersDb = [
    { id: 'user123', username: 'john.doe', hashedPassword: 'hashed_password_john', role: 'user' },
    { id: 'admin456', username: 'jane.admin', hashedPassword: 'hashed_password_jane', role: 'admin' }
];

/** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) {
    // Si l'utilisateur est déjà connecté, rediriger vers le tableau de bord
    if (locals.user) {
        throw redirect(302, '/dashboard');
    }
    return {}; // Retourne un objet vide si l'utilisateur n'est pas connecté
}

/** @type {import('./$types').Actions} */
export const actions = {
    login: async ({ request, cookies }) => {
        const data = await request.formData();
        const username = data.get('username');
        const password = data.get('password');

        if (!username || !password) {
            return fail(400, { username, missing: true, message: 'Veuillez remplir tous les champs.' });
        }

        const user = usersDb.find(u => u.username === username);

        if (!user || password !== 'test_password') { // Simplifié
            return fail(401, { username, incorrect: true, message: 'Nom d\'utilisateur ou mot de passe incorrect.' });
        }

        const sessionId = user.id;
        cookies.set('session_id', sessionId, {
            path: '/',
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'lax',
            maxAge: 60 * 60 * 24 * 7
        });

        throw redirect(302, '/dashboard'); // Rediriger après une connexion réussie
    },
    logout: async ({ cookies }) => {
        cookies.delete('session_id', { path: '/' });
        throw redirect(302, '/'); // Rediriger vers la page d'accueil après déconnexion
    }
};
<!-- src/routes/login/+page.svelte -->
<script>
    import { enhance } from '$app/forms';
    export let form; // Accède aux données retournées par `fail` des actions
</script>

<h1>Connexion</h1>

<form method="POST" action="?/login" use:enhance>
    <label for="username">Nom d'utilisateur:</label>
    <input type="text" id="username" name="username" value={form?.username ?? ''} />
    {#if form?.missing && form?.username === undefined}
        <p class="error">Veuillez entrer un nom d'utilisateur.</p>
    {/if}
    
    <label for="password">Mot de passe:</label>
    <input type="password" id="password" name="password" />
    {#if form?.missing && form?.password === undefined}
        <p class="error">Veuillez entrer un mot de passe.</p>
    {/if}

    {#if form?.incorrect}
        <p class="error">{form.message}</p>
    {/if}

    <button type="submit">Se connecter</button>
</form>

<style>
    .error {
        color: red;
        font-size: 0.9em;
    }
</style>

Explication du code :

  • +page.server.js: Le bloc actions contient deux fonctions : login et logout. Elles reçoivent l'objet request (pour les données du formulaire) et cookies. login valide les informations d'identification et crée un cookie de session avant de rediriger. logout supprime le cookie et redirige. L'utilisation de fail est cruciale pour retourner des messages d'erreur au client sans provoquer de redirection complète, permettant d'afficher des messages d'erreur spécifiques au formulaire.
  • +page.svelte: Le formulaire utilise method="POST" et action="?/login" pour cibler l'action login dans +page.server.js. L'attribut use:enhance améliore l'expérience utilisateur en permettant des soumissions de formulaire sans rechargement complet de la page (SPA-like), tout en gardant la logique côté serveur. Les variables form contiennent les données retournées par fail, permettant d'afficher les messages d'erreur.

Flux Pratique d'Authentification et d'Autorisation

Reprenons le cheminement d'une requête :

  1. Requête du navigateur : Un utilisateur demande /profile.
  2. src/hooks.server.js : Intercepte la requête.
    • Lit le cookie session_id.
    • Si le cookie est valide, charge l'utilisateur (par ex. john.doe, role: 'user') et l'attache à event.locals.user.
    • Si le cookie est manquant ou invalide, event.locals.user reste null.
  3. /profile/+page.server.js load function : S'exécute sur le serveur.
    • Accède à locals.user.
    • Si locals.user est null ou si le user.role n'est pas "admin", il peut throw redirect(302, '/login') ou throw error(403, 'Accès non autorisé').
    • Si locals.user est présent et autorisé, il charge les données spécifiques au profil et les retourne.
  4. Rendu de la page : SvelteKit rend src/routes/profile/+page.svelte en utilisant les données fournies par la fonction load.
  5. Actions Utilisateur :
    • L'utilisateur se déconnecte via un bouton (<form action="?/logout" method="POST">).
    • L'action logout dans src/routes/login/+page.server.js (ou src/routes/auth/+server.js) s'exécute, supprime le cookie de session et redirige l'utilisateur.

Considérations Avancées et Bonnes Pratiques

  • Hashing de Mots de Passe : Ne jamais stocker les mots de passe en clair. Utilisez des fonctions de hachage robustes comme bcrypt (synchrones pour les logins, asynchrones pour les inscriptions) pour stocker les hachages des mots de passe.
  • Tokens (JWT) : Pour les applications à grande échelle ou les API sans état, les JSON Web Tokens (JWT) peuvent être une alternative aux sessions gérées par le serveur. SvelteKit peut gérer les JWT en les stockant dans des cookies HttpOnly et en les validant dans les hooks.
  • Fournisseurs d'Authentification Tiers (OAuth, OpenID Connect) : Pour simplifier l'authentification (Google, GitHub, etc.), des bibliothèques comme Auth.js (initialement NextAuth.js) ou Lucia offrent des abstractions puissantes pour SvelteKit.
  • Variables d'Environnement : Utilisez import.meta.env.VITE_... pour les variables côté client et process.env.VITE_... (ou directement process.env.... si SvelteKit est configuré) pour les variables côté serveur. Ne jamais exposer de clés secrètes côté client.
  • Protection CSRF : SvelteKit protège en grande partie contre les attaques CSRF pour les Form Actions. Pour les endpoints +server.js qui acceptent des requêtes POST/PUT/DELETE, assurez-vous d'implémenter des protections appropriées (par exemple, jetons CSRF ou vérification de l'en-tête Referer).
  • Gestion des Erreurs : Utilisez error et fail de @sveltejs/kit pour gérer les erreurs et fournir un feedback clair à l'utilisateur.
  • Sécurité des Cookies : Assurez-vous que vos cookies de session sont configurés avec les attributs httpOnly, secure, sameSite, et un maxAge approprié.

Conclusion

L'authentification et l'autorisation sont des piliers de la sécurité web. SvelteKit fournit un ensemble d'outils puissants et flexibles – les hooks serveur, les fonctions load côté serveur, les endpoints +server.js et les Form Actions – pour implémenter ces fonctionnalités de manière robuste.

En tirant parti de ces mécanismes, vous pouvez créer des applications SvelteKit sécurisées qui gèrent efficacement l'identité des utilisateurs et contrôlent leur accès aux ressources, tout en offrant une expérience de développement fluide et performante. N'oubliez jamais l'importance des bonnes pratiques de sécurité, notamment le hachage des mots de passe et la configuration sécurisée des cookies, pour protéger votre application et vos utilisateurs.