Front-end angularforall.com

- localStorage vs sessionStorage vs cookies : différences

Localstorage Sessionstorage Cookies Httponly-Cookie Samesite Xss Csrf Broadcastchannel Storage-Api Quotaexceedederror Web-Crypto-Api Javascript
localStorage vs sessionStorage vs cookies : différences

localStorage vs sessionStorage vs cookies : quotas, securite XSS/CSRF, flags httpOnly SameSite, wrapper TypeScript et synchronisation BroadcastChannel.

Introduction aux solutions de stockage côté client

Les applications web modernes ont souvent besoin de stocker des données côté client pour améliorer l'expérience utilisateur, réduire les appels serveur et permettre un fonctionnement hors ligne partiel. JavaScript propose trois solutions principales : localStorage, sessionStorage et les cookies.

Chacune de ces technologies a ses propres caractéristiques, avantages et limitations. Comprendre leurs différences est essentiel pour choisir la solution adaptée à vos besoins spécifiques.

À retenir : Le choix entre localStorage, sessionStorage et cookies dépend de trois critères principaux : la durée de vie des données, leur taille et la nécessité ou non de les envoyer au serveur.

localStorage : stockage persistant

Le localStorage est un mécanisme de stockage de type clé-valeur qui persiste même après la fermeture du navigateur. Les données restent disponibles tant qu'elles ne sont pas explicitement supprimées.

Caractéristiques principales

  • Capacité : 5 à 10 MB selon les navigateurs (généralement 5 MB)
  • Persistance : Les données survivent à la fermeture du navigateur
  • Portée : Partagé entre tous les onglets et fenêtres du même domaine
  • Format : Stockage uniquement de chaînes de caractères (strings)
  • Synchrone : Les opérations bloquent le thread principal

API localStorage

L'API est simple et intuitive, avec quatre méthodes principales :

// Stocker une donnée
localStorage.setItem('username', 'JohnDoe');
localStorage.setItem('theme', 'dark');

// Récupérer une donnée
const username = localStorage.getItem('username');
console.log(username); // "JohnDoe"

// Supprimer une donnée spécifique
localStorage.removeItem('theme');

// Supprimer toutes les données
localStorage.clear();

// Accéder directement (syntaxe alternative, moins recommandée)
localStorage.username = 'JaneDoe';
const user = localStorage.username;

Stocker des objets JSON

Comme localStorage stocke uniquement des chaînes, il faut sérialiser les objets :

// Stocker un objet
const user = {
    name: 'Alice',
    age: 30,
    preferences: { theme: 'dark', language: 'fr' }
};
localStorage.setItem('user', JSON.stringify(user));

// Récupérer l'objet
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // "Alice"

// Vérifier l'existence avant de parser
const theme = localStorage.getItem('theme');
if (theme) {
    console.log('Theme saved:', theme);
}

Vérifier la capacité disponible

// Estimer la taille utilisée en localStorage
function getLocalStorageSize() {
    let total = 0;
    for (let key in localStorage) {
        if (localStorage.hasOwnProperty(key)) {
            total += localStorage[key].length + key.length;
        }
    }
    return (total / 1024).toFixed(2) + ' KB';
}
console.log('localStorage size:', getLocalStorageSize());
À retenir : localStorage est parfait pour les préférences utilisateur, le cache de données et tout ce qui doit persister entre les sessions.

sessionStorage : stockage temporaire

Le sessionStorage fonctionne exactement comme localStorage, mais avec une durée de vie limitée à la session du navigateur. Les données sont automatiquement supprimées lorsque l'onglet est fermé.

Caractéristiques principales

  • Capacité : Identique à localStorage (5 à 10 MB)
  • Persistance : Données effacées à la fermeture de l'onglet
  • Portée : Isolé par onglet – chaque onglet a son propre sessionStorage
  • Format : Stockage uniquement de chaînes de caractères
  • API : Identique à localStorage

API sessionStorage

L'API est identique à localStorage :

// Stocker une donnée
sessionStorage.setItem('currentStep', '3');
sessionStorage.setItem('formData', JSON.stringify({ email: 'test@example.com' }));

// Récupérer une donnée
const step = sessionStorage.getItem('currentStep');
console.log(step); // "3"

// Supprimer une donnée
sessionStorage.removeItem('currentStep');

// Tout supprimer
sessionStorage.clear();

Exemple pratique : formulaire multi-étapes

// Sauvegarder l'état du formulaire à chaque étape
class MultiStepForm {
    constructor() {
        this.storageKey = 'multistep-form-data';
        this.loadData();
    }

