diff --git a/src/app/api/auth/me/sessions/[id]/route.ts b/src/app/api/auth/me/sessions/[id]/route.ts index 4a98aed9d..5b9dfd040 100644 --- a/src/app/api/auth/me/sessions/[id]/route.ts +++ b/src/app/api/auth/me/sessions/[id]/route.ts @@ -11,7 +11,7 @@ import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiSuccess, apiError, getClientIp } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; -import { destroySessionById } from "@/lib/auth/session"; +import { destroySessionById, sessionHandle } from "@/lib/auth/session"; export const dynamic = "force-dynamic"; @@ -25,15 +25,21 @@ export const DELETE = apiHandler( return apiError("Session not found", 404); } + // The path carries the public handle, never the row id, so "is this the + // caller's own session" compares handles. A Bearer caller's `session.id` + // is a token id, whose handle matches no session, which is the right + // answer for it too. + const self = id === sessionHandle(session.id); + await auditLog("auth.session.revoke", { userId: user.id, ipAddress: getClientIp(request), - details: { self: id === session.id }, + details: { self }, }); annotate({ action: { name: "auth.session.revoke" }, - meta: { self: id === session.id }, + meta: { self }, }); return apiSuccess({ revoked: true }); diff --git a/tests/integration/routes/auth-me-sessions.test.ts b/tests/integration/routes/auth-me-sessions.test.ts index 2113336b7..c04f0ebff 100644 --- a/tests/integration/routes/auth-me-sessions.test.ts +++ b/tests/integration/routes/auth-me-sessions.test.ts @@ -449,11 +449,12 @@ describe("DELETE /api/auth/me/sessions/{id}", () => { ).toBeNull(); expect((await listSessions(laptop)).status).toBe(401); expect((await listSessions(current)).status).toBe(200); - expect( - await prisma.auditLog.count({ - where: { userId: owner.id, action: "auth.session.revoke" }, - }), - ).toBe(1); + const audits = await prisma.auditLog.findMany({ + where: { userId: owner.id, action: "auth.session.revoke" }, + }); + expect(audits.map((a) => JSON.parse(a.details ?? "{}"))).toEqual([ + { self: false }, + ]); }); it("revokes the caller's own session when it names its own handle", async () => { @@ -469,6 +470,13 @@ describe("DELETE /api/auth/me/sessions/{id}", () => { expect(res.status).toBe(200); expect((await listSessions(current)).status).toBe(401); + // The audit row says the person signed out the session they were using. + const audits = await prisma.auditLog.findMany({ + where: { userId: owner.id, action: "auth.session.revoke" }, + }); + expect(audits.map((a) => JSON.parse(a.details ?? "{}"))).toEqual([ + { self: true }, + ]); }); it("answers 404 for another account's session and leaves it signed in", async () => {