first-commit

This commit is contained in:
2025-08-25 15:46:12 +08:00
commit f4d95dfff4
5665 changed files with 705359 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"bytes"
"encoding/base64"
"html/template"
"image/png"
"net/http"
"strings"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
func RegenerateScratchTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
} else {
ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
}
return
}
token, err := t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
return
}
if err = auth.UpdateTwoFactor(ctx, t); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DisableTwoFactor deletes the user's 2FA settings.
func DisableTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
} else {
ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
}
return
}
if err = auth.DeleteTwoFactorByID(ctx, t.ID, ctx.Doer.ID); err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
// There is a potential DB race here - we must have been disabled by another request in the intervening period
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
} else {
ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
func twofaGenerateSecretAndQr(ctx *context.Context) bool {
var otpKey *otp.Key
var err error
uri := ctx.Session.Get("twofaUri")
if uri != nil {
otpKey, err = otp.NewKeyFromURL(uri.(string))
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed NewKeyFromURL: ", err)
return false
}
}
// Filter unsafe character ':' in issuer
issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "")
if otpKey == nil {
otpKey, err = totp.Generate(totp.GenerateOpts{
SecretSize: 40,
Issuer: issuer,
AccountName: ctx.Doer.Name,
})
if err != nil {
ctx.ServerError("SettingsTwoFactor: totpGenerate Failed", err)
return false
}
}
ctx.Data["TwofaSecret"] = otpKey.Secret()
img, err := otpKey.Image(320, 240)
if err != nil {
ctx.ServerError("SettingsTwoFactor: otpKey image generation failed", err)
return false
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, img); err != nil {
ctx.ServerError("SettingsTwoFactor: otpKey png encoding failed", err)
return false
}
ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
if err := ctx.Session.Set("twofaSecret", otpKey.Secret()); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaSecret", err)
return false
}
if err := ctx.Session.Set("twofaUri", otpKey.String()); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaUri", err)
return false
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
return true
}
// EnrollTwoFactor shows the page where the user can enroll into 2FA.
func EnrollTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
// already enrolled - we should redirect back!
log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.Doer)
ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
return
}
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
}
// EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
// already enrolled
ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor: Failed to check if already enrolled with GetTwoFactorByUID", err)
return
}
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
return
}
secretRaw := ctx.Session.Get("twofaSecret")
if secretRaw == nil {
ctx.Flash.Error(ctx.Tr("settings.twofa_failed_get_secret"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
return
}
secret := secretRaw.(string)
if !totp.Validate(form.Passcode, secret) {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
return
}
t = &auth.TwoFactor{
UID: ctx.Doer.ID,
}
err = t.SetSecret(secret)
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set secret", err)
return
}
token, err := t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
return
}
newTwoFactorErr := auth.NewTwoFactor(ctx, t)
if newTwoFactorErr == nil {
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
}
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
if err := ctx.Session.Delete("twofaSecret"); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to delete twofaSecret from the session: Error: %v", err)
}
if err := ctx.Session.Delete("twofaUri"); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to delete twofaUri from the session: Error: %v", err)
}
if err := ctx.Session.Release(); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to save changes to the session: %v", err)
}
if newTwoFactorErr != nil {
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
// If there is a unique constraint fail we should just tolerate the error
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", newTwoFactorErr)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@@ -0,0 +1,141 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
// OpenIDPost response for change user's openid
func OpenIDPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.AddOpenIDForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.HasError() {
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
return
}
// WARNING: specifying a wrong OpenID here could lock
// a user out of her account, would be better to
// verify/confirm the new OpenID before storing it
// Also, consider allowing for multiple OpenID URIs
id, err := openid.Normalize(form.Openid)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
return
}
form.Openid = id
log.Trace("Normalized id: " + id)
oids, err := user_model.GetUserOpenIDs(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = oids
// Check that the OpenID is not already used
for _, obj := range oids {
if obj.URI == id {
loadSecurityData(ctx)
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form)
return
}
}
redirectTo := setting.AppURL + "user/settings/security"
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
return
}
ctx.Redirect(url)
}
func settingsOpenIDVerify(ctx *context.Context) {
log.Trace("Incoming call to: " + ctx.Req.URL.String())
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
log.Trace("Full URL: " + fullURL)
id, err := openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{
Openid: id,
})
return
}
log.Trace("Verified ID: " + id)
oid := &user_model.UserOpenID{UID: ctx.Doer.ID, URI: id}
if err = user_model.AddUserOpenID(ctx, oid); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
log.Trace("Associated OpenID %s to user %s", id, ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
ctx.ServerError("DeleteUserOpenID", err)
return
}
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
// ToggleOpenIDVisibility response for toggle visibility of user's openid
func ToggleOpenIDVisibility(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
return
}
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@@ -0,0 +1,160 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"net/http"
"sort"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
)
const (
tplSettingsSecurity templates.TplName = "user/settings/security/security"
tplSettingsTwofaEnroll templates.TplName = "user/settings/security/twofa_enroll"
)
// Security render change user's password page and 2FA
func Security(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer,
setting.UserFeatureManageMFA, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings.security")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.FormString("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
return
}
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
}
// DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
id := ctx.FormInt64("id")
if id <= 0 {
ctx.Flash.Error("Account link id is not given")
} else {
if _, err := user_model.RemoveAccountLink(ctx, ctx.Doer, id); err != nil {
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
}
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
func loadSecurityData(ctx *context.Context) {
enrolled, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Data["TOTPEnrolled"] = enrolled
credentials, err := auth_model.GetWebAuthnCredentialsByUID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
ctx.Data["WebAuthnCredentials"] = credentials
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
accountLinks, err := db.Find[user_model.ExternalLoginUser](ctx, user_model.FindExternalUserOptions{
UserID: ctx.Doer.ID,
OrderBy: "login_source_id DESC",
})
if err != nil {
ctx.ServerError("ListAccountLinks", err)
return
}
// map the provider display name with the AuthSource
sources := make(map[*auth_model.Source]string)
for _, externalAccount := range accountLinks {
if authSource, err := auth_model.GetSourceByID(ctx, externalAccount.LoginSourceID); err == nil {
var providerDisplayName string
type DisplayNamed interface {
DisplayName() string
}
type Named interface {
Name() string
}
if displayNamed, ok := authSource.Cfg.(DisplayNamed); ok {
providerDisplayName = displayNamed.DisplayName()
} else if named, ok := authSource.Cfg.(Named); ok {
providerDisplayName = named.Name()
} else {
providerDisplayName = authSource.Name
}
sources[authSource] = providerDisplayName
}
}
ctx.Data["AccountLinks"] = sources
authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{
IsActive: optional.None[bool](),
LoginType: auth_model.OAuth2,
})
if err != nil {
ctx.ServerError("FindSources", err)
return
}
var orderedOAuth2Names []string
oauth2Providers := make(map[string]oauth2.Provider)
for _, source := range authSources {
provider, err := oauth2.CreateProviderFromSource(source)
if err != nil {
ctx.ServerError("CreateProviderFromSource", err)
return
}
oauth2Providers[source.Name] = provider
if source.IsActive {
orderedOAuth2Names = append(orderedOAuth2Names, source.Name)
}
}
sort.Strings(orderedOAuth2Names)
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
ctx.Data["OAuth2Providers"] = oauth2Providers
openid, err := user_model.GetUserOpenIDs(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openid
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
}

View File

@@ -0,0 +1,141 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"errors"
"net/http"
"strconv"
"time"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
)
// WebAuthnRegister initializes the webauthn registration procedure
func WebAuthnRegister(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm)
if form.Name == "" {
// Set name to the hexadecimal of the current time
form.Name = strconv.FormatInt(time.Now().UnixNano(), 16)
}
cred, err := auth.GetWebAuthnCredentialByName(ctx, ctx.Doer.ID, form.Name)
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
if cred != nil {
ctx.HTTPError(http.StatusConflict, "Name already taken")
return
}
_ = ctx.Session.Delete("webauthnRegistration")
if err := ctx.Session.Set("webauthnName", form.Name); err != nil {
ctx.ServerError("Unable to set session key for webauthnName", err)
return
}
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration(webAuthnUser, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
ResidentKey: protocol.ResidentKeyRequirementRequired,
}))
if err != nil {
ctx.ServerError("Unable to BeginRegistration", err)
return
}
// Save the session data as marshaled JSON
if err = ctx.Session.Set("webauthnRegistration", sessionData); err != nil {
ctx.ServerError("Unable to set session", err)
return
}
ctx.JSON(http.StatusOK, credentialOptions)
}
// WebauthnRegisterPost receives the response of the security key
func WebauthnRegisterPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
name, ok := ctx.Session.Get("webauthnName").(string)
if !ok || name == "" {
ctx.ServerError("Get webauthnName", errors.New("no webauthnName"))
return
}
// Load the session data
sessionData, ok := ctx.Session.Get("webauthnRegistration").(*webauthn.SessionData)
if !ok || sessionData == nil {
ctx.ServerError("Get registration", errors.New("no registration"))
return
}
defer func() {
_ = ctx.Session.Delete("webauthnRegistration")
}()
// Verify that the challenge succeeded
webAuthnUser := wa.NewWebAuthnUser(ctx, ctx.Doer)
cred, err := wa.WebAuthn.FinishRegistration(webAuthnUser, *sessionData, ctx.Req)
if err != nil {
if pErr, ok := err.(*protocol.Error); ok {
log.Error("Unable to finish registration due to error: %v\nDevInfo: %s", pErr, pErr.DevInfo)
}
ctx.ServerError("CreateCredential", err)
return
}
dbCred, err := auth.GetWebAuthnCredentialByName(ctx, ctx.Doer.ID, name)
if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
if dbCred != nil {
ctx.HTTPError(http.StatusConflict, "Name already taken")
return
}
// Create the credential
_, err = auth.CreateCredential(ctx, ctx.Doer.ID, name, cred)
if err != nil {
ctx.ServerError("CreateCredential", err)
return
}
_ = ctx.Session.Delete("webauthnName")
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
ctx.JSON(http.StatusCreated, cred)
}
// WebauthnDelete deletes an security key by id
func WebauthnDelete(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.WebauthnDeleteForm)
if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("GetWebAuthnCredentialByID", err)
return
}
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}