Comment concevoir une couche de synchronisation fiable des sessions dans les architectures hybrides pour éviter les déconnexions intempestives lors du changement d'entreprise.
Dans les plateformes professionnelles multi-locataires (comme les ERP ou les outils de facturation), les utilisateurs gèrent ou naviguent souvent entre plusieurs entreprises. La conception de ce flux nécessite un système de gestion de session particulièrement robuste.
Un problème fréquent dans les architectures hybrides est la perte de persistance des sessions : lors du rechargement de la page ou du passage d'une entreprise à une autre, l'utilisateur se retrouve subitement déconnecté en raison d'un désalignement entre le LocalStorage client et les cookies de session du serveur. Voyons comment résoudre ce dysfonctionnement.
Lorsqu'un utilisateur bascule de l'entreprise A vers l'entreprise B, le client effectue des appels API pour charger les nouvelles données. Si le client met à jour son active_company_id dans le LocalStorage mais que le jeton de session ou le cookie CSRF côté serveur n'est pas correctement synchronisé ou renouvelé, les intercepteurs renvoient une erreur 401 Unauthorized ou 419 Page Expired, forçant une déconnexion intempestive.
Pour éviter la perte de jeton lors de navigations rapides, utilisez un intercepteur Axios chargeant dynamiquement les en-têtes d'envoi.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// lib/http-client.ts
import axios from 'axios';
const httpClient = axios.create({
baseURL: '/api',
withCredentials: true, class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Crucial pour inclure les cookies de session
});
httpClient.interceptors.request.use((config) => {
const activeCompanyId = localStorage.getItem('active_company_id');
if (activeCompanyId) {
config.headers['X-Company-ID'] = activeCompanyId;
}
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Récupère automatiquement le jeton CSRF depuis les cookies du document
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (csrfToken) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(csrfToken);
}
return config;
}, (error) => {
return Promise.reject(error);
});Côté backend, assurez-vous que l'identifiant de l'entreprise demandé dans les en-têtes correspond bien aux entreprises associées à l'utilisateur authentifié. En cas d'incohérence, renvoyez une réponse d'erreur maîtrisée plutôt que de détruire directement la session.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// app/Http/Middleware/TenantContextMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class TenantContextMiddleware
{
public function handle(Request $request, Closure $next)
{
$companyId = $request->header('X-Company-ID');
$user = $request->user();
if ($companyId && $user) {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Vérifie que l'utilisateur appartient bien à l'entreprise demandée
$hasAccess = $user->companies()->where('companies.id', $companyId)->exists();
if ($hasAccess) {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Instancie l'entreprise active dans le conteneur global
app()->instance('active_company', $user->companies()->find($companyId));
} else {
return response()->json([
'error' => 'Contexte entreprise invalide. Veuillez rafraîchir la page.'
], 403);
}
}
return $next($request);
}
}Si une requête HTTP échoue avec un code 401 ou 419, tentez de renouveler silencieusement le jeton de session avant de rediriger l'utilisateur vers la page de connexion.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// lib/http-client.ts (Intercepteur de réponse)
httpClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if ((error.response?.status === 401 || error.response?.status === 419) && !originalRequest._retry) {
originalRequest._retry = true;
try {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Demande de renouvellement de cookie de session
await axios.get('/api/csrf-cookie');
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Relance la requête originale
return httpClient(originalRequest);
} catch (refreshError) {
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Redirection forcée uniquement si le renouvellement échoue
localStorage.removeItem('active_company_id');
window.location.href = '/login?session_expired=true';
}
}
return Promise.reject(error);
}
);L'utilisation combinée d'un intercepteur synchronisé et d'un middleware de vérification préserve les sessions utilisateur lors des bascules d'environnements SaaS.