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)
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:
The device_code is single-use — it is consumed when the tokens are issued.
2b. Rotate (refresh_token grant)
- A valid, current refresh token →
200with a newaccess_tokenand a newrefresh_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.