    saveStep(stepNumber, data) {
        const formData = this.getData() || {};
        formData[`step${stepNumber}`] = data;
        formData.currentStep = stepNumber;
        sessionStorage.setItem(this.storageKey, JSON.stringify(formData));
    }

    getData() {
        const data = sessionStorage.getItem(this.storageKey);
        return data ? JSON.parse(data) : null;
    }

    loadData() {
        const data = this.getData();
        if (data && data.currentStep) {
            console.log(`Reprendre à l'étape ${data.currentStep}`);
            return data;
        }
        return null;
    }

    clearData() {
        sessionStorage.removeItem(this.storageKey);
    }
}

// Utilisation
const form = new MultiStepForm();
form.saveStep(1, { name: 'John', email: 'john@example.com' });
form.saveStep(2, { address: '123 Main St', city: 'Paris' });
À retenir : sessionStorage est idéal pour les données temporaires qui ne doivent pas persister entre les sessions (formulaires multi-étapes, état de navigation, données de session).

Cookies : stockage avec communication serveur

Les cookies sont des petits fichiers texte envoyés par le serveur et stockés par le navigateur. Contrairement à localStorage et sessionStorage, les cookies sont automatiquement envoyés au serveur à chaque requête HTTP.

Caractéristiques principales

  • Capacité : Très limitée – 4 KB maximum par cookie
  • Nombre : Maximum ~50 cookies par domaine (varie selon les navigateurs)
  • Persistance : Contrôlable via l'attribut expires ou max-age
  • Portée : Configurable via les attributs domain et path
  • Communication serveur : Envoyés automatiquement dans les headers HTTP
  • Sécurité : Attributs Secure, HttpOnly, SameSite

Créer et lire des cookies en JavaScript

// Créer un cookie simple
document.cookie = "username=JohnDoe";

// Créer un cookie avec expiration (30 jours)
const expires = new Date();
expires.setDate(expires.getDate() + 30);
document.cookie = `token=abc123; expires=${expires.toUTCString()}; path=/`;

// Créer un cookie sécurisé (HTTPS uniquement)
document.cookie = "sessionId=xyz789; Secure; SameSite=Strict; path=/";

// Lire tous les cookies
console.log(document.cookie); // "username=JohnDoe; token=abc123; sessionId=xyz789"

Helper pour manipuler les cookies

const CookieManager = {
    // Créer ou mettre à jour un cookie
    set(name, value, days = 7, options = {}) {
        let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;

        if (days) {
            const expires = new Date();
            expires.setDate(expires.getDate() + days);
            cookie += `; expires=${expires.toUTCString()}`;
        }

        cookie += `; path=${options.path || '/'}`;

        if (options.domain) {
            cookie += `; domain=${options.domain}`;
        }

        if (options.secure) {
            cookie += '; Secure';
        }

        if (options.sameSite) {
            cookie += `; SameSite=${options.sameSite}`;
        }

        document.cookie = cookie;
    },

    // Lire un cookie
    get(name) {
        const nameEQ = encodeURIComponent(name) + '=';
        const cookies = document.cookie.split(';');

        for (let cookie of cookies) {
            cookie = cookie.trim();
            if (cookie.startsWith(nameEQ)) {
                return decodeURIComponent(cookie.substring(nameEQ.length));
            }
        }
        return null;
    },

    // Supprimer un cookie
    delete(name, options = {}) {
        this.set(name, '', -1, options);
    },

    // Vérifier l'existence
    exists(name) {
        return this.get(name) !== null;
    }
};

// Utilisation
CookieManager.set('userTheme', 'dark', 365);
const theme = CookieManager.get('userTheme');
CookieManager.delete('oldCookie');

Cookies côté serveur (exemple PHP)

// Créer un cookie (PHP)
setcookie('user_id', '12345', [
    'expires' => time() + 86400, // 24 heures
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'
]);

// Lire un cookie
$userId = $_COOKIE['user_id'] ?? null;
À retenir : Les cookies sont indispensables pour l'authentification et la communication serveur, mais leur faible capacité (4 KB) et leur envoi automatique en font un mauvais choix pour stocker de grandes quantités de données.

Tableau comparatif détaillé

Voici un comparatif complet des trois solutions :

