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.
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.authforgeor~/Library/Application Support/... - Linux:
/etc/<app>/license.authforgeor$XDG_CONFIG_HOME/<app>/license.authforge
- 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:- Surface the expiry. After
loginFromFile, readexpiresAtfromgetOfflineLicense()and show it in the About/License screen. Start warning at 14 days and again at 3 days. - 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. - Track the fleet. For every offline machine keep the license key, HWID, file
jti, andexpiresAtin your CRM or asset inventory. The Developer APIGET /v1/licenses/{licenseKey}/offline-filesreturns the same fields if you prefer to sync from AuthForge. - Let a new file replace an old one without downtime. Import the new file, verify it with
verifyLicenseFilebefore overwriting the old one, then swap. A file with a laterexpiresAtfor the same license is always safe to install early.
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
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
anyfiles 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