mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(auth): record a self sign-out as one in the session revoke audit
DELETE /api/auth/me/sessions/{id} takes the session's public handle, an HMAC
of the row id, and compared that handle with the caller's row id to decide
whether the caller signed out the session it was using. The two never match,
so every revoke was audited and annotated with self: false, including the
ones where a person ended their own session. The handle is compared with the
handle of the caller's session now.
This commit is contained in:
1 parent
f353f55212
commit
09f088e8a7
2 files changed
+22
-8
No files matched your search
@@ -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 });
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user