Critère localStorage sessionStorage Cookies
Capacité 5-10 MB 5-10 MB 4 KB
Persistance Permanente (jusqu'à suppression manuelle) Session (onglet fermé = données effacées) Configurable (expires, max-age)
Portée Tous les onglets du même domaine Isolé par onglet Tous les onglets (configurable par path/domain)
Envoi au serveur Non Non Oui (automatique dans headers)
API Synchrone, simple (getItem, setItem) Synchrone, simple (getItem, setItem) Chaîne à parser (document.cookie)
Performance Rapide, bloquant Rapide, bloquant Impact sur requêtes HTTP (overhead)
Sécurité Vulnérable XSS Vulnérable XSS Options Secure, HttpOnly, SameSite
Support navigateurs IE 8+, tous les modernes IE 8+, tous les modernes Universel

Cas d'usage concrets

Quand utiliser localStorage ?

  • Préférences utilisateur : thème sombre/clair, langue, options d'affichage
  • Cache de données : données API pour réduire les appels serveur
  • Panier e-commerce : conserver les articles entre les sessions
  • Historique local : dernières recherches, pages visitées
  • Brouillons : sauvegardes automatiques d'articles, posts, commentaires
// Exemple : système de préférences
class UserPreferences {
    constructor() {
        this.key = 'user-preferences';
        this.defaults = {
            theme: 'light',
            language: 'fr',
            notifications: true
        };
    }

    get(key) {
        const prefs = this.getAll();
        return prefs[key] ?? this.defaults[key];
    }

    set(key, value) {
        const prefs = this.getAll();
        prefs[key] = value;
        localStorage.setItem(this.key, JSON.stringify(prefs));
    }

    getAll() {
        const data = localStorage.getItem(this.key);
        return data ? JSON.parse(data) : { ...this.defaults };
    }
}

const prefs = new UserPreferences();
prefs.set('theme', 'dark');
console.log(prefs.get('theme')); // "dark"

Quand utiliser sessionStorage ?

  • Formulaires multi-étapes : sauvegarder la progression sans persister
  • État de navigation : filtres actifs, pagination, scroll position
  • Données temporaires : résultats de recherche, sélections temporaires
  • One-time tokens : jetons éphémères qui ne doivent pas survivre à la session
// Exemple : gestion d'état de filtres
class FilterState {
    constructor(storageKey) {
        this.key = storageKey;
    }

    save(filters) {
        sessionStorage.setItem(this.key, JSON.stringify(filters));
    }

    load() {
        const data = sessionStorage.getItem(this.key);
        return data ? JSON.parse(data) : {};
    }

    clear() {
        sessionStorage.removeItem(this.key);
    }
}

const filters = new FilterState('search-filters');
filters.save({ category: 'electronics', priceMax: 500 });
const current = filters.load();
console.log(current.category); // "electronics"

Quand utiliser les cookies ?

  • Authentification : tokens JWT, session IDs (avec HttpOnly)
  • Tracking : analytics, publicité (avec consentement RGPD)
  • Préférences serveur : langue, région (envoyées à chaque requête)
  • CSRF tokens : protection contre les attaques CSRF
// Exemple : gestion de session authentifiée
class AuthManager {
    setSession(token, expiresInDays = 7) {
        // Cookie HttpOnly créé côté serveur pour le token principal
        // Cookie JavaScript pour le refresh token (moins sensible)
        CookieManager.set('refresh_token', token, expiresInDays, {
            secure: true,
            sameSite: 'Strict'
        });
    }

    isAuthenticated() {
        // Vérifier si le cookie de session existe
        return CookieManager.exists('session_id') ||
               CookieManager.exists('refresh_token');
    }

    logout() {
        CookieManager.delete('refresh_token');
        // Le cookie HttpOnly sera supprimé par une requête serveur
    }
}

Considérations de sécurité

Vulnérabilités XSS (Cross-Site Scripting)

localStorage et sessionStorage sont vulnérables aux attaques XSS. Un script malveillant injecté peut lire toutes les données :

// ⚠️ Vulnérable si un script malveillant est injecté
const sensitiveData = localStorage.getItem('user-token');
// Le script peut voler le token

// ✅ Meilleures pratiques
// 1. Ne JAMAIS stocker de tokens sensibles dans localStorage
// 2. Valider et échapper toutes les entrées utilisateur
// 3. Utiliser Content Security Policy (CSP)

Cookies sécurisés : attributs critiques

  • HttpOnly : le cookie ne peut pas être lu par JavaScript (protection XSS)
  • Secure : le cookie n'est envoyé que sur HTTPS
  • SameSite : protection contre les attaques CSRF
    • Strict : cookie jamais envoyé depuis un autre site
    • Lax : cookie envoyé uniquement sur navigation top-level
    • None : cookie envoyé partout (requiert Secure)
// ✅ Cookie sécurisé (côté serveur, exemple Node.js)
res.cookie('session_id', sessionId, {
    httpOnly: true,  // ✅ Protection XSS
    secure: true,    // ✅ HTTPS uniquement
    sameSite: 'strict', // ✅ Protection CSRF
    maxAge: 86400000 // 24 heures
});

// ❌ Cookie non sécurisé
document.cookie = "token=abc123"; // Vulnérable XSS, HTTP, CSRF

Bonnes pratiques de sécurité

  • Ne jamais stocker de mots de passe ou tokens sensibles en clair
  • Chiffrer les données sensibles avant stockage (crypto-js, Web Crypto API)
  • Utiliser HttpOnly cookies pour les tokens d'authentification
  • Implémenter une Content Security Policy (CSP) stricte
  • Valider et échapper toutes les entrées utilisateur
  • Utiliser HTTPS en production (obligatoire pour Secure cookies)
  • Limiter la durée de vie des données sensibles
  • Nettoyer les données lors de la déconnexion

Performance et limites

Limites de stockage

Chaque solution a des limites différentes :

// Tester la limite de localStorage (varie selon les navigateurs)
function testStorageLimit() {
    try {
        let i = 0;
        const testKey = 'storage-test';
        const chunk = new Array(1024).join('x'); // 1 KB

        while (true) {
            localStorage.setItem(testKey + i, chunk);
            i++;
        }
    } catch (e) {
        console.log(`Limite atteinte: environ ${i} KB`);
        // Nettoyer
        for (let j = 0; j < i; j++) {
            localStorage.removeItem('storage-test' + j);
        }
    }
}

// En général : 5-10 MB pour localStorage/sessionStorage

Impact sur les performances

  • localStorage/sessionStorage : opérations synchrones qui bloquent le thread principal
    • Éviter de stocker de gros objets
    • Utiliser des workers pour les opérations lourdes
  • Cookies : ajoutent un overhead à chaque requête HTTP
    • Limiter le nombre et la taille des cookies
    • Utiliser le paramètre path pour limiter l'envoi
// ⚠️ Mauvaise pratique : parsing lourd en synchrone
const bigData = JSON.parse(localStorage.getItem('huge-dataset')); // Bloque le thread

// ✅ Meilleure approche : utiliser un worker ou limiter la taille
// Ou utiliser IndexedDB pour les gros volumes

Gestion des erreurs

// Toujours encapsuler dans try/catch
function safeLocalStorage() {
    return {
        set(key, value) {
            try {
                localStorage.setItem(key, JSON.stringify(value));
                return true;
            } catch (e) {
                if (e.name === 'QuotaExceededError') {
                    console.error('Quota de stockage dépassé');
                    // Nettoyer les anciennes données
                    this.cleanup();
                }
                return false;
            }
        },

        get(key) {
            try {
                const item = localStorage.getItem(key);
                return item ? JSON.parse(item) : null;
            } catch (e) {
                console.error('Erreur parsing JSON:', e);
                return null;
            }
        },

        cleanup() {
            // Supprimer les données les plus anciennes
            const keys = Object.keys(localStorage);
            keys.slice(0, Math.floor(keys.length / 2)).forEach(key => {
                localStorage.removeItem(key);
            });
        }
    };
}

const storage = safeLocalStorage();
storage.set('user', { name: 'Alice' });

Bonnes pratiques

1. Valider l'existence et le support

// Vérifier le support de localStorage
function isLocalStorageAvailable() {
    try {
        const test = '__storage_test__';
        localStorage.setItem(test, test);
        localStorage.removeItem(test);
        return true;
    } catch (e) {
        return false;
    }
}

if (!isLocalStorageAvailable()) {
    console.warn('localStorage non disponible, utilisation de fallback');
    // Utiliser un polyfill ou une alternative
}

2. Utiliser des clés préfixées

// ✅ Bon : évite les conflits entre applications
localStorage.setItem('myapp:user:preferences', data);
localStorage.setItem('myapp:cache:products', data);

// ❌ Mauvais : risque de conflit
localStorage.setItem('preferences', data);

3. Implémenter l'expiration des données

const StorageWithExpiry = {
    set(key, value, ttlInSeconds) {
        const now = Date.now();
        const item = {
            value: value,
            expiry: now + (ttlInSeconds * 1000)
        };
        localStorage.setItem(key, JSON.stringify(item));
    },

    get(key) {
        const itemStr = localStorage.getItem(key);
        if (!itemStr) return null;

        const item = JSON.parse(itemStr);
        const now = Date.now();

        // Vérifier l'expiration
        if (now > item.expiry) {
            localStorage.removeItem(key);
            return null;
        }

        return item.value;
    }
};

// Utilisation
StorageWithExpiry.set('cache:products', products, 3600); // 1 heure
const products = StorageWithExpiry.get('cache:products');

4. Écouter les événements storage

// Synchroniser les données entre onglets
window.addEventListener('storage', (event) => {
    if (event.key === 'user-preferences') {
        console.log('Préférences mises à jour dans un autre onglet');
        console.log('Ancienne valeur:', event.oldValue);
        console.log('Nouvelle valeur:', event.newValue);

        // Recharger les préférences
        updateUI(JSON.parse(event.newValue));
    }
});

// Note : l'événement 'storage' ne se déclenche PAS dans l'onglet qui modifie

5. Créer une abstraction réutilisable

class StorageAdapter {
    constructor(storage = localStorage, prefix = '') {
        this.storage = storage;
        this.prefix = prefix;
    }

    key(name) {
        return this.prefix ? `${this.prefix}:${name}` : name;
    }

    set(name, value, ttl = null) {
        const key = this.key(name);
        const data = ttl ? { value, expiry: Date.now() + ttl * 1000 } : value;
        this.storage.setItem(key, JSON.stringify(data));
    }

    get(name) {
        const key = this.key(name);
        const item = this.storage.getItem(key);
        if (!item) return null;

        const data = JSON.parse(item);

        if (data.expiry && Date.now() > data.expiry) {
            this.remove(name);
            return null;
        }

        return data.value ?? data;
    }

    remove(name) {
        this.storage.removeItem(this.key(name));
    }

    clear() {
        if (this.prefix) {
            const keys = Object.keys(this.storage);
            keys.filter(k => k.startsWith(this.prefix)).forEach(k => {
                this.storage.removeItem(k);
            });
        } else {
            this.storage.clear();
        }
    }
}

// Utilisation
const appStorage = new StorageAdapter(localStorage, 'myapp');
appStorage.set('user', { name: 'Alice' }, 3600);
const user = appStorage.get('user');

Alternatives modernes

IndexedDB pour les gros volumes

Pour des besoins de stockage plus importants (> 10 MB) ou des données structurées complexes, utilisez IndexedDB.

  • Capacité : plusieurs centaines de MB (selon le disque disponible)
  • API asynchrone : ne bloque pas le thread principal
  • Indexation : recherches rapides sur de gros datasets
  • Transactions : opérations ACID
// Exemple simple avec IndexedDB (API complexe, privilégier une lib)
const openDB = indexedDB.open('MyDatabase', 1);

openDB.onsuccess = (event) => {
    const db = event.target.result;
    // Utiliser la base
};

// Mieux : utiliser une bibliothèque comme Dexie.js ou idb
import { openDB } from 'idb';

const db = await openDB('MyDatabase', 1, {
    upgrade(db) {
        db.createObjectStore('products', { keyPath: 'id' });
    }
});

await db.add('products', { id: 1, name: 'Product A' });
À retenir : Pour les gros volumes ou les données structurées complexes, préférez IndexedDB avec une bibliothèque comme Dexie.js ou idb pour simplifier l'API.

Comparaison rapide avec IndexedDB

Critère localStorage/sessionStorage Cookies IndexedDB
Capacité 5-10 MB 4 KB Centaines de MB+
API Synchrone, simple Texte à parser Asynchrone, complexe
Performance Rapide (petits volumes) Overhead HTTP Rapide (gros volumes)
Cas d'usage Préférences, cache léger Auth, communication serveur Données massives, offline-first

Synchronisation multi-onglets

Un utilisateur ouvre votre app dans deux onglets, se déconnecte dans l'un — l'autre doit suivre. Trois mécanismes natifs permettent cette coordination.

Event storage — historique mais limité

// onglet B reçoit les modifs faites dans onglet A
window.addEventListener('storage', (e) => {
    if (e.key === 'auth_token' && e.newValue === null) {
        // Token effacé dans un autre onglet → logout ici aussi
        location.href = '/login';
    }
});

Limitations : l'event ne se déclenche pas dans l'onglet qui a fait la modification. Et il ne fonctionne qu'avec localStorage, pas sessionStorage (par design — sessionStorage est par onglet).

BroadcastChannel — l'API moderne

// Communication bidirectionnelle entre onglets du même origin
const channel = new BroadcastChannel('auth');

channel.postMessage({ type: 'LOGOUT' });

channel.onmessage = (event) => {
    if (event.data.type === 'LOGOUT') {
        clearLocalAuthState();
        router.navigate(['/login']);
    }
};

// À la destruction du composant
channel.close();

BroadcastChannel est supporté par tous les navigateurs modernes (Chrome 54+, Firefox 38+, Safari 15.4+). Plus performant que l'event storage, gère des objets sérialisés, et ne dépend pas du stockage. C'est le choix recommandé en 2026.

SharedWorker — partage de connexion

Pour les besoins avancés (WebSocket partagée entre onglets, état applicatif unique), SharedWorker maintient un script qui survit aux onglets et leur envoie des messages via port.postMessage(). Plus complexe mais évite la duplication de connexions réseau. Indisponible sur Safari iOS.

Web Locks API — sérialisation des accès

Pour éviter les race conditions entre onglets sur une même donnée (deux onglets qui veulent refresher un token simultanément), Web Locks API garantit l'exécution séquentielle :

// Un seul onglet exécute refresh à la fois
await navigator.locks.request('token-refresh', async (lock) => {
    const expired = isTokenExpired();
    if (expired) await refreshToken();
});

Les autres onglets attendent que le lock soit libéré. Supporté Chrome 69+, Firefox 96+, Safari 15.4+. Particulièrement utile pour les apps multi-onglets avec auth partagée.

Pattern : wrapper type-safe pour localStorage

L'API native localStorage.getItem() retourne string | null sans typage. Un wrapper TypeScript élimine les erreurs de parsing JSON et fournit l'autocomplétion :

// storage.ts
type StorageSchema = {
    user: { id: string; name: string };
    theme: 'light' | 'dark';
    drafts: { id: string; content: string }[];
};

class TypedStorage<T extends Record<string, unknown>> {
    constructor(private readonly prefix = 'app:') {}

    get<K extends keyof T>(key: K): T[K] | null {
        const raw = localStorage.getItem(this.prefix + String(key));
        if (raw === null) return null;
        try {
            return JSON.parse(raw) as T[K];
        } catch {
            return null;
        }
    }

    set<K extends keyof T>(key: K, value: T[K]): void {
        try {
            localStorage.setItem(this.prefix + String(key), JSON.stringify(value));
        } catch (e) {
            if (e instanceof DOMException && e.name === 'QuotaExceededError') {
                console.error('localStorage plein — nettoyage requis');
            }
            throw e;
        }
    }

    remove<K extends keyof T>(key: K): void {
        localStorage.removeItem(this.prefix + String(key));
    }
}

// Usage avec inférence complète
const storage = new TypedStorage<StorageSchema>();
storage.set('theme', 'dark');         // ✓ valeur restreinte aux littéraux
storage.set('theme', 'rouge');        // ❌ erreur TypeScript
const user = storage.get('user');     // type: { id: string; name: string } | null

Ce pattern élimine les bugs de parsing (JSON corrompu, clé manquante) et l'autocomplétion sur les clés évite les fautes de frappe. À combiner avec Zod pour valider les données à la lecture si elles peuvent être manipulées par l'utilisateur.

Cookies modernes — flags de sécurité 2026

Un cookie sans flags est une faille de sécurité ouverte. Les attributs obligatoires pour tout cookie sensible :

  • HttpOnly — inaccessible au JavaScript, immunisé XSS. Indispensable pour les tokens d'authentification.
  • Secure — transmis uniquement en HTTPS. Obligatoire en production.
  • SameSite=Strict — bloque l'envoi du cookie depuis un autre site (protection CSRF native).
  • SameSite=Lax — variante moins stricte qui autorise la navigation GET (lien externe). Préféré pour les cookies d'auth dans les SaaS multi-domaines.
  • Path=/ — limite la portée du cookie à un sous-chemin spécifique.
  • Max-Age=N ou Expires — durée de vie. Sans, le cookie expire à la fermeture du navigateur (cookie de session).
  • Domain=.example.com — partage entre sous-domaines. À éviter sauf nécessité.
// Côté serveur Node/Express — cookie d'authentification sécurisé
res.cookie('refreshToken', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
    path: '/api/auth',
});

Chrome 80+ bloque par défaut les cookies tiers sans SameSite explicite — sans cette annotation, votre cookie ne sera pas envoyé en cross-origin. Lighthouse audit signale les cookies non conformes en 2026.

__Host- et __Secure- prefixes

// Prefixe __Host- — cookie LOCKED à l'origine actuelle
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Strict
// Exige : Secure + Path=/ + sans Domain attribute
// Empêche un sous-domaine compromis de set ce cookie

// Prefixe __Secure- — cookie limité au HTTPS
Set-Cookie: __Secure-csrf=xyz; Secure; SameSite=Strict
// Exige : Secure (mais Domain et Path autorisés)

Ces prefixes signalent au navigateur d'appliquer des règles strictes. Support : tous les navigateurs modernes depuis 2017. Pratique recommandée pour tout cookie d'authentification en production.

Partitioned cookies (CHIPS) — 2024+

Le flag Partitioned (Chrome 114+) isole un cookie tiers par site partenaire — un cookie iframe sur siteA.com n'est plus le même que sur siteB.com, même si le domaine cookie est identique. Cela permet aux widgets embarqués (chat, paiement) de garder un état sans tracker cross-site. Format : Set-Cookie: id=abc; SameSite=None; Secure; Partitioned. Indispensable pour préparer la fin des third-party cookies en 2026-2027.

Mini-projet appliqué — auth tokens 2026 (cookies httpOnly + refresh + cross-tab sync)

Voici le pattern d'authentification recommandé en 2026 : access token court (15 min) dans la mémoire JS, refresh token long (7 jours) dans un cookie httpOnly Secure SameSite=Strict, synchronisation cross-tab via BroadcastChannel, renewal coordonné via Web Locks. C'est exactement le squelette utilisé par Linear, Vercel, Notion et toutes les SaaS sérieuses depuis l'avis OWASP 2020.

1. Côté serveur — émission des tokens à la connexion

Pour le détail des patterns JWT et leur sécurisation, voir le guide JWT Node.js + Angular.

// POST /api/auth/login — Express + jsonwebtoken
app.post('/api/auth/login', async (req, res) => {
    const { email, password } = req.body;
    const user = await authenticateUser(email, password);
    if (!user) return res.status(401).json({ error: 'Invalid credentials' });

    const accessToken = jwt.sign(
        { sub: user.id, role: user.role },
        process.env.JWT_SECRET,
        { expiresIn: '15m' }
    );

    const refreshToken = jwt.sign(
        { sub: user.id, type: 'refresh', jti: crypto.randomUUID() },
        process.env.JWT_REFRESH_SECRET,
        { expiresIn: '7d' }
    );

    // Stocker le jti en DB pour révocation possible
    await db.refreshTokens.create({ jti: getJti(refreshToken), userId: user.id });

    res.cookie('__Host-refresh', refreshToken, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        path: '/api/auth',
        maxAge: 7 * 24 * 60 * 60 * 1000,
    });

    res.json({ accessToken, user: { id: user.id, email: user.email, role: user.role } });
});

