From 0a1001eb0bd2d1f7c0624ae1ef8ae7ccdb3447d4 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 5 Feb 2026 15:35:01 -1000 Subject: Add passkey (WebAuthn) authentication support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable passwordless login via passkeys as an alternative to password auth. Users register passkeys from Settings; the login page offers both options. WebAuthn is optional — only active when WEBAUTHN_RP_ID and WEBAUTHN_ORIGIN env vars are set. Co-Authored-By: Claude Opus 4.6 --- cmd/dashboard/main.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'cmd/dashboard/main.go') diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index db1f66d..0553041 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -23,6 +23,8 @@ import ( "task-dashboard/internal/handlers" appmiddleware "task-dashboard/internal/middleware" "task-dashboard/internal/store" + + "github.com/go-webauthn/webauthn/webauthn" ) func main() { @@ -78,8 +80,23 @@ func main() { log.Printf("Warning: failed to parse auth templates: %v", err) } + // Initialize WebAuthn (optional - only if configured) + var wa *webauthn.WebAuthn + if cfg.WebAuthnRPID != "" && cfg.WebAuthnOrigin != "" { + var err error + wa, err = webauthn.New(&webauthn.Config{ + RPDisplayName: "Task Dashboard", + RPID: cfg.WebAuthnRPID, + RPOrigins: []string{cfg.WebAuthnOrigin}, + }) + if err != nil { + log.Fatalf("Failed to initialize WebAuthn: %v", err) + } + log.Printf("WebAuthn initialized (RP ID: %s, Origin: %s)", cfg.WebAuthnRPID, cfg.WebAuthnOrigin) + } + // Initialize auth handlers - authHandlers := auth.NewHandlers(authService, sessionManager, authTemplates) + authHandlers := auth.NewHandlers(authService, sessionManager, authTemplates, wa) // Initialize API clients todoistClient := api.NewTodoistClient(cfg.TodoistAPIKey) @@ -146,6 +163,10 @@ func main() { r.With(authRateLimiter.Limit).Post("/login", authHandlers.HandleLogin) r.Post("/logout", authHandlers.HandleLogout) + // WebAuthn public routes (rate-limited) + r.With(authRateLimiter.Limit).Post("/passkeys/login/begin", authHandlers.HandlePasskeyLoginBegin) + r.With(authRateLimiter.Limit).Post("/passkeys/login/finish", authHandlers.HandlePasskeyLoginFinish) + // Serve static files (public) fileServer := http.FileServer(http.Dir(cfg.StaticDir)) r.Handle("/static/*", http.StripPrefix("/static/", fileServer)) @@ -235,6 +256,12 @@ func main() { r.Post("/shopping/mode/{store}/toggle", h.HandleShoppingModeToggle) r.Post("/shopping/mode/{store}/complete", h.HandleShoppingModeComplete) + // Passkey management (WebAuthn) + r.Get("/settings/passkeys", authHandlers.HandleListPasskeys) + r.Post("/passkeys/register/begin", authHandlers.HandlePasskeyRegisterBegin) + r.Post("/passkeys/register/finish", authHandlers.HandlePasskeyRegisterFinish) + r.Delete("/passkeys/{id}", authHandlers.HandleDeletePasskey) + // Settings r.Get("/settings", h.HandleSettingsPage) r.Post("/settings/sync", h.HandleSyncSources) -- cgit v1.2.3