Newer
Older
navi-1 / docs / api_tokens.md
@Eugene Sukhodolskiy Eugene Sukhodolskiy 2 days ago 5 KB Add NAVI_AUTH_ENABLED switch for optional auth

API Token Authentication

API tokens allow headless and non-browser clients (scripts, voice assistants, IoT devices, smart watches) to authenticate independently of the OAuth2 cookie flow. Tokens are long-lived, user-scoped, and grant the same access as the owning user.

When NAVI_AUTH_ENABLED=false, API-token auth is effectively unused because every request already has full admin access. Tokens can still be created, but they all belong to the fixed anonymous user and offer no additional isolation.

Overview

  • Format: nav_<43-char URL-safe string> — e.g. nav_aB3xYz9WqLmNpQrStUvXyZaB
  • Storage: SHA-256 hash stored in PostgreSQL; plain text shown only once on creation
  • Transport: X-Api-Token HTTP header for REST; ?api_token= query parameter for WebSocket
  • Scope: Full user access (same role/permissions as the user who created it). No per-token scopes in MVP.
  • Revocation: Soft delete (revoked_at timestamp). Revoked tokens immediately return 401.

Architecture

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ Headless     │────▶│  X-Api-Token │────▶│  navi_users  │
│ client       │     │  header or   │     │  + api_tokens│
│              │◀────│  ?api_token  │◀────│  (PostgreSQL)│
└──────────────┘     └──────────────┘     └──────────────┘

Token resolution flow (navi/auth/deps.py::_resolve_user_from_api_token):

  1. Read X-Api-Token header or api_token query param
  2. SHA-256 hash → lookup in api_tokens JOIN navi_users
  3. Check revoked_at IS NULL
  4. Build User from navi_users row
  5. Best-effort UPDATE api_tokens SET last_used_at = NOW()

Database schema

CREATE TABLE api_tokens (
    id           SERIAL PRIMARY KEY,
    user_id      TEXT NOT NULL REFERENCES navi_users(id) ON DELETE CASCADE,
    name         TEXT NOT NULL,
    token_hash   TEXT NOT NULL UNIQUE,
    token_prefix TEXT NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL,
    last_used_at TIMESTAMPTZ,
    revoked_at   TIMESTAMPTZ
);
Column Description
token_hash SHA-256 of the plain token. Never stores plain text.
token_prefix First 12 chars of the token (nav_aB3xYz9W…) for UI identification.
revoked_at Soft-delete marker. NULL = active.

REST endpoints

POST /api-tokens

Create a new API token. Plain token returned only in this response.

Auth: requires authenticated user.

Request

{ "name": "Smart Watch" }

Response 200

{
  "id": 1,
  "name": "Smart Watch",
  "token": "nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX",
  "token_prefix": "nav_aB3xYz9W…",
  "created_at": "2026-05-24T10:00:00+00:00",
  "last_used_at": null
}

GET /api-tokens

List active (non-revoked) tokens for the current user. Does not expose plain tokens.

Auth: requires authenticated user.

Response 200

{
  "items": [
    {
      "id": 1,
      "name": "Smart Watch",
      "token_prefix": "nav_aB3xYz9W…",
      "created_at": "2026-05-24T10:00:00+00:00",
      "last_used_at": "2026-05-24T12:00:00+00:00"
    }
  ]
}

DELETE /api-tokens/{token_id}

Revoke (soft-delete) a token belonging to the current user.

Auth: requires authenticated user.

Response 204 — no body

Errors

  • 404 — token not found or does not belong to user

Using tokens

REST requests

curl -H "X-Api-Token: nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX" \
  https://navi.gnexus.space/api/agents/profiles

WebSocket connection

Append the token as a query parameter:

const ws = new WebSocket(
  `wss://navi.gnexus.space/ws/sessions/${sessionId}?api_token=${encodeURIComponent(token)}`
)

Security note: The query parameter is visible in server access logs. For production deployments with strict log retention policies, consider implementing a {type: "auth", api_token: "..."} WebSocket message sent immediately after connect.

Web client

The web client stores the token in localStorage under key navi_api_token. When present, the WebSocket composable automatically appends ?api_token= on connect.

Security considerations

Risk Mitigation Status
Token in URL query param → server logs Documented; future: auth WS message Accepted
Token in localStorage → XSS exposure Single-origin policy; no third-party JS Accepted
No rate limiting on token creation Limited by requiring authenticated user Accepted
No per-token scopes or expiration MVP simplification; schema supports adding later Accepted
Token hash collision SHA-256 with 256-bit random input — negligible Mitigated
Brute-force token guessing 43-char URL-safe = ~256 bits; computationally infeasible Mitigated

Web UI

The web client exposes token management at #settings (Settings → API Keys):

  • Create: name input → modal shows plain token with copy button and warning "Copy now — never shown again"
  • List: table with name, prefix, created date, last used, revoke button
  • Revoke: confirmation dialog → soft delete