Skip to content

Device-code authentication (direct adapters)

Direct desktop / in-document adapters (the Office.js Word add-in, the LibreOffice UNO adapter) authenticate with a Myra-native device authorization grant modelled on RFC 8628. It is not an Entra / Microsoft flow — it issues a Myra credential a user approves in their browser, so a residency-strict tenant keeps every token on Myra infrastructure.

The flow issues a short-lived access token plus a rotating, one-time-use refresh token. The access token is an ordinary gateway token: a front door presents it as Authorization: Bearer myra_... to the /v1 inference plane (or /copilot/v1) with no special handling — it is validated exactly like a personal API token (tenant fence, account-state gates, viewer bar, and its own short expiry all apply).


1. Start a flow — device authorization endpoint

POST /admin/auth/device/authorize — no session required (the caller is a headless client).

Request (JSON):

Field Type Required Notes
gateway_id string yes The target gateway (the tenant's document-AI gateway).
client_label string no Free text shown to the approver (e.g. "LibreOffice on host-42"). Control characters are stripped and it is truncated to 255 bytes.

Response 200:

{
  "device_code": "dvc_9f8e…",
  "user_code": "WXYZ-2345",
  "verification_uri": "https://ai.myra.eu/device",
  "verification_uri_complete": "https://ai.myra.eu/device?user_code=WXYZ-2345",
  "expires_in": 600,
  "interval": 5
}

The client shows user_code and verification_uri (or opens verification_uri_complete) to the user, then polls the token endpoint every interval seconds until the user approves.

Rejected / errors (all application/json):

Condition Status error
gateway_id absent, empty, non-string, or unknown 400 invalid_request (an unknown gateway is deliberately indistinguishable from a bad request — no existence oracle)
Too many pending authorizations for the gateway 429 slow_down

2. Poll / rotate — token endpoint

POST /admin/auth/device/token — no session required. JSON body (a Myra-native deviation from RFC 8628's form-encoding). grant_type is an exact allowlist:

2a. Poll (device_code grant)

{ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "dvc_9f8e…" }

While pending, the endpoint returns an RFC 8628 error (HTTP 400 for all of them, per RFC 6749 §5.2):

Situation error
Not yet approved authorization_pending
Polling faster than interval slow_down
The user denied the device access_denied
The device_code expired (600 s) expired_token
Unknown / already-consumed device_code invalid_grant

On approval, 200:

{
  "access_token": "myra_…",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "dvr_…"
}

The device_code is single-use — it is consumed when the tokens are issued.

2b. Rotate (refresh_token grant)

{ "grant_type": "refresh_token", "refresh_token": "dvr_…" }
  • A valid, current refresh token → 200 with a new access_token and a new refresh_token (the old refresh token is now dead — store the new one).
  • A replayed refresh token (one already rotated away, or from a revoked family) is treated as token theft: the entire token family is revoked immediately (every access + refresh token in it) and the response is 400 invalid_grant. The client must re-run the device flow.
  • Past the family's 90-day absolute lifetime → the family is revoked and the response is 400 invalid_grant; re-authenticate.

Input validation (fail-closed). grant_type absent → 400 invalid_request; an unknown or non-string grant_type → 400 unsupported_grant_type (absent and malformed are distinct — neither falls through to a grant). device_code / refresh_token must be non-empty strings; a missing or wrong-typed value is 400 invalid_request (it is never fed to the hasher).


3. Approve in the browser — verification endpoints

These require an authenticated aig_admin session (the user logs in normally). They are the JSON contract behind the /device verification page in the web app — the human front door the verification_uri (https://ai.myra.eu/device) and verification_uri_complete (…/device?user_code=…) open. That page previews the pending authorization and offers Approve / Deny; it is also reachable from Settings › Devices. A bad, unknown or expired code shows a single generic "no matching request" state (it never distinguishes them — mirroring the uniform 404 below), and the client is never the authz boundary: the server re-checks every call.

  • GET /admin/v1/device/pending?user_code=WXYZ-2345 — preview: { gateway_id, gateway_name, client_label } so the page can show what is being authorized.
  • POST /admin/v1/device/approve — body { "user_code": "WXYZ-2345" }. Approves the device; the minted token acts as the approving user.
  • POST /admin/v1/device/deny — body { "user_code": "WXYZ-2345" }.

Authorization. The approver must have access to the code's gateway (platform admin, or same tenant). Approve additionally requires a role that may mint credentials (a viewer / read-only or impersonating session is refused). An unknown user_code and a user_code the caller may not reach both return the same 404 — there is no cross-tenant existence oracle. user_code is case-insensitive and the dash is optional.


4. Manage & revoke device sessions

Session-gated:

  • GET /admin/v1/device/families — the caller's own active device sessions (id, gateway, label, timestamps — never any secret). Admins may pass ?user_id=<id> (subject to user-access rules) to list another user's sessions for targeted revocation.
  • DELETE /admin/v1/device/families/{id} — server-side revocation. The owner may revoke their own session; an admin may revoke a session on a gateway they can reach. Revoking kills the access token, the family, and every refresh token in it.

In the web app these back the device-session list on /device (Settings › Devices): each active family is shown with its label, gateway and timestamps, and a Revoke action calls the DELETE above (with a confirm).

Disabling, deleting, or deprovisioning a user also revokes all of their device families automatically, and a per-tenant sweep reaps expired authorizations and long-dead families.


5. What a front door consumes

The access_token is a standard myra_ gateway token bound to the approved user + gateway, scoped ["inference"], labelled device, with a short expiry. A front door presents it as Authorization: Bearer <access_token> on /v1 or /copilot/v1 — no device-specific validation is needed. Because the token is gateway-scoped, an exfiltrated device token grants inference only on its one gateway, as its one user, and only until it expires (≤ 15 min) or is revoked.

Client token storage is covered in the internal runbook (docs/internal/device-code-auth-runbook.md): the OS keyring is required and a plaintext fallback is refused — a headless client with no secret store re-authenticates instead of persisting a refresh token in the clear.