Unverified Commit fce0fcea authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

feat(passkey): wire enrollment verifier into felis-api

Construct the go-webauthn verifier at the composition root and attach
it to the API when auth.panel_hostname is configured (RP id = panel
hostname, origin = https://<panel hostname>, display name Felis). When
the hostname is unset or the verifier fails to build it stays nil and
the passkey ceremony routes report 503, matching the existing
nil-when-unconfigured subsystem pattern. An admin passkey, if ever
added, is a separate relying party on the admin host and is
intentionally not wired here.
parent 02612049
Loading
Loading
Loading
Loading
+22 −0
Changes for cmd/felis/api.go: 22 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import (
	"felis.lolicon.best/internal/build"
	"felis.lolicon.best/internal/config"
	"felis.lolicon.best/internal/panel"
	"felis.lolicon.best/internal/passkey"
	"felis.lolicon.best/internal/restore"
	"felis.lolicon.best/internal/store"
	"felis.lolicon.best/internal/submit"
@@ -165,6 +166,27 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
	}
	fmt.Fprintln(stderr, "felis api: external face fails closed (Access JWKS key function not configured)")

	// Passkey (WebAuthn) enrollment verifier (spec §14, Phase 6). The relying party is
	// the panel (app) face: the RP id is the panel hostname and the single permitted
	// origin is that host over https, so a credential enrolled here is scoped to the
	// panel. It is wired only when auth.panel_hostname is configured; otherwise a.Passkey
	// stays nil and the enrollment begin/finish routes honestly return 503 (the
	// authenticated enrollment boundary is still enforced by the handlers). Scope is
	// ENROLLMENT only — the login/assertion path is a deferred slice, and credentials
	// enrolled under this RP id MUST be asserted under the same RP id when that slice
	// lands. An admin passkey (if ever added) is a SEPARATE relying party on the admin
	// host and is not wired here.
	if cfg.Auth.PanelHostname != "" {
		pv, err := passkey.New(cfg.Auth.PanelHostname, "Felis", []string{"https://" + cfg.Auth.PanelHostname})
		if err != nil {
			fmt.Fprintf(stderr, "felis api: passkey verifier disabled: %v — passkey endpoints return 503\n", err)
		} else {
			a.Passkey = pv
		}
	} else {
		fmt.Fprintln(stderr, "felis api: passkey verifier disabled (auth.panel_hostname unset) — passkey endpoints return 503")
	}

	externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain)
	internalSrv := &http.Server{Addr: *internalAddr, Handler: a.InternalHandler()}
	externalSrv := &http.Server{Addr: cfg.Server.Listen, Handler: externalHandler}