Skip to content

feat(sight): add token-based dashboard authentication#1403

Merged
Daydreamer-Li merged 5 commits into
alibaba:mainfrom
chengshuyi:feat/dashboard-auth
Jul 9, 2026
Merged

feat(sight): add token-based dashboard authentication#1403
Daydreamer-Li merged 5 commits into
alibaba:mainfrom
chengshuyi:feat/dashboard-auth

Conversation

@chengshuyi

Copy link
Copy Markdown
Collaborator

Description

AgentSight Dashboard (port 7396) currently has no user authentication. When exposed on public networks, anyone can access the dashboard without credentials, posing a security risk.

This PR implements token-based API Key authentication for the dashboard:

  • Auto-generates a 32-byte random token on first startup, persisted to .dashboard_token (mode 0600)
  • Actix-web middleware intercepts all /api/* requests, checking Bearer token, query param, or signed session cookie
  • Localhost (127.0.0.1) requests bypass auth automatically for dev convenience
  • Frontend SPA handles auth client-side via AuthGate component with auto-login from URL ?token=xxx parameter
  • New agentsight dashboard CLI command outputs ready-to-share URLs with embedded token

Related Issue

no-issue: internal requirement (Aone #83726576)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional change)
  • Performance improvement
  • CI/CD or build changes

Scope

  • sight (agentsight)

Key Changes

  • src/server/auth.rs: New auth module — DashboardAuth (token gen, cookie signing/verify), AuthMiddleware (actix-web Transform/Service), exempt path logic
  • src/server/handlers.rs: Add POST /api/auth/login, GET /api/auth/status, GET /api/auth/verify endpoints
  • src/server/mod.rs: Integrate AuthMiddleware into HttpServer, add auth: Arc<DashboardAuth> to AppState
  • src/config.rs: Add ServerAuthConfig (enabled, token, token_file) parsed from agentsight.json server.auth section
  • src/bin/cli/dashboard.rs: New agentsight dashboard CLI showing auth status, local/network URLs with token
  • src/bin/cli/serve.rs: Add --no-auth and --token flags for dev override
  • src/bin/agentsight.rs + src/bin/cli/mod.rs: Register dashboard subcommand
  • dashboard/src/pages/LoginPage.tsx: New token input form component
  • dashboard/src/App.tsx: Add AuthGate with URL token auto-login, client-side auth state machine
  • dashboard/src/utils/apiClient.ts: Add credentials: 'same-origin', 401 redirect, auth API functions
  • agentsight.json: Add server.auth config section
  • AGENTS.md: Document new CLI command and API endpoints
  • src/server/token_savings.rs: Update test AppState with auth field

Checklist

  • I have read the Contributing Guide
  • cargo fmt --check pass
  • cargo clippy --all-targets — no new warnings from changed files (pre-existing warnings in other files)
  • cargo test pass (52 server tests including 11 new auth tests)
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly (AGENTS.md, agentsight.json)
  • Lock files are up to date (Cargo.lock)

Testing

  1. Local bypass: curl http://127.0.0.1:7396/api/sessions returns 200 without token
  2. Network auth required: curl http://<host-ip>:7396/api/sessions returns 401
  3. Login flow: POST /api/auth/login with correct token sets agentsight_session cookie (24h TTL, httpOnly)
  4. Cookie auth: Subsequent requests with session cookie return 200
  5. URL auto-login: Open http://<host-ip>:7396/?token=xxx in browser → auto-authenticates, token removed from URL
  6. CLI: agentsight dashboard shows local URL (no auth) and network URL with embedded token
  7. Unit tests: 11 tests in auth.rs covering token generation, cookie signing/verify, exempt paths, query parsing

@github-actions github-actions Bot added component:sight src/agentsight/ scope:documentation ./docs/|./*.md|./NOTICE labels Jul 8, 2026
@chengshuyi chengshuyi force-pushed the feat/dashboard-auth branch 2 times, most recently from 7cc2611 to 78e38cf Compare July 8, 2026 13:50
@jfeng18

jfeng18 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Nice work shipping auth for the dashboard — the overall design (auto-generated token, loopback bypass, signed session cookies) is well-suited for a local observability tool. A few findings from a deep review, grouped by priority:

Worth addressing before merge

1. Token file TOCTOU race (auth.rs:131-137)
std::fs::write creates the token file with default umask (0644 on most systems), then a separate set_permissions call restricts to 0600. Between the two calls the token is world-readable. Both create_dir_all and set_permissions use let _ = to discard errors — if chmod fails the token stays world-readable permanently with no log. The parent directory (/var/log/sysak/.agentsight/) is 0777. Using std::fs::OpenOptions with std::os::unix::fs::OpenOptionsExt::mode(0o600) would create with correct permissions atomically.

2. AuthMiddleware has no HTTP-level integration test (auth.rs:271-448)
The underlying functions are well-tested (cookie sign/verify, exempt paths, token comparison, query param extraction — 11 unit tests). But the middleware itself — the code that actually calls those functions, checks loopback bypass, and returns 401 — has no test that sends an HTTP request through it. Deleting the middleware's decision logic would not break any test. At least one smoke test per path (unauthenticated → 401, valid Bearer → pass, loopback → bypass) would close this gap.

Worth noting

3. Auth handler endpoints have no handler-level tests (handlers.rs:41-95)
auth_login, auth_status, auth_verify — the cookie attributes (HttpOnly, SameSite, path), status codes, and JSON response shapes consumed by the frontend are not covered. Low regression risk since they're thin wrappers, but the HTTP contract is untested.

4. generate_token() is not a CSPRNG (auth.rs:73-104)
It uses DefaultHasher::new() (SipHash with fixed keys 0,0) over timestamp/pid, then XORs with /dev/urandom. Real entropy depends entirely on the /dev/urandom read, which uses is_ok() and silently degrades to zero on failure. On Linux this is fine in practice (/dev/urandom is always available), but the docstring claims "cryptographically random" which is stronger than what the code guarantees. The getrandom crate would be a trivially correct alternative.

5. sign_cookie() is not HMAC despite documentation (auth.rs:29-35)
The implementation is SHA256(token . expires . token) — a custom sandwich construction, not standard HMAC. The commit message and docstring claim "HMAC-SHA256 signed." No concrete forgery path exists for this specific pattern, but the documentation is inaccurate. Using the hmac crate would match the stated intent.

Defense-in-depth (default localhost deployment neutralizes these)

6. Loopback bypass via peer_addr() (auth.rs:325-327) — behind a same-host reverse proxy, all connections arrive from 127.0.0.1 and bypass auth. Not exploitable in default deployment (bind 127.0.0.1, no reverse proxy model).

7. Token in query param + CLI stdout (auth.rs:377, dashboard.rs:62)?token= can leak to browser history; dashboard CLI prints full token URL to stdout. Mitigated: frontend cleans URL via replaceState, token upgrades to session cookie, localhost bypasses auth.

8. No logout mechanism — the HttpOnly 24h cookie cannot be revoked. Only matters when bound to a non-loopback address with shared browser access.

9. --token CLI flag in /proc/pid/cmdline (serve.rs:29) — adding env = "AGENTSIGHT_TOKEN" would be easy hardening. Optional dev override, config file alternative exists.

10. --no-auth has no stderr warning (mod.rs:220) — INFO log exists but no eprintln. Requires explicit flag to trigger.

Overall the auth is solid for its intended deployment model. Items 1-2 are the ones I'd flag for this round; the rest are noted for awareness.

@chengshuyi chengshuyi force-pushed the feat/dashboard-auth branch from 78e38cf to 4dfade1 Compare July 9, 2026 01:48
- Add AuthMiddleware with token/cookie verification for actix-web
- Auto-generate 32-byte random token on first startup, persist to .dashboard_token
- Localhost (127.0.0.1) requests bypass auth automatically
- Add POST /api/auth/login, GET /api/auth/status, GET /api/auth/verify endpoints
- Add LoginPage component with AuthGate in frontend SPA
- URL token parameter auto-login: copy link with ?token=xxx for one-click access
- Add 'agentsight dashboard' CLI command showing auth status and access URLs
- Add 'agentsight serve --no-auth / --token' flags for dev override
- Session cookie (HMAC-SHA256 signed, 24h TTL) for persistent login
- Update agentsight.json with server.auth config section
- Update AGENTS.md with new CLI command and API endpoints
@chengshuyi chengshuyi force-pushed the feat/dashboard-auth branch from 4dfade1 to 8bb6f85 Compare July 9, 2026 01:54
@chengshuyi

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review @jfeng18 — really helpful feedback. Here's what's been addressed:

Addressed in this round

1. Token file TOCTOU race (auth.rs:131-137)
Fixed. Now using std::fs::OpenOptions with OpenOptionsExt::mode(0o600) to create the token file atomically with correct permissions. The parent directory create_dir_all error is also logged instead of silently discarded.

2. AuthMiddleware HTTP-level integration tests
Added. 37 tests now cover the middleware end-to-end (disabled bypass, exempt paths GET+POST, loopback bypass, missing/invalid bearer → 401, valid query token, session cookie round-trip, non-API path pass-through).

3. Auth handler endpoint tests
Added. 7 handler-level tests cover auth_login (success + wrong token), auth_status (enabled + disabled), and auth_verify (disabled, no cookie, valid cookie).

4. generate_token() CSPRNG claim
Fixed the docstring — removed the "cryptographically random" claim. Now accurately describes the entropy sources (hash-based + /dev/urandom XOR) and notes that on Linux, /dev/urandom provides the real cryptographic entropy.

5. sign_cookie() not HMAC
Fixed the docstring — replaced "SHA-256 HMAC-like" with "SHA-256 keyed hash" and added a note clarifying it is a simple sandwich construction, not RFC 2104 HMAC, and sufficient for short-lived session cookies in a local deployment context.

Acknowledged for follow-up

6–10: Noted — loopback bypass behind reverse proxy, token in query param, no logout mechanism, --token in /proc/pid/cmdline, and --no-auth stderr warning. All neutralized by the default localhost binding. Happy to address any of these in a follow-up if the team considers them urgent.

@jfeng18

jfeng18 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Round-2 follow-up — went deeper on a few areas and this time actually built and ran reproduction tests.

Empirical (rustc 1.89.0, a real actix test wired exactly like run_server):

F2_PREFLIGHT_STATUS          = 401 Unauthorized   // production: .wrap(cors).wrap(AuthMiddleware)
F2_CONTROL_REVERSED_PREFLIGHT = 200 OK            // control:    .wrap(auth).wrap(cors)
unauth GET = 401 / authed GET = 200
test result: ok. 2 passed

1. server.auth JSON config is dead-wired (high).
serve.rs::execute() builds ServerAuthConfig { enabled: !no_auth, token, token_file: None } purely from CLI flags and never loads AgentsightConfig. config.rs:1106-1125 parses the server.auth block into self.server_auth, but that field has no read site feeding run_server (only decl at :794 + default init at :871). Net effect: setting server.auth.enabled: false (or a token / token_file) in agentsight.json is silently ignored — auth stays on with the auto-generated token, and token_file is unreachable via serve at all. Only --no-auth / --token are functional. Since the commit ships a parser for server.auth plus an on-by-default block in the default agentsight.json, this looks like an intent/impl gap rather than deliberate CLI-only design. Origin-independent.

2. CORS preflight 401'd by middleware ordering (medium — reproduced above).
.wrap(cors).wrap(AuthMiddleware) (mod.rs:304-305) makes AuthMiddleware the outermost layer (actix runs the last-registered wrap first), and it has no OPTIONS/method exemption, so a cross-origin OPTIONS preflight to a non-exempt /api/* route is 401'd before Cors can answer it. The reversed-order control returning 200 isolates wrap ordering as the cause. Scope: only a remote, cross-origin browser with auth enabled — fully masked on localhost by the loopback bypass (auth.rs:325), so local/dev is unaffected; preflights to exempt /api/auth/* still pass.

3. Cross-origin build → login loop (low, code-trace only, not browser-executed).
apiClient uses credentials: 'same-origin' (apiClient.ts:77 and the auth calls), so in a REACT_APP_API_BASE-cross-origin build the agentsight_session cookie is neither stored nor sent → /api/auth/verify stays false → permanent LoginPage loop. It also isn't fixable client-side alone: CORS is allow_any_origin() (credentialed responses are rejected under ACAO: *) and the cookie is SameSite=Lax without Secure. Only affects the non-default cross-origin build; the embedded same-origin deployment and the webpack-proxied dev setup are same-origin and work.

4. Frontend gate fails open on probe error (low).
App.tsx AuthGate does catch { setAuthState('disabled') }, and 'disabled' renders <>{children}</> — so a transport error on /api/auth/status|verify (server unreachable / CORS block / 5xx) renders the dashboard chrome instead of LoginPage. Not a data-exposure issue — those two probes are exempt always-200 handlers, and every data endpoint independently returns 401 — it just shows empty chrome on an outage rather than a clean login page.

5. AuthGate re-probes on every navigation (low).
useEffect deps [location.pathname] with no dedup/cache → 2 auth requests per client-side navigation (status + verify) with auth on, 1 with auth off.

Items 1 and 2 are the ones I'd flag for this round; 3-5 are minor / scoped.

- Remove token and token_file fields from ServerAuthConfig (only enabled remains)
- Remove --no-auth CLI flag from serve command
- Add --config flag to both serve and dashboard commands (default: /etc/agentsight/config.json)
- Extract shared load_server_auth_config() helper in cli/mod.rs
- Both commands now read server.auth.enabled from the same agentsight.json
- DashboardAuth::init always auto-generates token via read_or_create_token()
- Simplify agentsight.json server.auth block to only contain enabled field
- Update all tests to use auto-generated tokens with unique temp dirs
Remove /metrics from EXEMPT_PREFIXES — token consumption statistics
should not be exposed to unauthenticated network clients.

Prometheus scraping use case: configure bearer_token in scrape config.
CLI 'agentsight metrics' reads SQLite directly, unaffected.
Add LOCALHOST_ONLY_PREFIXES for /health and /metrics — non-loopback
requests receive 403 Forbidden even if auth is disabled.

This prevents exposing health checks and Prometheus metrics to the
network while still allowing local monitoring tools.

Middleware flow:
1. Check localhost-only restriction first (403 if remote)
2. Then check auth disabled / loopback bypass / exempt paths
Add early return for OPTIONS method in AuthMiddleware.call() so that
CORS preflight requests are not 401'd before reaching the Cors
middleware layer.

Without this, cross-origin requests from remote browsers would fail
on preflight to non-exempt /api/* routes (e.g., /api/sessions).

Test verifies:
- GET /api/sessions from remote → 401 (auth required)
- OPTIONS /api/sessions from remote → not 401 (passes to CORS)
@chengshuyi

Copy link
Copy Markdown
Collaborator Author

Thanks for the round-2 deep dive @jfeng18 — here's what's been addressed:

Addressed

1. server.auth JSON config dead-wired (high)
Fixed. Both serve and dashboard commands now load from agentsight.json via a shared load_server_auth_config() helper in cli/mod.rs. Added --config flag (default /etc/agentsight/config.json, matching trace). Removed --no-auth CLI flag — server.auth.enabled is now the single source of truth.

Commits: f0c6c59, a549c6a

2. CORS preflight 401'd by middleware ordering (medium)
Fixed. Added early return for OPTIONS method in AuthMiddleware.call() so preflight requests pass through to the CORS layer. Test verifies:

  • GET /api/sessions from remote → 401
  • OPTIONS /api/sessions from remote → not 401 (passes to CORS)

Commit: a549c6a

Acknowledged for follow-up

3–5: Cross-origin login loop, AuthGate fail-open, and per-navigation re-probing — all scoped to non-default deployments or frontend UX. Noted for a follow-up PR if the team considers them urgent.


Also addressed from earlier rounds:

  • /metrics now requires auth (removed from EXEMPT_PREFIXES)
  • /health and /metrics restricted to localhost only (403 for remote requests)
  • Token file created atomically with mode(0o600) via OpenOptionsExt

@Daydreamer-Li Daydreamer-Li left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Daydreamer-Li Daydreamer-Li merged commit a0532cf into alibaba:main Jul 9, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:sight src/agentsight/ scope:documentation ./docs/|./*.md|./NOTICE

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants