Skip to content

fix(server): remove LOBU_NO_AUTH, add /api/exchange-token PAT handoff#827

Merged
buremba merged 1 commit into
mainfrom
fix/no-auth-refuse-proxied-requests
May 17, 2026
Merged

fix(server): remove LOBU_NO_AUTH, add /api/exchange-token PAT handoff#827
buremba merged 1 commit into
mainfrom
fix/no-auth-refuse-proxied-requests

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 17, 2026

Summary

`LOBU_NO_AUTH=1` attributed every request to a fixed bootstrap user/org without checking any credential. Its only safety was the boot guard that refused to start unless the listener was bound to loopback — but that guard knew only about the listener, not about out-of-process proxies (Tailscale Funnel, ngrok, cloudflared, nginx) that reverse-proxy public traffic into the loopback bind. With any such proxy running, the "loopback-only personal mode" guarantee silently broke: every public request landed authenticated as the bootstrap user with owner role.

Rather than paper over the gap with proxy detection (forwarded-* headers, opt-out env vars, version-dependent tailscale CLI probes), drop the shortcut entirely. The embedded local-Lobu experience keeps working through the existing bootstrap PAT (`ensureBootstrapPat()` already mints one and writes it to `<LOBU_DATA_DIR>/bootstrap-pat.txt` on first boot).

