Title: Gateway: API-key secret injection preempts OAuth app injection on shared hosts (www.googleapis.com) · Issue #428 · onecli/onecli · GitHub
Open Graph Title: Gateway: API-key secret injection preempts OAuth app injection on shared hosts (www.googleapis.com) · Issue #428 · onecli/onecli
X Title: Gateway: API-key secret injection preempts OAuth app injection on shared hosts (www.googleapis.com) · Issue #428 · onecli/onecli
Description: Summary When a host carries both a vaulted API-key secret and one or more OAuth app connections, the gateway injects only the secret and skips OAuth app resolution entirely for that host. OAuth requests then reach the provider with no Au...
Open Graph Description: Summary When a host carries both a vaulted API-key secret and one or more OAuth app connections, the gateway injects only the secret and skips OAuth app resolution entirely for that host. OAuth req...
X Description: Summary When a host carries both a vaulted API-key secret and one or more OAuth app connections, the gateway injects only the secret and skips OAuth app resolution entirely for that host. OAuth req...
Opengraph URL: https://github.com/onecli/onecli/issues/428
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Gateway: API-key secret injection preempts OAuth app injection on shared hosts (www.googleapis.com)","articleBody":"## Summary\n\nWhen a host carries **both** a vaulted API-key secret and one or more OAuth **app** connections, the gateway injects only the secret and **skips OAuth app resolution entirely** for that host. OAuth requests then reach the provider with no `Authorization` header and fail upstream with `401`, which the gateway surfaces as `app_not_connected`.\n\nThis breaks **every** Google Workspace OAuth app (Calendar, Drive, and the Gmail legacy path) for any project that also has a **YouTube Data API key** vaulted — because all of them live on `www.googleapis.com`. Requests to the providers' **dedicated** hosts (`gmail.googleapis.com`, `tasks.googleapis.com`) are unaffected, which is the tell.\n\nThis is distinct from #236/#237 (which added missing `/batch/` path rules): here the path rules are correct and present, but app resolution never runs because a secret is bound to the same host.\n\n## Repro\n\nIn a project that has (a) a Google Calendar OAuth app connected **and** (b) a YouTube Data API key vaulted as a secret on `www.googleapis.com` (path `/youtube/`, query param `key`):\n\n```bash\n# Agent request through the gateway, no auth header — gateway should inject the Bearer:\ncurl -s -o /dev/null -w \"%{http_code}\\n\" \\\n \"https://www.googleapis.com/calendar/v3/users/me/calendarList\"\n# =\u003e 401 (gateway logs: \"app not connected ... provider=google-calendar\")\n\n# Same OAuth connection, dedicated host — works:\ncurl -s -o /dev/null -w \"%{http_code}\\n\" \\\n \"https://gmail.googleapis.com/gmail/v1/users/me/profile\"\n# =\u003e 200\n```\n\nRemove the YouTube key (or query the provider before the key is vaulted) and the calendar request returns `200`. So it is the *coexistence* of a secret and an app on the same host that triggers it — the OAuth token itself is valid (it works on the dedicated host).\n\n## Root cause\n\nApp-connection resolution is gated on there being **no** secret rules. In `apps/gateway/src/gateway/mitm.rs`:\n\n```rust\nlet mut injection_rules = resp.injection_rules; // from secrets\n// If no secret rules, try app connections (per-request disambiguation)\nif injection_rules.is_empty() \u0026\u0026 !resp.app_connections.is_empty() {\n // ... resolve_app_injection_for_request ...\n injection_rules = rules; // and: replace, not merge\n}\n```\n\nThe YouTube key makes `injection_rules` non-empty, so the whole block is skipped and `resolve_app_injection_for_request` is never called. The identical pattern is in `apps/gateway/src/gateway.rs` (the HTTP-proxy path).\n\nSecret rules and app rules are both **path-scoped** `InjectionRule`s (`/youtube/*` for the key vs `/calendar/*` for the OAuth Bearer), so they can and should coexist rather than one preempting the other.\n\n## Evidence\n\nInstrumenting the `apply_injections` call site, a calendar request carries only the youtube rule:\n\n```\npath=/calendar/v3/... n_rules=1 patterns=[(\"/youtube/*\", 1)] injection_count=0 auth_len=0\n```\n\nAfter the fix (run app resolution regardless and merge), the same request carries both, and the `/calendar/*` Bearer applies:\n\n```\npath=/calendar/v3/... n_rules=3 patterns=[(\"/youtube/*\",1),(\"/calendar/*\",1),(\"/batch/calendar/*\",1)] injection_count=1 auth_len=260\n```\n\nA `/youtube/*` request still injects the key and correctly ignores the non-matching OAuth rules.\n\n## Proposed fix\n\nRun app resolution whenever app connections exist, and **merge** the resulting rules with any secret rules (`injection_rules.extend(rules)`) instead of gating on `injection_rules.is_empty()` and replacing. Apply to both `mitm.rs` and `gateway.rs`. Reproduced against `main` (32b5852).\n\nHappy to open a PR with the fix + a regression test (I have both ready) if the direction looks right.\n\n## Environment\n\n- OneCLI `main` (32b5852); also reproduced on the released `latest` image.\n- Self-hosted, Google BYOC OAuth apps (Gmail/Calendar/Tasks/Drive) + YouTube Data API key as a vaulted secret.\n\nRelated: #236 / #237 (batch path rules — different root cause), #307 (token endpoint routing — adjacent).\n\n---\n\n*Disclosure: I found this bug while integrating OneCLI, diagnosed it with an AI coding assistant, and verified the repro, root cause, and fix against a local build myself before filing.*\n","author":{"url":"https://github.com/jbaruch","@type":"Person","name":"jbaruch"},"datePublished":"2026-07-15T20:18:54.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/428/onecli/issues/428"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:5add4552-a30a-da84-bad8-00dcd47e4077 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 8D9E:2E960D:2D575FD:3D2C087:6A5DB5E1 |
| html-safe-nonce | dd470355feeb68f23ab42d92c3ca16a6a76f6d9ed410177a81f34973b9aab05e |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4RDlFOjJFOTYwRDoyRDU3NUZEOjNEMkMwODc6NkE1REI1RTEiLCJ2aXNpdG9yX2lkIjoiMzQ3MjgyMDkwODQ3Nzc1Njg5NyIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 174b555431571541e35a8a2f44cdba1e6c0a8ef757ded32ae31f4073807369ad |
| hovercard-subject-tag | issue:4896027236 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/onecli/onecli/428/issue_layout |
| twitter:image | https://opengraph.githubassets.com/47ec22008472479ea1f003cbd3361f469c205ec028f696fccaa418dad51a3da5/onecli/onecli/issues/428 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/47ec22008472479ea1f003cbd3361f469c205ec028f696fccaa418dad51a3da5/onecli/onecli/issues/428 |
| og:image:alt | Summary When a host carries both a vaulted API-key secret and one or more OAuth app connections, the gateway injects only the secret and skips OAuth app resolution entirely for that host. OAuth req... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | jbaruch |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| go-import | github.com/onecli/onecli git https://github.com/onecli/onecli.git |
| octolytics-dimension-user_id | 263660444 |
| octolytics-dimension-user_login | onecli |
| octolytics-dimension-repository_id | 1175900220 |
| octolytics-dimension-repository_nwo | onecli/onecli |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 1175900220 |
| octolytics-dimension-repository_network_root_nwo | onecli/onecli |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width