← Developers

03.1 — Authentication, roles & scopes

Spec matching SPEC-DX-01 / api v1

The Invariant

One credential, one boundary. A credential minted for one surface never silently works on another: edge keys drive the sync channel, PIN sessions drive kernel actions, portal sessions drive the tenant API, admin sessions drive operations. Least privilege is structural — a bridge token physically cannot open a lot.

The Contract

Credential Minted by Valid on Scope
Operator PIN session POST /api/v1/auth/login (edge) /api/v1/actions/*, /api/v1/auth/*, /api/v1/audit/verify role-scoped (manifest permissions)
Edge API key (bootstrap claim) POST /api/v1/bootstrap/claim /api/v1/sync/* sync (default) / full
Portal session POST /api/v1/portal/login or pi_* invite claim /api/v1/tenant/*, /api/v1/portal/* owner / member
Admin session POST /api/v1/admin/login /api/v1/admin/* ops only, every mutation audit-logged

Role registry (production-line manifest): MAGAZZINIERE (receiving triggers + reads), OPERATORE (work triggers), QUALITA (inspection triggers), ADMIN (wildcard). Roles live in the manifest’s rbac_engine — each vertical declares its own.

The Flow

edge:  PIN → /auth/login → session (12h TTL, ax_sessions, revocable)
            PIN shared? → 409 PIN_AMBIGUOUS {candidates:[{operatorId,name}]}
            → re-send with operatorId
cloud: email+password → /portal/login → session (12h)
            email on >1 tenant? → 401 TENANT_AMBIGUOUS → pass tenant_id
sync:  bt_* single-use token → /bootstrap/claim → {tenant_id, api_key}
            re-claim ROTATES the key — old key dies instantly

Payload Example

# Edge operator session
curl -s http://edge:8291/api/v1/auth/login -H 'content-type: application/json' \
  -d '{"tenantId":"<uuid>","deviceCode":"IPC-01","pin":"1234"}'
# → {"token":"…","expiresInHours":12,"context":{"tenantId":"…","operatorId":"…","role":"MAGAZZINIERE","deviceId":"IPC-01"}}

# Cloud portal session
curl -s https://cp.axiom-trace.eu/api/v1/portal/login -H 'content-type: application/json' \
  -d '{"email":"qa@customer.com","password":"…"}'
# → {"token":"…","tenant_id":"…","role":"owner","display_name":"…"}

Known Failures

Symptom Cause → fix
409 PIN_AMBIGUOUS PIN shared → re-send with operatorId from candidates
401 TENANT_AMBIGUOUS Email on multiple tenants → pass tenant_id explicitly
401 on /tenant/* with ak_* Key scope is sync → portal surface needs a portal session or full-scope key
403 with valid login Origin binding (portal_origin) or role ceiling (member vs owner-only)
Login works, actions 401 Session TTL 12h — refresh by re-login; never cache tokens beyond TTL