Skip to content

fix(gateway): require API key auth even in AUTH_MODE=local - #474

Open
erichanwang wants to merge 1 commit into
onecli:mainfrom
erichanwang:fix/loopback-local-auth-enforcement
Open

fix(gateway): require API key auth even in AUTH_MODE=local#474
erichanwang wants to merge 1 commit into
onecli:mainfrom
erichanwang:fix/loopback-local-auth-enforcement

Conversation

@erichanwang

Copy link
Copy Markdown

fixes #263

changes

This is a two-sided fix — a gateway-side hole, and a web-side dependency on that hole that had to be closed first.

Gateway (apps/gateway/src/auth.rs): AuthUser::from_request_parts tries validate_api_key (checks Authorization: Bearer oc_... against the DB) and only falls back to validate_requestvalidate_local when no such key is presented. validate_local then unconditionally authenticated the caller as local-admin — full owner — with no credential of any kind. Every gateway route (vault pair/status, cache invalidate, pending approvals, approval decisions) goes through this same extractor, so in AUTH_MODE=local (the shipped single-user-dev default) any bare request reaching the gateway's loopback port was authenticated as the admin. validate_local is removed; local mode now rejects a request that already failed the API-key check rather than auto-trusting it — that's the only credential path in local mode (there's no login/session mechanism to fall back to).

Web (apps/web/src/lib/gateway-auth.ts): the web app's own gateway calls relied on exactly this bypass — getGatewayFetchOptions sent headers: {}. Closing the gateway hole without fixing this would have broken vault pairing, cache invalidation, and approvals for every user. getGatewayFetchOptions now fetches/provisions the caller's real API key via the existing ensureApiKey-backed server action (the same one the API Keys settings page uses) and attaches it as Authorization: Bearer oc_.... This works unchanged under oauth mode too — sending a valid key alongside the session cookie just gives the gateway a stronger credential to check first.

Trust model, before → after:

  • Before: AUTH_MODE=local + no Authorization header → authenticated as local-admin (full owner), unconditionally.
  • After: AUTH_MODE=local + no valid Authorization: Bearer oc_...401 Unauthorized. The web app now always sends a valid key. A human hitting the gateway directly (curl, no web UI) gets their key from the existing API Keys settings page (ensureApiKey) — no functionality removed, just no longer free for the taking.

Updated two stale comments in apps/web/src/lib/api/{cache,approvals}.ts that described the old "edition-aware, no need to borrow an API key" design, which no longer matches.

verification

  • cargo fmt --manifest-path apps/gateway/Cargo.toml --check
  • cargo test --manifest-path apps/gateway/Cargo.toml auth:: — 4 passed, including new regression test validate_request_local_mode_rejects_bare_request (bare request in local mode → Err)
  • pnpm --filter web check-types
  • pnpm --filter web lint
  • git diff --check
  • pnpm check (full monorepo lint + check-types + format:check, also ran again via the pre-commit hook on commit)

@mrrobertkent

Copy link
Copy Markdown

Running this branch in production on a self-hosted TLS deployment, and it fixes a second problem beyond the one in the description — worth recording, because that problem makes multi-user mode unusable on its own.

The web half also repairs browser→gateway auth in oauth mode

validate_oauth cannot authenticate an Auth.js session in any release. It decodes the cookie with jsonwebtoken under Algorithm::HS256, i.e. a three-segment JWS, but Auth.js issues an encrypted token — the built bundle contains EncryptJWT/jwtDecrypt with A256CBC-HS512 and dir, and nextauth-config.ts sets no custom jwt.encode/decode. With a real session the gateway gets:

WARN onecli_gateway::auth: oauth auth: JWT decode failed error=Base64 error: Invalid symbol 46, offset 174

Symbol 46 is . — a five-segment JWE being read as a three-segment JWS.

git log -L '/^async fn validate_oauth/,/^\/\/ ── Helpers/' -- apps/gateway/src/auth.rs returns exactly one commit, and that same commit already pinned next-auth@5.0.0-beta.30 and shipped gateway-auth.ts with empty headers. So the session path appears never to have worked; it is masked on default installs because validate_local short-circuits before it is reached.

This PR's web half sidesteps that entirely. Once the browser sends Authorization: Bearer oc_…, validate_api_key matches before the mode branch and the token format never comes up. Measured on the same instance, before and after:

before   /v1/vault/onepassword/status   401   (oauth auth: JWT decode failed)
after    /v1/vault/onepassword/status   200   {"connected":false,…}

1Password pairing, vault status, and the approvals poll all work now; before, the vault panels were permanently 401 while the rest of the dashboard was fine.

Deployment it was verified on

Community edition, AUTH_MODE=oauth, behind Traefik with TLS, Authentik as the IdP via the generic OIDC provider from #430. The gateway half was exercised too: a bare request to the gateway returns 401, and the same request with a valid oc_… key returns 200.

Two notes for reviewers

The exposure argument is weaker than it looks. My first reaction was that attaching the key moves a long-lived credential into page JavaScript. It does not introduce that: overview/_components/api-key-card.tsx already fetches the raw oc_ key into React state and offers copy-to-clipboard, so it is in page JS today regardless.

The gateway half is a genuine behaviour break for anyone driving the local gateway with bare curl — which is #263's own repro. Intentional and argued in the description, but it may be worth a release note.

I opened #475 for the cookie-name half of the session path (__Secure- prefix on TLS). With this PR applied that path is unreachable, so #475 is optional — happy for it to be closed if you would rather not carry code for a path that no longer runs.

The gateway's AuthUser extractor fell back to an unconditional local-admin
auth in AUTH_MODE=local whenever no valid API key was presented — so any bare
request with no credentials at all was authenticated as full owner. That
defeats the point of binding the gateway to loopback: anything else on the
host could still reach every route (vault pair/status, cache invalidate,
approvals) with zero credentials.

validate_local is now removed; local mode rejects a request the API key
check already failed rather than auto-trusting it. The web app's own gateway
calls previously relied on exactly this bypass (sending no Authorization
header), so getGatewayFetchOptions now fetches/provisions the caller's real
API key (via the existing ensureApiKey-backed action) and attaches it as a
Bearer token.

Fixes onecli#263.
@erichanwang
erichanwang force-pushed the fix/loopback-local-auth-enforcement branch from 1da015a to 39bb74e Compare August 3, 2026 06:56
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.

Local gateway API does not enforce ONECLI_API_KEY when bound to TCP loopback

2 participants