ADR-02 — Auth via cookies HttpOnly uniquement
Pas de Bearer token côté client, cookies signés avec proxy Next rewrites en dev
Statut : Accepté Date : 2026-04 Sujet : Mécanisme d'authentification côté client web
Contexte
Dans l'ancien projet Bell, on mélangeait Bearer token (stocké en SecureStore/localStorage) et cookies HttpOnly (Better Auth par défaut). Ce mix a causé des heures de debug :
- PWA sur
localhost:8081+ API sur192.168.1.128:3000→ cookies cross-origin rejetés par les browsers credentials: "include"+ SameSite=Lax → inconsistant selon les navigateurs- Flag
bell_session_activeen localStorage pour compenser les races d'hydration → source de boucles/↔/auth/waiting authClient.getSession()parfois synchrone, parfois async → race conditions aprèssignUp.email
Résultat : un guard client fragile, des 401 aléatoires, des logouts silencieux après refresh.
Alternatives considérées
Option 1 — Bearer token only (localStorage)
Comme dans beaucoup de SPA tRPC/REST. Token lu au démarrage, envoyé en Authorization: Bearer.
Pour :
- Fonctionne cross-origin sans config CORS complexe
- Pas de dépendance aux cookies
Contre :
- Vulnérable XSS — un script injecté lit localStorage
- Pas de rotation automatique (il faut un refresh token manuel)
- Pas d'expiration côté serveur sans logique custom
- Mauvaise UX sur mobile PWA (localStorage parfois wipé)
Option 2 — Mix Bearer + cookies (ancien projet)
C'est ce qu'on faisait. Bearer pour cross-origin dev, cookies en prod.
Contre :
- Double source de vérité, deux chemins de bug
- Session peut désynchroniser (flag localStorage pense "connecté", cookie expiré côté serveur)
- Bugs déjà documentés dans l'ancien projet
Option 3 — Cookies HttpOnly uniquement (retenu)
Cookies signés par Better Auth, HttpOnly + Secure + SameSite=Lax. Pas de token côté JS.
Pour :
- Non accessible au JS → résiste XSS
- Rotation automatique par Better Auth
- Expiration côté serveur propre
- Standard web, pas de magie
Contre :
- Cross-origin nécessite config précise (credentials + CORS)
- En dev, si API et front sont sur des origins différents, les browsers bloquent les cookies third-party
Solution au contre : en dev, on utilise les Next.js rewrites pour faire apparaître l'API comme same-origin du front. En prod, même-domaine parent (.hoaiy.com) → cookies partagés entre bell-app.hoaiy.com et bell-staff.hoaiy.com.
Décision
Serveur Elysia / Better Auth
// packages/auth/src/index.ts
export const auth = betterAuth({
// ...
advanced: {
defaultCookieAttributes: {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
domain: baseDomain, // ".hoaiy.com" en prod, undefined en dev
},
},
plugins: [
organization({ /* ... */ }),
magicLink({ /* ... */ }),
// PAS DE bearer() — volontaire
],
});Clients (pwa et dashboard)
Dans apps/pwa/next.config.ts et apps/dashboard/next.config.ts :
export default {
async rewrites() {
return [
{ source: "/api/auth/:path*", destination: `${SERVER_URL}/api/auth/:path*` },
{ source: "/api/eden/:path*", destination: `${SERVER_URL}/:path*` },
];
},
};Côté client Eden Treaty :
// apps/pwa/src/lib/eden.ts
import { treaty } from "@elysiajs/eden";
import type { App } from "@bell/api";
export const eden = treaty<App>(
typeof window === "undefined" ? process.env.SERVER_URL! : "/api/eden",
{ fetch: { credentials: "include" } },
);Résultat : le navigateur voit toutes les requêtes sur le même origin que le front. Le cookie de session better-auth.session_token est transmis automatiquement. Pas de Bearer, pas de localStorage, pas de flag.
Middleware Next.js (server-side guard)
Le guard n'est pas côté client (fini la race d'hydration). C'est un middleware serveur :
// apps/pwa/src/middleware.ts
export async function middleware(req: NextRequest) {
const sessionToken = req.cookies.get("better-auth.session_token");
if (!sessionToken) return NextResponse.redirect("/auth/waiting");
// ... fetch session via API, check role, redirect selon checkInStatus
}Le client ne gère plus la redirection — c'est le serveur qui décide au moment du render.
Conséquences
Positives :
- Auth résistante XSS
- Plus de race d'hydration côté client, plus de flag localStorage
- Standard web, compréhensible par tout dev
- Middleware serveur = redirect avant le render = pas de flash d'UI
Négatives :
- Nécessite proxy Next.js en dev (config ajoutée aux deux apps)
- En prod,
bell-app.hoaiy.cometbell-staff.hoaiy.comdoivent partager.hoaiy.comcomme domaine cookie — contrainte DNS - Middleware serveur coûte quelques ms par request (négligeable, et amorti par edge runtime)
Métriques à surveiller
- Taux de
401 Unauthorizedcôté API (cible : < 0.1 %, surtout pas de spikes après deploy) - Temps de vie médian d'une session guest (cible : > 24h sans re-login)
- Temps de vie médian d'une session staff (cible : > 7 jours avec Remember Me)
- Nombre de signalements "je suis constamment déconnecté" → 0