fix(api): accept Pireon webhooks from the password client too

Pireon signs each webhook with the secret of the client that registered
it, and the API has two clients (the web app's and the password
grant's). Only PIREON_WEBHOOK_SECRET was accepted, so a token revoked
for someone signed in with a password went unheard. A delivery now
verifies with PIREON_WEBHOOK_SECRET or PIREON_PASSWORD_WEBHOOK_SECRET;
the same event arriving from both clients changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:29:27 +02:00
parent 66b09ecf4d
commit 5ae1d05d30
9 changed files with 94 additions and 19 deletions

View File

@@ -0,0 +1,48 @@
import { UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IdentityService } from '@task-stream/api/identity/application';
import { createHmac } from 'node:crypto';
import { PireonWebhookController } from './pireon-webhook.controller';
describe('PireonWebhookController', () => {
const handled: string[] = [];
const controller = (secrets: Record<string, string>) =>
new PireonWebhookController(
{
handleWebhook: async (event: string) => {
handled.push(event);
},
} as unknown as IdentityService,
{ get: (key: string) => secrets[key] } as unknown as ConfigService,
);
const body = { event: 'token.revoked', data: { token_id: 't-1' } };
const raw = Buffer.from(JSON.stringify(body));
const signed = (secret: string) =>
createHmac('sha256', secret).update(raw).digest('hex');
const request = { rawBody: raw, body } as never;
beforeEach(() => (handled.length = 0));
it("accepts a delivery signed with either Pireon client's secret", async () => {
const both = controller({
PIREON_WEBHOOK_SECRET: 'web-secret',
PIREON_PASSWORD_WEBHOOK_SECRET: 'password-secret',
});
await both.receive(request, signed('web-secret'));
await both.receive(request, `sha256=${signed('password-secret')}`);
expect(handled).toEqual(['token.revoked', 'token.revoked']);
});
it('refuses any other signature, and everything when no secret is set', async () => {
await expect(
controller({ PIREON_WEBHOOK_SECRET: 'web-secret' }).receive(
request,
signed('password-secret'),
),
).rejects.toThrow(UnauthorizedException);
await expect(
controller({}).receive(request, signed('web-secret')),
).rejects.toThrow(/disabled/);
expect(handled).toEqual([]);
});
});