Activate a Desktop App by Scanning a QR Code
Typing a 29-character license key on a kiosk, a TV-box or a locked-down workstation is error-prone and hostile. Here is how to build a device-pairing activation flow for a .NET desktop app with Keyright: the app shows a QR code, the user approves on their phone, and the desktop polls for a signed activation it caches and validates offline.
A license key is a good credential and a terrible thing to type. On a workstation with a keyboard it is a mild annoyance; on the surfaces where desktop apps increasingly live — a reception kiosk, a digital-signage player, a TV-style app driven by a remote, a locked-down machine where paste is disabled by policy — it is genuinely hostile. A Keyright key is grouped, dash-separated, case-sensitive entropy, typically 25 to 29 characters. Ask someone to enter that with an on-screen keyboard and a D-pad and you have designed a support ticket.
The fix is not a shorter key — that would weaken it. The fix is to move key entry off the awkward device entirely. The desktop app displays a QR code; the user scans it with the phone already in their pocket, enters (or pastes) the key there where typing is easy, approves, and the desktop receives its signed activation over a back channel. This is the same shape as the OAuth 2.0 device authorization grant, adapted to licensing, and Keyright’s .NET SDK ships the two calls you need to build it.
Key takeaway: Pair, don’t type. Have the desktop app begin a device activation, render the returned pairing code as a QR, and poll. The user approves on their phone with the actual key. The signed license comes back to the desktop over the back channel — never inside the QR — and is cached for offline Validate() on every launch after. The QR carries only a short-lived, single-use pairing code, so a photographed screen grants nothing.
The flow at a glance
Three parties take part: the desktop app that wants to be licensed, the issuing service (your self-hosted Keyright instance), and the phone the user approves on. The desktop never sees the key; the phone never sees the signed license. Each stays on the channel it is good at.
The critical property is in step 5: the thing that actually unlocks the product travels from service to desktop, over a channel the user’s phone never touches. The QR in step 1 holds only the pairing code and the URL.
Step 1 — begin activation and render the QR
The desktop calls BeginDeviceActivationAsync once, at the point the user asks to activate. It returns a DeviceActivationRequest: a DeviceCode the desktop keeps private for polling, a short human-readable UserCode to display as a fallback, a VerificationUrl, and a ready-made VerificationUrlComplete that folds the code into the URL so the phone needs no manual entry at all.
using Delta1Labs.Keyright;
static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "lobby-kiosk",
PublicKeyBase64 = "MIIBIjANBgkq...", // verify-only public key, pinned
ServiceUrl = "https://keyright.example.com" // your self-hosted instance
});
DeviceActivationRequest req = await License.BeginDeviceActivationAsync(ct);
// Encode the *complete* verification URL (verify_url + user_code) as a QR.
// Anything that turns a string into a QBitmap works; QRCoder shown here.
using var gen = new QRCoder.QRCodeGenerator();
var data = gen.CreateQrCode(req.VerificationUrlComplete, QRCoder.QRCodeGenerator.ECCLevel.Q);
var png = new QRCoder.PngByteQRCode(data).GetGraphic(8);
ShowQrOnScreen(png);
// Always show the short code and URL as a fallback for a phone that can't scan.
ShowText($"On your phone, go to {req.VerificationUrl} and enter: {req.UserCode}");
What goes into the QR is exactly VerificationUrlComplete — a URL plus the pairing code, nothing more. There is no license key and no signed token in that image. The pairing code is single-use and expires at req.ExpiresAt (minutes, not hours), so a photographed screen leads only to an approval page that still demands the real key, and only until the code lapses.
Step 2 — poll for the signed activation
While the QR is on screen, the desktop polls PollDeviceActivationAsync with the private DeviceCode. The SDK honors the server’s polling interval and surfaces a small status enum so you can drive the UI without catching exceptions for ordinary “not yet” states.
LicenseInfo? activated = null;
var deadline = req.ExpiresAt;
while (DateTimeOffset.UtcNow < deadline)
{
DeviceActivationPoll poll = await License.PollDeviceActivationAsync(req.DeviceCode, ct);
switch (poll.Status)
{
case DeviceActivationStatus.Pending: // user hasn't approved yet
await Task.Delay(poll.Interval, ct); // respect server backoff
continue;
case DeviceActivationStatus.SlowDown: // we're polling too fast
await Task.Delay(poll.Interval + TimeSpan.FromSeconds(5), ct);
continue;
case DeviceActivationStatus.Approved:
activated = poll.License; // signed license, already cached
break;
case DeviceActivationStatus.Denied:
case DeviceActivationStatus.Expired:
ShowError($"Activation {poll.Status}. Start over to try again.");
return;
}
if (activated is not null) break;
}
if (activated is { IsLicensed: true } info)
{
ShowLicensedUi(info.Tier);
logger.LogInformation("Activated {Tier}, bound to {Id}", info.Tier, info.MachineId);
}
When the status flips to Approved, the SDK has already written the signed license to its local cache and bound the activation to this machine’s fingerprint (or to a MachineId you pinned in KeyrightOptions, if you manage identity yourself — useful for re-imaged kiosks). The LicenseInfo you get back is the same object every other Keyright path returns, so the rest of your app does not know or care that activation happened by QR.
Step 3 — what the user does on the phone
The phone side is deliberately boring, and that is the point. The user scans the QR, which opens the verification page. They enter or paste the license key — on a real keyboard, with autocomplete and paste working normally — and approve. The approval page confirms the product and the machine it is about to license so the user can see what they are authorizing.
Because the key is entered on the phone and the signed license is delivered to the desktop, the key never transits the kiosk’s input, its clipboard, or its screen. For a shared or public terminal that is a real security improvement over typing the key into the box in front of everyone.
If a phone cannot scan — an old camera, a corporate MDM that blocks the scanner — the printed UserCode and VerificationUrl are the fallback. A six-to-eight character grouped user code is tolerable to type even on a bad keyboard, which is the whole reason BeginDeviceActivationAsync returns a short code distinct from the long license key.
After pairing: validate offline, forever
Pairing is a one-time online handshake. Everything after it is the ordinary Keyright offline path. On each launch the app calls Validate(), which checks the cached signed license against the pinned public key with no network call — a signature verification and an expiry check, nothing more.
LicenseInfo info = License.Validate(); // offline, no I/O, never throws
if (!info.IsLicensed)
{
// Cache missing or cleared (re-image, wiped profile). Re-pair with the QR flow.
StartDevicePairing();
return;
}
bool signage = info.Entitlements.IsEnabled("digital-signage");
int maxScreens = (int)info.Entitlements.GetLimit("screens", 1);
So the full cost of the network is one pairing per machine. A kiosk that is re-imaged clears the cache and simply re-pairs — a thirty-second QR scan during provisioning, not a key typed by a technician. If you are licensing a free tier as well, an unpaired machine falls back to Community Edition rather than going dark: Validate() returns a Community-edition LicenseInfo whose entitlements you gate against, and the QR flow upgrades it in place when someone pairs a paid key.
Hardening the flow
A few rules keep device pairing honest:
- Never put anything signed in the QR. The QR is a pairing code and a URL. The license — the thing that grants entitlements — comes back only over the polled back channel. Treat a leaked QR as worthless by design, not by obscurity.
- Keep the pairing code short-lived and single-use.
ExpiresAtshould be minutes. Once approved or expired, theDeviceCodemust never activate again. The SDK enforces this server-side; don’t re-show a stale QR. - Respect
SlowDown. The poll interval is set by the service to protect it. The SDK surfaces it; back off when told to, and stop polling atExpiresAtrather than forever. - Show the user what they’re approving. The approval page naming the product and machine is what stops a phished QR from silently binding a key to an attacker’s device — the user sees an unfamiliar machine name and declines.
- Pin the public key. As with every Keyright integration,
PublicKeyBase64is a verify-only key compiled into the app. OfflineValidate()is only as trustworthy as that pin.
Summary
Typing a license key is the wrong job for a kiosk, a signage box or a remote-driven TV app, so don’t ask the desktop to do it. Begin a device activation, render the returned pairing code as a QR, and poll: the user approves on the phone they already type on comfortably, the signed license is delivered to the desktop over a back channel the phone never touches, and from then on the app validates offline with Validate() like any other Keyright integration. The QR holds only a short-lived, single-use pairing code, so a photographed screen grants nothing. The Keyright.NET SDK gives you BeginDeviceActivationAsync and PollDeviceActivationAsync for the handshake and the same fail-closed LicenseInfo everywhere after, so you can license the most input-hostile surface you ship with a thirty-second scan.
Try Nebula.NET
Harden your .NET code in minutes — start with the free edition.