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 statuscommand - MCP clients: Claude Code, Codex connecting via stdio or HTTP
Validation: agentwatch/mcp/token_auth.py:validate_mcp_token() - constant-time comparison
Token 2: Auth Cookie¶
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
Token 3: Login Token (Magic Link)¶
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:
- Local API access (API token) - static, file-based
- Browser sessions (session cookie) - standard web auth
- Magic links (login token) - ephemeral bridge
- Remote provisioner (device JWT) - rotating, fingerprint-bound
The architecture is sound. All tokens live under ~/.config/agentwatch/tokens/ for clarity.