Skip to content

fix(a2a): accept dashboard session auth on /a2a route (#12888) - #13271

Merged
diegosouzapw merged 1 commit into
release/v3.8.51from
fix/12888-a2a-dashboard-auth
Sep 12, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.51from
fix/12888-a2a-dashboard-auth

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #12888

Root cause

Two related gaps:

  1. The A2A dashboard's "Run message/send"/"Run message/stream" buttons (A2ADashboard.tsx) call fetch("/a2a", ...) with only a Content-Type header — no Authorization.
  2. authenticateA2ARequest() (src/lib/a2a/authenticate.ts) only checks for a Bearer/x-api-key. Unlike clientApiPolicy (used for /api/v1/*), which falls back to isDashboardSessionAuthenticated() when no key is present, /a2a never consulted the dashboard's session cookie at all — so even a logged-in dashboard request hit Unauthorized: missing or invalid API key whenever REQUIRE_API_KEY or OMNIROUTE_API_KEY was configured.

Fix

  • authenticateA2ARequest() now falls back to isDashboardSessionAuthenticated(req) when no API key is present, mirroring clientApiPolicy's behavior — only adding the dashboard-session path, never widening anonymous access (REQUIRE_API_KEY / OMNIROUTE_API_KEY postures are unchanged for non-dashboard callers).
  • resolveA2AOwner() now returns a stable "dashboard" owner id for a session-authenticated caller with no API key, instead of undefined, so dashboard-originated tasks are scoped consistently (it's now async — the two call sites in src/app/a2a/route.ts and src/app/api/a2a/_auth.ts were updated to await it).
  • A2ADashboard.tsx's handleSmokeSend/handleSmokeStream fetches now send credentials: "same-origin" (defense-in-depth; same-origin fetch already sends cookies by default, but explicit matches the existing playgroundAuth.ts convention).

Regression test

tests/unit/a2a-dashboard-session-auth.test.ts (new):

  • RED (pre-fix): a request carrying a valid dashboard session cookie and no Authorization header passed isDashboardSessionAuthenticated() but authenticateA2ARequest() returned false.
  • GREEN (post-fix): same request now passes authenticateA2ARequest(); a request with neither a valid key nor a valid session cookie is still rejected; resolveA2AOwner() returns "dashboard" for the session-only caller.

Also updated two existing tests (tests/unit/a2a-task-owner-idor.test.ts, tests/unit/a2a-history-route.test.ts) to await resolveA2AOwner(...) now that it is async — legitimate alignment to the corrected (now-async) contract, no assertions weakened.

Gates run

  • node --import tsx/esm --test tests/unit/a2a-dashboard-session-auth.test.ts — 3/3 pass
  • node --import tsx/esm --test tests/unit/a2a-task-owner-idor.test.ts tests/unit/a2a-history-route.test.ts tests/unit/a2a-tasks-auth.test.ts tests/unit/dashboard-shell-tabs.test.ts — 36/36 pass (existing A2A/dashboard coverage, unaffected)
  • node scripts/check/check-file-size.mjs — OK
  • node scripts/check/check-complexity.mjs — OK (2798 violations, baseline 3218)
  • node scripts/check/check-cognitive-complexity.mjs — OK (1265 violations, baseline 1437)
  • npm run typecheck:core — exit 0
  • npm run check:dashboard-typecheck — exit 0
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> — exit 0
  • node scripts/check/check-changelog-integrity.mjs — OK

Scope note

Per task instructions, src/server/authz/policies/clientApi.ts was left untouched (another in-flight branch, fix/12745-memory-rerank-auth, also touches it).

⚠️ base-red inherited: #12732 — unit #12058, integration codex-cache, package-artifact, tarball-smoke, agent-skills-sync


CodeQL alert #998 — dismissed as false positive (Hard Rule #14)

js/insufficient-password-hash at src/lib/a2a/authenticate.ts:60.

That line is resolveA2AOwner(), which derives a stable, non-secret owner-scoping id for A2A
task visibility (GHSA-jcm5-6wpp-wjj8). It is never compared against a stored credential and never
gates authentication. Real API-key verification lives in the same file: timingSafeEqual
(tokensMatch) and isValidApiKey. Swapping in a KDF would only change the id format and orphan
already-persisted task ownership, with no security gain.

The flagged expression is byte-identical to release/v3.8.51 — this PR only changed the
surrounding control flow, which is why CodeQL re-reported a pre-existing line as new:

git diff origin/release/v3.8.51...origin/fix/12888-a2a-dashboard-auth -- src/lib/a2a/authenticate.ts

The A2A dashboard's Run message/send and Run message/stream buttons
fetch("/a2a", ...) with no Authorization header. authenticateA2ARequest()
only checked for a Bearer/x-api-key, unlike clientApiPolicy (/api/v1/*)
which falls back to isDashboardSessionAuthenticated() when no key is
present. Add that same fallback to the A2A auth helper, give a
session-authenticated caller with no API key a stable 'dashboard' owner
id in resolveA2AOwner(), and send credentials: same-origin from the
dashboard playground fetches so the session cookie always rides along.
Comment thread src/lib/a2a/authenticate.ts Dismissed
@diegosouzapw
diegosouzapw merged commit 1b2dd3d into release/v3.8.51 Sep 12, 2026
15 of 21 checks passed
Githab-capibara added a commit to Githab-capibara/OmniRoute that referenced this pull request Sep 17, 2026
…nce#12888) (cheaper-inference#13271)

Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for cheaper-inference#12945's image-only-model guard, landed separately in cheaper-inference#13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: cheaper-inference#12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…nce#12888) (cheaper-inference#13271)

Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for cheaper-inference#12945's image-only-model guard, landed separately in cheaper-inference#13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: cheaper-inference#12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
@diegosouzapw
diegosouzapw deleted the fix/12888-a2a-dashboard-auth branch October 18, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(api): A2A dashboard: 'Run message/send' fails with 'Unauthorized: missing or invalid API key' — request sent without Authorization header

2 participants