From 4bee263a59363e71df4c28e2d9f47eeccca4f5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 19 Jul 2026 12:20:25 +0200 Subject: [PATCH] test(sessions): revoke by the public handle, and prove the row id is not a key --- .../sessions-and-known-devices.test.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/integration/sessions-and-known-devices.test.ts b/tests/integration/sessions-and-known-devices.test.ts index 2ea8d6743..fb9cbc7c5 100644 --- a/tests/integration/sessions-and-known-devices.test.ts +++ b/tests/integration/sessions-and-known-devices.test.ts @@ -5,12 +5,17 @@ * Covers the DB-layer contracts the unit mocks can't pin: * - destroyOtherSessions keeps the current session, deletes the rest, revokes * native refresh tokens, and leaves API tokens untouched. - * - destroySessionById is scoped to the owning user (no cross-user delete). + * - destroySessionById is scoped to the owning user (no cross-user delete) + * and takes the public handle, never the row id. * - the (userId, deviceHash) unique index enforces the login-alert dedupe. */ import { beforeEach, describe, expect, it } from "vitest"; -import { destroyOtherSessions, destroySessionById } from "@/lib/auth/session"; +import { + destroyOtherSessions, + destroySessionById, + sessionHandle, +} from "@/lib/auth/session"; import { getPrismaClient, truncateAllTables } from "./setup"; beforeEach(async () => { @@ -85,14 +90,26 @@ describe("destroySessionById", () => { data: { userId: other.id, expiresAt: new Date(Date.now() + 1e6) }, }); - // Cross-user attempt: owner tries to delete the intruder's session. - expect(await destroySessionById(owner.id, otherSession.id)).toBe(false); + // Cross-user attempt: owner presents the intruder's handle. + expect( + await destroySessionById(owner.id, sessionHandle(otherSession.id)), + ).toBe(false); expect( await prisma.session.findUnique({ where: { id: otherSession.id } }), ).not.toBeNull(); - // Owned delete succeeds. - expect(await destroySessionById(owner.id, ownerSession.id)).toBe(true); + // The row id is not an accepted key: a caller who somehow learned it + // still cannot revoke with it, which is what keeps the id out of the + // client's hands from being load-bearing in both directions. + expect(await destroySessionById(owner.id, ownerSession.id)).toBe(false); + expect( + await prisma.session.findUnique({ where: { id: ownerSession.id } }), + ).not.toBeNull(); + + // Owned delete succeeds on the handle. + expect( + await destroySessionById(owner.id, sessionHandle(ownerSession.id)), + ).toBe(true); expect( await prisma.session.findUnique({ where: { id: ownerSession.id } }), ).toBeNull();