From 5f558e5539ab51396d26a9563e5afdd0f618ad99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 13 Mar 2026 09:20:14 +0100 Subject: [PATCH] fix: accept production enterprise keys in development environment (#18611) ## Problem When `NODE_ENV` is development, the server was only using the dev public key to verify enterprise JWTs. Production keys are signed with the production private key, so they failed verification with the dev public key, resulting in "Invalid enterprise key" errors. ## Solution Try both production and dev public keys when in development, so production keys work when testing locally. In production, only the production key is used (unchanged behavior). ## Changes - `enterprise-plan.service.ts`: Replaced `getPublicKey()` with `getPublicKeysToTry()` that returns both keys in development; updated `verifyJwt()` to try each key until one succeeds - `enterprise-plan.service.spec.ts`: Added test for production key acceptance when `NODE_ENV` is development Made with [Cursor](https://cursor.com) --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- .../__tests__/enterprise-plan.service.spec.ts | 15 ++++++ .../services/enterprise-plan.service.ts | 51 +++++++++++-------- 2 files changed, 44 insertions(+), 22 deletions(-) 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; }