feat: implement short-lived grant system for OTP verification and enhance rate limiting across authentication endpoints

This commit is contained in:
hamed
2026-06-20 12:51:10 +03:30
parent f9678026a8
commit e2636ce743
11 changed files with 396 additions and 107 deletions
+47 -35
View File
@@ -65,19 +65,22 @@ Verify OTP code. Returns whether this is a new or existing user.
{
"success": true,
"data": {
"message": "کد تأیید شد",
"is_new_user": false,
"uuid": "550e8400-e29b-41d4-a716-446655440000"
"message": "کد با موفقیت تایید شد.",
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039",
"is_new_user": false
}
}
```
> `grant` یک توکنِ **یک‌بارمصرف و کوتاه‌عمر (۱۲۰ ثانیه)** است که در مرحله‌ی بعد (`register` / `oauth/token` / `otp-login` / `reset-password`) مصرف می‌شود. خودِ OTP (`uuid`) پس از تأیید موفق حذف می‌شود. هر grant فقط یک‌بار قابل‌استفاده است؛ هر action که grant می‌خواهد یک grant جداگانه لازم دارد (یعنی برای new-user که هم `register` و هم login لازم است، باید دوبار verify انجام شود یا مستقیماً `oauth/token` صدا زده شود که خودش کاربر را می‌سازد).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_002` | 401 | Invalid OTP code |
| `ERR_AUTH_003` | 401 | OTP expired |
| `ERR_AUTH_002` | 400 | Invalid OTP code |
| `ERR_AUTH_003` | 400 | OTP expired |
| `ERR_VALIDATION_002` | 422 | Missing required field |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۱۰ تلاش در ۱۵ دقیقه (per-IP) |
---
@@ -90,14 +93,14 @@ Complete registration for new users (called only when `is_new_user: true`).
### Request Body
```json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039",
"real_name": "علی احمدی"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | ✅ | Verified UUID from `verify-code` |
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` |
| `real_name` | string | ❌ | User's full name |
### Response `201`
@@ -114,8 +117,8 @@ Complete registration for new users (called only when `is_new_user: true`).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | Missing uuid |
| `ERR_CONFLICT_001` | 409 | User already registered |
| `ERR_VALIDATION_002` | 422 | Missing grant |
| `ERR_AUTH_002` | 400 | grant نامعتبر یا منقضی/مصرف‌شده |
---
@@ -141,26 +144,28 @@ Login with mobile number and password (for users who set a password).
### Response `200`
```json
{
"success": true,
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200..."
}
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
> فقط برای کاربران staff (`ROLE_ADMIN/DOCTOR/CLINIC/SECRETARY`). Access token TTL: **۹۰۰ ثانیه (۱۵ دقیقه)**.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_005` | 401 | Wrong credentials |
| `ERR_AUTH_006` | 403 | Account suspended |
| `ERR_AUTH_004` | 429 | Too many attempts |
| `ERR_AUTH_006` | 403 | کاربر staff نیست |
| `429` | 429 | Too many attempts (۱۰ در دقیقه per-IP) |
---
## POST `/oauth/token`
Exchange verified UUID for JWT access token.
Exchange a one-time `grant` (from `verify-code`) for a JWT access token. اگر کاربری با آن موبایل وجود نداشته باشد، ساخته می‌شود.
**Permission:** `PUBLIC`
@@ -168,30 +173,36 @@ Exchange verified UUID for JWT access token.
```json
{
"grant_type": "mobile",
"uuid": "550e8400-e29b-41d4-a716-446655440000"
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `grant_type` | string | ✅ | Must be `"mobile"` |
| `uuid` | string | ✅ | UUID from verified OTP flow |
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` (عمر ۱۲۰ ثانیه) |
### Response `200`
```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200..."
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
> JWT payload: `{ username: mobile_number, roles: [...], iat, exp }`
> Access token TTL: **1 hour** | Refresh token TTL: **30 days** (stored in Redis)
> Access token TTL: **۹۰۰ ثانیه (۱۵ دقیقه)** | Refresh token TTL: **۳۰ روز** (در cache، هش‌شده و rotate-on-use)
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_002` | 400 | Invalid or expired UUID |
| `ERR_VALIDATION_001` | 400 | `grant_type` نامعتبر |
| `ERR_VALIDATION_002` | 422 | `grant` ارسال نشده |
| `ERR_AUTH_002` | 400 | grant نامعتبر، منقضی یا قبلاً مصرف‌شده |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۱۰ درخواست در ۵ دقیقه (per-IP) |
---
@@ -578,12 +589,12 @@ Submit a pre-registration request (doctor or clinic). Public endpoint — no aut
### Request Body
```json
{ "uuid": "550e8400-e29b-41d4-a716-446655440000" }
{ "grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | ✅ | UUID از `verify-code` (باید قبلاً verify شده باشد) |
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` |
### Response `200`
```json
@@ -591,7 +602,7 @@ Submit a pre-registration request (doctor or clinic). Public endpoint — no aut
"access_token": "eyJ...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
@@ -599,10 +610,10 @@ Submit a pre-registration request (doctor or clinic). Public endpoint — no aut
### Error Codes
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | uuid ارسال نشده |
| `ERR_AUTH_002` | 400 | uuid نامعتبر یا تأیید نشده |
| `ERR_AUTH_003` | 400 | OTP منقضی شده |
| `ERR_VALIDATION_002` | 422 | grant ارسال نشده |
| `ERR_AUTH_002` | 400 | grant نامعتبر، منقضی یا مصرف‌شده |
| `ERR_AUTH_005` | 401 | کاربری با این شماره یافت نشد |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۱۰ درخواست در ۵ دقیقه (per-IP) |
---
@@ -615,15 +626,15 @@ Submit a pre-registration request (doctor or clinic). Public endpoint — no aut
### Request Body
```json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"new_password": "newpass123"
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039",
"new_password": "newpass1234"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `uuid` | string | ✅ | UUID از `verify-code` (باید قبلاً verify شده باشد) |
| `new_password` | string | ✅ | حداقل ۶ کاراکتر |
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` |
| `new_password` | string | ✅ | حداقل ۸ کاراکتر |
### Response `200`
```json
@@ -636,6 +647,7 @@ Submit a pre-registration request (doctor or clinic). Public endpoint — no aut
### Error Codes
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | uuid یا new_password نادرست/ناقص |
| `ERR_AUTH_002` | 400 | uuid نامعتبر یا تأیید نشده |
| `ERR_VALIDATION_001` | 422 | grant ارسال نشده یا رمز کمتر از ۸ کاراکتر |
| `ERR_AUTH_002` | 400 | grant نامعتبر، منقضی یا مصرف‌شده |
| `ERR_NOT_FOUND_001` | 404 | کاربری با این شماره یافت نشد |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۵ درخواست در ۶۰ دقیقه (per-IP) |