diff options
Diffstat (limited to 'routers/user/setting.go')
-rw-r--r-- | routers/user/setting.go | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/routers/user/setting.go b/routers/user/setting.go index bba0f1e876..8fc2be0ce4 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -5,12 +5,19 @@ package user import ( + "bytes" "errors" "fmt" "io/ioutil" "strings" "github.com/Unknwon/com" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + + "encoding/base64" + "html/template" + "image/png" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" @@ -28,6 +35,8 @@ const ( tplSettingsSSHKeys base.TplName = "user/settings/sshkeys" tplSettingsSocial base.TplName = "user/settings/social" tplSettingsApplications base.TplName = "user/settings/applications" + tplSettingsTwofa base.TplName = "user/settings/twofa" + tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll" tplSettingsDelete base.TplName = "user/settings/delete" tplSecurity base.TplName = "user/security" ) @@ -437,6 +446,191 @@ func SettingsDeleteApplication(ctx *context.Context) { }) } +// SettingsTwoFactor renders the 2FA page. +func SettingsTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsTwofa"] = true + + enrolled := true + _, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + if models.IsErrTwoFactorNotEnrolled(err) { + enrolled = false + } else { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + } + + ctx.Data["TwofaEnrolled"] = enrolled + ctx.HTML(200, tplSettingsTwofa) +} + +// SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code. +func SettingsTwoFactorRegenerateScratch(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsTwofa"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + if err = t.GenerateScratchToken(); err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + if err = models.UpdateTwoFactor(t); err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken)) + ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor") +} + +// SettingsTwoFactorDisable deletes the user's 2FA settings. +func SettingsTwoFactorDisable(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsTwofa"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.twofa_disabled")) + ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor") +} + +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 otpKey == nil { + err = nil // clear the error, in case the URL was invalid + otpKey, err = totp.Generate(totp.GenerateOpts{ + Issuer: setting.AppName, + AccountName: ctx.User.Name, + }) + if err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return false + } + } + + ctx.Data["TwofaSecret"] = otpKey.Secret() + img, err := otpKey.Image(320, 240) + if err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return false + } + + var imgBytes bytes.Buffer + if err = png.Encode(&imgBytes, img); err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return false + } + + ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes())) + ctx.Session.Set("twofaSecret", otpKey.Secret()) + ctx.Session.Set("twofaUri", otpKey.String()) + return true +} + +// SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA. +func SettingsTwoFactorEnroll(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsTwofa"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if t != nil { + // already enrolled + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + if !twofaGenerateSecretAndQr(ctx) { + return + } + + ctx.HTML(200, tplSettingsTwofaEnroll) +} + +// SettingsTwoFactorEnrollPost handles enrolling the user into 2FA. +func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsTwofa"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if t != nil { + // already enrolled + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + if ctx.HasError() { + if !twofaGenerateSecretAndQr(ctx) { + return + } + ctx.HTML(200, tplSettingsTwofaEnroll) + return + } + + secret := ctx.Session.Get("twofaSecret").(string) + if !totp.Validate(form.Passcode, secret) { + if !twofaGenerateSecretAndQr(ctx) { + return + } + ctx.Flash.Error(ctx.Tr("settings.passcode_invalid")) + ctx.HTML(200, tplSettingsTwofaEnroll) + return + } + + t = &models.TwoFactor{ + UID: ctx.User.ID, + } + err = t.SetSecret(secret) + if err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + err = t.GenerateScratchToken() + if err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + if err = models.NewTwoFactor(t); err != nil { + ctx.Handle(500, "SettingsTwoFactor", err) + return + } + + ctx.Session.Delete("twofaSecret") + ctx.Session.Delete("twofaUri") + ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken)) + ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor") +} + // SettingsDelete render user suicide page and response for delete user himself func SettingsDelete(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") |