2. Côté client — gestion en mémoire de l'access token (jamais en localStorage)

// auth-store.ts — accessToken vit UNIQUEMENT en mémoire JS
class AuthStore {
    private accessToken: string | null = null;
    private accessTokenExpiresAt: number = 0;
    private user: User | null = null;

    setSession({ accessToken, user }: { accessToken: string; user: User }) {
        this.accessToken = accessToken;
        this.user = user;
        // Décoder l'expiration depuis le payload JWT
        const payload = JSON.parse(atob(accessToken.split('.')[1]));
        this.accessTokenExpiresAt = payload.exp * 1000;
    }

    getAccessToken(): string | null {
        if (this.accessTokenExpiresAt < Date.now()) return null;
        return this.accessToken;
    }

    isExpiringSoon(): boolean {
        // Refresh quand < 2 min restantes
        return this.accessTokenExpiresAt - Date.now() < 2 * 60 * 1000;
    }

    clear() {
        this.accessToken = null;
        this.accessTokenExpiresAt = 0;
        this.user = null;
    }
}

export const authStore = new AuthStore();

3. Refresh coordonné via Web Locks API (anti race-condition multi-onglets)

Sans Web Locks, 5 onglets ouverts déclenchent 5 requêtes /refresh en parallèle → serveur saturé + risque de rotation infinie de refresh tokens. La solution moderne :

