fix(auth): bypass auth for local API requests during onboarding#386
fix(auth): bypass auth for local API requests during onboarding#386
Conversation
During first-time onboarding, the STT "Test" button fails with 401
because transcribeAudio() uses HTTP fetch (POST /api/sessions/{key}/upload)
which goes through auth_gate, unlike the WS-based voice config RPCs.
Add a narrow bypass in auth_gate's Unauthorized branch: when the request
is local, targets /api/* or /ws/*, and onboarding hasn't completed yet
(wizard_status reports onboarded=false), allow the request through with
Loopback identity.
Also fix a pre-existing missing .send().await.unwrap() in the /ws
public route test and two nightly rustfmt formatting issues.
Closes #378
Entire-Checkpoint: 333257de8c57
Greptile SummaryThis PR fixes a
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Client (local)
participant AG as auth_gate
participant CA as check_auth
participant OS as OnboardingService
C->>AG: POST /api/sessions/{key}/upload (no session cookie)
AG->>CA: check_auth(store, headers, is_local=true)
CA-->>AG: AuthResult::Unauthorized
Note over AG: New bypass logic (Unauthorized branch)
AG->>AG: is_local=true AND path starts with /api/?
AG->>OS: wizard_status()
OS-->>AG: {"onboarded": false}
AG->>AG: !onboarded → true → bypass activates
AG->>AG: insert AuthIdentity { method: Loopback }
AG->>C: 200 OK (request passes through)
Note over AG: After onboarding completes
C->>AG: POST /api/sessions/{key}/upload (no session cookie)
AG->>CA: check_auth(store, headers, is_local=true)
CA-->>AG: AuthResult::Unauthorized
AG->>OS: wizard_status()
OS-->>AG: {"onboarded": true}
AG->>AG: !onboarded → false → bypass skipped
AG->>C: 401 AUTH_NOT_AUTHENTICATED
|
| .await | ||
| .ok() | ||
| .and_then(|v| v.get("onboarded").and_then(|v| v.as_bool())) | ||
| .unwrap_or(false); |
There was a problem hiding this comment.
Fail-open default makes bypass permanent if onboarding service errors
unwrap_or(false) means that if wizard_status() returns an Err or a response without an "onboarded" boolean key, onboarded defaults to false, so !onboarded is true and the bypass activates. In a post-onboarding production deployment, if the onboarding service becomes unavailable (crash, misconfiguration, etc.) at any point, every unauthenticated local GET /api/* or POST /api/* request will be allowed through with Loopback identity — completely bypassing auth.
The safe default should be unwrap_or(true): if we cannot determine whether onboarding is done, assume it is done and require authentication.
| .unwrap_or(false); | |
| .unwrap_or(true); |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b64ff28a60
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // session cookie (e.g. STT test button uses HTTP fetch, not WS). | ||
| // Allow them through with Loopback identity so the onboarding | ||
| // flow can complete without requiring a login first. | ||
| if is_local && (path.starts_with("/api/") || path.starts_with("/ws/")) { |
There was a problem hiding this comment.
Restrict onboarding auth bypass to required routes only
This branch bypasses authentication for every local /api/* and /ws/* request while onboarded=false, which is much broader than the STT upload use case and exposes privileged handlers (for example /api/config and /api/restart in crates/web/src/lib.rs) without credentials during onboarding. I checked crates/auth/src/locality.rs and it already documents a known case where proxied traffic can be misclassified as local (bare_proxy_without_env_var_appears_local), so this new broad bypass can turn that misclassification into unauthenticated remote API access until onboarding completes.
Useful? React with 👍 / 👎.
Prefix unused `config` params with underscore and replace `vec!` with array literal in `generate_launchd_plist` / `generate_systemd_unit`. Entire-Checkpoint: 6b182a1aca6f
Entire-Checkpoint: cc7596feb984
Revert NoopOnboardingService to not include "onboarded" field — the chat_ui test depends on noop returning a not-onboarded state. Instead, change the auth bypass default from unwrap_or(false) to unwrap_or(true): when the onboarded field is absent (noop service) or wizard_status() fails, assume onboarded (safe default = require auth). The bypass only activates when onboarded is explicitly false. Revert node-host service.rs to main — the clippy warnings and test failure are pre-existing and unrelated to this PR. Entire-Checkpoint: a6217c77e389
The generate_launchd_plist() and generate_systemd_unit() functions accepted a ServiceConfig parameter but never used it, causing clippy unused-variable warnings. Wire the config fields (gateway-url, device-token, node-id, name, working-dir, timeout) into the generated service templates as CLI arguments. Entire-Checkpoint: dc2070e5181f
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fad7cc001
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let mut extra_args = format!( | ||
| " --gateway-url {} --device-token {}", | ||
| config.gateway_url, config.device_token |
There was a problem hiding this comment.
Remove unsupported flags from node service command
moltis node run only accepts the optional --timeout flag (crates/cli/src/node_commands.rs), but this unit template now always appends --gateway-url and --device-token (plus other optional flags), so the service process will exit on argument parsing before it can start the node host. This breaks newly installed node services on Linux (and the same pattern is present in the launchd generator), so the service command should stay compatible with the Run subcommand or the CLI needs matching Run options.
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
- Restrict onboarding auth bypass to narrow path set (sessions, bootstrap, gon, tailscale, ws) instead of all /api/* and /ws/* paths - Add test verifying privileged endpoints remain protected during onboarding - Consolidate duplicate test helpers into single start_server_with_onboarding() - Fix node-host: only pass --timeout to service templates (the only flag `moltis node run` accepts); fix pre-existing broken tests that asserted config fields in template output Entire-Checkpoint: 38493b795e8f
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 702bb6476c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| path.starts_with("/api/sessions/") // STT upload / media | ||
| || path.starts_with("/api/bootstrap") | ||
| || path == "/api/gon" | ||
| || path.starts_with("/api/tailscale/") |
There was a problem hiding this comment.
Remove tailscale routes from onboarding auth bypass
Fresh evidence after the earlier narrowing: is_onboarding_bypass_path() still allows "/api/tailscale/", and tailscale_router() exposes POST /configure (configure_handler) that applies Serve/Funnel mode changes. Because this bypass runs before the normal 401 path, an unauthenticated request is accepted whenever onboarded=false and the request is classified local; with the documented bare-proxy locality limitation, that can permit remote callers to mutate networking config during onboarding.
Useful? React with 👍 / 👎.
| /// `/api/restart` are never reachable without credentials. | ||
| #[cfg(feature = "web-ui")] | ||
| fn is_onboarding_bypass_path(path: &str) -> bool { | ||
| path.starts_with("/api/sessions/") // STT upload / media |
There was a problem hiding this comment.
Limit sessions bypass to upload/media endpoints only
Using path.starts_with("/api/sessions/") bypasses auth for the whole sessions namespace, not just STT upload/media; this includes /api/sessions/{session_key}/history, which returns conversation history from api_session_history_handler. That means unauthenticated local requests can read chat data while onboarding is incomplete, which is broader than the stated upload use case and can expose existing data in installs that are still onboarded=false (for example when config/data exists without the onboarding sentinel).
Useful? React with 👍 / 👎.
Resolve conflict in crates/node-host/src/service.rs — main also fixed the unused-config clippy warning (prefixed _config). Kept our version that passes --timeout to the service templates since `node run` accepts it, and restored the `config` parameter name accordingly. Entire-Checkpoint: 2a076d9d6f26
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5e5ba407d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #[cfg(feature = "web-ui")] | ||
| fn is_onboarding_bypass_path(path: &str) -> bool { | ||
| path.starts_with("/api/sessions/") // STT upload / media | ||
| || path.starts_with("/api/bootstrap") |
There was a problem hiding this comment.
Restrict bootstrap bypass to onboarding-safe payload
Allowlisting path.starts_with("/api/bootstrap") for unauthenticated local requests during onboarding exposes more data than the wizard needs: BootstrapQuery defaults every include_* flag to true in crates/web/src/api.rs, so calling /api/bootstrap without query params returns sessions/projects/models metadata and identity. The onboarding UI only needs the reduced payload (include_* = false in crates/web/src/assets/js/onboarding-view.js), so this bypass lets any unauthenticated local caller read existing workspace metadata while onboarded=false (e.g., partial/migrated installs).
Useful? React with 👍 / 👎.
* fix(auth): bypass auth for local API requests during onboarding
During first-time onboarding, the STT "Test" button fails with 401
because transcribeAudio() uses HTTP fetch (POST /api/sessions/{key}/upload)
which goes through auth_gate, unlike the WS-based voice config RPCs.
Add a narrow bypass in auth_gate's Unauthorized branch: when the request
is local, targets /api/* or /ws/*, and onboarding hasn't completed yet
(wizard_status reports onboarded=false), allow the request through with
Loopback identity.
Also fix a pre-existing missing .send().await.unwrap() in the /ws
public route test and two nightly rustfmt formatting issues.
Closes #378
Entire-Checkpoint: 333257de8c57
* fix: resolve pre-existing clippy warnings in node-host service
Prefix unused `config` params with underscore and replace `vec!` with
array literal in `generate_launchd_plist` / `generate_systemd_unit`.
Entire-Checkpoint: 6b182a1aca6f
* style: fix nightly rustfmt formatting in node-host service
Entire-Checkpoint: cc7596feb984
* fix: revert NoopOnboardingService and node-host changes
Revert NoopOnboardingService to not include "onboarded" field — the
chat_ui test depends on noop returning a not-onboarded state.
Instead, change the auth bypass default from unwrap_or(false) to
unwrap_or(true): when the onboarded field is absent (noop service)
or wizard_status() fails, assume onboarded (safe default = require
auth). The bypass only activates when onboarded is explicitly false.
Revert node-host service.rs to main — the clippy warnings and test
failure are pre-existing and unrelated to this PR.
Entire-Checkpoint: a6217c77e389
* fix(node-host): wire ServiceConfig fields into launchd/systemd templates
The generate_launchd_plist() and generate_systemd_unit() functions
accepted a ServiceConfig parameter but never used it, causing clippy
unused-variable warnings. Wire the config fields (gateway-url,
device-token, node-id, name, working-dir, timeout) into the generated
service templates as CLI arguments.
Entire-Checkpoint: dc2070e5181f
* fix: address PR review comments
- Restrict onboarding auth bypass to narrow path set (sessions, bootstrap,
gon, tailscale, ws) instead of all /api/* and /ws/* paths
- Add test verifying privileged endpoints remain protected during onboarding
- Consolidate duplicate test helpers into single start_server_with_onboarding()
- Fix node-host: only pass --timeout to service templates (the only flag
`moltis node run` accepts); fix pre-existing broken tests that asserted
config fields in template output
Entire-Checkpoint: 38493b795e8f
…is-org#386) * fix(auth): bypass auth for local API requests during onboarding During first-time onboarding, the STT "Test" button fails with 401 because transcribeAudio() uses HTTP fetch (POST /api/sessions/{key}/upload) which goes through auth_gate, unlike the WS-based voice config RPCs. Add a narrow bypass in auth_gate's Unauthorized branch: when the request is local, targets /api/* or /ws/*, and onboarding hasn't completed yet (wizard_status reports onboarded=false), allow the request through with Loopback identity. Also fix a pre-existing missing .send().await.unwrap() in the /ws public route test and two nightly rustfmt formatting issues. Closes moltis-org#378 Entire-Checkpoint: 333257de8c57 * fix: resolve pre-existing clippy warnings in node-host service Prefix unused `config` params with underscore and replace `vec!` with array literal in `generate_launchd_plist` / `generate_systemd_unit`. Entire-Checkpoint: 6b182a1aca6f * style: fix nightly rustfmt formatting in node-host service Entire-Checkpoint: cc7596feb984 * fix: revert NoopOnboardingService and node-host changes Revert NoopOnboardingService to not include "onboarded" field — the chat_ui test depends on noop returning a not-onboarded state. Instead, change the auth bypass default from unwrap_or(false) to unwrap_or(true): when the onboarded field is absent (noop service) or wizard_status() fails, assume onboarded (safe default = require auth). The bypass only activates when onboarded is explicitly false. Revert node-host service.rs to main — the clippy warnings and test failure are pre-existing and unrelated to this PR. Entire-Checkpoint: a6217c77e389 * fix(node-host): wire ServiceConfig fields into launchd/systemd templates The generate_launchd_plist() and generate_systemd_unit() functions accepted a ServiceConfig parameter but never used it, causing clippy unused-variable warnings. Wire the config fields (gateway-url, device-token, node-id, name, working-dir, timeout) into the generated service templates as CLI arguments. Entire-Checkpoint: dc2070e5181f * fix: address PR review comments - Restrict onboarding auth bypass to narrow path set (sessions, bootstrap, gon, tailscale, ws) instead of all /api/* and /ws/* paths - Add test verifying privileged endpoints remain protected during onboarding - Consolidate duplicate test helpers into single start_server_with_onboarding() - Fix node-host: only pass --timeout to service templates (the only flag `moltis node run` accepts); fix pre-existing broken tests that asserted config fields in template output Entire-Checkpoint: 38493b795e8f
Summary
401 AUTH_NOT_AUTHENTICATEDbecausetranscribeAudio()uses HTTP fetch (POST /api/sessions/{key}/upload) which goes throughauth_gate, unlike the WS-based voice config RPCsauth_gate'sUnauthorizedbranch: when the request is local, targets/api/*or/ws/*, and onboarding hasn't completed yet (wizard_statusreportsonboarded=false), allow the request through withLoopbackidentityNoopOnboardingService::wizard_status()to return"onboarded": trueso the bypass doesn't activate in existing testsValidation
Completed
cargo test -p moltis-gateway --test auth_middleware— all 53 tests pass (3 new)cargo +nightly-2025-11-30 fmt --all -- --check— cleancargo +nightly-2025-11-30 clippy -p moltis-gateway --all-targets -- -D warnings— cleanRemaining
./scripts/local-validate.sh— full CI validationManual QA
.onboardedsentinel)Closes #378