feat(user): allow renaming security keys (webauthn/passkey) (#39413)

Closes https://github.com/go-gitea/gitea/issues/39287

Security key nicknames could only be set at registration, so a skipped
nickname left an auto-generated hex name until the key was
re-registered. Each key now has a Rename button opening a dialog with
the current nickname. A nickname used by another of the user's keys
(case-insensitive) or a blank nickname is rejected. Renames are recorded
as `user:webauth:rename` audit events.

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-05 19:12:22 +02:00
1 parent 66546045b5
commit d16b20b972
17 files changed
+142 -21

No files matched your search

+33 -4
View File
@@ -139,20 +139,49 @@ func WebauthnRegisterPost(ctx *context.Context) {
ctx.JSON(http.StatusCreated, cred)
}
// WebauthnDelete deletes an security key by id
// WebauthnRename changes the nickname of a security key
func WebauthnRename(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := context.GetFetchActionForm[*forms.WebauthnRenameForm](ctx)
if form == nil {
return
}
cred, err := auth.GetWebAuthnCredentialByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
if err != nil {
ctx.JSONErrorAuto(err)
return
}
renamed, err := auth.RenameCredential(ctx, ctx.Doer.ID, form.ID, form.Name)
if err != nil {
ctx.JSONErrorAuto(err)
return
}
if renamed {
audit.Record(ctx, audit_model.UserWebAuthRename, ctx.Doer, "previous_credential", cred.Name, "credential", form.Name)
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
// WebauthnDelete deletes a security key by id
func WebauthnDelete(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
cred, err := auth.GetWebAuthnCredentialByID(ctx, ctx.FormInt64("id"))
cred, err := auth.GetWebAuthnCredentialByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
if err != nil {
ctx.NotFoundOrServerError("GetWebAuthnCredentialByID", auth.IsErrWebAuthnCredentialNotExist, err)
ctx.JSONErrorAuto(err)
return
}
if ok, err := auth.DeleteCredential(ctx, cred.ID, ctx.Doer.ID); err != nil {
if ok, err := auth.DeleteCredential(ctx, ctx.Doer.ID, cred.ID); err != nil {
ctx.ServerError("DeleteCredential", err)
return
} else if ok {