fix: TOTP login flow loses remember_me — sessions expire after 1 hour
The TOTP verification endpoint always created refresh tokens with remember_me=false and 1-hour expiry, regardless of what the user selected at login. Fix: - Frontend now sends remember_me in the TOTP verify request body - Backend reads remember_me and uses it for token expiry (30 days) and cookie maxAge Users with 2FA who checked "remember me" will now stay logged in for 30 days instead of being kicked out after 1 hour. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -198,7 +198,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({ login_token: loginToken, totp_code: code }),
|
body: JSON.stringify({ login_token: loginToken, totp_code: code, remember_me: remember }),
|
||||||
})
|
})
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export default async function authRoutes(fastify: FastifyInstance): Promise<void
|
|||||||
const parsed = parseBody(TotpVerifySchema, request.body);
|
const parsed = parseBody(TotpVerifySchema, request.body);
|
||||||
if ('error' in parsed) return error(reply, parsed.error, 400);
|
if ('error' in parsed) return error(reply, parsed.error, 400);
|
||||||
const { login_token, totp_code } = parsed.data;
|
const { login_token, totp_code } = parsed.data;
|
||||||
|
const rawBody = request.body as unknown as Record<string, unknown>;
|
||||||
|
const rememberMe = rawBody.remember_me === true || rawBody.remember_me === 'true';
|
||||||
|
|
||||||
const tokenHash = crypto.createHash('sha256').update(login_token).digest('hex');
|
const tokenHash = crypto.createHash('sha256').update(login_token).digest('hex');
|
||||||
|
|
||||||
@@ -127,18 +129,22 @@ export default async function authRoutes(fastify: FastifyInstance): Promise<void
|
|||||||
const refreshTokenRaw = crypto.randomBytes(32).toString('hex');
|
const refreshTokenRaw = crypto.randomBytes(32).toString('hex');
|
||||||
const refreshTokenHash = crypto.createHash('sha256').update(refreshTokenRaw).digest('hex');
|
const refreshTokenHash = crypto.createHash('sha256').update(refreshTokenRaw).digest('hex');
|
||||||
|
|
||||||
|
const expiresIn = rememberMe
|
||||||
|
? config.jwt.refreshTokenRememberExpiry
|
||||||
|
: config.jwt.refreshTokenSessionExpiry;
|
||||||
|
|
||||||
await prisma.refresh_tokens.create({
|
await prisma.refresh_tokens.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
token_hash: refreshTokenHash,
|
token_hash: refreshTokenHash,
|
||||||
expires_at: new Date(Date.now() + config.jwt.refreshTokenSessionExpiry * 1000),
|
expires_at: new Date(Date.now() + expiresIn * 1000),
|
||||||
remember_me: false,
|
remember_me: rememberMe,
|
||||||
ip_address: request.ip,
|
ip_address: request.ip,
|
||||||
user_agent: request.headers['user-agent'] ?? null,
|
user_agent: request.headers['user-agent'] ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setRefreshCookie(reply, refreshTokenRaw, false);
|
setRefreshCookie(reply, refreshTokenRaw, rememberMe);
|
||||||
return success(reply, { access_token: accessToken, user: authData });
|
return success(reply, { access_token: accessToken, user: authData });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user