# Activate a Desktop App by Scanning a QR Code


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.

<figure class="my-8"><svg viewBox="0 0 700 360" role="img" aria-label="Device pairing sequence: the desktop app begins activation and receives a device code and user code, renders a QR, and polls; the user scans with a phone, opens the verification URL, enters the license key and approves; the service binds the activation and the next poll returns a signed license the desktop caches." xmlns="http://www.w3.org/2000/svg" style="max-width:700px;width:100%;height:auto;font-family:system-ui,sans-serif"><text x="90" y="24" fill="#6366f1" font-size="13" font-weight="700" text-anchor="middle">Desktop app</text><text x="350" y="24" fill="#0891b2" font-size="13" font-weight="700" text-anchor="middle">Issuing service</text><text x="610" y="24" fill="#10b981" font-size="13" font-weight="700" text-anchor="middle">Phone</text><line x1="90" y1="34" x2="90" y2="340" stroke="#6366f1" stroke-width="1.5"/><line x1="350" y1="34" x2="350" y2="340" stroke="#0891b2" stroke-width="1.5"/><line x1="610" y1="34" x2="610" y2="340" stroke="#10b981" stroke-width="1.5"/><text x="220" y="58" fill="#64748b" font-size="11" text-anchor="middle">1. BeginDeviceActivation</text><line x1="90" y1="64" x2="350" y2="64" stroke="#64748b" stroke-width="1.3" marker-end="url(#ah)"/><text x="220" y="86" fill="#64748b" font-size="11" text-anchor="middle">device_code, user_code, verify_url</text><line x1="350" y1="92" x2="90" y2="92" stroke="#64748b" stroke-width="1.3" marker-end="url(#ah)"/><rect x="40" y="104" width="100" height="26" rx="5" fill="none" stroke="#6366f1" stroke-width="1.3"/><text x="90" y="121" fill="#6366f1" font-size="10" text-anchor="middle">render QR</text><text x="350" y="150" fill="#64748b" font-size="11" text-anchor="middle">2. scan QR, open verify_url</text><line x1="610" y1="156" x2="350" y2="156" stroke="#10b981" stroke-width="1.3" marker-end="url(#ah)"/><text x="480" y="178" fill="#64748b" font-size="11" text-anchor="middle">3. enter license key + approve</text><line x1="610" y1="184" x2="350" y2="184" stroke="#10b981" stroke-width="1.3" marker-end="url(#ah)"/><rect x="300" y="196" width="100" height="26" rx="5" fill="none" stroke="#0891b2" stroke-width="1.3"/><text x="350" y="213" fill="#0891b2" font-size="10" text-anchor="middle">bind activation</text><text x="220" y="248" fill="#64748b" font-size="11" text-anchor="middle">4. PollDeviceActivation (repeat)</text><line x1="90" y1="254" x2="350" y2="254" stroke="#94a3b8" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#ah)"/><text x="220" y="276" fill="#94a3b8" font-size="10" text-anchor="middle">pending… pending…</text><line x1="350" y1="286" x2="90" y2="286" stroke="#94a3b8" stroke-width="1.1" stroke-dasharray="3 4" marker-end="url(#ah)"/><text x="220" y="312" fill="#10b981" font-size="11" font-weight="600" text-anchor="middle">5. signed license</text><line x1="350" y1="318" x2="90" y2="318" stroke="#10b981" stroke-width="1.6" marker-end="url(#ah)"/><rect x="40" y="328" width="100" height="24" rx="5" fill="none" stroke="#6366f1" stroke-width="1.3"/><text x="90" y="344" fill="#6366f1" font-size="10" text-anchor="middle">cache locally</text><defs><marker id="ah" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#64748b"/></marker></defs></svg></figure>

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.

```csharp
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.

```csharp
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.

```csharp
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.** `ExpiresAt` should be minutes. Once approved or expired, the `DeviceCode` must 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 at `ExpiresAt` rather 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, `PublicKeyBase64` is a verify-only key compiled into the app. Offline `Validate()` 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](https://delta1labs.com/docs/keyright/integration-dotnet) 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.

