package auth import ( "database/sql" "encoding/json" "log/slog" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "github.com/go-webauthn/webauthn/webauthn" ) type WebAuthnHandler struct { db *sql.DB wa *webauthn.WebAuthn sessions *SessionStore logger *slog.Logger } func NewWebAuthnHandler(db *sql.DB, cfg *config.Config, sessions *SessionStore, logger *slog.Logger) (*WebAuthnHandler, error) { wa, err := webauthn.New(&webauthn.Config{ RPDisplayName: "Dumpster", RPID: cfg.Host, RPOrigins: []string{"https://" + cfg.Host, "http://" + cfg.Host + ":" + cfg.Port}, }) if err != nil { return nil, err } return &WebAuthnHandler{ db: db, wa: wa, sessions: sessions, logger: logger, }, nil } func (h *WebAuthnHandler) RegisterRoutes(r *http.ServeMux) { r.HandleFunc("POST /auth/webauthn/register/begin", h.RegisterBegin) r.HandleFunc("POST /auth/webauthn/register/finish", h.RegisterFinish) r.HandleFunc("POST /auth/webauthn/login/begin", h.LoginBegin) r.HandleFunc("POST /auth/webauthn/login/finish", h.LoginFinish) } func (h *WebAuthnHandler) RegisterBegin(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } var user WebAuthnUser err := h.db.QueryRowContext(r.Context(), `SELECT id, username, display_name FROM users WHERE id = $1`, userID, ).Scan(&user.ID, &user.Username, &user.DisplayName) if err != nil { http.Error(w, `{"error":"user not found"}`, http.StatusNotFound) return } // Get existing credentials rows, _ := h.db.QueryContext(r.Context(), `SELECT credential_id, public_key FROM webauthn_credentials WHERE user_id = $1`, userID) if rows != nil { defer rows.Close() for rows.Next() { var credID, pubKey []byte rows.Scan(&credID, &pubKey) user.credentials = append(user.credentials, webauthn.Credential{ ID: credID, PublicKey: pubKey, }) } } options, sessionData, err := h.wa.BeginRegistration(&user) if err != nil { http.Error(w, `{"error":"registration failed"}`, http.StatusInternalServerError) return } // Store session data (simplified: in production use a proper session store) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(options) _ = sessionData // TODO: store in session for finish step } func (h *WebAuthnHandler) RegisterFinish(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } // TODO: implement full registration finish with session data http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented) _ = userID } func (h *WebAuthnHandler) LoginBegin(w http.ResponseWriter, r *http.Request) { // TODO: implement login begin http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented) } func (h *WebAuthnHandler) LoginFinish(w http.ResponseWriter, r *http.Request) { // TODO: implement login finish http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented) } // WebAuthnUser implements the webauthn.User interface type WebAuthnUser struct { ID string Username string DisplayName string credentials []webauthn.Credential } func (u *WebAuthnUser) WebAuthnID() []byte { return []byte(u.ID) } func (u *WebAuthnUser) WebAuthnName() string { return u.Username } func (u *WebAuthnUser) WebAuthnDisplayName() string { return u.DisplayName } func (u *WebAuthnUser) WebAuthnIcon() string { return "" } func (u *WebAuthnUser) WebAuthnCredentials() []webauthn.Credential { return u.credentials }