Delete stored security keys

This commit is contained in:
Daniel Redetzke
2026-10-04 22:35:19 +03:00
parent 6570611ed8
commit 0a8dc8f7af
5 changed files with 53 additions and 22 deletions
+21 -19
View File
@@ -42,17 +42,29 @@ type UserMFA struct {
Handle []byte `json:"handle,omitempty"` // WebAuthn user handle
}
// MFAKey is a passkey. Keys added by v0.3.0 as plain security keys have
// Passkey false; they still work as the second step.
// MFAKey is a passkey.
type MFAKey struct {
ID string `json:"id"`
Name string `json:"name"`
Passkey bool `json:"passkey"` // discoverable: signs in without a password
Passkey bool `json:"passkey"` // false only for security keys added by v0.3.0, which are deleted
Created time.Time `json:"created"`
LastUsed *time.Time `json:"lastUsed,omitempty"`
Credential webauthn.Credential `json:"credential"`
}
// dropSecurityKeys deletes the security keys v0.3.0 could add; only
// passkeys are supported. A user left without a method loses their
// recovery codes too.
func dropSecurityKeys(u *User) {
if u.MFA == nil {
return
}
u.MFA.Keys = slices.DeleteFunc(u.MFA.Keys, func(k MFAKey) bool { return !k.Passkey })
if !u.hasMFA() {
u.MFA.RecoveryCodes = nil
}
}
func (u *User) hasMFA() bool {
return u.MFA != nil && (u.MFA.TOTPSecret != "" || len(u.MFA.Keys) > 0)
}
@@ -205,7 +217,6 @@ type ticket struct {
type ceremony struct {
userID string // "" for a passkey sign-in
passkey bool
data *webauthn.SessionData
expires time.Time
}
@@ -563,7 +574,7 @@ func (a *App) loginPasskeyFinish(w http.ResponseWriter, r *http.Request) {
u := &cfg.Users[i]
if u.MFA != nil && len(u.MFA.Handle) > 0 && bytes.Equal(u.MFA.Handle, handle) {
for _, k := range u.MFA.Keys {
if k.Passkey && bytes.Equal(k.Credential.ID, rawID) {
if bytes.Equal(k.Credential.ID, rawID) {
found = u
return waUser{u}, nil
}
@@ -589,7 +600,6 @@ func (a *App) loginPasskeyFinish(w http.ResponseWriter, r *http.Request) {
type keyView struct {
ID string `json:"id"`
Name string `json:"name"`
Passkey bool `json:"passkey"`
Created time.Time `json:"created"`
LastUsed *time.Time `json:"lastUsed"`
}
@@ -606,7 +616,7 @@ func (a *App) mfaStatus(w http.ResponseWriter, r *http.Request) {
if m := u.MFA; m != nil {
keys := []keyView{}
for _, k := range m.Keys {
keys = append(keys, keyView{k.ID, k.Name, k.Passkey, k.Created, k.LastUsed})
keys = append(keys, keyView{k.ID, k.Name, k.Created, k.LastUsed})
}
out["totp"], out["totpAdded"], out["keys"], out["recoveryLeft"] = m.TOTPSecret != "", m.TOTPAdded, keys, len(m.RecoveryCodes)
}
@@ -752,7 +762,7 @@ func (a *App) keyBegin(w http.ResponseWriter, r *http.Request) {
return
}
a.auth.mu.Lock()
a.auth.mfa.enrolls[p.UserID] = &ceremony{userID: p.UserID, passkey: true, data: data, expires: time.Now().Add(ticketTTL)}
a.auth.mfa.enrolls[p.UserID] = &ceremony{userID: p.UserID, data: data, expires: time.Now().Add(ticketTTL)}
a.auth.mu.Unlock()
writeJSON(w, http.StatusOK, opts)
}
@@ -792,7 +802,7 @@ func (a *App) keyFinish(w http.ResponseWriter, r *http.Request) {
name = name[:maxKeyName]
}
var codes []string
key := MFAKey{ID: newID(), Name: name, Passkey: cer.passkey, Created: time.Now().UTC(), Credential: *cred}
key := MFAKey{ID: newID(), Name: name, Passkey: true, Created: time.Now().UTC(), Credential: *cred}
if err := a.store.Update(func(c *Config) error {
_, u := c.userByID(p.UserID)
if u == nil || u.MFA == nil {
@@ -911,17 +921,9 @@ func (a *App) resetMFA(w http.ResponseWriter, r *http.Request) {
// mfaSummary is what user lists show.
func mfaSummary(u *User) map[string]any {
out := map[string]any{"totp": false, "keys": 0, "passkeys": 0}
out := map[string]any{"totp": false, "passkeys": 0}
if m := u.MFA; m != nil {
keys, passkeys := 0, 0
for _, k := range m.Keys {
if k.Passkey {
passkeys++
} else {
keys++
}
}
out["totp"], out["keys"], out["passkeys"] = m.TOTPSecret != "", keys, passkeys
out["totp"], out["passkeys"] = m.TOTPSecret != "", len(m.Keys)
}
return out
}