To make that PAT ergonomic for browser-based clients (macOS menu-bar app, deep links from the operator's terminal):

  • `GET /api/exchange-token?token=&next=` — validates the PAT, mints a Better Auth session via `internalAdapter.createSession()`, signs the session token matching Better Auth's cookie format, and 302s to `next`. `next` is restricted to relative paths to prevent open-redirect abuse; `Referrer-Policy: no-referrer` keeps the PAT out of the next page's Referer. The cookie name picks up the `__Secure-` prefix from the canonical baseURL (`resolveBaseUrl`), so it matches whatever Better Auth would set during normal sign-in even when TLS is terminated by a reverse proxy.
  • SPA hook (owletto#157, included via submodule bump): `?lobu_token=` on any URL strips the token and redirects to the exchange endpoint with `next=` — operators can paste pre-authed deep links.
  • Mac app refactor (owletto#158, included via submodule bump): the menu bar drops `LOBU_NO_AUTH=1` from the runner spawn env and reads the bootstrap PAT instead of synthesising `"noauth"`.

Net diff: −531 lines (mostly the deleted `docs/plans/personal-mode-auth.md` and `utils/loopback.ts`, plus the bypass branch in `workspace/multi-tenant.ts`).

Files removed

  • `docs/plans/personal-mode-auth.md` — design doc for the removed mode.
  • `packages/server/src/utils/loopback.ts` — only ever used by the no-auth boot guards.

Test plan

  • `make typecheck` clean.
  • End-to-end: `GET /api/exchange-token?token=&next=/buremba` → 302 with `Set-Cookie: __Secure-better-auth.session_token=.; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800; Secure`. Subsequent `GET /api/auth/get-session` with that cookie returns 200 with the right user and session row.
  • Invalid PAT → 401. Missing `token` query → 400. `next=https://attacker.example.com\` (open-redirect attempt) → ignored, redirects to `/`.
  • (Mac app) Fresh `~/lobu` → runner spawns → menu bar shows "Local Developer" identity → API calls succeed against the post-removal server.

Out-of-repo impact

None — `LOBU_NO_AUTH=1` had no documented external consumers besides the in-repo Mac app, which is updated in the bundled submodule bump.

Summary by CodeRabbit

  • New Features

    • Added an endpoint to exchange Personal Access Tokens for authenticated session cookies.
  • Chores

    • Updated a subproject pointer (project dependency).
    • Removed the development-only no-auth startup mode and its loopback host enforcement.
    • Removed the "Personal-mode auth" planning document.

Review Change Stack

@buremba buremba enabled auto-merge (squash) May 17, 2026 20:02
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f10171b-a314-4dd5-9cda-d8ff777ae41f

📥 Commits

Reviewing files that changed from the base of the PR and between a955087 and 4da1a6b.

📒 Files selected for processing (8)
  • docs/plans/personal-mode-auth.md
  • packages/owletto
  • packages/server/src/auth/routes.ts
  • packages/server/src/index.ts
  • packages/server/src/server.ts
  • packages/server/src/start-local.ts
  • packages/server/src/utils/loopback.ts
  • packages/server/src/workspace/multi-tenant.ts

📝 Walkthrough

Walkthrough

This PR removes legacy LOBU_NO_AUTH local-dev support across server startup, middleware, and multi-tenant auth resolution, and adds a new GET /exchange-token endpoint to exchange a Personal Access Token (PAT) for a Better Auth session cookie.

Changes

Transition from LOBU_NO_AUTH to token exchange

Layer / File(s) Summary
Token exchange endpoint
packages/server/src/auth/routes.ts
New GET /exchange-token endpoint validates a token query param as a PAT, verifies it, creates a Better Auth session, HMAC-signs the session cookie using BETTER_AUTH_SECRET, sets a secure cookie name when base URL is HTTPS, and redirects to a sanitized next path; returns JSON errors for validation/configuration failures.
Remove CSRF middleware from request pipeline
packages/server/src/index.ts
Deletes isLoopbackHost import and the LOBU_NO_AUTH CSRF defense middleware that validated loopback Host headers and required same-origin or native-client indicators for mutating requests.
Remove loopback guards from server startup
packages/server/src/server.ts
Removes isLoopbackHost import and pre-/post-listen enforcement that refused LOBU_NO_AUTH=1 startup when configured/bound host was not loopback; httpServer.listen() now proceeds without those checks.
Refine bootstrap and startup logic
packages/server/src/start-local.ts
Removes isLoopbackHost import and no-auth fail-fast checks; clarifies bootstrap re-run conditions when bootstrap rows are missing; simplifies listen callback to basic logging.
Remove no-auth from multi-tenant provider
packages/server/src/workspace/multi-tenant.ts
Removes module-local no-auth bootstrap identity, cache, NoAuthUser type, getNoAuthUser helper, and the LOBU_NO_AUTH === '1' branch in MultiTenantProvider.resolveAuth; clearMultiTenantCachesForTests is now a direct re-export.
Owletto submodule update
packages/owletto
Updates submodule pointer to a different pinned commit.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • lobu-ai/lobu#779: Related auth/CSRF/no-auth work; previous additions of loopback guards and no-auth short-circuit overlap with these changes.
  • lobu-ai/lobu#785: Overlaps with removal/fixes for LOBU_NO_AUTH middleware, startup guards, and multi-tenant no-auth logic.
  • lobu-ai/lobu#774: Removal of docs/plans/personal-mode-auth.md is related to the implemented token exchange and cleanup.

Suggested labels

skip-size-check

🐰 I swapped the old loopback key for a token bright and true,
PATs now trade for sessions in a neat, signed queue.
The localhost guards hopped away like a hare,
Cookies set with care, redirecting where you dare.
Hop on, test and play — the exchange is new! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the two main changes: removing LOBU_NO_AUTH and adding the /api/exchange-token PAT handoff endpoint.
Description check ✅ Passed The description comprehensively covers the motivation, implementation details, files changed, and test plan. It follows the template structure with Summary and Test plan sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/no-auth-refuse-proxied-requests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a95508728a

ℹ️ 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".


const auth = await createAuth(c.env, c.req.raw);
const ctx = await auth.$context;
const session = await ctx.internalAdapter.createSession(authInfo.userId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict PAT exchange before minting full sessions

When a user creates a scoped PAT (for example mcp:read for CI or an external MCP client), this endpoint now accepts that token and mints an unrestricted Better Auth web session for the PAT's user. That bypasses the existing authSource === 'pat' guard on privileged web-only actions such as creating new server tokens, because the follow-up request is authenticated as a normal session rather than as the original scoped PAT. The exchange should be limited to the bootstrap/local handoff token or otherwise enforce high-trust scopes/purpose before creating a browser session.

Useful? React with 👍 / 👎.

Comment on lines +260 to +267
'Path=/',
'HttpOnly',
'SameSite=Lax',
`Max-Age=${60 * 60 * 24 * 7}`,
];
if (isHttps) cookieParts.push('Secure');
c.header('Set-Cookie', cookieParts.join('; '));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve cross-subdomain cookie attributes

In hosted deployments with AUTH_COOKIE_DOMAIN enabled, normal Better Auth sign-in sets the session cookie for the shared domain so browsers on org subdomains can call the canonical app/API host. This manually constructed cookie is host-only because it never adds the configured Domain, so a token exchanged on acme.lobu.ai will not authenticate subsequent credentialed requests to app.lobu.ai, breaking the cross-subdomain flow that the auth config explicitly supports.

Useful? React with 👍 / 👎.

LOBU_NO_AUTH=1 attributed every request to a fixed bootstrap-user/org
pair without checking any credential. Its only safety was the boot guard
that refused to start unless the listener was bound to loopback — but the
guard knew only about the listener, not about out-of-process proxies
(Tailscale Funnel, ngrok, cloudflared, nginx) that reverse-proxy public
traffic into the loopback bind. With any such proxy running, the
"loopback-only personal mode" guarantee silently broke: every public
request landed authenticated as the bootstrap user with owner role.

Rather than paper over the gap with proxy detection (forwarded-* headers,
opt-out env vars, version-dependent tailscale CLI probes), drop the
shortcut entirely. The embedded local-Lobu experience keeps working
through the existing bootstrap PAT (ensureBootstrapPat() already mints
one and writes it to <LOBU_DATA_DIR>/bootstrap-pat.txt on first boot).

To make that PAT ergonomic for browser-based clients (macOS menu-bar
app, deep links from the operator's terminal), add GET
/api/exchange-token?token=<PAT>&next=<path>: validates the PAT, mints
a Better Auth session via internalAdapter.createSession(), signs the
session token matching Better Auth's cookie format, and 302s to next.
Next is restricted to relative paths to prevent open-redirect abuse;
Referrer-Policy: no-referrer keeps the PAT out of the next page's
Referer. The cookie name picks up the __Secure- prefix from the
canonical baseURL (resolveBaseUrl), so it matches whatever Better Auth
would set during normal sign-in even when TLS is terminated by a
reverse proxy.

The companion SPA hook (?lobu_token=<PAT> on any URL) shipped in
owletto#157 — submodule pointer bumped to 0eaaa9aa.

Files removed: utils/loopback.ts (only ever used by the no-auth boot
guards), docs/plans/personal-mode-auth.md (the design doc for the
removed mode).
@buremba buremba force-pushed the fix/no-auth-refuse-proxied-requests branch from a955087 to 4da1a6b Compare May 17, 2026 21:05
@buremba buremba merged commit 4fddc72 into main May 17, 2026
14 of 18 checks passed
@buremba buremba deleted the fix/no-auth-refuse-proxied-requests branch May 17, 2026 21:05
@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

2 participants