Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
audit_model "gitea.dev/models/audit"
|
|
"gitea.dev/models/auth"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/session"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/templates"
|
|
"gitea.dev/modules/web"
|
|
"gitea.dev/services/audit"
|
|
"gitea.dev/services/context"
|
|
"gitea.dev/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 performAutoLogin(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[*forms.TwoFactorAuthForm](ctx)
|
|
ctx.Data["Title"] = ctx.Tr("twofa")
|
|
|
|
// Ensure user is in a 2FA session.
|
|
id, hasSession := ctx.Session.Get("twofaUid").(int64)
|
|
if !hasSession {
|
|
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
|
|
return
|
|
}
|
|
|
|
twofa, err := auth.GetTwoFactorByUID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
// Validate the passcode and atomically consume it to prevent reuse/replay.
|
|
ok, err := twofa.ValidateAndConsumeTOTP(ctx, form.Passcode)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
if ok {
|
|
remember := ctx.Session.Get("twofaRemember").(bool) //nolint:forcetypeassert // must exist
|
|
u, err := user_model.GetUserByID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
if err = completePendingLinks(ctx, u); err != nil {
|
|
ctx.ServerError("completePendingLinks", err)
|
|
return
|
|
}
|
|
|
|
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
|
|
handleSignIn(ctx, u, remember)
|
|
return
|
|
}
|
|
|
|
if u, err := user_model.GetUserByID(ctx, id); err == nil {
|
|
audit.RecordAs(ctx, u, audit_model.UserAuthenticationFailTwoFactor, u)
|
|
}
|
|
|
|
ctx.RenderWithErrDeprecated(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 performAutoLogin(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[*forms.TwoFactorScratchAuthForm](ctx)
|
|
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
|
|
|
|
// Ensure user is in a 2FA session.
|
|
id, hasSession := ctx.Session.Get("twofaUid").(int64)
|
|
if !hasSession {
|
|
ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
|
|
return
|
|
}
|
|
|
|
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) //nolint:forcetypeassert // must exist
|
|
u, err := user_model.GetUserByID(ctx, id)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
|
|
if err = completePendingLinks(ctx, u); err != nil {
|
|
ctx.ServerError("completePendingLinks", err)
|
|
return
|
|
}
|
|
|
|
handleSignInFull(ctx, u, remember)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
|
|
return
|
|
}
|
|
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
|
|
}
|