Skip to content

Commit b98f261

Browse files
Add offline JWT-based license validation system for React on Rails Pro
Implements a pure offline license validation system using JWT tokens signed with RSA-256. No internet connectivity required for validation. Key features: - JWT-based licenses verified with embedded public key (RSA-256) - Offline validation in Ruby gem and Node renderer - Environment variable or config file support - Development-friendly (warnings) vs production (errors) - Zero impact on browser bundle size - Comprehensive test coverage Changes: - Add JWT dependencies (Ruby jwt gem, Node jsonwebtoken) - Create license validation modules for Ruby and Node - Integrate validation into Rails context (rorPro field) - Add license check on Node renderer startup - Update .gitignore for license file - Add comprehensive tests for both Ruby and Node - Create LICENSE_SETUP.md documentation The system validates licenses at: 1. Ruby gem initialization (Rails startup) 2. Node renderer startup 3. Browser relies on server validation (railsContext.rorPro) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7bce94a commit b98f261

File tree

14 files changed

+879
-2
lines changed

14 files changed

+879
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,6 @@ ssr-generated
7575

7676
# Claude Code local settings
7777
.claude/settings.local.json
78+
79+
# React on Rails Pro license file
80+
config/react_on_rails_pro_license.key

lib/react_on_rails/helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,10 @@ def rails_context(server_side: true)
377377
i18nDefaultLocale: I18n.default_locale,
378378
rorVersion: ReactOnRails::VERSION,
379379
# TODO: v13 just use the version if existing
380-
rorPro: ReactOnRails::Utils.react_on_rails_pro?
380+
rorPro: ReactOnRails::Utils.react_on_rails_pro_licence_valid?
381381
}
382382

383-
if ReactOnRails::Utils.react_on_rails_pro?
383+
if ReactOnRails::Utils.react_on_rails_pro_licence_valid?
384384
result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version
385385

