feat: harden sessions and track downstream acknowledgements

This commit is contained in:
hectorzhao
2026-07-14 14:18:43 +08:00
parent 3d37adcc9f
commit 8c03663f24
43 changed files with 1733 additions and 150 deletions
@@ -1,27 +1,51 @@
import { UnauthorizedException } from '@nestjs/common';
import { SessionValidationMiddleware, type SessionRequest } from './session-validation.middleware';
function request(authorization?: string): SessionRequest {
return { header: jest.fn().mockReturnValue(authorization) };
const record = {
userId: 'user-1', portal: 'admin' as const, sessionVersion: 3, createdAt: 1,
lastActivityAt: 1, lastAuthenticatedAt: 1, absoluteExpiresAt: Date.now() + 1000,
};
function request(path = '/api/admin/users', cookie = 'cmpp_session=opaque-token'): SessionRequest {
return {
originalUrl: path,
header: jest.fn((name: string) => name === 'cookie' ? cookie : undefined),
};
}
describe('SessionValidationMiddleware', () => {
it('accepts the current user session version and exposes the session user id', async () => {
it('accepts an active Redis session and exposes its user and record', async () => {
const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 3 }) } };
const middleware = new SessionValidationMiddleware(prisma as never);
const currentRequest = request('Bearer dev-token:user-1:3');
const sessions = { validate: jest.fn().mockResolvedValue({ status: 'active', record }), remove: jest.fn() };
const middleware = new SessionValidationMiddleware(prisma as never, sessions as never);
const currentRequest = request();
const next = jest.fn();
await middleware.use(currentRequest, {} as never, next);
await middleware.use(currentRequest, {}, next);
expect(next).toHaveBeenCalledTimes(1);
expect(currentRequest.sessionUserId).toBe('user-1');
expect(currentRequest.sessionToken).toBe('opaque-token');
expect(currentRequest.authSession).toEqual(record);
});
it('rejects an old session token after the user session version changes', async () => {
it('rejects a session after the user session version changes', async () => {
const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 4 }) } };
const middleware = new SessionValidationMiddleware(prisma as never);
const sessions = { validate: jest.fn().mockResolvedValue({ status: 'active', record }), remove: jest.fn() };
const middleware = new SessionValidationMiddleware(prisma as never, sessions as never);
await expect(middleware.use(request('Bearer dev-token:user-1:3'), {} as never, jest.fn())).rejects.toBeInstanceOf(UnauthorizedException);
await expect(middleware.use(request(), {}, jest.fn())).rejects.toBeInstanceOf(UnauthorizedException);
expect(sessions.remove).toHaveBeenCalledWith('opaque-token');
});
it('only lets a locked session reach unlock and logout endpoints', async () => {
const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 3 }) } };
const sessions = { validate: jest.fn().mockResolvedValue({ status: 'locked', record }), remove: jest.fn() };
const middleware = new SessionValidationMiddleware(prisma as never, sessions as never);
await expect(middleware.use(request(), {}, jest.fn())).rejects.toBeInstanceOf(UnauthorizedException);
const next = jest.fn();
await middleware.use(request('/api/auth/session/unlock'), {}, next);
expect(next).toHaveBeenCalled();
});
});