# Sign-in

> For the complete documentation index, see [llms.txt](/llms.txt)

Opening `https://<box>/` sends you to the identity provider the box is
configured with. After sign-in you land on **Environments**, pick one and
open its viewer. Every sign-in, refusal and sign-out is a line in the box's
[audit trail](/box/administration/#the-audit-trail).

## 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's `hd` claim 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.

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](/box/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](/box/administration/#the-audit-trail).
