Newer
Older
navi-1 / docs / api_tokens.md

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.

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