summaryrefslogtreecommitdiffstats
path: root/routers/web/user/setting/security
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/web/user/setting/security
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--routers/web/user/setting/security/2fa.go263
-rw-r--r--routers/web/user/setting/security/openid.go126
-rw-r--r--routers/web/user/setting/security/security.go148
-rw-r--r--routers/web/user/setting/security/webauthn.go137
4 files changed, 674 insertions, 0 deletions
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
new file mode 100644
index 0000000..7c85c0e
--- /dev/null
+++ b/routers/web/user/setting/security/2fa.go
@@ -0,0 +1,263 @@
+// 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"
+ "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"
+ "code.gitea.io/gitea/services/mailer"
+
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
+)
+
+// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
+func RegenerateScratchTwoFactor(ctx *context.Context) {
+ 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) {
+ 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
+ }
+
+ if err := mailer.SendDisabledTOTP(ctx, ctx.Doer); err != nil {
+ ctx.ServerError("SendDisabledTOTP", 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) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ 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) {
+ form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsSecurity"] = true
+
+ 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
+ }
+
+ // 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 err := mailer.SendTOTPEnrolled(ctx, ctx.Doer); err != nil {
+ ctx.ServerError("SendTOTPEnrolled", err)
+ return
+ }
+
+ if err = auth.NewTwoFactor(ctx, t); err != 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", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go
new file mode 100644
index 0000000..8f788e1
--- /dev/null
+++ b/routers/web/user/setting/security/openid.go
@@ -0,0 +1,126 @@
+// 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) {
+ 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 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 err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil {
+ ctx.ServerError("ToggleUserOpenIDVisibility", err)
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+}
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
new file mode 100644
index 0000000..8d6859a
--- /dev/null
+++ b/routers/web/user/setting/security/security.go
@@ -0,0 +1,148 @@
+// 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/base"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsSecurity base.TplName = "user/settings/security/security"
+ tplSettingsTwofaEnroll base.TplName = "user/settings/security/twofa_enroll"
+)
+
+// Security render change user's password page and 2FA
+func Security(ctx *context.Context) {
+ 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) {
+ 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
+}
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
new file mode 100644
index 0000000..bfbc06c
--- /dev/null
+++ b/routers/web/user/setting/security/webauthn.go
@@ -0,0 +1,137 @@
+// 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"
+ wa "code.gitea.io/gitea/modules/auth/webauthn"
+ "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"
+ "code.gitea.io/gitea/services/mailer"
+
+ "github.com/go-webauthn/webauthn/protocol"
+ "github.com/go-webauthn/webauthn/webauthn"
+)
+
+// WebAuthnRegister initializes the webauthn registration procedure
+func WebAuthnRegister(ctx *context.Context) {
+ 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.Error(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
+ }
+
+ credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.Doer))
+ 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) {
+ 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
+ cred, err := wa.WebAuthn.FinishRegistration((*wa.User)(ctx.Doer), *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.Error(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.JSON(http.StatusCreated, cred)
+}
+
+// WebauthnDelete deletes an security key by id
+func WebauthnDelete(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.WebauthnDeleteForm)
+ cred, err := auth.GetWebAuthnCredentialByID(ctx, form.ID)
+ if err != nil || cred.UserID != ctx.Doer.ID {
+ if err != nil && !auth.IsErrWebAuthnCredentialNotExist(err) {
+ log.Error("GetWebAuthnCredentialByID: %v", err)
+ }
+
+ ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
+ return
+ }
+
+ if _, err := auth.DeleteCredential(ctx, form.ID, ctx.Doer.ID); err != nil {
+ ctx.ServerError("GetWebAuthnCredentialByID", err)
+ return
+ }
+
+ if err := mailer.SendRemovedSecurityKey(ctx, ctx.Doer, cred.Name); err != nil {
+ ctx.ServerError("SendRemovedSecurityKey", err)
+ return
+ }
+
+ ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
+}