fix(auth): the ingest and idempotency Bearer resolvers refuse a pending account

Both resolve a Bearer token outside resolveBearerToken and checked only
revoked and expiry. The deletion revokes every token in its own transaction,
so only a token issued in that same instant could get through, but the rule
should hold everywhere a token is resolved: both now refuse an account
pending deletion.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 17:33:16 +01:00
1 parent 69c5c65376
commit e6c709876d
4 files changed
+53 -2

No files matched your search

+12
View File
@@ -426,6 +426,18 @@ describe("defaultUserIdResolver (audit C-4)", () => {
expect(await defaultUserIdResolver()).toBeNull();
});
it("rejects the Bearer token of an account pending deletion", async () => {
vi.mocked(getSession).mockResolvedValue(null);
mockHeader("Bearer hlk_abcdef");
vi.mocked(prisma.apiToken.findUnique).mockResolvedValue({
userId: "u-bearer",
revoked: false,
expiresAt: null,
user: { deletionRequestedAt: new Date() },
} as never);
expect(await defaultUserIdResolver()).toBeNull();
});
it("rejects expired Bearer tokens", async () => {
vi.mocked(getSession).mockResolvedValue(null);
mockHeader("Bearer hlk_abcdef");
+9 -1
View File
@@ -558,11 +558,19 @@ export async function defaultUserIdResolver(): Promise<string | null> {
const apiToken = await prisma.apiToken
.findUnique({
where: { tokenHash: tokenHashValue },
select: { userId: true, revoked: true, expiresAt: true },
select: {
userId: true,
revoked: true,
expiresAt: true,
user: { select: { deletionRequestedAt: true } },
},
})
.catch(() => null);
// An account pending deletion replays nothing, as `resolveBearerToken`
// refuses it.
if (!apiToken || apiToken.revoked) return null;
if (apiToken.user?.deletionRequestedAt) return null;
if (apiToken.expiresAt && apiToken.expiresAt <= new Date()) return null;
return apiToken.userId;
}