fix(api-key): enforce requested product binding
This commit is contained in:
parent
daec38faf7
commit
841d2f5129
@ -81,6 +81,30 @@ describe('api key auth', () => {
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('rejects api key when x-product-id targets another product', async () => {
|
||||
await seedApiKeyToken(['jobs:read']);
|
||||
const app = Fastify();
|
||||
await registerOptionalApiKeyContext(app);
|
||||
|
||||
app.get('/probe', async req => {
|
||||
return requireJwtOrApiKey(req, {
|
||||
apiKeyScopes: ['jobs:read'],
|
||||
});
|
||||
});
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/probe',
|
||||
headers: {
|
||||
'x-api-key': rawApiKey,
|
||||
'x-product-id': 'chronomind',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(JSON.parse(res.body).error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('rate limits per api key and action key', async () => {
|
||||
process.env.API_KEY_RATE_LIMIT_CONFIG_JSON = JSON.stringify({
|
||||
'jobs:write': {
|
||||
|
||||
@ -118,6 +118,13 @@ function ensureApiKeyScopes(req: FastifyRequest, requiredScopes: string[] = []):
|
||||
throw new UnauthorizedError('API key required');
|
||||
}
|
||||
|
||||
const productIdHeader = req.headers['x-product-id'];
|
||||
if (typeof productIdHeader === 'string' && productIdHeader.length > 0) {
|
||||
if (productIdHeader !== apiKey.productId) {
|
||||
throw new ForbiddenError('API key is not valid for the requested product');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
requiredScopes.length > 0 &&
|
||||
!requiredScopes.every(scope => tokenHasScope(apiKey.scopes, scope))
|
||||
@ -181,17 +188,12 @@ export async function registerOptionalApiKeyContext(app: FastifyInstance): Promi
|
||||
if (!rawKey) return;
|
||||
|
||||
const prefix = rawKey.slice(0, 12);
|
||||
const productIdHeader = req.headers['x-product-id'];
|
||||
const filter: Record<string, unknown> = {
|
||||
prefix,
|
||||
status: 'active',
|
||||
expiresAt: { $gte: new Date().toISOString() },
|
||||
};
|
||||
|
||||
if (typeof productIdHeader === 'string' && productIdHeader.length > 0) {
|
||||
filter.productId = productIdHeader;
|
||||
}
|
||||
|
||||
const candidates = await tokenCollection().findMany({
|
||||
filter,
|
||||
limit: 10,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user