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
This commit is contained in:
2025-07-15 02:55:26 +03:00
parent d8ea9f79c6
commit ee6cff4104

View File

@@ -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 // FIXME: review possible problems due to a large pipeline request
pipe := _redis.Pipeline() pipe := _redis.Pipeline()
for _, guid := range guids { 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()) panic("Failed to cache terminated session: " + err.Err().Error())
} }
} }
@@ -95,7 +95,25 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
defer helper.RollbackOnError(err) 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{ if occupationStatus, err = db.TXQueries.CheckUserRegistrationAvailability(db.CTX, database.CheckUserRegistrationAvailabilityParams{
Email: request.Email, Email: request.Email,
@@ -118,7 +136,19 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
} else if occupationStatus.EmailBusy { } else if occupationStatus.EmailBusy {
// Falsely confirm in order to avoid disclosing registered email addresses // 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( a.log.Warn(
"Attempted registration for a taken email", "Attempted registration for a taken email",
zap.String("email", request.Email), zap.String("email", request.Email),
@@ -211,6 +241,19 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
zap.String("code", generatedCode)) 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 { if err = helper.Commit(); err != nil {
a.log.Error( a.log.Error(
"Failed to commit transaction", "Failed to commit transaction",