Repository navigation
Install without GitHub: a setup code, then an admin account with a password - #664
Conversation
…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>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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.IsWizardModebecomes!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 newblog_users.password_hashbcrypt column, a shared_password_login/_password_login_pagetemplate, andgoblog 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.
Closes #654. Closes #658.
Problem
Setup code
While a site has no admin, goblog prints a random code to its log at startup:
The wizard asks for it before anything else. Being able to read the server's log is the proof of running the server.
/test_db,/wizard_db, the GitHub step (/wizardand its callback), the new/wizard/admin, and the pre-admin exemptions onPATCH /api/v1/settingsandPOST /api/v1/upload(IsWizardModenow means "no admin and this browser entered the code").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.CreatePasswordAdmincreates the user (providerpassword) 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 newblog_users.password_hashcolumn.POST /login/passwordsigns 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._password_login, which the default theme'slogin.htmlincludes. When a password is the only way to sign in, or at/login?password=1, goblog renders its own_password_login_pageinstead of the theme'slogin.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 aWORKDIRso it works underdocker exec..envor the site has an admin; previously only the former.Also
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
AutoMigrateadds the one column.Testing
Not covered
Secure, so the setup code will not stick there unlessSESSION_SECURE=falseis set. The setup-code page says so, but goblog does not detect it..env; there is no admin page for it./login?password=1always works, but on a site with both GitHub and a password admin those themes show only GitHub..envloses 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