fix(core): Make sure middleware works with legacy API Keys (#13390)

This commit is contained in:
Ricardo Espinoza 2025-02-20 09:10:54 -05:00 committed by GitHub
parent c9c0716a69
commit ca76ef4bc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 14 deletions

View file

@ -1,5 +1,8 @@
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { DateTime } from 'luxon';
import type { InstanceSettings } from 'n8n-core'; import type { InstanceSettings } from 'n8n-core';
import { randomString } from 'n8n-workflow';
import type { OpenAPIV3 } from 'openapi-types'; import type { OpenAPIV3 } from 'openapi-types';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository'; import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
@ -143,6 +146,85 @@ describe('PublicApiKeyService', () => {
}), }),
); );
}); });
it('should return false if expired JWT is used', async () => {
//Arrange
const path = '/test';
const method = 'GET';
const apiVersion = 'v1';
const publicApiKeyService = new PublicApiKeyService(
apiKeyRepository,
userRepository,
jwtService,
eventService,
);
const dateInThePast = DateTime.now().minus({ days: 1 }).toUnixInteger();
const owner = await createOwnerWithApiKey({
expiresAt: dateInThePast,
});
const [{ apiKey }] = owner.apiKeys;
const middleware = publicApiKeyService.getAuthMiddleware(apiVersion);
//Act
const response = await middleware(mockReqWith(apiKey, path, method), {}, securitySchema);
//Assert
expect(response).toBe(false);
});
it('should work with non JWT (legacy) api keys', async () => {
//Arrange
const path = '/test';
const method = 'GET';
const apiVersion = 'v1';
const legacyApiKey = `n8n_api_${randomString(10)}`;
const publicApiKeyService = new PublicApiKeyService(
apiKeyRepository,
userRepository,
jwtService,
eventService,
);
const owner = await createOwnerWithApiKey();
const [{ apiKey }] = owner.apiKeys;
await Container.get(ApiKeyRepository).update({ apiKey }, { apiKey: legacyApiKey });
const middleware = publicApiKeyService.getAuthMiddleware(apiVersion);
//Act
const response = await middleware(
mockReqWith(legacyApiKey, path, method),
{},
securitySchema,
);
//Assert
expect(response).toBe(true);
expect(eventService.emit).toHaveBeenCalledTimes(1);
expect(eventService.emit).toHaveBeenCalledWith(
'public-api-invoked',
expect.objectContaining({
userId: owner.id,
path,
method,
apiVersion: 'v1',
}),
);
});
}); });
describe('redactApiKey', () => { describe('redactApiKey', () => {

View file

@ -17,6 +17,7 @@ const API_KEY_AUDIENCE = 'public-api';
const API_KEY_ISSUER = 'n8n'; const API_KEY_ISSUER = 'n8n';
const REDACT_API_KEY_REVEAL_COUNT = 4; const REDACT_API_KEY_REVEAL_COUNT = 4;
const REDACT_API_KEY_MAX_LENGTH = 10; const REDACT_API_KEY_MAX_LENGTH = 10;
const PREFIX_LEGACY_API_KEY = 'n8n_api_';
@Service() @Service()
export class PublicApiKeyService { export class PublicApiKeyService {
@ -107,14 +108,17 @@ export class PublicApiKeyService {
if (!user) return false; if (!user) return false;
try { // Legacy API keys are not JWTs and do not need to be verified.
this.jwtService.verify(providedApiKey, { if (!providedApiKey.startsWith(PREFIX_LEGACY_API_KEY)) {
issuer: API_KEY_ISSUER, try {
audience: API_KEY_AUDIENCE, this.jwtService.verify(providedApiKey, {
}); issuer: API_KEY_ISSUER,
} catch (e) { audience: API_KEY_AUDIENCE,
if (e instanceof TokenExpiredError) return false; });
throw e; } catch (e) {
if (e instanceof TokenExpiredError) return false;
throw e;
}
} }
this.eventService.emit('public-api-invoked', { this.eventService.emit('public-api-invoked', {

View file

@ -80,23 +80,30 @@ export async function createUserWithMfaEnabled(
}; };
} }
export const addApiKey = async (user: User) => { export const addApiKey = async (
user: User,
{ expiresAt = null }: { expiresAt?: number | null } = {},
) => {
return await Container.get(PublicApiKeyService).createPublicApiKeyForUser(user, { return await Container.get(PublicApiKeyService).createPublicApiKeyForUser(user, {
label: randomName(), label: randomName(),
expiresAt: null, expiresAt,
}); });
}; };
export async function createOwnerWithApiKey() { export async function createOwnerWithApiKey({
expiresAt = null,
}: { expiresAt?: number | null } = {}) {
const owner = await createOwner(); const owner = await createOwner();
const apiKey = await addApiKey(owner); const apiKey = await addApiKey(owner, { expiresAt });
owner.apiKeys = [apiKey]; owner.apiKeys = [apiKey];
return owner; return owner;
} }
export async function createMemberWithApiKey() { export async function createMemberWithApiKey({
expiresAt = null,
}: { expiresAt?: number | null } = {}) {
const member = await createMember(); const member = await createMember();
const apiKey = await addApiKey(member); const apiKey = await addApiKey(member, { expiresAt });
member.apiKeys = [apiKey]; member.apiKeys = [apiKey];
return member; return member;
} }