Skip to content

Commit d7fc4f0

Browse files
Add tests for required exp field validation
1 parent 97ece1e commit d7fc4f0

File tree

3 files changed

+96
-11
lines changed

3 files changed

+96
-11
lines changed

react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def validate_license
3434

3535
begin
3636
license = load_and_decode_license
37-
return false unless license
37+
# If no license found, load_license_string already handled the error
38+
return development_mode unless license
3839

3940
# Check that exp field exists
4041
unless license["exp"]
@@ -51,6 +52,9 @@ def validate_license
5152
end
5253

5354
true
55+
rescue ReactOnRailsPro::Error
56+
# Re-raise errors from handle_invalid_license in production mode
57+
raise
5458
rescue JWT::DecodeError => e
5559
@validation_error = "Invalid license signature: #{e.message}"
5660
handle_invalid_license(development_mode, @validation_error)
@@ -73,7 +77,9 @@ def load_and_decode_license
7377
# NOTE: Never remove the 'algorithm' parameter from JWT.decode to prevent algorithm bypassing vulnerabilities.
7478
# Ensure to hardcode the expected algorithm.
7579
# See: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
76-
algorithm: "RS256"
80+
algorithm: "RS256",
81+
# Disable automatic expiration verification so we can handle it manually with custom logic
82+
verify_expiration: false
7783
).first
7884
end
7985

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,49 @@ describe('LicenseValidator', () => {
112112
consoleSpy.mockRestore();
113113
});
114114

115+
it('returns false for license missing exp field in production', () => {
116+
const payloadWithoutExp = {
117+
118+
iat: Math.floor(Date.now() / 1000)
119+
// exp field is missing
120+
};
121+
122+
const tokenWithoutExp = jwt.sign(payloadWithoutExp, testPrivateKey, { algorithm: 'RS256' });
123+
process.env.REACT_ON_RAILS_PRO_LICENSE = tokenWithoutExp;
124+
process.env.NODE_ENV = 'production';
125+
126+
const module = require('../src/shared/licenseValidator');
127+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
128+
129+
expect(module.isLicenseValid()).toBe(false);
130+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('License is missing required expiration field'));
131+
132+
consoleSpy.mockRestore();
133+
});
134+
135+
it('returns true for license missing exp field in development with warning', () => {
136+
const payloadWithoutExp = {
137+
138+
iat: Math.floor(Date.now() / 1000)
139+
// exp field is missing
140+
};
141+
142+
const tokenWithoutExp = jwt.sign(payloadWithoutExp, testPrivateKey, { algorithm: 'RS256' });
143+
process.env.REACT_ON_RAILS_PRO_LICENSE = tokenWithoutExp;
144+
process.env.NODE_ENV = 'development';
145+
146+
const module = require('../src/shared/licenseValidator');
147+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
148+
149+
expect(module.isLicenseValid()).toBe(true);
150+
expect(consoleSpy).toHaveBeenCalledWith(
151+
expect.any(String),
152+
expect.stringContaining('License is missing required expiration field')
153+
);
154+
155+
consoleSpy.mockRestore();
156+
});
157+
115158
it('returns false for invalid signature', () => {
116159
// Generate a different key pair for invalid signature
117160
const { privateKey: wrongKey } = crypto.generateKeyPairSync('rsa', {

react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030

3131
before do
3232
described_class.reset!
33-
# Stub the public key to use our test key
34-
allow(ReactOnRailsPro::LicensePublicKey).to receive(:KEY).and_return(test_public_key)
33+
# Stub the public key constant to use our test key
34+
stub_const("ReactOnRailsPro::LicensePublicKey::KEY", test_public_key)
3535
# Clear ENV variable
3636
ENV.delete("REACT_ON_RAILS_PRO_LICENSE")
3737
end
@@ -65,10 +65,10 @@
6565
ENV["REACT_ON_RAILS_PRO_LICENSE"] = expired_token
6666
end
6767

68-
it "returns false in production" do
68+
it "raises error in production" do
6969
allow(Rails.env).to receive(:development?).and_return(false)
7070
allow(Rails.env).to receive(:test?).and_return(false)
71-
expect(described_class.valid?).to be false
71+
expect { described_class.valid? }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
7272
end
7373

7474
it "returns true in development with warning" do
@@ -78,17 +78,51 @@
7878
end
7979
end
8080

81+
context "with license missing exp field" do
82+
let(:payload_without_exp) do
83+
{
84+
85+
iat: Time.now.to_i
86+
# exp field is missing
87+
}
88+
end
89+
90+
before do
91+
token_without_exp = JWT.encode(payload_without_exp, test_private_key, "RS256")
92+
ENV["REACT_ON_RAILS_PRO_LICENSE"] = token_without_exp
93+
end
94+
95+
it "raises error in production" do
96+
allow(Rails.env).to receive(:development?).and_return(false)
97+
allow(Rails.env).to receive(:test?).and_return(false)
98+
expect { described_class.valid? }.to raise_error(ReactOnRailsPro::Error, /License is missing required expiration field/)
99+
end
100+
101+
it "returns true in development with warning" do
102+
allow(Rails.env).to receive(:development?).and_return(true)
103+
expect(Rails.logger).to receive(:warn).with(/License is missing required expiration field/)
104+
expect(described_class.valid?).to be true
105+
end
106+
107+
it "sets appropriate validation error in development" do
108+
allow(Rails.env).to receive(:development?).and_return(true)
109+
allow(Rails.logger).to receive(:warn)
110+
described_class.valid?
111+
expect(described_class.validation_error).to eq("License is missing required expiration field")
112+
end
113+
end
114+
81115
context "with invalid signature" do
82116
before do
83117
wrong_key = OpenSSL::PKey::RSA.new(2048)
84118
invalid_token = JWT.encode(valid_payload, wrong_key, "RS256")
85119
ENV["REACT_ON_RAILS_PRO_LICENSE"] = invalid_token
86120
end
87121

88-
it "returns false in production" do
122+
it "raises error in production" do
89123
allow(Rails.env).to receive(:development?).and_return(false)
90124
allow(Rails.env).to receive(:test?).and_return(false)
91-
expect(described_class.valid?).to be false
125+
expect { described_class.valid? }.to raise_error(ReactOnRailsPro::Error, /Invalid license signature/)
92126
end
93127

94128
it "returns true in development with warning" do
@@ -99,9 +133,11 @@
99133
end
100134

101135
context "with missing license" do
136+
let(:config_path) { double("Pathname", exist?: false) }
137+
102138
before do
103139
ENV.delete("REACT_ON_RAILS_PRO_LICENSE")
104-
allow(File).to receive(:read).and_raise(Errno::ENOENT)
140+
allow(Rails.root).to receive(:join).with("config", "react_on_rails_pro_license.key").and_return(config_path)
105141
end
106142

107143
it "returns false in production with error" do
@@ -152,8 +188,8 @@
152188
before do
153189
expired_token = JWT.encode(expired_payload, test_private_key, "RS256")
154190
ENV["REACT_ON_RAILS_PRO_LICENSE"] = expired_token
155-
allow(Rails.env).to receive(:development?).and_return(false)
156-
allow(Rails.env).to receive(:test?).and_return(false)
191+
allow(Rails.env).to receive(:development?).and_return(true)
192+
allow(Rails.logger).to receive(:warn)
157193
end
158194

159195
it "returns the error message" do

0 commit comments

Comments
 (0)