add plausible
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 29s
Build & Deploy Landing / build-and-deploy (push) Successful in 17s
Build & Deploy API / build-and-deploy (push) Successful in 1m20s

This commit is contained in:
ordinarthur 2026-05-08 13:08:07 +02:00
parent 05cfa598b2
commit 7c45ee4490
3 changed files with 60 additions and 5 deletions

View File

@ -34,13 +34,20 @@ function ttlDays(): number {
*
* `path: /api/v1/auth` : le browser n'envoie le cookie qu'aux endpoints
* d'auth, pas sur chaque requête API. Réduit la surface d'attaque CSRF.
*
* `sameSite: 'lax'` (et pas 'strict') : `strict` bloque l'envoi du cookie
* sur les navigations top-level (URL tapée, bookmark, restauration
* d'onglets) surtout Safari iOS et casse le bootstrap SPA après
* fermeture/réouverture du navigateur. Lax envoie le cookie sur les
* top-level GET (qui sont safe) tout en bloquant les requêtes cross-site
* embedded protection CSRF préservée pour le POST /auth/refresh.
*/
function setRefreshCookie(ctx: HttpContext, plain: string) {
const maxAgeSeconds = ttlDays() * 24 * 60 * 60
ctx.response.cookie(REFRESH_COOKIE_NAME, plain, {
httpOnly: true,
secure: env.get('COOKIE_SECURE', false),
sameSite: 'strict',
sameSite: 'lax',
path: '/api/v1/auth',
domain: env.get('COOKIE_DOMAIN') || undefined,
maxAge: maxAgeSeconds,
@ -78,13 +85,28 @@ export async function issueRefreshToken(
return { token, plain }
}
/**
* Fenêtre de grâce après révocation pendant laquelle on tolère la
* réutilisation d'un token révoqué SI un successeur actif existe pour
* accommoder les courses entre onglets (deux refresh parallèles avec le
* même cookie). Au-delà : présomption de vol panic mode.
*/
const ROTATION_GRACE_SECONDS = 60
/**
* Valide le cookie reçu et révoque l'ancien token. Retourne le user
* authentifié le contrôleur appelle ensuite `issueRefreshToken` (via
* emitAuthSession) pour poser un nouveau cookie. Rotation complète.
*
* Si le user envoie un token déjà révoqué, on suppose un vol potentiel
* et on révoque TOUS les tokens actifs du user (panic mode).
* Si le user envoie un token déjà révoqué :
* - et qu'un successeur actif a é créé pour le même user dans les
* {ROTATION_GRACE_SECONDS} dernières secondes c'est très probablement
* un refresh parallèle d'un autre onglet, pas un vol. On rotate ce
* successeur et on retourne la session normalement (les 2 onglets
* restent connectés).
* - sinon, c'est une réutilisation tardive présomption de vol, on
* coupe TOUS les tokens actifs du user (panic mode). Le vrai
* propriétaire devra se re-logger.
*/
export async function consumeRefreshToken(
ctx: HttpContext
@ -100,9 +122,28 @@ export async function consumeRefreshToken(
return { errorCode: 'session_expired' }
}
// Token déjà révoqué = signal de vol potentiel : on coupe tout pour
// l'user concerné. Le vrai propriétaire devra se re-logger.
// Token révoqué : check pour course entre onglets avant de paniquer.
if (stored.revokedAt) {
const graceCutoff = stored.revokedAt.plus({ seconds: ROTATION_GRACE_SECONDS })
const successor = await RefreshToken.query()
.where('user_id', stored.userId)
.whereNull('revoked_at')
.where('expires_at', '>', DateTime.now().toSQL())
.where('created_at', '>=', stored.revokedAt.toSQL())
.where('created_at', '<=', graceCutoff.toSQL())
.orderBy('created_at', 'desc')
.first()
if (successor) {
// Course détectée — rotate le successeur et continue normalement.
successor.revokedAt = DateTime.now()
successor.lastUsedAt = DateTime.now()
await successor.save()
const user = await User.findOrFail(successor.userId)
return { user }
}
// Vrai vol présumé : tout révoquer.
await revokeAllForUser(stored.userId)
clearRefreshCookie(ctx)
return { errorCode: 'session_expired' }

View File

@ -20,6 +20,13 @@
<meta name="apple-mobile-web-app-title" content="Rubis" />
<title>Rubis Sur l'Ongle</title>
<!-- Privacy-friendly analytics by Plausible (RGPD-friendly, no cookies) -->
<script async src="https://plausible.io/js/pa-zLenWxEI1wBQxOWaZrS94.js"></script>
<script>
window.plausible = window.plausible || function () { (plausible.q = plausible.q || []).push(arguments) }, plausible.init = plausible.init || function (i) { plausible.o = i || {} };
plausible.init()
</script>
</head>
<body>
<div id="root"></div>

View File

@ -1900,6 +1900,13 @@
display: flex;
}
</style>
<!-- Privacy-friendly analytics by Plausible (RGPD-friendly, no cookies) -->
<script async src="https://plausible.io/js/pa-zLenWxEI1wBQxOWaZrS94.js"></script>
<script>
window.plausible = window.plausible || function () { (plausible.q = plausible.q || []).push(arguments) }, plausible.init = plausible.init || function (i) { plausible.o = i || {} };
plausible.init()
</script>
</head>
<body>