feat(auth)!: go fully passwordless and fix cross-check review findings
Remove password authentication everywhere; the only session doors are passkey (WebAuthn), email OTP, in-game bind codes, QR scan-login, and op-login vouching. Remediates the 33-finding cross-check review across backend, CLI, panel, plugins, and docs. Backend/CLI: - Drop password routes and fields from account/user/onboard/auth handlers; align tests (new account subtests, naming reserves "console", op-login/onboard/qr-login test updates). - Add migrations 0016_op_login.sql and 0017_drop_password.sql. - Thread panel/admin hostnames from hostcfg through api.go, setup_panel.go, tui_root.go and tui_preflight.go instead of hardcoding; bootstrap.sh writes panel-hostname/admin-hostname into felis.toml. - Reword breakglass and TUI copy for passwordless flows. Panel: - Delete the ChangePassword page and all password UI; align login/auth/api/types with the passwordless contract; add the migration and op-login approval flows. - i18n: convert ImageBuildPage durations/status badges and ServerLuckPerms strings to translation keys; drop 72 orphan keys per locale; unify the title as "Felis - Console". Plugins (all six rebuilt): - Velocity waiting router returns 503 at_capacity during wake; MOTD/control-channel copy and config comments. - Paper zh menu title; Limbo bind-code TTL 600s with panel_url preference; unified /link lines in fabric/forge/neoforge; shared link-client javadoc contract fixes. Docs: openapi.yaml, sequence-diagrams.md, deploy/limbo/README.md and plugins/README.md aligned with the implementation. BREAKING CHANGE: migration 0017 irreversibly drops users.password_hash and users.must_change_password; password login cannot be restored after migrating.
This commit is contained in:
97 files changed
+1923
-1444
No files matched your search
+15
-7
@@ -215,12 +215,13 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
|
||||
Restorer: restorer,
|
||||
Backuper: backuper,
|
||||
Submissions: submissions,
|
||||
// The external face is fronted by SessionAuth: it prefers a local-password
|
||||
// session cookie and otherwise delegates to the Cloudflare-Access JWT verifier,
|
||||
// so both auth models coexist on one face. The delegate's Keyfunc is
|
||||
// intentionally nil — the JWT path fails closed until a JWKS-backed key function
|
||||
// is wired (deployment integration point) — while the local-password path is
|
||||
// live the moment `felis breakGlass` flips local_auth_enabled on.
|
||||
// The external face is fronted by SessionAuth: it prefers a local session
|
||||
// cookie (minted by the passwordless doors) and otherwise delegates to the
|
||||
// Cloudflare-Access JWT verifier, so both auth models coexist on one face. The
|
||||
// delegate's Keyfunc is intentionally nil — the JWT path fails closed until a
|
||||
// JWKS-backed key function is wired (deployment integration point) — while the
|
||||
// local session path is live the moment `felis breakGlass` flips
|
||||
// local_auth_enabled on.
|
||||
External: api.SessionAuth{
|
||||
Repo: repo,
|
||||
Delegate: api.AccessVerifier{Audience: cfg.Auth.AccessJWTAud},
|
||||
@@ -229,6 +230,7 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
|
||||
},
|
||||
RootDomain: cfg.Server.RootDomain,
|
||||
AdminHostname: cfg.Auth.AdminHostname,
|
||||
PanelHostname: cfg.Auth.PanelHostname,
|
||||
WakeCooldown: 30 * time.Second,
|
||||
// Bound concurrent console/build-log SSE streams per principal. Generous enough
|
||||
// for legitimate multi-tab / multi-server watching, while capping how many
|
||||
@@ -271,7 +273,13 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
|
||||
fmt.Fprintln(stderr, "felis api: passkey verifier disabled (auth.panel_hostname unset) — passkey endpoints return 503")
|
||||
}
|
||||
|
||||
externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain, cfg.Auth.PanelHostname, cfg.Auth.AdminHostname, resolvedVersion())
|
||||
// Derive the console hostnames when felis.toml leaves them unset, exactly as the
|
||||
// setup/breakGlass paths do — otherwise the SPA cannot tell which face it is
|
||||
// serving and falls back to the player console on op.console.<root>.
|
||||
externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain,
|
||||
defaultPanelHostname(cfg.Server.RootDomain, cfg.Auth.PanelHostname),
|
||||
defaultAdminHostname(cfg.Server.RootDomain, cfg.Auth.AdminHostname),
|
||||
resolvedVersion())
|
||||
internalSrv := newAPIServer(*internalAddr, a.InternalHandler())
|
||||
externalSrv := newAPIServer(cfg.Server.Listen, externalHandler)
|
||||
|
||||
|
||||
Reference in new issue
Block a user