feat: accept a one-time auth token from the URL (?token=) for tunnel/proxy setups#866
feat: accept a one-time auth token from the URL (?token=) for tunnel/proxy setups#866valteribeiro wants to merge 1 commit into
Conversation
…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>
📝 WalkthroughWalkthroughThis PR adds a one-time URL token bootstrap routine that executes on application startup. It extracts a ChangesURL Token Bootstrap
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/bootstrap-url-token.jssrc/main.jsx
| if (t && !localStorage.getItem('auth-token')) { | ||
| localStorage.setItem('auth-token', t); |
There was a problem hiding this comment.
🧩 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 jsxRepository: 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.tsRepository: 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.jsRepository: 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.jsRepository: 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" . || trueRepository: 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 || trueRepository: 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 || trueRepository: 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 || trueRepository: 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).
| if (t && !localStorage.getItem('auth-token')) { | ||
| localStorage.setItem('auth-token', t); | ||
| u.searchParams.delete('token'); | ||
| window.history.replaceState({}, '', u.pathname + u.search + u.hash); | ||
| } |
There was a problem hiding this comment.
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.
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 anssh -Ltunnel 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
cloudclibound to127.0.0.1, and forward it to the user's machine overssh -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 theauth-tokenkey, and the backend already issues/verifies these JWTs (/api/auth/registerand/api/auth/loginreturn one) — this just wires the bootstrap path, as the very first import insrc/main.jsxso it runs before anything reads the token.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=truebypasses the JWT check in the backend (middleware/auth.js), but the published frontend still renders the login screen (it doesn't readisPlatform), 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