Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 150 additions & 3 deletions services/event-service/src/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { clearSecretCacheForTest, signKiloToken } from '@kilocode/worker-utils';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
clearSecretCacheForTest,
EVENT_SERVICE_AUDIENCE,
signKiloToken,
} from '@kilocode/worker-utils';
import { type KiloUserPepperResult } from '@kilocode/worker-utils/kilo-token-auth';
import { type AuthEnv, authenticateToken } from '../auth';

Expand All @@ -25,14 +29,53 @@ function authenticateTestToken(token: string | null) {
return authenticateToken(token, makeEnv(), { getUserPepper });
}

function signEventServiceToken(params: { pepper?: string | null; env?: string } = {}) {
return signKiloToken({
userId: 'user-xyz-789',
pepper: params.pepper,
secret: TEST_JWT_SECRET,
expiresInSeconds: 3600,
env: params.env,
audience: EVENT_SERVICE_AUDIENCE,
});
}

async function signWithAudience(aud: unknown): Promise<string> {
const now = Math.floor(Date.now() / 1000);
const encode = (bytes: Uint8Array) =>
btoa(String.fromCharCode(...bytes))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const json = (value: unknown) => encode(new TextEncoder().encode(JSON.stringify(value)));
const input = `${json({ alg: 'HS256', typ: 'JWT' })}.${json({
version: 3,
kiloUserId: 'user-xyz-789',
apiTokenPepper: 'pepper-current',
env: 'production',
aud,
iat: now,
exp: now + 3600,
})}`;
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(TEST_JWT_SECRET),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(input));
return `${input}.${encode(new Uint8Array(signature))}`;
}

describe('authenticateToken', () => {
beforeEach(() => {
clearSecretCacheForTest();
currentPepperByUserId.clear();
currentPepperByUserId.set('user-xyz-789', { pepper: 'pepper-current', blockedReason: null });
});

it('authenticates a kilo-chat token with the current pepper', async () => {
it('authenticates a legacy one-hour kilo-chat token without an audience', async () => {
const { token } = await signKiloToken({
userId: 'user-xyz-789',
pepper: 'pepper-current',
Expand Down Expand Up @@ -60,6 +103,49 @@ describe('authenticateToken', () => {
});
});

it('authenticates event-service audience claims as strings and arrays', async () => {
const arrayAudienceToken = await signWithAudience(['another-service', EVENT_SERVICE_AUDIENCE]);
const stringAudienceToken = await signEventServiceToken({
pepper: 'pepper-current',
env: 'production',
});

await expect(authenticateTestToken(stringAudienceToken.token)).resolves.toEqual({
userId: 'user-xyz-789',
});
await expect(authenticateTestToken(arrayAudienceToken)).resolves.toEqual({
userId: 'user-xyz-789',
});
});

it('rejects wrong or malformed event-service audiences before pepper lookup', async () => {
const malformedAudienceToken = await signWithAudience([
EVENT_SERVICE_AUDIENCE,
EVENT_SERVICE_AUDIENCE,
]);
const wrongAudienceToken = await signKiloToken({
userId: 'user-xyz-789',
pepper: 'pepper-current',
secret: TEST_JWT_SECRET,
expiresInSeconds: 3600,
env: 'production',
audience: 'another-service',
});
let lookupCount = 0;
const lookup = async (...args: Parameters<typeof getUserPepper>) => {
lookupCount++;
return getUserPepper(...args);
};

await expect(
authenticateToken(wrongAudienceToken.token, makeEnv(), { getUserPepper: lookup })
).resolves.toBeNull();
await expect(
authenticateToken(malformedAudienceToken, makeEnv(), { getUserPepper: lookup })
).resolves.toBeNull();
expect(lookupCount).toBe(0);
});

it('rejects a valid kilo-chat JWT with a stale pepper', async () => {
const { token } = await signKiloToken({
userId: 'user-xyz-789',
Expand All @@ -86,6 +172,16 @@ describe('authenticateToken', () => {
await expect(authenticateTestToken(token)).resolves.toBeNull();
});

it('rejects event-service tokens with missing or mismatched environments', async () => {
const [missingEnvironment, mismatchedEnvironment] = await Promise.all([
signEventServiceToken({ pepper: 'pepper-current' }),
signEventServiceToken({ pepper: 'pepper-current', env: 'development' }),
]);

await expect(authenticateTestToken(missingEnvironment.token)).resolves.toBeNull();
await expect(authenticateTestToken(mismatchedEnvironment.token)).resolves.toBeNull();
});

it('rejects a token for a blocked user even when pepper matches', async () => {
currentPepperByUserId.set('user-xyz-789', {
pepper: 'pepper-current',
Expand All @@ -102,4 +198,55 @@ describe('authenticateToken', () => {

await expect(authenticateTestToken(token)).resolves.toBeNull();
});

it('rejects missing users, stale peppers, and a null pepper against a stored pepper', async () => {
const [stalePepper, nullPepper] = await Promise.all([
signEventServiceToken({ pepper: 'pepper-stale', env: 'production' }),
signEventServiceToken({ pepper: null, env: 'production' }),
]);

await expect(authenticateTestToken(stalePepper.token)).resolves.toBeNull();
await expect(authenticateTestToken(nullPepper.token)).resolves.toBeNull();
const validToken = await signEventServiceToken({ pepper: 'pepper-current', env: 'production' });
await expect(authenticateTestToken(validToken.token)).resolves.toEqual({
userId: 'user-xyz-789',
});
currentPepperByUserId.clear();
const lookup = vi.fn(getUserPepper);
await expect(
authenticateToken(validToken.token, makeEnv(), { getUserPepper: lookup })
).resolves.toBeNull();
expect(lookup).toHaveBeenCalledWith('postgres://test', 'user-xyz-789');
});

it('allows absent peppers but rejects explicit null peppers for a user with a current pepper', async () => {
const absentPepper = await signEventServiceToken({ env: 'production' });
const nullPepper = await signEventServiceToken({ pepper: null, env: 'production' });

await expect(authenticateTestToken(absentPepper.token)).resolves.toEqual({
userId: 'user-xyz-789',
});
await expect(authenticateTestToken(nullPepper.token)).resolves.toBeNull();
});

it('propagates secret and pepper lookup failures', async () => {
const { token } = await signEventServiceToken({ pepper: 'pepper-current', env: 'production' });

await expect(
authenticateToken(
token,
{
...makeEnv(),
NEXTAUTH_SECRET: { get: async () => Promise.reject(new Error('secret unavailable')) },
},
{ getUserPepper }
)
).rejects.toThrow('secret unavailable');
clearSecretCacheForTest();
await expect(
authenticateToken(token, makeEnv(), {
getUserPepper: async () => Promise.reject(new Error('lookup unavailable')),
})
).rejects.toThrow('lookup unavailable');
});
});
2 changes: 2 additions & 0 deletions services/event-service/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EVENT_SERVICE_AUDIENCE } from '@kilocode/worker-utils/internal-service-token-audiences';
import {
type GetKiloUserPepper,
verifyKiloBearerAgainstCurrentPepper,
Expand All @@ -21,6 +22,7 @@ export async function authenticateToken(
nextAuthSecret: env.NEXTAUTH_SECRET,
workerEnv: env.WORKER_ENV,
connectionString: env.HYPERDRIVE.connectionString,
resourceAudience: { audience: EVENT_SERVICE_AUDIENCE, mode: 'allow-legacy' },
...(options.getUserPepper ? { getUserPepper: options.getUserPepper } : {}),
});
}
Loading