fix(security): per-mobile OTP cap + refresh-token rotation (M6, M7)
M6: send-code rate-limited only per IP, so a victim's number could be SMS-flooded from rotating IPs. Add a per-mobile bucket (same 5/hour policy) keyed by the validated mobile. M7: /oauth/token/refresh reused the presented refresh token verbatim (no rotation) and never re-checked the user. The rotation infra already existed (issueTokens mints a fresh refresh token) — the controller just discarded it. Now revoke the presented token (single-use), issue a fresh pair, and reject a suspended user (status != 1). Regressions: tests/Auth/SendCodeMobileRateLimitTest, tests/Auth/RefreshTokenRotationTest (both fail without the fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,13 @@ class AuthController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فرمت شماره موبایل نادرست است', 422, 'mobile');
|
||||
}
|
||||
|
||||
// Per-mobile cap (in addition to per-IP) so a victim's number can't be
|
||||
// SMS-flooded from rotating IPs.
|
||||
$mobileLimiter = $this->sendCodeLimiter->create('mobile:' . $mobile);
|
||||
if (!$mobileLimiter->consume(1)->isAccepted()) {
|
||||
return $this->error(ErrorCodes::ERR_RATE_LIMIT_001, ErrorCodes::message(ErrorCodes::ERR_RATE_LIMIT_001), 429);
|
||||
}
|
||||
|
||||
$uuid = $this->otpService->sendCode($mobile);
|
||||
|
||||
return new JsonResponse(['uuid' => $uuid, 'message' => 'کد تایید با موفقیت ارسال شد.']);
|
||||
@@ -470,14 +477,15 @@ class AuthController extends BaseController
|
||||
$result = $this->tokenService->refreshToken($refreshToken);
|
||||
$user = $this->userRepo->find($result['userId']);
|
||||
|
||||
if ($user === null) {
|
||||
if ($user === null || $user->getStatus() !== 1) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_001, ErrorCodes::message(ErrorCodes::ERR_AUTH_001), 401);
|
||||
}
|
||||
|
||||
$tokens = $this->tokenService->issueTokens($user);
|
||||
$tokens['refresh_token'] = $result['rawToken'];
|
||||
// Rotate: the presented refresh token is single-use. Revoke it and issue a
|
||||
// fresh access + refresh pair, so a stolen token can't be reused.
|
||||
$this->tokenService->revokeRefreshToken($refreshToken);
|
||||
|
||||
return new JsonResponse($tokens);
|
||||
return new JsonResponse($this->tokenService->issueTokens($user));
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
|
||||
Reference in New Issue
Block a user