From d0fa01cc0ac65eadc0ebcd4711143f2da91bbeec Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 20 Feb 2026 07:02:18 -0300 Subject: [PATCH] fix: increase JWT session to 30 days + auto-refresh in middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JWT session tokens now last 30 days instead of 24 hours. The middleware silently refreshes the token when it has less than 7 days remaining, so active users never get locked out of the dashboard. Auto-refresh fires on any /dashboard request and sets a fresh 30-day cookie via Set-Cookie header — completely transparent to the user. --- src/app/api/auth/login/route.ts | 2 +- src/proxy.ts | 39 +++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 381ea1a226..3b7bc97284 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -57,7 +57,7 @@ export async function POST(request) { const token = await new SignJWT({ authenticated: true }) .setProtectedHeader({ alg: "HS256" }) - .setExpirationTime("24h") + .setExpirationTime("30d") .sign(SECRET); const cookieStore = await cookies(); diff --git a/src/proxy.ts b/src/proxy.ts index b56ce32aa2..8faf273d78 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { jwtVerify } from "jose"; +import { jwtVerify, SignJWT } from "jose"; import { generateRequestId } from "./shared/utils/requestId"; import { getSettings } from "./lib/localDb"; import { isPublicRoute, verifyAuth, isAuthRequired } from "./shared/utils/apiAuth"; @@ -81,7 +81,42 @@ export async function proxy(request) { if (token) { try { - await jwtVerify(token, SECRET); + const { payload } = await jwtVerify(token, SECRET); + + // Auto-refresh: if token expires within 7 days, issue a fresh 30-day token + const exp = payload.exp as number; + const now = Math.floor(Date.now() / 1000); + const REFRESH_WINDOW = 7 * 24 * 60 * 60; // 7 days in seconds + if (exp && exp - now < REFRESH_WINDOW) { + try { + const freshToken = await new SignJWT({ authenticated: true }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("30d") + .sign(SECRET); + + // Detect secure context + const fwdProto = (request.headers.get("x-forwarded-proto") || "") + .split(",")[0] + .trim() + .toLowerCase(); + const isHttps = fwdProto === "https" || request.nextUrl?.protocol === "https:"; + const useSecure = process.env.AUTH_COOKIE_SECURE === "true" || isHttps; + + response.cookies.set("auth_token", freshToken, { + httpOnly: true, + secure: useSecure, + sameSite: "lax", + path: "/", + }); + console.log( + `[Middleware] JWT auto-refreshed for ${pathname} (was expiring in ${Math.round((exp - now) / 3600)}h)` + ); + } catch (refreshErr) { + // Refresh failed — continue with existing valid token + console.error("[Middleware] JWT auto-refresh failed:", refreshErr.message); + } + } + return response; } catch (err) { // FASE-01: Log auth errors instead of silently redirecting