Skip to content

Install without GitHub: a setup code, then an admin account with a password - #664

Merged
compscidr merged 1 commit into
mainfrom
feat/setup-code-admin-password
Oct 3, 2026
Merged

compscidr merged 1 commit into
mainfrom
feat/setup-code-admin-password

Conversation

@compscidr

Copy link
Copy Markdown
Collaborator

Closes #654. Closes #658.

Problem

Setup code

While a site has no admin, goblog prints a random code to its log at startup:

GoBlog setup code: ABCD-EFGH-JKMN

The wizard asks for it before anything else. Being able to read the server's log is the proof of running the server.

  • Required by: every wizard page, /test_db, /wizard_db, the GitHub step (/wizard and its callback), the new /wizard/admin, and the pre-admin exemptions on PATCH /api/v1/settings and POST /api/v1/upload (IsWizardMode now means "no admin and this browser entered the code").
  • In memory only; a restart makes a new one, which also locks any browser unlocked with the old one. Cleared once the site has an admin.
  • Compared in constant time, 12 symbols from a 31-symbol alphabet, guesses limited to 10 per 10 minutes per client address.

Admin account with a password

The wizard's last step is now "Create your admin account": an email and a password. It signs the browser in and ends the install. GitHub is still offered underneath as the alternative.

  • auth.CreatePasswordAdmin creates the user (provider password) and the admin row in one transaction and refuses if an admin exists. Passwords: at least 10 characters, at most 72 bytes (bcrypt's limit; refused rather than truncated), stored as bcrypt hashes in a new blog_users.password_hash column.
  • POST /login/password signs in. A wrong password and an unknown email get the same answer and take the same time; attempts are limited to 10 per 10 minutes per client address; each sign-in rotates the session token.
  • The form is a shared partial, _password_login, which the default theme's login.html includes. When a password is the only way to sign in, or at /login?password=1, goblog renders its own _password_login_page instead of the theme's login.html, so a theme that predates this cannot lock the admin out.
  • goblog reset-admin-password [email] prints a new random password and signs out existing sessions. The Docker image gains a WORKDIR so it works under docker exec.
  • "Installed" now means GitHub is configured in .env or the site has an admin; previously only the former.

Also

  • The session key is no longer written to the log (three places).

Existing sites

Nothing changes for a site that already has an admin: no setup code is printed, the login page is the same unless a password user exists, and AutoMigrate adds the one column.

Testing

  • New unit tests: the setup code (right, wrong, other browser, after a restart, no code set, rate limit), password admin creation and its refusals, password login and its refusals, rate limiting, and reset. The endpoint-guard test now also covers "no setup code".
  • The install smoke test now reads the setup code from the log, checks the wizard refuses without it, creates the admin, checks the wizard is closed afterwards, signs in with the password after the container is replaced, and resets the password. It passes locally for sqlite (both layouts), mysql and postgres.
  • I looked at screenshots of the setup-code page, the admin-account step and the password login page in the default theme.

Not covered

  • The GitHub alternative is still untested end to end; CI cannot authorise a GitHub app.
  • Plain http on a non-localhost address: the session cookie is Secure, so the setup code will not stick there unless SESSION_SECURE=false is set. The setup-code page says so, but goblog does not detect it.
  • Adding GitHub login later to a password-only site is by editing .env; there is no admin page for it.
  • Other themes' login pages (Forest, Minimal, the site theme) do not include the form yet. They fall back to goblog's own page when a password is the only login, and /login?password=1 always works, but on a site with both GitHub and a password admin those themes show only GitHub.
  • .env loses its session key at the database step (it is rewritten with only the database settings), so the first restart after an install signs everyone out. That predates this PR.

🤖 Generated with Claude Code

…ssword

Finishing an install meant creating a GitHub OAuth app, and until that
was done the wizard worked for anyone who found the site.

Setup code. While a site has no admin, goblog prints a random code to
its log at startup and the wizard asks for it before anything else. The
database endpoints, the GitHub step and the pre-admin settings and
upload endpoints all require a browser that has entered it (#658).

Admin password. The wizard's last step now creates an admin account
with an email and a password (bcrypt), signs the browser in and ends the
install; GitHub is still offered as the alternative. The login page gets
a password form, a shared partial, and goblog renders its own login page
when a password is the only way in, so a theme whose login.html predates
this cannot lock the admin out. Sign-in attempts are rate limited.
`goblog reset-admin-password` prints a new password from the server.

Also: the session key is no longer written to the log, and the Docker
image has a WORKDIR so the reset command finds .env under docker exec.

The install smoke test now reads the setup code from the log, checks the
wizard refuses without it, creates the admin, signs in with the password
after the container is replaced, and resets the password.

Closes #654. Closes #658.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 16:02
@compscidr
compscidr merged commit bc607e9 into main Oct 3, 2026
6 of 8 checks passed
@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.17910% with 72 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
auth/password.go 63.11% 30 Missing and 15 partials ⚠️
cmd_reset_password.go 0.00% 21 Missing ⚠️
blog/blog.go 69.23% 2 Missing and 2 partials ⚠️
auth/setup.go 94.28% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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 review overview

🔵 Needs a closer look

It is a large, security-sensitive change to authentication and the install flow (setup codes, password storage/login, admin creation) that warrants final human review despite appearing correct and well-tested.

Review effort: Balanced
Findings: None

What changed in this PR

This PR lets an operator finish a GoBlog install without creating a GitHub OAuth app, closing two issues: the GitHub-only admin requirement (#654) and the anonymous pre-admin window where settings (including raw HTML header/footer) could be changed by anyone who found a half-installed site (#658). It introduces an in-memory setup code printed to the server log that gates every wizard step, and a password admin account created as the wizard's final step, plus a password login flow and a reset-admin-password CLI command.

Changes:

  • Setup code: a random 12-symbol code printed at startup while no admin exists; the wizard and pre-admin endpoints (/wizard_db, /test_db, /wizard*, PATCH /api/v1/settings, POST /api/v1/upload) now require it, with constant-time comparison and per-IP rate limiting. IsWizardMode becomes !AdminExists() && SetupUnlocked(c).
  • Password admin: CreatePasswordAdmin (transactional, refuses a second admin), PasswordLogin (constant-time, rate-limited, same answer for wrong password vs. unknown email), a new blog_users.password_hash bcrypt column, a shared _password_login/_password_login_page template, and goblog reset-admin-password.
  • Supporting changes: "installed" now means GitHub configured or an admin exists; session key no longer logged; Dockerfile WORKDIR; docs and smoke-test updated to cover the new flow.
File Description
auth/​setup.go New in-memory setup code: generation, normalization, constant-time match, session unlock, rate limiting.
auth/​password.go Password admin creation, login (timing-safe), reset, and password validation.
auth/​auth.go Adds passwordLimiter, AdminExists(), redefines IsWizardMode to require the setup code.
auth/​otp.go Generalizes the IP limiter with configurable limit/window.
auth/​user.go Adds PasswordHash field and ProviderPassword.
goblog.go Wizard unlock/admin handlers, installed(), setup-code generation at startup, /login/password route, stops logging the session key.
cmd_reset_password.go New reset-admin-password subcommand.
blog/​blog.go Login page picks _password_login_page when a password is the only option; passes password_login_enabled/login_error.
templates/​shared/​_password_login.html Shared password form and standalone page partial.
themes/​default/​templates/​wizard_auth.html "Create admin account" step with GitHub offered as an alternative.
themes/​default/​templates/​wizard_unlock.html New setup-code entry page.
themes/​default/​templates/​login.html Includes the password form when enabled.
Dockerfile Adds WORKDIR so docker exec ... reset-admin-password finds .env.
go.mod Promotes golang.org/x/crypto to a direct dependency.
README.md, plugins/​docs/​content/​writing-a-theme.md, plugins/​docs/​content_test.go Document the setup code, password admin, and new template keys.
auth/​*_test.go, wizard_install_only_test.go, scripts/​install-smoke-test.sh Unit and smoke coverage for the new flow.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

Settings can be changed by anyone until the first admin logs in Finishing the install requires creating a GitHub OAuth app

2 participants