From 1daa6d452c89da89ecb971be0193834ec9362af4 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 17 Jun 2026 08:42:45 +0330 Subject: [PATCH] =?UTF-8?q?fix(admin):=20admin=20OTP=20login=20always=20fa?= =?UTF-8?q?iled=20=E2=80=94=20rate-limit=20key=20clobbered=20the=20OTP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The admin send-otp used the SAME Redis key ("otp:admin:{phone}") for both the OTP value and the per-hour attempts counter. After storing the code and SMSing it, the rate-limit StringIncrementAsync ran on that same key, turning the stored value into code+1 (e.g. SMS said 337835, Redis held 337836). verify-otp then compared the entered code to the incremented value, never matched, and returned INVALID_OTP → 400. Admin OTP login could never succeed. Give the attempts counter its own key ("otp:admin:attempts:{phone}"), exactly like the main API (otp:{phone} vs otp:attempts:{phone}). Password login was unaffected. Co-Authored-By: Claude Opus 4.8 --- src/Meezi.Admin.API/Services/AdminAuthService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Meezi.Admin.API/Services/AdminAuthService.cs b/src/Meezi.Admin.API/Services/AdminAuthService.cs index 430b91c..ec21b01 100644 --- a/src/Meezi.Admin.API/Services/AdminAuthService.cs +++ b/src/Meezi.Admin.API/Services/AdminAuthService.cs @@ -77,7 +77,10 @@ public class AdminAuthService : IAdminAuthService var redis = _redis.GetDatabase(); var maxAttempts = _configuration.GetValue("Auth:MaxOtpAttemptsPerHour", DefaultMaxOtpAttemptsPerHour); - var attemptsKey = $"otp:admin:{phone}"; + // MUST differ from the OTP value key ($"otp:admin:{phone}") — sharing one + // key made the rate-limit INCR overwrite the stored OTP (337835 → 337836), + // so every admin OTP verification failed. Mirror the main API's split keys. + var attemptsKey = $"otp:admin:attempts:{phone}"; if (maxAttempts > 0) { var attempts = await redis.StringGetAsync(attemptsKey);