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.
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());
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' });
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 siteLax: cookie envoyé uniquement sur navigation top-levelNone: 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
pathpour 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' });
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.
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());
6. Tableau récapitulatif — où stocker quoi en 2026
| Donnée | Où la stocker | Pourquoi |
|---|---|---|
| Access token JWT (15 min) | Mémoire JS | Court, immunisé XSS par sa durée |
| Refresh token (7 jours) | Cookie __Host- httpOnly Secure SameSite=Strict | Immunisé XSS, révocable serveur |
| Préférences UI (thème, langue) | localStorage | Non-sensibles, persistants, instantanés |
| État de formulaire multi-étapes | sessionStorage | Réservé à l'onglet, perdu à la fermeture |
| Données métier (notes, todos) | IndexedDB | Volume, structure, async — voir le guide IndexedDB + offline-first |
| Identifiant utilisateur public | Cookie Secure SameSite=Lax (1 an) | Lisible client + envoyé serveur, non-sensible |
| Cache d'assets (images, JSON) | Cache API + Service Worker | API 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
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.