mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-05 15:32:53 +00:00
Fix #880 Design: 1. A global setting `security.TWO_FACTOR_AUTH`. * To support org-level config, we need to introduce a better "owner setting" system first (in the future) 2. A user without 2FA can login and may explore, but can NOT read or write to any repositories via API/web. 3. Keep things as simple as possible. * This option only aggressively suggest users to enable their 2FA at the moment, it does NOT guarantee that users must have 2FA before all other operations, it should be good enough for real world use cases. * Some details and tests could be improved in the future since this change only adds a check and seems won't affect too much. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
166 lines
4.2 KiB
Go
166 lines
4.2 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"code.gitea.io/gitea/models/auth"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/session"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/templates"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/services/context"
|
|
"code.gitea.io/gitea/services/externalaccount"
|
|
"code.gitea.io/gitea/services/forms"
|
|
)
|
|
|
|
var (
|
|
tplTwofa templates.TplName = "user/auth/twofa"
|
|
tplTwofaScratch templates.TplName = "user/auth/twofa_scratch"
|
|
)
|
|
|
|
// TwoFactor shows the user a two-factor authentication page.
|
|
func TwoFactor(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("twofa")
|
|
|
|
if CheckAutoLogin(ctx) {
|
|
return
|
|
}
|
|
|
|
// Ensure user is in a 2FA session.
|
|
if ctx.Session.Get("twofaUid") == nil {
|
|
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplTwofa)
|
|
}
|
|
|
|
// TwoFactorPost validates a user's two-factor authentication token.
|
|
func TwoFactorPost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
|
|
ctx.Data["Title"] = ctx.Tr("twofa")
|
|
|
|
// Ensure user is in a 2FA session.
|
|
idSess := ctx.Session.Get("twofaUid")
|
|
if idSess == nil {
|
|
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
|
|
return
|
|
}
|
|
|
|
id := idSess.(int64)
|
|
twofa, err := auth.GetTwoFactorByUID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
// Validate the passcode with the stored TOTP secret.
|
|
ok, err := twofa.ValidateTOTP(form.Passcode)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
if ok && twofa.LastUsedPasscode != form.Passcode {
|
|
remember := ctx.Session.Get("twofaRemember").(bool)
|
|
u, err := user_model.GetUserByID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
if ctx.Session.Get("linkAccount") != nil {
|
|
err = externalaccount.LinkAccountFromStore(ctx, ctx.Session, u)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
twofa.LastUsedPasscode = form.Passcode
|
|
if err = auth.UpdateTwoFactor(ctx, twofa); err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
|
handleSignIn(ctx, u, remember)
|
|
return
|
|
}
|
|
|
|
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
|
|
}
|
|
|
|
// TwoFactorScratch shows the scratch code form for two-factor authentication.
|
|
func TwoFactorScratch(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
|
|
|
|
if CheckAutoLogin(ctx) {
|
|
return
|
|
}
|
|
|
|
// Ensure user is in a 2FA session.
|
|
if ctx.Session.Get("twofaUid") == nil {
|
|
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplTwofaScratch)
|
|
}
|
|
|
|
// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
|
|
func TwoFactorScratchPost(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.TwoFactorScratchAuthForm)
|
|
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
|
|
|
|
// Ensure user is in a 2FA session.
|
|
idSess := ctx.Session.Get("twofaUid")
|
|
if idSess == nil {
|
|
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
|
|
return
|
|
}
|
|
|
|
id := idSess.(int64)
|
|
twofa, err := auth.GetTwoFactorByUID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
// Validate the passcode with the stored TOTP secret.
|
|
if twofa.VerifyScratchToken(form.Token) {
|
|
// Invalidate the scratch token.
|
|
_, err = twofa.GenerateScratchToken()
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
if err = auth.UpdateTwoFactor(ctx, twofa); err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
remember := ctx.Session.Get("twofaRemember").(bool)
|
|
u, err := user_model.GetUserByID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
handleSignInFull(ctx, u, remember, false)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
|
return
|
|
}
|
|
|
|
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
|
|
}
|