diff --git a/packages/twenty-server/src/engine/core-modules/enterprise/services/__tests__/enterprise-plan.service.spec.ts b/packages/twenty-server/src/engine/core-modules/enterprise/services/__tests__/enterprise-plan.service.spec.ts index 558cdedde3..2ed3ffbdbf 100644 --- a/packages/twenty-server/src/engine/core-modules/enterprise/services/__tests__/enterprise-plan.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/enterprise/services/__tests__/enterprise-plan.service.spec.ts @@ -5,6 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity'; import { EnterprisePlanService } from 'src/engine/core-modules/enterprise/services/enterprise-plan.service'; +import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; import { ConfigVariableException, ConfigVariableExceptionCode, @@ -353,6 +354,20 @@ describe('EnterprisePlanService', () => { expect(service.isValidEnterpriseKeyFormat(invalidKey)).toBe(false); }); + + it('should accept production key when NODE_ENV is development', () => { + configGetMock.mockImplementation((key: string) => { + if (key === 'NODE_ENV') return NodeEnvironment.DEVELOPMENT; + if (key === 'ENTERPRISE_API_URL') return MOCK_API_URL; + + return undefined; + }); + mockCryptoVerify.mockReturnValueOnce(false).mockReturnValueOnce(true); + const productionKey = createFakeJwt(MOCK_KEY_PAYLOAD); + + expect(service.isValidEnterpriseKeyFormat(productionKey)).toBe(true); + expect(mockCryptoVerify).toHaveBeenCalledTimes(2); + }); }); describe('getLicenseInfo', () => { diff --git a/packages/twenty-server/src/engine/core-modules/enterprise/services/enterprise-plan.service.ts b/packages/twenty-server/src/engine/core-modules/enterprise/services/enterprise-plan.service.ts index 535e59602b..a38f036194 100644 --- a/packages/twenty-server/src/engine/core-modules/enterprise/services/enterprise-plan.service.ts +++ b/packages/twenty-server/src/engine/core-modules/enterprise/services/enterprise-plan.service.ts @@ -477,12 +477,15 @@ export class EnterprisePlanService implements OnModuleInit { } } - private getPublicKey(): string { + // In development, try both keys so production keys work when testing locally + private getPublicKeysToTry(): string[] { const nodeEnv = this.twentyConfigService.get('NODE_ENV'); - return nodeEnv === NodeEnvironment.DEVELOPMENT - ? ENTERPRISE_JWT_DEV_PUBLIC_KEY - : ENTERPRISE_JWT_PUBLIC_KEY; + if (nodeEnv === NodeEnvironment.DEVELOPMENT) { + return [ENTERPRISE_JWT_PUBLIC_KEY, ENTERPRISE_JWT_DEV_PUBLIC_KEY]; + } + + return [ENTERPRISE_JWT_PUBLIC_KEY]; } private verifyJwt>( @@ -504,27 +507,31 @@ export class EnterprisePlanService implements OnModuleInit { 'base64', ); - const isValid = crypto.verify( - 'sha256', - Buffer.from(signingInput), - { - key: this.getPublicKey(), - padding: crypto.constants.RSA_PKCS1_PADDING, - }, - signatureBuffer, - ); + const publicKeys = this.getPublicKeysToTry(); - if (!isValid) { - return null; + for (const publicKey of publicKeys) { + const isValid = crypto.verify( + 'sha256', + Buffer.from(signingInput), + { + key: publicKey, + padding: crypto.constants.RSA_PKCS1_PADDING, + }, + signatureBuffer, + ); + + if (isValid) { + const payloadStr = Buffer.from( + encodedPayload.replace(/-/g, '+').replace(/_/g, '/') + + '='.repeat((4 - (encodedPayload.length % 4)) % 4), + 'base64', + ).toString('utf-8'); + + return JSON.parse(payloadStr) as T; + } } - const payloadStr = Buffer.from( - encodedPayload.replace(/-/g, '+').replace(/_/g, '/') + - '='.repeat((4 - (encodedPayload.length % 4)) % 4), - 'base64', - ).toString('utf-8'); - - return JSON.parse(payloadStr) as T; + return null; } catch { return null; }