- Status: Accepted
- Date: 2026-05-28
ADR 0034 §5 and ADR 0035 §5 both pinned the v1 stance — "no authentication, loopback only" — as the cost of shipping the HTTP + WebSocket transports without holding for an authn design. Operators who needed lab-network access either ran a reverse proxy with its own auth or accepted that any local user could drive the instruments. This ADR retires that constraint with a PAT (Personal Access Token) flow: tokens are minted by a CLI verb, persisted as SHA-256 hashes, validated by a single middleware that gates both HTTP and WebSocket entry points.
ivicli_pat_<base64url(32 random bytes)>
- 32 bytes from
RandomNumberGenerator.GetBytes→ base64url (no padding,-and_replacements) → ~54-character total length. - The
ivicli_pat_prefix lets log scanners / GitHub secret-detection tooling recognise leaked tokens. - Tokens are shown to the operator exactly once at creation time; only the SHA-256 hex of the raw token is persisted.
- File:
<auth-dir>/api-tokens.toml.auth-dirresolves per OS viaIviPaths.ResolveAuthDirectory():- Linux:
$XDG_CONFIG_HOME/ivi-cli/auth/(default~/.config/ivi-cli/auth/) - macOS:
~/.config/ivi-cli/auth/ - Windows:
%LOCALAPPDATA%\ivi-cli\auth\
- Linux:
- Override:
IVICLI_AUTH_DIRenvironment variable. - Layout: one
[[token]]table per entry withid/hash/label/createdAt/lastUsedAt?. - Atomic write:
<path>.tmp→ move. Same pattern asTomlScenarioStore.
These paths always pass authentication (so reverse proxies and health-check probes don't need to know about tokens):
GET /healthzGET /openapi/v1.json
The CLI api start verb refuses to bind a non-loopback address
unless one of:
- ≥ 1 token is configured (
ivicli api token create). --allow-anonymousis passed (explicit opt-out).
The gate runs before the listener binds, so a misconfigured operator gets a remediation message immediately instead of an exposed listener.
| Transport | Where the token comes from |
|---|---|
| HTTP routes | Authorization: Bearer <token> header |
| WebSocket upgrade | Sec-WebSocket-Protocol: ivi-cli-pat.<token> (one of the requested sub-protocols) |
The WebSocket convention exists because browsers cannot send custom
HTTP headers on the WS handshake; carrying the token as a
sub-protocol is the standard browser-WS auth pattern. The server
does not echo the token back in the negotiated sub-protocol —
only the ivi-cli-pat prefix.
Token hashes are compared with
System.Security.Cryptography.CryptographicOperations.FixedTimeEquals
against the lowercase-ASCII hex of the SHA-256 to eliminate timing
side-channels.
On a successful match the middleware fires-and-forgets a SaveAsync
that updates LastUsedAt on the matching token. The request is never
blocked by the touch; if the save fails the operator simply sees a
stale value in ivicli api token list. v2 may add structured logging
for the failure.
401 responses match the ADR 0034 §3 shape:
{ "error": { "code": "unauthorized", "message": "..." } }Codes:
unauthorized— generic missing / invalid / expired token (v1 rolls them all into one to avoid an oracle).token_store_unavailable— the on-disk file could not be read.
Token expiry / rotation policy— landed in ADR 0044.--expiresaccepts duration shortcuts or ISO-8601; expired tokens auditAuthFailed("expired_token")and 401. Refresh-token flow stays deferred.Scopes / per-route permissions— landed in ADR 0044. Four v1 scopes (read:devices,read:servers,read:scenarios,write:scpi);RoutePermissionsstatic table maps method+path to required scope; mismatches return 403 +insufficient_scopeaudit.- TLS / mTLS — landed in ADR 0039.
TLS is opt-in (
[api.tls] enabled = falseby default). PAT and mTLS compose: mTLS gates who can connect to the listener, PAT gates what they can do. - OIDC / SSO federation — separate ADR.
Audit log of auth events— landed in ADR 0043. The middleware emitsAuthSucceeded/AuthFailedevents with stable reason codes (missing_token,invalid_token,no_tokens_configured,token_store_unavailable, plusexpired_token/insufficient_scopefrom ADR 0044) per request. If a future Management-API path mutates config (none today), theIAuditSubjectextension surface letsConfigMutated.Subjectbe sourced fromHttpContext.Userasapi/{token.Label}— mirrors thecli/{user}convention.- Rate limiting / brute-force protection — v1 stance is loopback-default + opt-in non-loopback; v2 can layer in IP throttling once concrete deployments emerge.
- The non-loopback HTTP + WebSocket surface is safe to expose to the lab network as long as at least one token is configured.
- ADRs 0034 §5 and 0035 §5 update to point here for the active authn contract.
- No new package dependency; SHA-256 +
CryptographicOperations.FixedTimeEqualsare BCL-tier. - The
ivi-cli-pat_prefix is now a stable identifier that lets third-party secret scanners detect leaked tokens.