// auth-refresh.ts — refresh sérialisé entre onglets
let refreshPromise: Promise<string | null> | null = null;

export async function refreshAccessToken(): Promise<string | null> {
    // Coalesce les appels concurrents dans le MÊME onglet
    if (refreshPromise) return refreshPromise;

    refreshPromise = navigator.locks.request('auth-refresh', async () => {
        // Re-check après acquisition du lock : un autre onglet a peut-être refreshé
        const current = authStore.getAccessToken();
        if (current && !authStore.isExpiringSoon()) {
            return current;
        }

        const res = await fetch('/api/auth/refresh', {
            method: 'POST',
            credentials: 'include', // le cookie httpOnly est envoyé automatiquement
        });

        if (!res.ok) {
            authStore.clear();
            broadcastLogout();
            return null;
        }

        const { accessToken, user } = await res.json();
        authStore.setSession({ accessToken, user });
        broadcastSessionUpdate({ accessToken, user });
        return accessToken;
    });

    try {
        return await refreshPromise;
    } finally {
        refreshPromise = null;
    }
}

4. Synchronisation cross-tab via BroadcastChannel

Quand un onglet refresh ou logout, les autres doivent suivre instantanément. BroadcastChannel est l'API moderne dédiée à ce cas — voir aussi le guide des closures pour comprendre la mécanique sous-jacente d'encapsulation.

