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.
This commit is contained in:
flyemoji committed 2026-07-01 14:36:28 +09:00
1 parent 0261204979
commit fce0fceac4
1 file changed
+22
+22
View File
@@ -14,6 +14,7 @@ import (
"felis.lolicon.best/internal/build" "felis.lolicon.best/internal/build"
"felis.lolicon.best/internal/config" "felis.lolicon.best/internal/config"
"felis.lolicon.best/internal/panel" "felis.lolicon.best/internal/panel"
"felis.lolicon.best/internal/passkey"
"felis.lolicon.best/internal/restore" "felis.lolicon.best/internal/restore"
"felis.lolicon.best/internal/store" "felis.lolicon.best/internal/store"
"felis.lolicon.best/internal/submit" "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)") 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) externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain)
internalSrv := &http.Server{Addr: *internalAddr, Handler: a.InternalHandler()} internalSrv := &http.Server{Addr: *internalAddr, Handler: a.InternalHandler()}
externalSrv := &http.Server{Addr: cfg.Server.Listen, Handler: externalHandler} externalSrv := &http.Server{Addr: cfg.Server.Listen, Handler: externalHandler}