Security Best Practices
How to make your license integration resistant to cracking, reverse engineering, and tampering. OnyxAuth signs every validation response with Ed25519, but what you do client-side matters just as much.
Response Signature Verification
Every response from /api/v1/validate is signed with your project's Ed25519 private key. Your public key is available in the dashboard. Without signature verification, an attacker can trivially fake responses by pointing your API calls to a local server that returns { "valid": true }.
How it works
- Your app sends a validation request with an optional nonce (random string)
- OnyxAuth validates the license and builds a response payload
- The payload is signed with your project's Ed25519 private key (stored server-side, never exposed)
- Your app receives the response and verifies the signature using the public key embedded in your binary
- If the signature is invalid or the nonce doesn't match, the response has been tampered with
Canonical Signing Payload
The signature covers a JSON string built from these fields in this exact order. To verify, reconstruct this exact JSON and verify against the signature field.
{
"licenseId": "550e8400-e29b-41d4-a716-446655440000",
"valid": true,
"expiresAt": "2026-12-31T23:59:59.000Z",
"timeLeft": 31536000,
"productName": "My App",
"signedAt": "2026-02-12T10:30:00.000Z",
"nonce": "your-random-nonce"
}Nonce (Replay Prevention)
Without a nonce, an attacker could capture a legitimate signed response and replay it forever. By sending a fresh random nonce with each request and verifying it's echoed back in the signed payload, you ensure the response was generated specifically for this request.
// 1. Generate a random nonce before each request
const nonce = crypto.randomBytes(16).toString('hex');
// 2. Include it in the validation request
const response = await fetch('/api/v1/validate', {
method: 'POST',
body: JSON.stringify({ clientId, licenseKey, fingerprint, nonce })
});
// 3. After verifying the signature, check the nonce matches
if (data.nonce !== nonce) {
throw new Error('Nonce mismatch - possible replay attack');
}Integration Hardening
Signature verification stops response forgery, but a determined attacker can still patch your binary. These techniques make that significantly harder.
1. Scatter License Checks
Don't check the license in one place. Validate at startup, periodically during runtime, and before sensitive operations. If an attacker patches one check, the others still catch it. Use different code paths for each check so they can't NOP a single function.
2. Avoid Boolean Flags
Never store the validation result in a single isLicensed = true variable. A cracker can find this in memory or in the binary with a simple search. Instead, derive the "licensed" state from the cryptographic verification itself. For example, use data from the signed response (like metadata or product name) as input to unlock functionality.
3. Periodic Re-validation (Heartbeat)
Don't only validate at startup. Re-validate periodically (every 15-60 minutes) during runtime. Use the signedAt timestamp in responses to detect stale cached responses. If validation fails mid-session, degrade functionality gracefully rather than crashing (which makes it obvious where the check is).
4. Binary Integrity Verification
Compute a checksum of your critical code sections at build time, then verify them at runtime. If an attacker patches your binary to skip the license check, the checksum will mismatch. Store the expected checksum in an obfuscated form.
5. Anti-Debugging
Detect debuggers and disassemblers at runtime. This raises the bar for reverse engineering.
| Platform | Technique |
|---|---|
| Windows | IsDebuggerPresent(), CheckRemoteDebuggerPresent(), timing checks |
| macOS/Linux | ptrace(PTRACE_TRACEME), /proc/self/status TracerPid check |
| .NET / Java | Debugger.IsAttached, timing-based detection |
6. Code Obfuscation
Obfuscate the license check code and signature verification to make static analysis harder. Choose tools appropriate for your language:
| Language | Recommended Tools |
|---|---|
| C / C++ | VMProtect, Themida, LLVM Obfuscator |
| C# / .NET | .NET Reactor, ConfuserEx, Dotfuscator |
| Java | ProGuard, Zelix KlassMaster, Allatori |
| Rust | obfstr (string obfuscation), release builds with LTO + strip |
| Go | garble, gobfuscate |
| Python | PyArmor, Cython compilation, Nuitka |
7. Feature Gating via Server Data
Instead of just gating on a boolean, use the signed response data your application needs to function. Store feature flags and configuration in your license's metadata field (set via the management API), then fetch it alongside validation. If the validation is skipped entirely, the application lacks the data it needs to work. This makes "just NOP the check" impossible because the check delivers functional requirements, not just a gate.
// On your license, set metadata like:
// { "features": ["export", "analytics"], "encryptionSeed": "x9k2m..." }
const result = await validateLicense(clientId, key, fingerprint);
// Use the signed productName or other response fields
// to derive which features to enable
if (!result.valid) {
disableAllFeatures();
return;
}
// Use the productName from the signed response to determine tier
const tier = result.productName; // e.g. "My App Pro"
if (tier.includes('Pro')) {
enableExportModule();
enableAnalyticsDashboard();
}Security Checklist
No protection is unbreakable. The goal is to raise the cost of cracking high enough that it's not worth the effort. Signature verification alone stops the vast majority of casual cracking attempts. Combined with obfuscation and the techniques above, you're well protected.
See the License Validation docs for full code examples in all 9 supported languages.