# 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

```sql
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**
```json
{ "name": "Smart Watch" }
```

**Response `200`**
```json
{
  "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`**
```json
{
  "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

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

### WebSocket connection

Append the token as a query parameter:

```javascript
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
