Skip to content

Token & Authentication Architecture

Executive Summary

agentwatch uses 5 distinct tokens across both repos serving different purposes:

Token Location Purpose Lifetime
API Token ~/.config/agentwatch/tokens/api API/MCP client auth Static (manual rotation)
Auth Cookie Browser {host}_agentwatch_auth Web viewer login 24 hours
Login Token In-memory only Magic link login 5 minutes, single-use
Device Token ~/.config/agentwatch/tokens/device Provisioner API 30 days (rotates every heartbeat)
Dashboard Session Browser cookie (provisioner) Dashboard web UI 4 hours

Part 1: agentwatch Token Map

Token 1: API Token

File: ~/.config/agentwatch/tokens/api Format: 32 bytes, URL-safe base64 (~43 chars) Generated by: agentwatch setup or daemon startup Permissions: 0o600 (owner only)

Used for:

  • Bearer token in Authorization: Bearer <token> header
  • API endpoints: /sessions, /sessions/{id}, /mcp, /api/login-token
  • CLI: agentwatch status command
  • MCP clients: Claude Code, Codex connecting via stdio or HTTP

Validation: agentwatch/mcp/token_auth.py:validate_mcp_token() - constant-time comparison

Name: Host-specific, e.g. localhost_agentwatch_auth, sleet_agentwatch_auth (for sleet.agentwatch.sh) Format: Base64 JSON + HMAC-SHA256 signature Generated by: Login via OS credentials or magic link

Used for:

  • Web viewer browser authentication
  • WebSocket connection auth
  • All /api/* endpoints except those using Bearer token

Validation: agentwatch/server/auth.py:auth_middleware() - global middleware

Storage: In-memory LoginTokenManager Format: 32 bytes, URL-safe base64 Generated by: agentwatch --login-link or /api/login-token API

Used for:

  • One-time passwordless login
  • URL format: https://host/auth/token/{token}

Lifecycle: 5 minutes, single-use, lost on server restart

Token 4: Device Token (JWT)

File: ~/.config/agentwatch/tokens/device Format: JWT (header.payload.signature) Generated by: agentwatch-provisioner dashboard

Used for:

  • Cloudflare Tunnel provisioning API
  • Heartbeat authentication
  • Tunnel credential fetching

Headers sent:

  • Authorization: Bearer <JWT>
  • X-Device-Fingerprint: <SHA256 hash>

Lifecycle:

  • Initial: 15 minutes (unbound)
  • After first heartbeat: 30 days (bound to fingerprint)
  • Rotates: Every heartbeat (5 min interval)

Part 2: Auth Middleware Architecture

The Two-Track System

                    ┌─────────────────────────────────────┐
                    │          Incoming Request           │
                    └─────────────────────────────────────┘
                    ┌─────────────────┴─────────────────┐
                    │         auth_middleware            │
                    │    (session cookie checker)        │
                    └─────────────────┬─────────────────┘
              ┌───────────────────────┼───────────────────────┐
              │                       │                       │
    ┌─────────▼─────────┐   ┌─────────▼─────────┐   ┌─────────▼─────────┐
    │   PUBLIC_PATHS    │   │  Session Cookie   │   │    401/Redirect   │
    │ (bypass to next)  │   │     Valid         │   │    (no cookie)    │
    └─────────┬─────────┘   └─────────┬─────────┘   └───────────────────┘
              │                       │
              │                       │
    ┌─────────▼─────────┐   ┌─────────▼─────────┐
    │  Bearer Token     │   │   Route Handler   │
    │  Check (manual)   │   │   (authenticated) │
    └─────────┬─────────┘   └───────────────────┘
    ┌─────────▼─────────┐
    │ /sessions, /mcp   │
    │ require API token │
    └───────────────────┘

PUBLIC_PATHS (Skip Session Middleware)

PUBLIC_PATHS = (
    "/health",                # No auth
    "/login",                 # Login page
    "/auth/token",            # Magic link
    "/api/login",             # Login API
    "/api/security-context",  # Security check
    "/mcp",                   # Uses Bearer token instead (includes /mcp/*)
    "/static",                # Static files
)

Key insight: /mcp and /mcp/* endpoints are "public" to the session middleware but manually validate Bearer tokens inside their handlers.

Endpoint Auth Matrix

Endpoint Session Cookie Bearer Token No Auth
GET /health
GET /login
POST /api/login
GET / (web viewer)
GET /api/sessions
WS /ws/{id}
GET /mcp/sessions
POST /mcp
POST /mcp/hooks
GET /api/login-token

Part 3: Provisioner Token Map

Token 5: Dashboard Session JWT

Storage: HTTP-only cookie Format: JWT with claims: sub, jti, github_id, github_username Lifetime: 4 hours Secret: JWT_SESSION_SECRET (separate from device token)

Device Token Flow (Provisioner-side)

1. User creates tunnel in dashboard
   └─► Issues UNBOUND JWT (15 min expiry)

2. User copies token to machine
   └─► Saved to ~/.config/agentwatch/tokens/device

3. Daemon sends first heartbeat
   ├─► Authorization: Bearer <JWT>
   └─► X-Device-Fingerprint: <hash>

4. Server binds fingerprint to tunnel
   └─► Issues BOUND JWT (30 day expiry)

5. Every 5 min: daemon heartbeat
   └─► Server issues fresh JWT (30 day expiry reset)

Part 4: Design Rationale

Why Separate Tokens?

API Token vs Device Token

  • API token is local-only (never leaves machine)
  • Device token goes over the internet to provisioner
  • Different threat models: local file permissions vs network auth
  • Device token rotates; API token is static (simpler for CLI tools)

Session Cookies vs Bearer Tokens

  • Cookies work automatically for browser navigation
  • Bearer tokens require JavaScript to attach headers
  • Cookies have HttpOnly protection (XSS-resistant)
  • Standard web security practice to use cookies for browsers

Token File Organization

All tokens now live in a unified directory:

~/.config/agentwatch/tokens/
├── api      # API/MCP authentication (32 bytes, base64)
└── device   # Provisioner JWT (rotates automatically)

This provides: - Clear mental model for where tokens are stored - Single directory with restrictive permissions (0o700) - Easy backup and migration


Part 5: Quick Reference Card

Token Lookup Table

When you need to... Use this token Location
Call agentwatch API from CLI API Token ~/.config/agentwatch/tokens/api
Connect MCP client (Claude/Codex) API Token Same file
Log into web viewer Session Cookie Browser (auto)
Generate magic login link API Token To call /api/login-token
Provision tunnel (daemon) Device Token ~/.config/agentwatch/tokens/device
Log into provisioner dashboard Dashboard JWT Browser cookie (auto)

Debugging Auth Issues

# Check API token exists and is readable
cat ~/.config/agentwatch/tokens/api

# Test API auth
curl -sk -H "Authorization: Bearer $(cat ~/.config/agentwatch/tokens/api)" \
  https://localhost:8081/mcp/sessions

# Check device token (JWT)
cat ~/.config/agentwatch/tokens/device | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# View token expiry
python3 -c "import jwt; print(jwt.decode(open('$HOME/.config/agentwatch/tokens/device').read(), options={'verify_signature': False}))"

Conclusion

You don't have too many tokens. You have exactly the right number for:

  1. Local API access (API token) - static, file-based
  2. Browser sessions (session cookie) - standard web auth
  3. Magic links (login token) - ephemeral bridge
  4. Remote provisioner (device JWT) - rotating, fingerprint-bound

The architecture is sound. All tokens live under ~/.config/agentwatch/tokens/ for clarity.