Skip to main content

SDK Login

Authenticates an existing end-user (AppUser) with their username and password, binds the machine to a device, runs every gate, and returns a session token. Unlike /api/v1/activate (which is license-key-first), this route is credential-first — the user signs in, and the server picks the best license attached to their account.
This is for end-users signing into your application, not operators signing into the console.

Request

The JS SDK fills hwid, os, appVersion, components, and virtual for you — pass only what the method doesn’t already know:
extra covers the rest: client.login(user, pass, { twofa: "123456", hash, label: "Alex's desktop" }).

Parameters

owner, product, hwid, and os are capped at 256 characters. username is trimmed but not length-capped.

Responses

Two extras can ride along in data and the SDK handles both automatically:
  • offline: { assertion, notAfter, keyId } — a signed offline assertion. The SDK caches it and checkOffline() can verify it locally against the pinned public key while the server is unreachable. Present when the server has a signing key configured and the login provided an HWID.
  • product.updateAvailable: true, product.latest, product.downloadUrl? — a soft “a newer build exists” hint, sent only when the operator switched autoUpdate on and your appVersion is behind. The SDK re-emits it as an updateAvailable event ({ current, latest, downloadUrl? }). The hard version of the same decision is forceLatest, which refuses outright with UPDATE_REQUIRED.
session is only present when the operator’s plan includes the token system. Without it, the response omits session and the SDK stays session-less.

Behavior

  • Product resolution: owner + product resolve by ID first, then name/slug fallback, scoped to the owner — IDs can’t cross accounts. The owner must be Active, or the pair answers APP_NOT_FOUND.
  • Request order: missing fields → product lookup → freshness (if requireSignedRequests) → lock check → user lookup → password verify → account status → 2FA → clear failures → license selection → gates.
  • Kill switch: appDisabled is evaluated inside the gate pipeline (after auth), not before.
  • One refusal code for bad credentials: unknown user and wrong password both answer INVALID_PASSWORD. Telling a stranger which half was wrong turns the endpoint into a username oracle.
  • Two-factor: same module as the panel (services/otp.services.js). If enabled and no twofa sent, responds with TWO_FACTOR_REQUIRED + twoFactorRequired: true. Recovery codes are single-use.
  • License selection: when an account holds multiple licenses on the product, usable ones (ACTIVE/TRIAL, unexpired) come first — perpetual keys first, then longest remaining expiry. If none are usable, the server falls back to sorting all of them the same way so the refusal names the real problem.
  • No license = refusal: an account with zero licenses on this product gets LICENSE_NOT_FOUND (403). Gate failures (expired, revoked, blacklisted) get their own accurate code from runGates rather than a generic refusal.
  • Gates also cover: product status (Maintenance → MAINTENANCE, anything not Active/Beta → APP_NOT_FOUND), platform mismatch, unregistered build hash, forceLatest/minVersion (UPDATE_REQUIRED), HWID/IP/license/username blacklist, geo policies, and version mismatch. The blockVirtualMachines setting is stored but not currently enforced by any gate.
  • Gate failures don’t count as credential failures: the per-account failure counter is cleared once the password verifies, before license lookup and gates run. A paying customer who needs to renew doesn’t get locked out on top of it.
  • Session tokens: JWT signed with HS512 carrying lic (license ID), dev (device ID), pid (product ID), uid (app user ID), and jti (for revocation). Lifetime is LICENSE_TOKEN_TTL (default 900s), heartbeat cadence is HEARTBEAT_WINDOW (default 180s).
  • Audit trail: logins write LicenseEvent rows (account <user> login). No webhook fires on login.
  • SDK result: login() stores the session/license/device/account/subscriptions and emits loggedIn, then activated. client.snapshot() returns { app, license, product, device, user, account, subscriptions, valid }.

Signed Responses

Every response carries Ed25519 signature headers when a signing key is configured (v2 scheme — the signed input binds the exact request context): The SDK sends a random X-Frost-Nonce per request, rejects answers signed more than 60 seconds ago, rejects answers to a different request, and refuses unsigned answers outright — except unsigned 429s from the rate limiter, which it treats as retryable (FrostAuthError with code: "RATE_LIMITED", retryable: true, and retryAfterSeconds from Retry-After). Failed calls throw FrostAuthError carrying the server’s errorCode as code, plus status and requestId. err.needsActivation (404, non-retryable) and err.terminal (non-retryable 4xx) tell the heartbeat when to stop. When requireSignedRequests is enabled for the product, inbound freshness is enforced before the handler runs: timestamp must be within 60 seconds, nonce must be 16–128 chars and unique (Redis SET NX, 180s TTL). Stale requests get SESSION_EXPIRED, replays get FORBIDDEN, missing timestamp/nonce gets MISSING_FIELDS.

Rate Limits

The per-identity lockout uses the same module as the panel (services/panel.services.js) but different keys (client:... vs panel:...) — lockouts do not cross between login and panel. The lockout answer embeds the wait in the message (Too many attempts, try again in Ns); the abuse-guard answer carries retryAfterSeconds. The login limiter’s own answer (Too many login attempts) carries no errorCode.

Errors

Operators can reword any message per product — match on errorCode, never on message text.