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 <noreply@anthropic.com>
This commit is contained in:
+15
@@ -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', () => {
|
||||
|
||||
+29
-22
@@ -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<T extends Record<string, unknown>>(
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user