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
|
* `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.
|
* 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) {
|
function setRefreshCookie(ctx: HttpContext, plain: string) {
|
||||||
const maxAgeSeconds = ttlDays() * 24 * 60 * 60
|
const maxAgeSeconds = ttlDays() * 24 * 60 * 60
|
||||||
ctx.response.cookie(REFRESH_COOKIE_NAME, plain, {
|
ctx.response.cookie(REFRESH_COOKIE_NAME, plain, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: env.get('COOKIE_SECURE', false),
|
secure: env.get('COOKIE_SECURE', false),
|
||||||
sameSite: 'strict',
|
sameSite: 'lax',
|
||||||
path: '/api/v1/auth',
|
path: '/api/v1/auth',
|
||||||
domain: env.get('COOKIE_DOMAIN') || undefined,
|
domain: env.get('COOKIE_DOMAIN') || undefined,
|
||||||
maxAge: maxAgeSeconds,
|
maxAge: maxAgeSeconds,
|
||||||
@ -78,13 +85,28 @@ export async function issueRefreshToken(
|
|||||||
return { token, plain }
|
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
|
* Valide le cookie reçu et révoque l'ancien token. Retourne le user
|
||||||
* authentifié — le contrôleur appelle ensuite `issueRefreshToken` (via
|
* authentifié — le contrôleur appelle ensuite `issueRefreshToken` (via
|
||||||
* emitAuthSession) pour poser un nouveau cookie. Rotation complète.
|
* emitAuthSession) pour poser un nouveau cookie. Rotation complète.
|
||||||
*
|
*
|
||||||
* Si le user envoie un token déjà révoqué, on suppose un vol potentiel
|
* Si le user envoie un token déjà révoqué :
|
||||||
* et on révoque TOUS les tokens actifs du user (panic mode).
|
* - 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(
|
export async function consumeRefreshToken(
|
||||||
ctx: HttpContext
|
ctx: HttpContext
|
||||||
@ -100,9 +122,28 @@ export async function consumeRefreshToken(
|
|||||||
return { errorCode: 'session_expired' }
|
return { errorCode: 'session_expired' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token déjà révoqué = signal de vol potentiel : on coupe tout pour
|
// Token révoqué : check pour course entre onglets avant de paniquer.
|
||||||
// l'user concerné. Le vrai propriétaire devra se re-logger.
|
|
||||||
if (stored.revokedAt) {
|
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)
|
await revokeAllForUser(stored.userId)
|
||||||
clearRefreshCookie(ctx)
|
clearRefreshCookie(ctx)
|
||||||
return { errorCode: 'session_expired' }
|
return { errorCode: 'session_expired' }
|
||||||
|
|||||||
@ -20,6 +20,13 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="Rubis" />
|
<meta name="apple-mobile-web-app-title" content="Rubis" />
|
||||||
|
|
||||||
<title>Rubis Sur l'Ongle</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -1900,6 +1900,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user