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:
hamed
2026-06-28 20:25:22 +03:30
co-authored by Claude Opus 4.8
parent fe6383314e
commit 670cef24f4
5 changed files with 122 additions and 9 deletions
+9 -3
View File
@@ -38,6 +38,7 @@ Send OTP code to mobile number.
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | Invalid mobile format |
| `ERR_AUTH_004` | 429 | OTP rate limit exceeded |
| `ERR_RATE_LIMIT_001` | 429 | بیش از حد مجاز — هم per-IP (۵ در ۶۰ دقیقه) و هم **per-mobile** (۵ در ۶۰ دقیقه؛ ضد flood از IPهای چرخشی) |
---
@@ -219,18 +220,23 @@ Refresh expired JWT using refresh token.
}
```
> **چرخش (rotation):** هر refresh token **یک‌بارمصرف** است. با هر فراخوانی، توکن ارائه‌شده باطل می‌شود و یک جفت `access_token` + `refresh_token` تازه صادر می‌گردد. توکن قبلی دیگر کار نمی‌کند (`401`). کاربر **معلق** (`status != 1`) نمی‌تواند refresh کند.
### Response `200`
```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200..."
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "<توکن جدید — با قبلی فرق دارد>",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Invalid or expired refresh token |
| `ERR_AUTH_001` | 401 | Invalid/expired/already-rotated refresh token, or suspended user |
---