386386
if ReactOnRails::Utils.rsc_support_enabled?
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# React on Rails Pro License Setup
2+
3+
This document explains how to configure your React on Rails Pro license for the Pro features to work properly.
4+
5+
## Prerequisites
6+
7+
- React on Rails Pro gem installed
8+
- React on Rails Pro Node packages installed
9+
- Valid license key from [ShakaCode](https://shakacode.com/react-on-rails-pro)
10+
11+
## License Configuration
12+
13+
### Method 1: Environment Variable (Recommended)
14+
15+
Set the `REACT_ON_RAILS_PRO_LICENSE` environment variable with your license key:
16+
17+
```bash
18+
export REACT_ON_RAILS_PRO_LICENSE="your_jwt_license_token_here"
19+
```
20+
21+
For production deployments, add this to your deployment configuration:
22+
23+
- **Heroku**: `heroku config:set REACT_ON_RAILS_PRO_LICENSE="your_token"`
24+
- **Docker**: Add to your Dockerfile or docker-compose.yml
25+
- **Kubernetes**: Add to your secrets or ConfigMap
26+
27+
### Method 2: Configuration File
28+
29+
Create a file at `config/react_on_rails_pro_license.key` in your Rails root directory:
30+
31+
```bash
32+
echo "your_jwt_license_token_here" > config/react_on_rails_pro_license.key
33+
```
34+
35+
**Important**: This file is automatically excluded from Git via .gitignore. Never commit your license key to version control.
36+
37+
## License Validation
38+
39+
The license is validated at multiple points:
40+
41+
1. **Ruby Gem**: Validated when Rails initializes
42+
2. **Node Renderer**: Validated when the Node renderer starts
43+
3. **Browser Package**: Relies on server-side validation (via `railsContext.rorPro`)
44+
45+
### Development vs Production
46+
47+
- **Development Environment**:
48+
- Invalid or missing licenses show warnings but allow continued usage
49+
- 30-day grace period for evaluation
50+
51+
- **Production Environment**:
52+
- Invalid or missing licenses will prevent Pro features from working
53+
- The application will raise errors if license validation fails
54+
55+
## Verification
56+
57+
To verify your license is properly configured:
58+
59+
### Ruby Console
60+
61+
```ruby
62+
rails console
63+
> ReactOnRails::Utils.react_on_rails_pro_licence_valid?
64+
# Should return true if license is valid
65+
```
66+
67+
### Node Renderer
68+
69+
When starting the Node renderer, you should see:
70+
71+
```
72+
[React on Rails Pro] License validation successful
73+
```
74+
75+
### Rails Context
76+
77+
In your browser's JavaScript console:
78+
79+
```javascript
80+
window.railsContext.rorPro
81+
// Should return true if license is valid
82+
```
83+
84+
## Troubleshooting
85+
86+
### Common Issues
87+
88+
1. **"No license found" error**
89+
- Verify the environment variable is set: `echo $REACT_ON_RAILS_PRO_LICENSE`
90+
- Check the config file exists: `ls config/react_on_rails_pro_license.key`
91+
92+
2. **"Invalid license signature" error**
93+
- Ensure you're using the complete JWT token (it should be a long string starting with "eyJ")
94+
- Verify the license hasn't been modified or truncated
95+
96+
3. **"License has expired" error**
97+
- Contact ShakaCode support to renew your license
98+
- In development, this will show as a warning but continue working
99+
100+
4. **Node renderer fails to start**
101+
- Check that the same license is available to the Node process
102+
- Verify NODE_ENV is set correctly (development/production)
103+
104+
### Debug Mode
105+
106+
For more detailed logging, set:
107+
108+
```bash
109+
export REACT_ON_RAILS_PRO_DEBUG=true
110+
```
111+
112+
## License Format
113+
114+
The license is a JWT (JSON Web Token) signed with RSA-256. It contains:
115+
116+
- Subscriber email
117+
- Issue date
118+
- Expiration date (if applicable)
119+
120+
The token is verified using a public key embedded in the code, ensuring authenticity without requiring internet connectivity.
121+
122+
## Support
123+
124+
If you encounter any issues with license validation:
125+
126+
1. Check this documentation
127+
2. Review the troubleshooting section above
128+
3. Contact ShakaCode support at [email protected]
129+
4. Visit https://shakacode.com/react-on-rails-pro for license management
130+
131+
## Security Notes
132+
133+
- Never share your license key publicly
134+
- Never commit the license key to version control
135+
- Use environment variables for production deployments
136+
- The license key is tied to your organization's subscription

react_on_rails_pro/lib/react_on_rails_pro.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
require "react_on_rails_pro/error"
1010
require "react_on_rails_pro/utils"
1111
require "react_on_rails_pro/configuration"
12+
require "react_on_rails_pro/license_public_key"
13+
require "react_on_rails_pro/license_validator"
1214
require "react_on_rails_pro/cache"
1315
require "react_on_rails_pro/stream_cache"
1416
require "react_on_rails_pro/server_rendering_pool/pro_rendering"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module ReactOnRailsPro
4+
module LicensePublicKey
5+
# This is a placeholder public key for development/testing
6+
# In production, this should be replaced with ShakaCode's actual public key
7+
# The private key corresponding to this public key should NEVER be committed to the repository
8+
KEY = OpenSSL::PKey::RSA.new(<<~PEM)
9+
-----BEGIN PUBLIC KEY-----
10+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF/NP
11+
lRQkqfph3x6TEOirFCpDfRjowDXAk66dPmLzw5qVOmGVPKgpJBjZR7oMIMgxBPUoj00F
12+
DwlhUGmOVoqnVWGFHVUHDL5qYQaZzRdp4Bh9fxnN52Yk8+FuHsT+5lxLcaRV6mRtX7OT
13+
5pQbxV0o0/OxPFC1Hz9RdLPUevnWNbLe8f5ePHivmqsoAH9HE4g03WkFZEqBLmjqpJj8
14+
VqGR0q8CPPRCFGAr9S4WCQqBhLDH0j/JR+FpPX9Df8vfFJhHdBGdTGjN4g9g6qwPYmVH
15+
ukAErHNIJMNmzYjFIT4+Xwp6xKHyUqL3w3JZDQnFywIDAQAB
16+
-----END PUBLIC KEY-----
17+
PEM
18+
end
19+
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# frozen_string_literal: true
2+
3+
require "jwt"
4+
require "pathname"
5+
6+
module ReactOnRailsPro
7+
class LicenseValidator
8+
class << self
9+
def valid?
10+
return @valid if defined?(@valid)
11+
12+
@valid = validate_license
13+
end
14+
15+
def reset!
16+
remove_instance_variable(:@valid) if defined?(@valid)
17+
remove_instance_variable(:@license_data) if defined?(@license_data)
18+
remove_instance_variable(:@validation_error) if defined?(@validation_error)
19+
end
20+
21+
def license_data
22+
@license_data ||= load_and_decode_license
23+
end
24+
25+
def validation_error
26+
@validation_error
27+
end
28+
29+
private
30+
31+
def validate_license
32+
# In development, show warnings but allow usage
33+
development_mode = Rails.env.development? || Rails.env.test?
34+
35+
begin
36+
license = load_and_decode_license
37+
return false unless license
38+
39+
# Check expiry if present
40+
if license["exp"] && Time.now.to_i > license["exp"]
41+
@validation_error = "License has expired"
42+
handle_invalid_license(development_mode, @validation_error)
43+
return development_mode
44+
end
45+
46+
true
47+
rescue JWT::DecodeError => e
48+
@validation_error = "Invalid license signature: #{e.message}"
49+
handle_invalid_license(development_mode, @validation_error)
50+
development_mode
51+
rescue StandardError => e
52+
@validation_error = "License validation error: #{e.message}"
53+
handle_invalid_license(development_mode, @validation_error)
54+
development_mode
55+
end
56+
end
57+
58+
def load_and_decode_license
59+
license_string = load_license_string
60+
return nil unless license_string
61+
62+
JWT.decode(
63+
license_string,
64+
public_key,
65+
true,
66+
algorithm: "RS256"
67+
).first
68+
end
69+
70+
def load_license_string
71+
# First try environment variable
72+
license = ENV["REACT_ON_RAILS_PRO_LICENSE"]
73+
return license if license.present?
74+
75+
# Then try config file
76+
config_path = Rails.root.join("config", "react_on_rails_pro_license.key")
77+
return File.read(config_path).strip if config_path.exist?
78+
79+
@validation_error = "No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable " \
80+
"or create config/react_on_rails_pro_license.key file. " \
81+
"Visit https://shakacode.com/react-on-rails-pro to obtain a license."
82+
handle_invalid_license(Rails.env.development? || Rails.env.test?, @validation_error)
83+
nil
84+
end
85+
86+
def public_key
87+
ReactOnRailsPro::LicensePublicKey::KEY
88+
end
89+
90+
def handle_invalid_license(development_mode, message)
91+
full_message = "[React on Rails Pro] #{message}"
92+
93+
if development_mode
94+
Rails.logger.warn(full_message)
95+
puts "\e[33m#{full_message}\e[0m" # Yellow warning in console
96+
else
97+
Rails.logger.error(full_message)
98+
raise ReactOnRailsPro::Error, full_message
99+
end
100+
end
101+
end
102+
end
103+
end

react_on_rails_pro/lib/react_on_rails_pro/utils.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def self.rorp_puts(message)
1616
puts "[ReactOnRailsPro] #{message}"
1717
end
1818

19+
def self.licence_valid?
20+
LicenseValidator.valid?
21+
end
22+
1923
def self.copy_assets
2024
return if ReactOnRailsPro.configuration.assets_to_copy.blank?
2125

react_on_rails_pro/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@fastify/multipart": "^8.3.1 || ^9.0.3",
4545
"fastify": "^4.29.0 || ^5.2.1",
4646
"fs-extra": "^11.2.0",
47+
"jsonwebtoken": "^9.0.2",
4748
"lockfile": "^1.0.4"
4849
},
4950
"devDependencies": {

react_on_rails_pro/packages/node-renderer/src/master.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,27 @@ import log from './shared/log';
77
import { buildConfig, Config, logSanitizedConfig } from './shared/configBuilder';
88
import restartWorkers from './master/restartWorkers';
99
import * as errorReporter from './shared/errorReporter';
10+
import { isLicenseValid, getLicenseValidationError } from './shared/licenseValidator';
1011

1112
const MILLISECONDS_IN_MINUTE = 60000;
1213

1314
export = function masterRun(runningConfig?: Partial<Config>) {
15+
// Validate license before starting
16+
if (!isLicenseValid()) {
17+
const error = getLicenseValidationError() || 'Invalid license';
18+
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
19+
20+
if (isDevelopment) {
21+
log.warn(`[React on Rails Pro] ${error}`);
22+
// Continue in development with warning
23+
} else {
24+
log.error(`[React on Rails Pro] ${error}`);
25+
process.exit(1);
26+
}
27+
} else {
28+
log.info('[React on Rails Pro] License validation successful');
29+
}
30+
1431
// Store config in app state. From now it can be loaded by any module using getConfig():
1532
const config = buildConfig(runningConfig);
1633
const { workersCount, allWorkersRestartInterval, delayBetweenIndividualWorkerRestarts } = config;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// This is a placeholder public key for development/testing
2+
// In production, this should be replaced with ShakaCode's actual public key
3+
// The private key corresponding to this public key should NEVER be committed to the repository
4+
export const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
5+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF/NP
6+
lRQkqfph3x6TEOirFCpDfRjowDXAk66dPmLzw5qVOmGVPKgpJBjZR7oMIMgxBPUoj00F
7+
DwlhUGmOVoqnVWGFHVUHDL5qYQaZzRdp4Bh9fxnN52Yk8+FuHsT+5lxLcaRV6mRtX7OT
8+
5pQbxV0o0/OxPFC1Hz9RdLPUevnWNbLe8f5ePHivmqsoAH9HE4g03WkFZEqBLmjqpJj8
9+
VqGR0q8CPPRCFGAr9S4WCQqBhLDH0j/JR+FpPX9Df8vfFJhHdBGdTGjN4g9g6qwPYmVH
10+
ukAErHNIJMNmzYjFIT4+Xwp6xKHyUqL3w3JZDQnFywIDAQAB
11+
-----END PUBLIC KEY-----`;

0 commit comments

Comments
 (0)