feat(auth): first account on a new server needs the setup token from the server log (M462 #4982)
test-go / test (push) Successful in 2m3s
test-web / test (push) Successful in 1m14s
test-go / integration (push) Successful in 4m38s
release / Build signed APK (releases and dev) (push) Successful in 5m36s
release / Build + push container image (push) Successful in 1m24s
release / Verify release artifacts (tag releases only) (push) Skipped
test-go / test (push) Successful in 2m3s
test-web / test (push) Successful in 1m14s
test-go / integration (push) Successful in 4m38s
release / Build signed APK (releases and dev) (push) Successful in 5m36s
release / Build + push container image (push) Successful in 1m24s
release / Verify release artifacts (tag releases only) (push) Skipped
While no accounts exist, the server mints a random setup token at boot and logs it. Registering the first account (which becomes admin) must carry it, so whoever reaches a freshly exposed instance first cannot claim it. The register page asks GET /api/auth/setup-status and shows a "Setup token" field in place of the invite field while setup is pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
||||
@@ -26,9 +29,12 @@ var usernameRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{3,32}$`)
|
||||
const minPasswordLength = 8
|
||||
|
||||
type registerReq struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
InviteToken string `json:"invite_token"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
InviteToken string `json:"invite_token"`
|
||||
// SetupToken is required only for the very first account; see
|
||||
// auth.SetupToken.
|
||||
SetupToken string `json:"setup_token"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
}
|
||||
|
||||
@@ -88,6 +94,19 @@ func (h *handlers) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// The first account becomes admin, so it must prove it can read the
|
||||
// server log. Checked before the invite logic, which the empty-users
|
||||
// state skips.
|
||||
if userCount == 0 && h.requireSetupToken && !h.setupToken.Matches(req.SetupToken) {
|
||||
// Repeat the token in the log at the moment someone needs it: the
|
||||
// boot line may have scrolled away, or users may have been deleted
|
||||
// since boot.
|
||||
h.logger.Warn("register: first-admin registration needs the setup token",
|
||||
"setup_token", h.setupToken.Value())
|
||||
writeErr(w, apierror.Forbidden("setup_token_invalid", "the setup token from the server log is required to create the first account"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate invite (skipped on empty-users state; skipped in 'open' mode).
|
||||
usedInviteToken := ""
|
||||
if userCount > 0 {
|
||||
@@ -232,3 +251,31 @@ func (h *handlers) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleSetupStatus implements GET /api/auth/setup-status. It tells the
|
||||
// register screen whether to ask for the setup token, i.e. whether no
|
||||
// account exists yet. Public, since it is needed before anyone can sign in;
|
||||
// "this server has no users" is not worth hiding from someone who could
|
||||
// simply try to register.
|
||||
func (h *handlers) handleSetupStatus(w http.ResponseWriter, r *http.Request) {
|
||||
n, err := dbq.New(h.pool).CountUsers(r.Context())
|
||||
if err != nil {
|
||||
writeErrWithLog(w, h.logger, "setup status: count users failed", apierror.Internal(err))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"setup_required": n == 0})
|
||||
}
|
||||
|
||||
// logSetupTokenIfNeeded writes the setup token to the log at boot when the
|
||||
// instance has no accounts yet, with the instruction for using it.
|
||||
func logSetupTokenIfNeeded(pool *pgxpool.Pool, logger *slog.Logger, token *auth.SetupToken) {
|
||||
if pool == nil || logger == nil {
|
||||
return
|
||||
}
|
||||
n, err := dbq.New(pool).CountUsers(context.Background())
|
||||
if err != nil || n > 0 {
|
||||
return
|
||||
}
|
||||
logger.Warn("no accounts yet: open the web app, choose Create account, and enter this setup token to become the admin",
|
||||
"setup_token", token.Value())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user