Every licensing integration has a happy path: the app starts, validates a license, unlocks the right features, and runs. That path is easy. The entire value of a licensing system lives in the unhappy path — the moment the app cannot validate. The network is down. The server is mid-deploy. The clock is wrong. The license file was deleted. A disk is read-only. What your application does in that instant is the whole design, and most integrations get it wrong in one of two equally bad directions.
Fail open — "if I can't check, assume it's fine" — makes piracy trivial: block the validation endpoint with a firewall rule and the product runs unlicensed forever. Fail closed naively — "if I can't check, shut down" — turns a thirty-second server blip, or a laptop on a plane, into a hard outage for a paying customer. Neither is acceptable. The right answer is fail closed, but not immediately, and getting the "not immediately" part right is what separates a licensing system you can ship from one that generates support tickets.
Leases turn one check into a window of trust
The mechanism underneath is a lease. When Keyright validates a license — online or from a signed offline file — it does not return a boolean. It returns a lease: a validation result that is good until an explicit expiry, carried with a signature so it cannot be forged or extended by editing a file.
public sealed record Lease(
LicenseStatus Status, // Active, Grace, Expired, Revoked
Entitlements Entitlements, // what this license unlocks
DateTimeOffset IssuedAt,
DateTimeOffset ValidUntil, // hard stop for THIS lease
DateTimeOffset GraceUntil); // keep running, degraded-trust, until here
The client caches the lease and, crucially, does its own enforcement against the clock without calling the server again until the lease is due for renewal. A typical desktop product revalidates daily but holds a lease valid for, say, seven days. That gap is deliberate: it is the buffer that absorbs outages.
async Task<Lease> EnsureLeaseAsync(Lease? cached, CancellationToken ct)
{
var now = _clock.UtcNow;
// Still comfortably inside the current lease — don't even phone home.
if (cached is { } l && now < l.ValidUntil - _renewAhead)
return l;
try
{
// Renew. On success we get a fresh ValidUntil pushed into the future.
return await _client.RenewAsync(ct);
}
catch (Exception ex) when (ex is HttpRequestException or TimeoutException)
{
// Could not reach the server. This is the decision point.
return Degrade(cached, now);
}
}
The catch is where the philosophy lives. We did not reach the server — but we are not blind, because we still hold the last signed lease. The question is not "online or offline"; it is "how long since we last had proof, and is that still within tolerance?"
The grace window: fail closed, but give the world a chance to come back
Degrade implements the actual policy. A lease has two horizons: ValidUntil (when this proof-of-validation goes stale) and GraceUntil (the hard wall). Between them the app keeps running, but it knows its trust is degrading and it says so.
Lease Degrade(Lease? cached, DateTimeOffset now)
{
if (cached is null)
// Never validated on this machine, and can't now. Nothing to trust.
throw new LicensingException(Reason.NoLease);
if (now < cached.ValidUntil)
return cached; // not even stale yet
if (now < cached.GraceUntil)
return cached with { Status = LicenseStatus.Grace }; // stale, still allowed
// Past the grace wall with no successful renewal. Now we fail closed.
return cached with { Status = LicenseStatus.Expired };
}
Three things make this humane rather than hostile:
-
The grace window is tied to real time, not app launches. A machine that stays offline for a weekend is fine on Monday. The clock advancing does not, by itself, end the lease — only crossing
GraceUntildoes, and every successful renewal pushes it forward. -
Grace is loud, Active is silent. In
Active, the user sees nothing — enforcement that nags a paying customer is a bug. InGrace, the app surfaces a quiet, dismissable "couldn't reach licensing, retrying" notice and keeps trying in the background. The customer finds out before the wall, not at it. -
Expiry degrades, it rarely kills.
LicenseStatus.Expiredshould gate the licensed capabilities, notEnvironment.Exit. A tool that deletes the user's unsaved work because a server was briefly unreachable has confused enforcement with sabotage.
Clock tampering is the obvious attack — handle it explicitly
A reader three steps ahead has spotted the hole: this is all driven by _clock.UtcNow, so can't an attacker just set their clock back to stay in Active forever? Yes — if you trust the system clock blindly. Keyright does not. The lease records the highest timestamp the client has ever seen (from signed server responses and its own monotonic readings), and refuses to believe the wall clock has moved backward past it.
DateTimeOffset MonotonicNow()
{
var wall = DateTimeOffset.UtcNow;
// Anchor: (serverTimeAtLastSync, Stopwatch elapsed since that sync).
var anchored = _anchorServerTime + _sinceSync.Elapsed;
// Trust whichever is LATER — rolling the wall clock back cannot rewind us.
return wall > anchored ? wall : anchored;
}
Rolling the clock back now does nothing: the monotonic anchor keeps advancing, the lease ages normally, and the attacker lands in Grace and then fail-closed on schedule. Rolling it forward only ends their own lease faster. This is the same clock-tamper defense used for offline validation — enforcement and offline both lean on it.
Where self-hosting changes the calculus
If you run Keyright self-hosted, you own the issuing service, and "the server was unreachable" now includes "my own instance was down." The grace window is what buys you room to restart, migrate, or move the database without a single customer noticing — their leases are valid for days, not seconds. Set the lease lifetime and grace window to comfortably exceed your worst realistic maintenance window. A product that requires its licensing server to have higher uptime than the product itself has inverted the dependency; the lease is the inversion-breaker.
The default worth internalizing: fail closed, after a grace window longer than any outage you can plausibly cause, with a clock you don't blindly trust. Fail open and you are not really licensed. Fail closed instantly and you have built a denial-of-service against your own customers. The lease — a signed, time-bounded, forward-only window of trust — is how you get to be strict without being brittle.
Keyright ships lease-based, fail-closed-after-grace enforcement in every SDK. See the Keyright getting-started guide to wire it into your product.
Top comments (0)