add plausible
This commit is contained in:
parent
05cfa598b2
commit
7c45ee4490
@ -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 été 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' }
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user