Skip to content

feat: accept a one-time auth token from the URL (?token=) for tunnel/proxy setups#866

Open
valteribeiro wants to merge 1 commit into
siteboon:mainfrom
valteribeiro:feat/url-token-bootstrap
Open

feat: accept a one-time auth token from the URL (?token=) for tunnel/proxy setups#866
valteribeiro wants to merge 1 commit into
siteboon:mainfrom
valteribeiro:feat/url-token-bootstrap

Conversation

@valteribeiro

@valteribeiro valteribeiro commented Jun 11, 2026

Copy link
Copy Markdown

Problem

The web UI authenticates purely from localStorage['auth-token'], which is only ever set after a manual login/register in the browser (src/utils/api.js). For deployments where access is already gated upstream — a reverse proxy, or an ssh -L tunnel to a remote box — there's no way to pre-authenticate: every fresh deployment forces a manual login even though reaching the page already required authenticating to the tunnel/proxy.

Concretely, we provision ephemeral dev boxes, run cloudcli bound to 127.0.0.1, and forward it to the user's machine over ssh -L. The ssh tunnel is the real security boundary, but the user still has to hand-log-in to the CloudCLI account on every new box.

Change

On boot, accept a token passed via the URL and persist it as auth-token, then strip it from the URL. The frontend already has the auth-token key, and the backend already issues/verifies these JWTs (/api/auth/register and /api/auth/login return one) — this just wires the bootstrap path, as the very first import in src/main.jsx so it runs before anything reads the token.

// src/bootstrap-url-token.js
try {
  const u = new URL(window.location.href);
  const t = u.searchParams.get('token');
  if (t && !localStorage.getItem('auth-token')) {
    localStorage.setItem('auth-token', t);
    u.searchParams.delete('token');
    window.history.replaceState({}, '', u.pathname + u.search + u.hash);
  }
} catch (e) { /* non-fatal */ }

It's gated on no existing token and strips the token from the URL after storing it (so it isn't left in history or shared links). Happy to gate it behind an opt-in env (e.g. ALLOW_URL_TOKEN=true) if you'd prefer it off by default.

Why not platform mode

