Skip to content

Commit 118b9f1

Browse files
Refactor license claims in JWT to use standard 'iss' identifier and update related tests for improved clarity and consistency
1 parent 9626ff2 commit 118b9f1

File tree

4 files changed

+55
-28
lines changed

4 files changed

+55
-28
lines changed

react_on_rails_pro/LICENSE_SETUP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ The license is a JWT (JSON Web Token) signed with RSA-256, containing:
191191
"exp": 1234567890, // Expiration timestamp (REQUIRED)
192192
"plan": "free", // License plan: "free" or "paid" (Optional)
193193
"organization": "Your Company", // Organization name (Optional)
194-
"issued_by": "api" // License issuer identifier (Optional)
194+
"iss": "api" // Issuer identifier (Optional, standard JWT claim)
195195
}
196196
```
197197

react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,12 @@ def load_and_decode_license
6868
license_string = load_license_string
6969

7070
JWT.decode(
71+
# The JWT token containing the license data
7172
license_string,
73+
# RSA public key used to verify the JWT signature
7274
public_key,
75+
# verify_signature: NEVER set to false! When false, signature verification is skipped,
76+
# allowing anyone to forge licenses. Must always be true for security.
7377
true,
7478
# NOTE: Never remove the 'algorithm' parameter from JWT.decode to prevent algorithm bypassing vulnerabilities.
7579
# Ensure to hardcode the expected algorithm.
@@ -91,7 +95,7 @@ def load_license_string
9195
return File.read(config_path).strip if config_path.exist?
9296

9397
@validation_error = "No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable " \
94-
"or create config/react_on_rails_pro_license.key file. " \
98+
"or create #{config_path} file. " \
9599
"Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
96100
handle_invalid_license(@validation_error)
97101
end
@@ -108,10 +112,10 @@ def handle_invalid_license(message)
108112

109113
def log_license_info(license)
110114
plan = license["plan"]
111-
issued_by = license["issued_by"]
115+
iss = license["iss"]
112116

113117
Rails.logger.info("[React on Rails Pro] License plan: #{plan}") if plan
114-
Rails.logger.info("[React on Rails Pro] Issued by: #{issued_by}") if issued_by
118+
Rails.logger.info("[React on Rails Pro] Issued by: #{iss}") if iss
115119
end
116120
end
117121
end

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import * as path from 'path';
44
import { PUBLIC_KEY } from './licensePublicKey';
55

66
interface LicenseData {
7-
sub?: string; // Subject (email for whom the license is issued)
8-
iat?: number; // Issued at timestamp
9-
exp: number; // Required: expiration timestamp
10-
plan?: string; // Optional: license plan (e.g., "free", "paid")
11-
issued_by?: string; // Optional: who issued the license
7+
// Subject (email for whom the license is issued)
8+
sub?: string;
9+
// Issued at timestamp
10+
iat?: number;
11+
// Required: expiration timestamp
12+
exp: number;
13+
// Optional: license plan (e.g., "free", "paid")
14+
plan?: string;
15+
// Issuer (who issued the license)
16+
iss?: string;
1217
// Allow additional fields
1318
[key: string]: unknown;
1419
}
@@ -34,13 +39,13 @@ function handleInvalidLicense(message: string): never {
3439
* @private
3540
*/
3641
function logLicenseInfo(license: LicenseData): void {
37-
const { plan, issued_by: issuedBy } = license;
42+
const { plan, iss } = license;
3843

3944
if (plan) {
4045
console.log(`[React on Rails Pro] License plan: ${plan}`);
4146
}
42-
if (issuedBy) {
43-
console.log(`[React on Rails Pro] Issued by: ${issuedBy}`);
47+
if (iss) {
48+
console.log(`[React on Rails Pro] Issued by: ${iss}`);
4449
}
4550
}
4651

@@ -57,18 +62,19 @@ function loadLicenseString(): string | never {
5762
}
5863

5964
// Then try config file (relative to project root)
65+
let configPath;
6066
try {
61-
const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
67+
configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
6268
if (fs.existsSync(configPath)) {
6369
return fs.readFileSync(configPath, 'utf8').trim();
6470
}
65-
} catch {
66-
// File doesn't exist or can't be read
71+
} catch (error) {
72+
console.error(`[React on Rails Pro] Error reading license file: ${(error as Error).message}`);
6773
}
6874

6975
cachedValidationError =
7076
'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
71-
'or create config/react_on_rails_pro_license.key file. ' +
77+
`or create ${configPath ?? 'config/react_on_rails_pro_license.key'} file. ` +
7278
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
7379

7480
handleInvalidLicense(cachedValidationError);
@@ -114,6 +120,7 @@ function performValidation(): boolean | never {
114120
}
115121

116122
// Check expiry
123+
// Date.now() returns milliseconds, but JWT exp is in Unix seconds, so divide by 1000
117124
if (Date.now() / 1000 > license.exp) {
118125
cachedValidationError =
119126
'License has expired. ' +

react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ describe('LicenseValidator', () => {
2020
let testPublicKey: string;
2121
let mockProcessExit: jest.SpyInstance;
2222
let mockConsoleError: jest.SpyInstance;
23+
let originalEnv: NodeJS.ProcessEnv;
2324

2425
beforeEach(() => {
26+
// Store original environment
27+
originalEnv = { ...process.env };
28+
2529
// Clear the module cache to get a fresh instance
2630
jest.resetModules();
2731

@@ -69,10 +73,22 @@ describe('LicenseValidator', () => {
6973
});
7074

7175
afterEach(() => {
72-
delete process.env.REACT_ON_RAILS_PRO_LICENSE;
76+
// Restore original environment
77+
process.env = originalEnv;
7378
jest.restoreAllMocks();
7479
});
7580

81+
/**
82+
* Helper function to mock the REACT_ON_RAILS_PRO_LICENSE environment variable
83+
*/
84+
const mockLicenseEnv = (token: string | undefined) => {
85+
if (token === undefined) {
86+
delete process.env.REACT_ON_RAILS_PRO_LICENSE;
87+
} else {
88+
process.env.REACT_ON_RAILS_PRO_LICENSE = token;
89+
}
90+
};
91+
7692
describe('validateLicense', () => {
7793
it('validates successfully for valid license in ENV', () => {
7894
const validPayload = {
@@ -82,7 +98,7 @@ describe('LicenseValidator', () => {
8298
};
8399

84100
const validToken = jwt.sign(validPayload, testPrivateKey, { algorithm: 'RS256' });
85-
process.env.REACT_ON_RAILS_PRO_LICENSE = validToken;
101+
mockLicenseEnv(validToken);
86102

87103
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
88104
expect(module.validateLicense()).toBe(true);
@@ -96,7 +112,7 @@ describe('LicenseValidator', () => {
96112
};
97113

98114
const expiredToken = jwt.sign(expiredPayload, testPrivateKey, { algorithm: 'RS256' });
99-
process.env.REACT_ON_RAILS_PRO_LICENSE = expiredToken;
115+
mockLicenseEnv(expiredToken);
100116

101117
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
102118

@@ -117,7 +133,7 @@ describe('LicenseValidator', () => {
117133
};
118134

119135
const tokenWithoutExp = jwt.sign(payloadWithoutExp, testPrivateKey, { algorithm: 'RS256' });
120-
process.env.REACT_ON_RAILS_PRO_LICENSE = tokenWithoutExp;
136+
mockLicenseEnv(tokenWithoutExp);
121137

122138
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
123139

@@ -152,7 +168,7 @@ describe('LicenseValidator', () => {
152168
};
153169

154170
const invalidToken = jwt.sign(validPayload, wrongKey, { algorithm: 'RS256' });
155-
process.env.REACT_ON_RAILS_PRO_LICENSE = invalidToken;
171+
mockLicenseEnv(invalidToken);
156172

157173
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
158174

@@ -164,7 +180,7 @@ describe('LicenseValidator', () => {
164180
});
165181

166182
it('calls process.exit for missing license', () => {
167-
delete process.env.REACT_ON_RAILS_PRO_LICENSE;
183+
mockLicenseEnv(undefined);
168184

169185
// Mock fs.existsSync to return false (no config file)
170186
jest.mocked(fs.existsSync).mockReturnValue(false);
@@ -178,7 +194,7 @@ describe('LicenseValidator', () => {
178194
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('FREE evaluation license'));
179195
});
180196

181-
it('loads license from config file when ENV not set', () => {
197+
it('validates license from ENV variable after reset', () => {
182198
const validPayload = {
183199
184200
iat: Math.floor(Date.now() / 1000),
@@ -189,7 +205,7 @@ describe('LicenseValidator', () => {
189205

190206
// Set the license in ENV variable instead of file
191207
// (file-based testing is complex due to module caching)
192-
process.env.REACT_ON_RAILS_PRO_LICENSE = validToken;
208+
mockLicenseEnv(validToken);
193209

194210
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
195211

@@ -208,15 +224,15 @@ describe('LicenseValidator', () => {
208224
};
209225

210226
const validToken = jwt.sign(validPayload, testPrivateKey, { algorithm: 'RS256' });
211-
process.env.REACT_ON_RAILS_PRO_LICENSE = validToken;
227+
mockLicenseEnv(validToken);
212228

213229
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
214230

215231
// First call
216232
expect(module.validateLicense()).toBe(true);
217233

218234
// Change ENV (shouldn't affect cached result)
219-
delete process.env.REACT_ON_RAILS_PRO_LICENSE;
235+
mockLicenseEnv(undefined);
220236

221237
// Second call should use cache
222238
expect(module.validateLicense()).toBe(true);
@@ -233,7 +249,7 @@ describe('LicenseValidator', () => {
233249
};
234250

235251
const validToken = jwt.sign(payload, testPrivateKey, { algorithm: 'RS256' });
236-
process.env.REACT_ON_RAILS_PRO_LICENSE = validToken;
252+
mockLicenseEnv(validToken);
237253

238254
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
239255
const data = module.getLicenseData();
@@ -253,7 +269,7 @@ describe('LicenseValidator', () => {
253269
};
254270

255271
const expiredToken = jwt.sign(expiredPayload, testPrivateKey, { algorithm: 'RS256' });
256-
process.env.REACT_ON_RAILS_PRO_LICENSE = expiredToken;
272+
mockLicenseEnv(expiredToken);
257273

258274
const module = jest.requireActual<LicenseValidatorModule>('../src/shared/licenseValidator');
259275

0 commit comments

Comments
 (0)