Skip to main content
This page assumes you have read Offline license files and decided that a machine genuinely cannot reach AuthForge. If it can reach the internet even occasionally, the default online activation + grace period is simpler, cheaper and revocable; use that instead.

Decide the policy before the first mint

Every .authforge file is a promise you cannot take back. Settle these three questions up front: Write the answers down as your offline SKU’s terms so support, sales and engineering all say the same thing to customers.

Collecting the HWID

A bound file needs the target machine’s HWID before you mint. Every SDK exposes it without any network access: Ship a small “Show machine ID” affordance (a menu item, a --hwid flag, a first-run screen) that prints the value and lets the user copy it — or, better, writes an activation request (.authforge-request) they can email you. The dashboard checksums that file so truncation and line-wrapping cannot silently mint to the wrong HWID. Because HWIDs are SDK-specific, collect it from the same SDK build that will later verify the file. If the customer already activated online once, the dashboard prefills the HWIDs bound to the license and you can skip this step.
For appliances you image yourself, pass hwidOverride with a serial number you control (for example unit:SN-00042) so the HWID survives disk swaps and NIC replacements.

Delivering the file

The file is plain text with PEM-style armor, so it survives every channel that carries text: email, a support portal download, a USB stick through a data diode, a QR code for very short payloads, or a printed sheet in a pinch. The parser tolerates CRLF conversion, a UTF-8 BOM, re-wrapped lines and surrounding text (an email preamble), so copy/paste damage is rare. Keep the extension .authforge. Use .authforge.lic only where a platform refuses unknown extensions. Treat the file like a bearer credential for bound files and like cash for unbound files. Send it over the same channel you would use for a password reset link.

Storing the file on the machine

Store the file in a per-user or per-machine application data directory, not next to the executable:
  • Windows: %ProgramData%\<Vendor>\<App>\license.authforge (machine-wide) or %LocalAppData%\... (per user)
  • macOS: /Library/Application Support/<App>/license.authforge or ~/Library/Application Support/...
  • Linux: /etc/<app>/license.authforge or $XDG_CONFIG_HOME/<app>/license.authforge
Recommendations:
  • Accept the file through an explicit Import license file… action that copies it into place, so users are not editing config folders by hand.
  • Do not encrypt or obfuscate the file. It is already signed; encrypting it buys nothing against a user who controls the machine and complicates support.
  • Keep the previous file around when you import a new one (license.authforge.bak) so a bad import is recoverable.
  • The SDKs accept either a filesystem path or the armored text, so you can also embed the file contents in your own settings store if that is easier.

Refreshing before expiry

Because revocation is impossible, expiry is the lifecycle. Build the refresh into the product rather than leaving it to a support ticket:
  1. Surface the expiry. After loginFromFile, read expiresAt from getOfflineLicense() and show it in the About/License screen. Start warning at 14 days and again at 3 days.
  2. Make re-issue a routine operator task. Mint the replacement from the license page (or script it against POST /v1/licenses/{licenseKey}/offline-files) a few weeks before the old file lapses. Mint history on the license page tells you what is outstanding.
  3. Track the fleet. For every offline machine keep the license key, HWID, file jti, and expiresAt in your CRM or asset inventory. The Developer API GET /v1/licenses/{licenseKey}/offline-files returns the same fields if you prefer to sync from AuthForge.
  4. Let a new file replace an old one without downtime. Import the new file, verify it with verifyLicenseFile before overwriting the old one, then swap. A file with a later expiresAt for the same license is always safe to install early.
If a customer churns, stop re-issuing and revoke the license online so no new files can be minted. The outstanding file runs out on schedule.

Writing software protected by an offline file

1

Verify at startup, once

Call loginFromFile at launch and gate the app on the result. Do not re-verify on every action; the file does not change while the app runs.
2

Show the reason before you exit

loginFromFile never terminates the process on its own. Map the code you receive in onFailure("offline_login_failed", error) to a human message:Always print the machine’s HWID on the failure screen: it is the one piece of information support needs to fix a mismatch.
3

Read entitlements from the file

getLicenseVariables() and getAppVariables() return the snapshots frozen at mint time. Use them for tiering and feature flags exactly as you would online. Remember they only change when a new file is imported.
4

Do not start the online machinery

loginFromFile never starts the grace-period timer or online check-ins, and you should not start them either. If your product supports both connected and air-gapped customers, branch on the presence of a license file: loginFromFile when one is installed, otherwise the normal online login().
5

Keep the app usable on expiry day

Decide what happens when the file lapses while the app is running: most products keep running until restart and refuse to start the next time. Whatever you choose, do not silently corrupt or lock user data.

Supporting both modes in one binary

Never fall back from a rejected offline file to an online login automatically; a rejected file means something is wrong that the user should see.

Threat model, honestly

  • Clock roll-back. The expiry check uses the machine’s clock. A user can extend a file by winding the clock back. Short expiries and HWID binding are the mitigations; there is no server to consult.
  • Copying a bound file. Useless on another machine unless the HWID collides, which is why bound is the default.
  • Copying an unbound file. Works everywhere. Only mint any files when you would be comfortable posting them publicly.
  • Tampering. Any edit to the payload fails bad_signature. Headers are not signed and are ignored by the SDKs.
  • Key rotation. Rotating your app signing key means new files carry a new kid. SDKs accept a list of public keys, so ship the old and new key together until every old file has expired.
  • Your own mistakes. Mint history and the audit log record every file (actor, time, expiry, binding, file id, hash). Review them when a customer reports something unexpected.

Checklist

  • Offline SKU terms written: expiry length, binding policy, lifetime policy
  • HWID visible in the product without network access
  • Import flow that validates before overwriting the previous file
  • Expiry shown in the UI, warnings at 14 and 3 days
  • Fleet inventory of license key / HWID / jti / expiresAt
  • Re-issue runbook (dashboard or scripted Developer API call) owned by a named team
  • Failure screen shows the code, a plain-language message, and the machine HWID
  • Air-gapped builds omit the App Secret (public key + app id + file is enough)
  • Online customers still use login() + grace period; offline files only where required