VITE_IS_PLATFORM=true bypasses the JWT check in the backend (middleware/auth.js), but the published frontend still renders the login screen (it doesn't read isPlatform), so platform mode alone doesn't help self-hosted/tunnel users. A URL-token bootstrap is the smallest change that does.

Note

I wasn't able to run the frontend build locally to smoke-test, so please double-check placement; the change is additive and self-contained.

Summary by CodeRabbit

  • New Features
    • URL tokens are now automatically extracted during app startup and stored locally if no existing token is present. Token parameters are removed from the URL for a cleaner interface, with error handling to ensure startup stability.

…proxy setups

The web UI only reads its auth token from localStorage, so deployments where
access is already gated upstream (a reverse proxy, or an ssh -L tunnel to a
remote box) still force a manual login on every fresh deployment even though
reaching the page already required authenticating to the tunnel/proxy.

This reads a `?token=` query param on boot, stores it as `auth-token`, and strips
it from the URL so it isn't left in history or shared links. Gated on no existing
token; the backend already issues/verifies these JWTs via /api/auth/*.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a one-time URL token bootstrap routine that executes on application startup. It extracts a token query parameter from the page URL, stores it in localStorage if no auth token already exists, and removes the parameter from browser history to avoid exposing the token.

Changes

URL Token Bootstrap

Layer / File(s) Summary
URL token bootstrap implementation
src/bootstrap-url-token.js
Boot-time routine reads ?token from window.location, stores it in localStorage['auth-token'] only if no existing auth token is present, removes the query parameter from the URL via history.replaceState, and wraps all logic in a try/catch to handle malformed URLs gracefully.
Bootstrap integration into app startup
src/main.jsx
Bootstrap module is imported at the top of main.jsx before React/DOM setup, ensuring the token extraction and storage logic runs during application initialization.

Suggested reviewers

  • viper151

🐰 A token hops in from the URL with care,
Stored safe in localStorage's lair,
The query string vanishes, clean and neat,
App boots up with auth complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: accepting a one-time auth token from URL query parameters for tunnel/proxy setups, matching the new bootstrap module that reads and stores the token.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bootstrap-url-token.js`:
- Around line 8-12: The URL scrub is currently conditional on storing the token;
change the logic so that you still call u.searchParams.delete('token') and
window.history.replaceState(...) whenever t is present, but only call
localStorage.setItem('auth-token', t) when !localStorage.getItem('auth-token')
is true — i.e., keep the "write only when absent" behavior for
localStorage.setItem('auth-token', t) while always executing
u.searchParams.delete('token') and window.history.replaceState({}, '',
u.pathname + u.search + u.hash) when t exists.
- Around line 8-9: Replace the hardcoded 'auth-token' literal with the exported
constant AUTH_TOKEN_STORAGE_KEY: add an import for AUTH_TOKEN_STORAGE_KEY from
src/components/auth/constants.ts at the top of src/bootstrap-url-token.js, then
use AUTH_TOKEN_STORAGE_KEY in both localStorage.getItem(...) and
localStorage.setItem(...) where the variable t is checked (the existing
conditional using t and localStorage access).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 83eac0da-9625-4b16-b293-f8f7d432a291

📥 Commits

Reviewing files that changed from the base of the PR and between 6a53c31 and 8cc54ed.

📒 Files selected for processing (2)
  • src/bootstrap-url-token.js
  • src/main.jsx

Comment on lines +8 to +9
if (t && !localStorage.getItem('auth-token')) {
localStorage.setItem('auth-token', t);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current literal usages and constant definition/callers.
rg -n -C2 "AUTH_TOKEN_STORAGE_KEY|auth-token" --type ts --type tsx --type js --type jsx

Repository: siteboon/claudecodeui

Length of output: 93


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find constant definition(s)
rg -n "AUTH_TOKEN_STORAGE_KEY" src || true

# 2) Find all usages of the literal storage key
rg -n "'auth-token'|\"auth-token\"" src || true

# 3) Inspect the specific file and constants file around the relevant lines
echo "---- src/bootstrap-url-token.js (around auth-token) ----"
rg -n "auth-token" src/bootstrap-url-token.js -n || true
nl -ba src/bootstrap-url-token.js | sed -n '1,120p'

echo "---- src/components/auth/constants.ts (AUTH_TOKEN_STORAGE_KEY) ----"
nl -ba src/components/auth/constants.ts | sed -n '1,200p'

Repository: siteboon/claudecodeui

Length of output: 1965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- src/bootstrap-url-token.js ----"
sed -n '1,120p' src/bootstrap-url-token.js

echo "---- src/components/auth/constants.ts ----"
sed -n '1,80p' src/components/auth/constants.ts

Repository: siteboon/claudecodeui

Length of output: 1230


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l src/bootstrap-url-token.js
sed -n '1,200p' src/bootstrap-url-token.js

Repository: siteboon/claudecodeui

Length of output: 884


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l src/bootstrap-url-token.js
sed -n '1,200p' src/bootstrap-url-token.js

Repository: siteboon/claudecodeui

Length of output: 884


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Where bootstrap-url-token is imported/used
rg -n "bootstrap-url-token" src || true

# See how constants.ts is imported elsewhere (including from JS)
rg -n "from '\\.\\./components/auth/constants'|from '\\.*/components/auth/constants'|AUTH_TOKEN_STORAGE_KEY" src || true

# Show bootstrap-url-token.js file name usage (in case of direct include)
rg -n "bootstrap-url-token\\.js" . || true

Repository: siteboon/claudecodeui

Length of output: 712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find JS files that import from TS modules in src
rg -n --type-add 'js:*.js' 'import .* from .*\\.ts' src || true
rg -n --type-add 'tsx:*.tsx' 'import \\{.*\\} from .*components/auth/constants' src || true
rg -n "from '\\.\\./components/auth/constants'|from '\\./components/auth/constants'|from '\\.\\./constants'|from '\\./constants'" src || true

Repository: siteboon/claudecodeui

Length of output: 1484


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Any JS importing from TS modules?
rg -n --type-add 'js:*.js' "import .* from .*\\.ts'|import .* from .*\\.ts\"" src || true

# 2) Any JS importing from components/auth/constants (directly)?
rg -n --type-add 'js:*.js' "components/auth/constants" src || true

# 3) Check tooling (Vite/webpack/etc.) to judge JS<->TS import compatibility
ls -1 package*.json 2>/dev/null || true
cat package.json 2>/dev/null || true

Repository: siteboon/claudecodeui

Length of output: 5431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -1 | rg -i "vite\\.config\\.(js|ts|mjs|cjs)$|tsconfig\\.json$|tsconfig\\.base\\.json$|jsconfig\\.json$" || true
cat tsconfig.json 2>/dev/null || true
cat vite.config.* 2>/dev/null || true

Repository: siteboon/claudecodeui

Length of output: 3239


Use AUTH_TOKEN_STORAGE_KEY in src/bootstrap-url-token.js (replace 'auth-token' literal)

In src/bootstrap-url-token.js lines 8-9, the key is hardcoded as 'auth-token' in both getItem/setItem, while src/components/auth/constants.ts already exports AUTH_TOKEN_STORAGE_KEY. Import AUTH_TOKEN_STORAGE_KEY in this file and use it instead.

  if (t && !localStorage.getItem('auth-token')) {
    localStorage.setItem('auth-token', t);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bootstrap-url-token.js` around lines 8 - 9, Replace the hardcoded
'auth-token' literal with the exported constant AUTH_TOKEN_STORAGE_KEY: add an
import for AUTH_TOKEN_STORAGE_KEY from src/components/auth/constants.ts at the
top of src/bootstrap-url-token.js, then use AUTH_TOKEN_STORAGE_KEY in both
localStorage.getItem(...) and localStorage.setItem(...) where the variable t is
checked (the existing conditional using t and localStorage access).

Comment on lines +8 to +12
if (t && !localStorage.getItem('auth-token')) {
localStorage.setItem('auth-token', t);
u.searchParams.delete('token');
window.history.replaceState({}, '', u.pathname + u.search + u.hash);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Always remove token from the URL, even when storage is skipped.

On Line 8, URL scrubbing is gated behind !localStorage.getItem('auth-token'). If a token already exists, the new ?token=... remains in the URL/history, which defeats the leak-prevention goal. Keep the “write only when absent” behavior, but scrub the query param whenever present.

Suggested patch
 try {
   const u = new URL(window.location.href);
   const t = u.searchParams.get('token');
-  if (t && !localStorage.getItem('auth-token')) {
-    localStorage.setItem('auth-token', t);
-    u.searchParams.delete('token');
-    window.history.replaceState({}, '', u.pathname + u.search + u.hash);
-  }
+  if (t && !localStorage.getItem('auth-token')) {
+    localStorage.setItem('auth-token', t);
+  }
+  if (t) {
+    u.searchParams.delete('token');
+    window.history.replaceState({}, '', u.pathname + u.search + u.hash);
+  }
 } catch (e) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bootstrap-url-token.js` around lines 8 - 12, The URL scrub is currently
conditional on storing the token; change the logic so that you still call
u.searchParams.delete('token') and window.history.replaceState(...) whenever t
is present, but only call localStorage.setItem('auth-token', t) when
!localStorage.getItem('auth-token') is true — i.e., keep the "write only when
absent" behavior for localStorage.setItem('auth-token', t) while always
executing u.searchParams.delete('token') and window.history.replaceState({}, '',
u.pathname + u.search + u.hash) when t exists.

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.

1 participant