Extend authenticated session activity

This commit is contained in:
2026-09-09 16:58:08 +09:00
parent 5f3ac952ed
commit 20e0902adc
7 changed files with 118 additions and 7 deletions
+32 -1
View File
@@ -1,7 +1,7 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createInitialPlay } from '../src/domain.js';
import { buildServer } from './app.js';
import { hashPassword } from './security.js';
@@ -56,6 +56,37 @@ describe('Fastify Mongo contract', () => {
const temporaryRoots = [];
afterEach(async () => { await app?.close(); await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); });
it('uses a one hour sliding session and never revives an expired session', async () => {
vi.useFakeTimers({ toFake: ['Date'] });
try {
const store = memoryStore(); await seedUser(store, { id: 'clock', email: 'clock@example.com', displayName: 'Clock' });
app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false });
const start = new Date('2026-01-01T00:00:00.000Z'); vi.setSystemTime(start);
const result = await login(app, 'clock@example.com', 'correct horse battery staple');
expect(result.response.headers['set-cookie']).toContain('Max-Age=3600');
const session = store.sessions.documents[0]; expect(session.expiresAt).toEqual(new Date(start.getTime() + 3_600_000));
vi.setSystemTime(new Date(start.getTime() + 50 * 60_000));
const renewed = await app.inject({ method: 'GET', url: '/api/teams', headers: { cookie: result.cookie } });
expect(renewed.statusCode).toBe(200); expect(session.expiresAt).toEqual(new Date(start.getTime() + 110 * 60_000));
vi.setSystemTime(new Date(start.getTime() + 70 * 60_000)); const activity = await app.inject({ method: 'POST', url: '/api/auth/activity', headers: { cookie: result.cookie }, payload: {} }); expect(activity.statusCode).toBe(200); expect(activity.headers['set-cookie']).toContain('Max-Age=3600');
vi.setSystemTime(new Date(start.getTime() + 130 * 60_000)); const expired = await app.inject({ method: 'POST', url: '/api/auth/activity', headers: { cookie: result.cookie }, payload: {} }); expect(expired.statusCode).toBe(401); expect(session.expiresAt).toEqual(new Date(start.getTime() + 130 * 60_000));
} finally { vi.useRealTimers(); }
});
it('does not renew on health, auth/me, or rejected origins', async () => {
const store = memoryStore(); await seedUser(store, { id: 'origin', email: 'origin@example.com', displayName: 'Origin' }); app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false });
const result = await login(app, 'origin@example.com', 'correct horse battery staple'); const session = store.sessions.documents[0]; const original = session.expiresAt;
expect((await app.inject({ method: 'GET', url: '/api/health', headers: { cookie: result.cookie } })).statusCode).toBe(200); expect(session.expiresAt).toEqual(original);
expect((await app.inject({ method: 'GET', url: '/api/auth/me', headers: { cookie: result.cookie } })).statusCode).toBe(200); expect(session.expiresAt).toEqual(original);
expect((await app.inject({ method: 'POST', url: '/api/auth/activity', headers: { cookie: result.cookie, origin: 'https://evil.example' }, payload: {} })).statusCode).toBe(403); expect(session.expiresAt).toEqual(original);
});
it('rejects a legacy session that has already been idle for an hour', async () => {
const store = memoryStore(); await seedUser(store, { id: 'legacy', email: 'legacy@example.com', displayName: 'Legacy' }); app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false });
const result = await login(app, 'legacy@example.com', 'correct horse battery staple'); const session = store.sessions.documents[0]; delete session.lastActivityAt; session.createdAt = new Date(Date.now() - 3_600_000); session.expiresAt = new Date(Date.now() + 86_400_000);
expect((await app.inject({ method: 'GET', url: '/api/teams', headers: { cookie: result.cookie } })).statusCode).toBe(401);
});
it('rejects unauthenticated and pending access', async () => {
const store = memoryStore(); app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false });
expect((await app.inject({ method: 'GET', url: '/api/teams' })).statusCode).toBe(401);