Enforce 2FA code expiration timeout before session timeout

This commit is contained in:
Taras Kushnir
2026-01-02 20:37:42 +02:00
parent 8112359ef9
commit d1a23760b1
4 changed files with 25 additions and 4 deletions

View File

@@ -155,6 +155,7 @@ func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
_ = sess.Set(session.KeyUserEmail, user.Email)
_ = sess.Set(session.KeyUserName, user.Name)
_ = sess.Set(session.KeyTwoFactorCode, code)
_ = sess.Set(session.KeyTwoFactorCodeTimestamp, time.Now().UTC())
_ = sess.Set(session.KeyUserID, user.ID)
// this is needed in case we will be routed to another server that does not have our session in memory
// (previously we persisted ONLY logged in sessions, but if we're rerouted during login, it will break)

View File

@@ -299,6 +299,7 @@ func (s *Server) editEmail(w http.ResponseWriter, r *http.Request) (*ViewModel,
}
func (s *Server) putGeneralSettings(w http.ResponseWriter, r *http.Request) (*ViewModel, error) {
tnow := time.Now().UTC()
ctx := r.Context()
user, err := s.SessionUser(ctx, s.Session(w, r))
@@ -332,13 +333,18 @@ func (s *Server) putGeneralSettings(w http.ResponseWriter, r *http.Request) (*Vi
}
sentCode, hasSentCode := sess.Get(ctx, session.KeyTwoFactorCode).(int)
codeTimestamp, ok := sess.Get(ctx, session.KeyTwoFactorCodeTimestamp).(time.Time)
if !ok {
slog.ErrorContext(ctx, "Failed to get verification code timestamp")
}
formCode := r.FormValue(common.ParamVerificationCode)
// we "used" the code now
_ = sess.Delete(session.KeyTwoFactorCode)
_ = sess.Delete(session.KeyTwoFactorCodeTimestamp)
if enteredCode, err := strconv.Atoi(formCode); !hasSentCode || (err != nil) || (enteredCode != sentCode) {
slog.WarnContext(ctx, "Code verification failed", "actual", formCode, "expected", sentCode, common.ErrAttr(err))
if enteredCode, err := strconv.Atoi(formCode); !hasSentCode || (err != nil) || (enteredCode != sentCode) || (!codeTimestamp.IsZero() && tnow.After(codeTimestamp.Add(twoFactorCodeDuration))) {
slog.WarnContext(ctx, "Code verification failed", "actual", formCode, "expected", sentCode, "timestamp", codeTimestamp, common.ErrAttr(err))
renderCtx.TwoFactorError = "Code is not valid."
return &ViewModel{Model: renderCtx, View: settingsGeneralFormTemplate}, nil
}

View File

@@ -6,16 +6,22 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/session"
)
const (
twoFactorCodeDuration = 10 * time.Minute
)
var (
renderContextNothing = struct{}{}
)
func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
tnow := time.Now().UTC()
ctx := r.Context()
err := r.ParseForm()
@@ -51,6 +57,11 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
return
}
codeTimestamp, ok := sess.Get(ctx, session.KeyTwoFactorCodeTimestamp).(time.Time)
if !ok {
slog.ErrorContext(ctx, "Failed to get verification code timestamp")
}
data := &loginRenderContext{
CsrfRenderContext: CsrfRenderContext{
Token: s.XSRF.Token(email),
@@ -59,9 +70,9 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
}
formCode := strings.TrimSpace(r.FormValue(common.ParamVerificationCode))
if enteredCode, err := strconv.Atoi(formCode); (err != nil) || (enteredCode != sentCode) {
if enteredCode, err := strconv.Atoi(formCode); (err != nil) || (enteredCode != sentCode) || (!codeTimestamp.IsZero() && tnow.After(codeTimestamp.Add(twoFactorCodeDuration))) {
data.CodeError = "Code is not valid."
slog.WarnContext(ctx, "Code verification failed", "actual", formCode, "expected", sentCode, common.ErrAttr(err))
slog.WarnContext(ctx, "Code verification failed", "actual", formCode, "expected", sentCode, "timestamp", codeTimestamp, common.ErrAttr(err))
s.render(w, r, "login/twofactor-form.html", data)
return
}
@@ -84,6 +95,7 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
_ = sess.Set(session.KeyLoginStep, loginStepCompleted)
_ = sess.Delete(session.KeyTwoFactorCode)
_ = sess.Delete(session.KeyTwoFactorCodeTimestamp)
_ = sess.Delete(session.KeyUserEmail)
_ = sess.Set(session.KeyPersistent, true)
@@ -124,5 +136,6 @@ func (s *Server) resend2fa(w http.ResponseWriter, r *http.Request) {
}
_ = sess.Set(session.KeyTwoFactorCode, code)
_ = sess.Set(session.KeyTwoFactorCodeTimestamp, time.Now().UTC())
s.render(w, r, "login/resend.html", renderContextNothing)
}

View File

@@ -27,6 +27,7 @@ const (
KeyPersistent
KeyNotificationID
KeyReturnURL
KeyTwoFactorCodeTimestamp
// Add new fields _above_
SESSION_KEYS_COUNT
)