Skip to content

feat(auth): replace default password with console setup token - #7

Merged
some-du6e merged 3 commits into
masterfrom
claude/default-password-security-f4f0f4
Aug 12, 2026
Merged

some-du6e merged 3 commits into
masterfrom
claude/default-password-security-f4f0f4

Conversation

@some-du6e

@some-du6e some-du6e commented Aug 12, 2026

Copy link
Copy Markdown
Owner

What

Removes the built-in default dashboard password (123456) and replaces first-run bootstrap with a Portainer-style console setup token.

Why

A fresh install accepted 123456, and the login page printed it on screen. Anyone who reached the port before the operator did could claim the instance.

How it works now

Fresh install — the server mints a one-time token, prints it to the host console, and locks everything (/dashboard and /login both 307 → /setup) until someone pastes that token plus a new password (min 8 chars). Console access is the proof of ownership.

────────────────────────────────────────────────────────────────
  10router setup required (first run)
  Open the dashboard at /setup and paste this token:

      iMy2dXkv2J3pQrVHxcjBsce05izk0ACp

  Valid for 5 minutes. Restart the server to get a new one.
────────────────────────────────────────────────────────────────

Existing installs — the DB migration stamps legacyDefaultPassword in _meta for DBs that already existed (a fresh DB is never stamped, so new installs can't slip into this path). Those get exactly one more login on the old default, then a forced password change before the dashboard loads.

Recovery — CLI → Settings → Reset Password clears the hash and prints a fresh token, instead of restoring a guessable credential.

Reviewer notes

Two behavior changes beyond the strict scope, both load-bearing for the gate:

  • requireLogin: false no longer bypasses an unclaimed instance (src/dashboardGuard.js). Without this the setup gate has a trivial hole. It still works normally once someone owns the instance.
  • INITIAL_PASSWORD still works as a headless Docker bootstrap and skips the setup flow (it's an explicit operator choice), but is now ignored when shorter than 8 chars or set to 123456 — logs a warning and falls back to the token flow.

Token minting is capped at once per process. That's what makes "restart for a new token" true while ensuring no request can refresh its own expiry window, and that a /api/auth/setup GET can't mint a second token that silently invalidates one the CLI just printed. Both of those were bugs found during live verification and are fixed + covered by tests.

Verification

  • 50 unit tests pass — new tests/unit/auth-setup.test.js plus 6 new guard cases in tests/unit/dashboard-guard.test.js.
  • Zero new failures in the full suite. The 16 files that fail here also fail identically (74) on a stashed clean checkout — all pre-existing. Note tests/__baseline__/verify-no-regression.mjs is unusable in a local checkout: it splits paths on /app/, so every failure reads as a regression.
  • Live end-to-end against a throwaway DATA_DIR: gate → token → claim → session cookie → login; old default rejected afterward; simulated legacy-DB upgrade (one grace login → forced change → default dead); CLI reset → re-claim. The 5-minute lock fired for real mid-verification and refused the claim until restart.

Docs

README.md, .env.example, docs/ARCHITECTURE.md, AGENTS.md updated, plus all GitBook locales (en/es/ja/vi/zh-CN) — dashboard-login, quick-start, FAQ and cloud-env sections now document the console setup token instead of 123456.

Summary by CodeRabbit

  • New Features

    • Added a guided first-run setup flow using a one-time setup token.
    • Added setup status handling, password creation, validation, expiration, and lockout messaging.
    • Added support for optional headless bootstrap with an environment-provided password.
  • Bug Fixes

    • Removed reliance on a hardcoded default password.
    • Improved legacy installation handling and required password changes.
  • Documentation

    • Updated deployment, security, authentication, and troubleshooting guidance.
  • Tests

    • Added coverage for setup states, tokens, password policies, and authentication redirects.

There was no way to run 10router without a well-known credential: a fresh
install accepted `123456`, and the login page printed it on screen. Anyone
who reached the port before the operator did could claim the instance.

First run now mints a one-time setup token, prints it to the host console,
and locks every dashboard/API route until /setup is completed with that
token plus a password (min 8 chars). Console access is the proof of
ownership. Portainer-style, the token expires 5 minutes after the server
starts; a restart mints a new one.

- Token is minted at most once per process, so no request can refresh its
  own expiry window or invalidate a token the CLI reset just printed.
- requireLogin=false no longer bypasses an unclaimed instance — that opt-out
  is only meaningful for an instance someone already owns.
- Existing installs are stamped `legacyDefaultPassword` in _meta by the DB
  migration (fresh DBs never are) and get exactly one more login on the old
  default before a password change is forced.
- INITIAL_PASSWORD still works as a headless bootstrap, but is ignored when
  shorter than 8 chars or set to the old default.
- CLI "Reset Password to Default" becomes "Reset Password (re-run setup)":
  it clears the hash and prints a fresh token instead of restoring a
  guessable credential.
Copilot AI lite review requested due to automatic review settings August 12, 2026 22:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@some-du6e, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cee99bd-6dda-4538-b9c1-c0dbe23fd870

📥 Commits

Reviewing files that changed from the base of the PR and between a322d78 and d954b1b.

📒 Files selected for processing (31)
  • docs/ARCHITECTURE.md
  • gitbook/content/en/deployment/cloud.md
  • gitbook/content/en/faq.md
  • gitbook/content/en/getting-started/installation.md
  • gitbook/content/en/getting-started/quick-start.md
  • gitbook/content/es/deployment/cloud.md
  • gitbook/content/es/faq.md
  • gitbook/content/es/getting-started/installation.md
  • gitbook/content/es/getting-started/quick-start.md
  • gitbook/content/ja/deployment/cloud.md
  • gitbook/content/ja/faq.md
  • gitbook/content/ja/getting-started/installation.md
  • gitbook/content/ja/getting-started/quick-start.md
  • gitbook/content/vi/deployment/cloud.md
  • gitbook/content/vi/faq.md
  • gitbook/content/vi/getting-started/installation.md
  • gitbook/content/vi/getting-started/quick-start.md
  • gitbook/content/zh-CN/deployment/cloud.md
  • gitbook/content/zh-CN/faq.md
  • gitbook/content/zh-CN/getting-started/installation.md
  • gitbook/content/zh-CN/getting-started/quick-start.md
  • src/app/api/auth/change-password/route.js
  • src/app/api/auth/login/route.js
  • src/app/api/auth/reset-password/route.js
  • src/app/api/auth/setup/route.js
  • src/app/api/settings/route.js
  • src/app/login/page.js
  • src/dashboardGuard.js
  • src/lib/auth/setupToken.js
  • tests/unit/auth-setup.test.js
  • tests/unit/dashboard-guard.test.js
📝 Walkthrough

Walkthrough

The change replaces default-password bootstrap with one-time setup tokens, an eight-character password policy, setup-state API handling, first-run routing, legacy-installation migration, CLI reset support, and updated documentation and tests.

Changes

Authentication bootstrap flow

Layer / File(s) Summary
Bootstrap state and setup tokens
src/lib/auth/setupState.js, src/lib/auth/setupToken.js, src/lib/auth/setupBootstrap.js
Authentication state now distinguishes configured, OIDC, environment-password, legacy, and setup states. Setup tokens persist securely, expire after five minutes, support constant-time verification, and print to the console.
Legacy migration and startup wiring
src/lib/db/migrate.js, src/shared/services/initializeApp.js
Migration records one-time legacy default-password access. Startup creates a setup token before deferred initialization.
Setup and authentication API flow
src/app/api/auth/setup/route.js, src/app/api/auth/login/route.js, src/app/api/auth/status/route.js, src/app/api/auth/reset-password/route.js, src/app/api/settings/route.js, src/lib/auth/dashboardSession.js
The APIs expose setup status, complete setup with token validation and rate limiting, block unclaimed access, support legacy bootstrap login, and clear setup metadata after password creation.
Setup routing and password UI
src/dashboardGuard.js, src/app/setup/page.js, src/app/login/page.js, src/app/(dashboard)/dashboard/profile/page.js
Pending instances route to /setup. The setup page accepts a token and password. Login and profile password forms enforce the eight-character minimum.
Operator guidance and validation
cli/src/cli/menus/settings.js, .env.example, AGENTS.md, README.md, docs/ARCHITECTURE.md, tests/unit/auth-setup.test.js, tests/unit/dashboard-guard.test.js
CLI reset messages, environment guidance, architecture documentation, and tests now describe and validate token-based setup and legacy-password behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: 🟠 High · up to a322d

The authentication change can still allow legacy installations to access the dashboard with the old default password, while concurrent setup or failed recovery writes can leave password state inconsistent or unusable. Merge should be blocked until the server-side legacy-session restriction and atomic setup/recovery transitions are fixed.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant SetupPage
  participant SetupRoute
  participant setupToken
  participant SettingsDatabase
  participant Dashboard
  Operator->>SetupPage: enter setup token and new password
  SetupPage->>SetupRoute: submit setup request
  SetupRoute->>setupToken: verify one-time token
  setupToken-->>SetupRoute: return validity and expiry state
  SetupRoute->>SettingsDatabase: store hashed password and clear setup metadata
  SetupRoute-->>SetupPage: return success and session cookie
  SetupPage->>Dashboard: redirect to dashboard
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.71% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes replacing the default password with a console-generated setup token.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/default-password-security-f4f0f4

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.

The GitBook pages still told users to log in with `123456`, which no
longer works and was the exact instruction the setup-token change exists
to remove. Updates the dashboard-login, quick-start, FAQ and cloud-env
sections across en/es/ja/vi/zh-CN to document the console setup token,
the 5-minute window, the 8-character minimum and the CLI reset path.

INITIAL_PASSWORD examples in the deployment sections are kept — it still
works as an explicit headless bootstrap — but are no longer described as
having a default.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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 `@docs/ARCHITECTURE.md`:
- Around line 527-530: Update the setup-token expiry statement in the
architecture documentation to say that each token expires five minutes after it
is minted or issued, rather than five minutes after server startup. Preserve the
surrounding behavior and restart details.

In `@src/app/api/auth/login/route.js`:
- Around line 67-74: Enforce legacy password changes server-side: in
src/app/api/auth/login/route.js lines 67-74, add a signed restricted-session
claim when bootstrapState is "legacy"; in src/dashboardGuard.js lines 165-170,
prevent restricted sessions from satisfying general authentication; and in
src/dashboardGuard.js lines 190-203, redirect restricted page sessions to the
password-change flow while allowing only the required password-update and logout
endpoints.

In `@src/app/api/auth/reset-password/route.js`:
- Around line 13-18: Update the password-reset flow around updateSettings and
setMeta so clearing the password and removing LEGACY_GRACE_META_KEY form one
required transition: propagate any setMeta failure and avoid leaving the
password cleared without the legacy-grace marker removed. Only call
issueSetupToken and printSetupBanner after both operations succeed; do not issue
a token when the transition fails.

In `@src/app/api/auth/setup/route.js`:
- Around line 63-84: Update the setup flow around verifySetupToken and
updateSettings so the password is validated before reserving the token, then
atomically reserve or consume the setup token across token verification,
password persistence, and clearSetupToken. Ensure concurrent requests cannot
both pass verification or create sessions; only the request that successfully
completes the atomic operation may update the password.

In `@src/app/api/settings/route.js`:
- Around line 89-95: Move clearLegacyGrace() and clearSetupToken() out of the
password-hashing block and invoke them only after updateSettings(body) completes
successfully, ensuring the replacement password hash is persisted before
recovery state is revoked.

In `@tests/unit/auth-setup.test.js`:
- Around line 163-168: Update the “mints and prints a token for an unclaimed
instance” test around ensureSetupToken to spy on printSetupBanner, then assert
the banner output includes the issued token. Preserve the existing truthiness
and verifySetupToken assertions while ensuring the banner invocation is
observed.

In `@tests/unit/dashboard-guard.test.js`:
- Around line 307-313: Update the test case around the existing
requireLogin=false scenario to request /dashboard instead of /api/settings, then
assert that the response redirects to /setup. Keep the mocked settings and
unclaimed-instance setup unchanged so the test specifically exercises the
first-run setup gate.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc3423d7-cac5-4d00-adfc-ed6c60126d52

📥 Commits

Reviewing files that changed from the base of the PR and between d661780 and a322d78.

📒 Files selected for processing (22)
  • .env.example
  • AGENTS.md
  • README.md
  • cli/src/cli/menus/settings.js
  • docs/ARCHITECTURE.md
  • src/app/(dashboard)/dashboard/profile/page.js
  • src/app/api/auth/login/route.js
  • src/app/api/auth/reset-password/route.js
  • src/app/api/auth/setup/route.js
  • src/app/api/auth/status/route.js
  • src/app/api/settings/route.js
  • src/app/login/page.js
  • src/app/setup/page.js
  • src/dashboardGuard.js
  • src/lib/auth/dashboardSession.js
  • src/lib/auth/setupBootstrap.js
  • src/lib/auth/setupState.js
  • src/lib/auth/setupToken.js
  • src/lib/db/migrate.js
  • src/shared/services/initializeApp.js
  • tests/unit/auth-setup.test.js
  • tests/unit/dashboard-guard.test.js

Comment thread docs/ARCHITECTURE.md Outdated
Comment thread src/app/api/auth/login/route.js
Comment thread src/app/api/auth/reset-password/route.js
Comment thread src/app/api/auth/setup/route.js Outdated
Comment thread src/app/api/settings/route.js Outdated
Comment thread tests/unit/auth-setup.test.js
Comment thread tests/unit/dashboard-guard.test.js
Review found the forced change was advisory only. A legacy login issued a
normal session cookie and returned `mustChangePassword` for the client to
act on; anyone who knew the old default could ignore the form, request
/dashboard, and get in — or PATCH /api/settings with requireLogin:false
and remove authentication altogether.

The legacy session now carries a `pwChange` claim. The guard treats such a
session as unauthenticated everywhere except /api/auth/change-password
(plus logout and status), redirecting pages to /login and answering APIs
with 403. The new endpoint is the only way to spend that session: it
accepts a password for a legacy install, persists the hash, clears the
grace flag, and swaps in a full session.

Also from review:

- Consume the setup token atomically. verify-then-clear left a window
  where two concurrent claims both passed and each wrote a hash; the
  password is now validated first (so a bad one cannot burn the token)
  and consumeSetupToken() verifies and unlinks in one synchronous step.
  Verified: 10 concurrent claims yield exactly one success.
- reset-password no longer swallows a failed legacy-grace clear. The flag
  is cleared before the hash, so a failure leaves the old password working
  rather than stranding the install between "legacy" and "setup".
- Settings PATCH persists the new hash before revoking the legacy grace
  and setup token, so a failed write cannot leave no password and no way
  back in.
- ARCHITECTURE.md: the token expires 5 minutes after it is minted, not
  after server start — a CLI reset issues one at an arbitrary time.
- Tests: assert the console banner actually carries the token; the
  requireLogin=false case now exercises the setup gate via /dashboard
  rather than an /api/* 401 that proved nothing.
@some-du6e
some-du6e merged commit 311db9a into master Aug 12, 2026
1 check passed
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