From ee6cff4104919b3bd484e4e59c30142a75a5f85c Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 15 Jul 2025 02:55:26 +0300 Subject: [PATCH] feat: add registration attempt rate limiting with Redis feat: prevent email enumeration by caching registration state fix: correct Redis key formatting for session termination cache refactor: improve registration flow with Redis cooldown checks chore: add Redis caching for registration in-progress state --- backend/internal/services/auth.go | 49 +++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go index cf8676e..5e5c21e 100644 --- a/backend/internal/services/auth.go +++ b/backend/internal/services/auth.go @@ -68,7 +68,7 @@ func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.C // FIXME: review possible problems due to a large pipeline request pipe := _redis.Pipeline() for _, guid := range guids { - if err := pipe.Set(ctx, fmt.Sprint("session:%s:is_terminated", guid), true, 0); err != nil { + if err := pipe.Set(ctx, fmt.Sprintf("session::%s::is_terminated", guid), true, 0); err != nil { panic("Failed to cache terminated session: " + err.Err().Error()) } } @@ -95,7 +95,25 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ defer helper.RollbackOnError(err) - // TODO: check occupation with redis + if isInProgress, err := a.redis.Get( + context.TODO(), + fmt.Sprintf("email::%s::registration_in_progress", + request.Email), + ).Bool(); err != nil { + if err != redis.Nil { + a.log.Error( + "Failed to look up cached registration_in_progress state of email as part of registration procedure", + zap.String("email", request.Email), + zap.Error(err)) + return false, errs.ErrServerError + } + isInProgress = false + } else if isInProgress { + a.log.Warn( + "Attempted to begin registration on email that is in progress of registration or on cooldown", + zap.String("email", request.Email)) + return false, errs.ErrTooManyRequests + } if occupationStatus, err = db.TXQueries.CheckUserRegistrationAvailability(db.CTX, database.CheckUserRegistrationAvailabilityParams{ Email: request.Email, @@ -118,7 +136,19 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ } else if occupationStatus.EmailBusy { // Falsely confirm in order to avoid disclosing registered email addresses - // TODO: save this email into redis + if err := a.redis.Set( + context.TODO(), + fmt.Sprintf("email::%s::registration_in_progress", request.Email), + true, + time.Duration(10 * time.Minute), // XXX: magic number + ).Err(); err != nil { + a.log.Error( + "Failed to falsely set cache registration_in_progress state for email as a measure to prevent email enumeration", + zap.String("email", request.Email), + zap.Error(err)) + return false, errs.ErrServerError + } + a.log.Warn( "Attempted registration for a taken email", zap.String("email", request.Email), @@ -211,6 +241,19 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ zap.String("code", generatedCode)) } + if err := a.redis.Set( + context.TODO(), + fmt.Sprintf("email::%s::registration_in_progress", request.Email), + true, + time.Duration(10 * time.Minute), // XXX: magic number + ).Err(); err != nil { + a.log.Error( + "Failed to cache registration_in_progress state for email", + zap.String("email", request.Email), + zap.Error(err)) + return false, errs.ErrServerError + } + if err = helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction",