diff --git a/internal/auth/account.go b/internal/auth/account.go index 893b200..b0e34bd 100644 --- a/internal/auth/account.go +++ b/internal/auth/account.go @@ -13,6 +13,7 @@ import ( type passkeyView struct { ID int64 ShortID string + Name string Added time.Time } @@ -48,7 +49,7 @@ func (s *Handlers) pageAccount(w http.ResponseWriter, r *http.Request) { if len(short) > 12 { short = short[:12] } - views = append(views, passkeyView{ID: c.ID, ShortID: short, Added: c.CreatedAt}) + views = append(views, passkeyView{ID: c.ID, ShortID: short, Name: c.Name, Added: c.CreatedAt}) } s.Render(w, r, "account.html", map[string]any{ "User": u, @@ -122,6 +123,24 @@ func (s *Handlers) handleChangePassword(w http.ResponseWriter, r *http.Request) accountRedirect(w, r, "ok", "Password updated.") } +// handleRenamePasskey sets or clears the human-friendly label on one of the +// user's passkeys. +func (s *Handlers) handleRenamePasskey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + uid := s.Auth.CurrentUserID(ctx) + credID, err := strconv.ParseInt(r.FormValue("credential_id"), 10, 64) + if err != nil { + passkeyRedirect(w, r, "pkerr", "Invalid passkey.") + return + } + name := NormalizePasskeyName(r.FormValue("name")) + if err := s.Store.SetCredentialName(ctx, uid, credID, name); err != nil { + passkeyRedirect(w, r, "pkerr", "Could not rename that passkey.") + return + } + passkeyRedirect(w, r, "pk", "Passkey name saved.") +} + // handleDeletePasskey removes one of the user's passkeys, refusing to remove // the last sign-in method. func (s *Handlers) handleDeletePasskey(w http.ResponseWriter, r *http.Request) { diff --git a/internal/auth/handlers.go b/internal/auth/handlers.go index e656b1a..fa2bd3b 100644 --- a/internal/auth/handlers.go +++ b/internal/auth/handlers.go @@ -53,12 +53,18 @@ func (s *Service) RegisterBegin(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var u *store.User + var name string // optional label, only for adding a passkey to an existing account if uid := s.CurrentUserID(ctx); uid != 0 { var err error if u, err = s.store.GetUserByID(ctx, uid); err != nil { httpError(w, http.StatusInternalServerError, "load user") return } + var body struct { + Name string `json:"name"` + } + _ = json.NewDecoder(r.Body).Decode(&body) + name = NormalizePasskeyName(body.Name) } else { username, displayName, ok := readCreds(r) if !ok { @@ -97,6 +103,7 @@ func (s *Service) RegisterBegin(w http.ResponseWriter, r *http.Request) { httpError(w, http.StatusInternalServerError, "save ceremony") return } + s.Sessions.Put(ctx, sessKeyWAName, name) writeJSON(w, options) } @@ -129,7 +136,9 @@ func (s *Service) RegisterFinish(w http.ResponseWriter, r *http.Request) { httpError(w, http.StatusInternalServerError, "marshal credential") return } - if err := s.store.AddCredential(ctx, u.ID, cred.ID, blob); err != nil { + name := s.Sessions.GetString(ctx, sessKeyWAName) + s.Sessions.Remove(ctx, sessKeyWAName) + if err := s.store.AddCredential(ctx, u.ID, cred.ID, blob, name); err != nil { httpError(w, http.StatusInternalServerError, "store credential") return } @@ -347,6 +356,15 @@ func NormalizeDisplayName(s string) string { return s } +// NormalizePasskeyName trims and bounds a passkey label (empty means "unnamed"). +func NormalizePasskeyName(s string) string { + s = strings.TrimSpace(s) + if len(s) > 64 { + s = s[:64] + } + return s +} + // ValidUsername reports whether s is 3–32 chars of [a-z0-9_.-]. func ValidUsername(s string) bool { if len(s) < 3 || len(s) > 32 { diff --git a/internal/auth/service.go b/internal/auth/service.go index 6862959..ebbbf44 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -19,6 +19,7 @@ const ( sessKeyUserID = "user_id" // int64: the authenticated user sessKeyWAUID = "wa_uid" // int64: user mid-ceremony sessKeyWAData = "wa_data" // []byte: marshaled webauthn.SessionData + sessKeyWAName = "wa_name" // string: pending passkey name for the in-flight registration sessKeyNext = "next" // string: post-auth redirect target ) diff --git a/internal/auth/templates/account.html b/internal/auth/templates/account.html index d2c6510..a720785 100644 --- a/internal/auth/templates/account.html +++ b/internal/auth/templates/account.html @@ -55,12 +55,17 @@ {{if .Passkeys}}