mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
Two factor authentication support (#630)
* Initial commit for 2FA support Signed-off-by: Andrew <write@imaginarycode.com> * Add vendored files * Add missing depends * A few clean ups * Added improvements, proper encryption * Better encryption key * Simplify "key" generation * Make 2FA enrollment page more robust * Fix typo * Rename twofa/2FA to TwoFactor * UNIQUE INDEX -> UNIQUE
This commit is contained in:
@@ -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")
|
||||
|
Reference in New Issue
Block a user