// auth-broadcast.ts — synchronisation entre onglets du même origin
const channel = new BroadcastChannel('auth');

export function broadcastSessionUpdate(session: { accessToken: string; user: User }) {
    channel.postMessage({ type: 'SESSION_UPDATE', session });
}

export function broadcastLogout() {
    channel.postMessage({ type: 'LOGOUT' });
}

channel.onmessage = (event) => {
    switch (event.data.type) {
        case 'SESSION_UPDATE':
            // Adopter la session refreshée par l'autre onglet
            authStore.setSession(event.data.session);
            break;
        case 'LOGOUT':
            authStore.clear();
            location.href = '/login';
            break;
    }
};

// Cleanup à la fermeture de l'onglet
window.addEventListener('beforeunload', () => channel.close());

5. Intercepteur HTTP — injection auto du token + retry au 401

// http-interceptor.ts — wrapper fetch avec refresh transparent
export async function apiFetch(input: RequestInfo, init: RequestInit = {}): Promise<Response> {
    // 1. Refresh préventif si le token expire bientôt
    if (authStore.isExpiringSoon()) {
        await refreshAccessToken();
    }

    const token = authStore.getAccessToken();
    const headers = new Headers(init.headers);
    if (token) headers.set('Authorization', `Bearer ${token}`);

    let response = await fetch(input, { ...init, headers, credentials: 'include' });

    // 2. Retry une fois sur 401 (token rejeté côté serveur)
    if (response.status === 401 && token) {
        const newToken = await refreshAccessToken();
        if (newToken) {
            headers.set('Authorization', `Bearer ${newToken}`);
            response = await fetch(input, { ...init, headers, credentials: 'include' });
        } else {
            // Refresh échoué → logout définitif
            location.href = '/login';
        }
    }

    return response;
}

// Usage transparent dans toute l'app
const users = await apiFetch('/api/users').then(r => r.json());
Pourquoi ce pattern bat localStorage + JWT : (1) Immunisé XSS : le refresh token httpOnly est inaccessible au JS, donc une injection ne peut PAS exfiltrer la session longue. (2) Access token court : si exfiltré (très rare car en mémoire JS), expiration sous 15 min → impact limité. (3) Révocation possible : le jti du refresh est en DB serveur — révoquer = supprimer la ligne. (4) Cross-tab fluide : BroadcastChannel évite que 5 onglets demandent 5 refresh simultanés. (5) Recommandation OWASP 2020+ : c'est officiellement le pattern à suivre pour les SPA en 2026.

6. Tableau récapitulatif — où stocker quoi en 2026

DonnéeOù la stockerPourquoi
Access token JWT (15 min)Mémoire JSCourt, immunisé XSS par sa durée
Refresh token (7 jours)Cookie __Host- httpOnly Secure SameSite=StrictImmunisé XSS, révocable serveur
Préférences UI (thème, langue)localStorageNon-sensibles, persistants, instantanés
État de formulaire multi-étapessessionStorageRéservé à l'onglet, perdu à la fermeture
Données métier (notes, todos)IndexedDBVolume, structure, async — voir le guide IndexedDB + offline-first
Identifiant utilisateur publicCookie Secure SameSite=Lax (1 an)Lisible client + envoyé serveur, non-sensible
Cache d'assets (images, JSON)Cache API + Service WorkerAPI dédiée, intégrée aux requêtes réseau

Pour pousser l'authentification à un autre niveau (Passkeys, WebAuthn, OAuth2 PKCE), lire le guide OAuth2 + OIDC + PKCE en Angular qui complète ce mini-projet avec l'authentification fédérée.

Conclusion

Le choix entre localStorage, sessionStorage et cookies dépend de vos besoins spécifiques :

  • Utilisez localStorage pour les données persistantes qui ne doivent pas être envoyées au serveur (préférences, cache)
  • Utilisez sessionStorage pour les données temporaires liées à la navigation dans un onglet (formulaires multi-étapes, état)
  • Utilisez les cookies pour les données qui doivent être envoyées au serveur (authentification) avec les attributs de sécurité appropriés
  • Passez à IndexedDB pour les gros volumes de données structurées
Règle d'or : Ne stockez jamais de données sensibles (mots de passe, tokens) en clair dans localStorage ou sessionStorage. Privilégiez les cookies HttpOnly pour l'authentification.

Avec une bonne compréhension de ces trois solutions, vous pourrez concevoir des applications web performantes, sécurisées et offrant une excellente expérience utilisateur.

Partager