Opening https://<box>/ sends you to the identity provider the box is
configured with. After sign-in you end up in a viewer. Which environment it
serves is the Environments screen’s business: it lists what this box is
configured with, and it is where you move between them — a box with a
choice to offer shows it before the viewer. Every sign-in, refusal and
sign-out is a line in the box’s
audit trail.
From the CLI: the cloud account
Renting a box, paying for it and reading its results from a terminal goes through one account — unotest cloud — and one sign-in:
npx @unotest/web loginThe command runs the device flow (RFC 8628):
it prints a link and an eight-character code, opens the browser when
there is one, and waits. On the page you sign in with Google or GitHub,
accept the terms of service if you never have (an agent cannot do that
for you), and approve “unotest-web on <your machine>” — if you did not
start this sign-in yourself, deny it. The CLI ends up holding an API key
labelled with your machine’s name; it is listed on the cabinet’s key
page and can be revoked there at any time. Over a remote shell the same
link and code are typed into any browser.
The key lives in ~/.config/unotest/credentials.json (mode 0600,
XDG_CONFIG_HOME honoured) and is never printed. A CI job exports
UNOTEST_CLOUD_TOKEN instead — a key minted on the cabinet’s key page —
which wins over the file. whoami says who you are and the balance;
logout revokes the key on the server and removes it locally. Running
login again with a key that was granted less than a sign-in asks for
signs in anew and revokes the old key once the new one is saved.
Signing in to the box itself — its viewer and admin pages — is the
single sign-on below, with your organisation’s identity provider. The
two are separate on purpose: the cloud account rents and pays, the box’s
provider says who may look at what runs there. box create writes the
box’s push and read tokens for you, so reading results from the CLI and
the MCP tools needs no second sign-in.
Single sign-on
A box authenticates against one OpenID Connect provider: authorization
code flow with PKCE, state and nonce, the ID token verified against
the issuer’s published keys. Two presets exist:
google— Google Workspace. The account chooser is always shown, and the token’shdclaim is checked against the Workspace domains you allow. A personal Google account is refused (“this is not a Google Workspace account”), and so is an account whose email Google reports as unverified.generic— Okta, Azure AD, Keycloak, Auth0 and any other compliant provider. An email the provider marks unverified is refused.
The redirect URI is fixed: https://<box>/_guard/login/callback. Register
exactly that at your provider — a mismatch shows up as
redirect_uri_mismatch at sign-in.
For Google Workspace, your administrator creates the OAuth client and — so that groups can decide roles — a service account with domain-wide delegation over two read-only scopes (groups and user status). unotest walks you through this at onboarding.
Groups → roles
Which groups a person is in comes from one of two sources:
- a groups claim in the ID token (Okta, Azure AD, Keycloak, Auth0);
- the Google Directory API, because Google does not put groups in a token. This is also the only source that can offboard someone while their session is open.
Your group → role mapping is deny by default:
- A group that is not mapped grants nothing. Someone who signs in successfully but belongs to no mapped group is refused — not admitted with reduced rights.
- Someone in two mapped groups gets the stronger role.
- Group names match case-insensitively.
- Nested groups are not followed: list the groups people are directly in.
Without any group mapping, every SSO user is readonly.
An email-domain allowlist can be added as an extra check; membership is still what decides.
Re-checks and offboarding
Every 15 minutes (configurable) the box re-asks the directory whether each open session is still entitled:
- Removed from every mapped group, suspended or deleted → the session ends,
recorded as
session.revoked. - Moved to a group with a different role → the live session’s role changes
in either direction, recorded as
role.changed. - Directory unreachable → the session is left alone and re-checked next time. An outage at your provider never signs your company out.
A login is stricter than a re-check: if the directory cannot be reached while someone is signing in, they are refused rather than admitted on trust.
Sessions
Sessions are held on the box, not in the cookie: the cookie carries an opaque signed id, the record lives in the guard. That is what lets an administrator end a session, and what makes a role change or a disable take effect immediately. A session expires after 24 hours by default.
The session store is in memory. After the box restarts, an SSO user is signed in again silently; a break-glass user retypes their password.
Login routes are rate-limited per client address (10 attempts per minute by default). A successful login resets the counter.
Break-glass account
A box may carry one or more local accounts with scrypt-hashed
passwords. They exist for the day the identity provider is unreachable and
are reachable from https://<box>/_guard/login/static; with SSO
configured, /_guard/login goes straight to your provider.
On the box itself this account is not called break-glass: the sign-in screen says Sign in to this box, and where several ways in are offered, its button is Box account. The term is ours, and the screen belongs to whoever bought the box. The user name and password for a rented box are on its card in the dashboard, under Login and Password, for as long as the box lives.
A break-glass account is an administrator by default. A local readonly
account is allowed too — intended for a box without a directory, where it is
the only way to hand someone view-only access; a box with a directory
should grant readonly through group mapping instead.
Removing an account from the box’s configuration ends its sessions at the next re-check.
Seats
Your licence names a number of seats. A seat is held by anyone who has signed in within the last 30 days (configurable) and is not disabled — the count is rolling, so a contractor who signed in once in March stops consuming a seat on their own.
When every seat is taken:
- someone who does not already hold a seat is refused with “This box
has no free seats. Ask your administrator to free one or raise the
licensed seat count.” and the refusal is recorded as
seat.denied; - everyone who already holds a seat keeps working, including on re-login.
The Administration page shows seats <used>/<total> in use. To free a
seat, disable the person — see Administration.
What is recorded
| Event | When |
|---|---|
login.succeeded | A sign-in completed; carries the provider, the role granted and the client address. |
login.refused | Credentials rejected, an account disabled on this box, or the directory refused the person; carries the reason. |
seat.denied | No free seat; carries seats and seatsUsed. |
logout | The person signed out. |
session.revoked | A session ended by a re-check, by an administrator, or because its provider was removed from the configuration. |
role.changed | A live session’s role moved (directory change or administrator override). |
The full list of events is in Administration.