Implement ALTCHA captcha service with challenge generation and solution verification
- Added AltchaService class for managing ALTCHA captcha challenges and solutions. - Created CaptchaController to handle API requests for generating challenges. - Introduced CaptchaGuard for validating captcha solutions on public endpoints. - Developed unit tests for AltchaService to ensure challenge creation and solution verification functionality. - Implemented integration tests for the Captcha API endpoint and captcha bypass behavior when disabled. - Added documentation for the Captcha API in the corresponding markdown file.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# یکپارچهسازی ALTCHA (کپچای Self-Hosted، Proof-of-Work) روی endpointهای عمومی
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (backend + پنل ادمین React).
|
||||
|
||||
> **Cross-repo:** سایت عمومی `nobat724_front` هم همان endpointهای عمومی را مصرف میکند (send-code / register / rate / comment / pre-registration). این پرامپت **قرارداد API کپچا** را در backend میسازد؛ سپس یک پرامپت جدا در `nobat724_front` برای نصب web-component و ضمیمهکردن `altcha` به بدنهی همان درخواستها لازم است. مسیر challenge و شکل payload که در این فایل تعریف میشود، همان است که سایت عمومی باید استفاده کند.
|
||||
|
||||
## زمینه
|
||||
|
||||
کاربر خواسته «به همهی فرمهای Symfony با ALTCHA کپچا اضافه شود» و صریحاً هر SaaS خارجی (Google/Cloudflare/hCaptcha/Friendly) را رد کرده — چون کاربران داخل ایراناند. انتخاب **ALTCHA** درست است: خودمیزبان، بدون تصویر، بدون تایپ، بدون CDN، Proof-of-Work سمت مرورگر.
|
||||
|
||||
**اما یک تصحیح معماری مهم:** این پروژه Symfony Forms / Twig برای فرمهای کاربری **ندارد**. `clinicpro` یک REST API با envelopeی JSON است و کلاینتها React SPA (پنل ادمین) و Next.js (`nobat724_front`) هستند. پس «Form Type / Form Extension / Form Theme / Twig block» موضوعیت ندارد. معادل درست در این معماری:
|
||||
|
||||
- یک endpoint برای صدور **challenge امضاشده**،
|
||||
- یک **verifier سرویس** که payload حلشدهی ALTCHA را سمت سرور اعتبارسنجی میکند،
|
||||
- اعمال verifier روی endpointهای POST عمومی که واقعاً قربانی اسپم/باتاند.
|
||||
|
||||
هیچکدام از فرمهای ادمین (پشت JWT + `ROLE_*`) نیازی به کپچا ندارند؛ کپچا فقط روی سطح **عمومی و بدون احراز هویت** معنی دارد.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
افزودن ALTCHA بهصورت یک ماژول مرکزی و قابلاستفادهمجدد، که با یک خط (`$this->altcha->assertValid($request)`) به هر endpoint عمومی اضافه شود؛ در `dev`/`test` غیرفعال و در `prod` فعال؛ با محافظت در برابر replay (استفادهی یکبارهی هر challenge).
|
||||
|
||||
## endpointهای عمومی که باید محافظت شوند
|
||||
|
||||
از `config/packages/security.yaml` → firewall `public_endpoints` (خط ۳۵). فقط POSTهای ارسالکنندهی داده/هزینهزا:
|
||||
|
||||
| Endpoint | Controller | چرا کپچا |
|
||||
|---|---|---|
|
||||
| `POST /api/v1/user/send-code` | `src/Auth/Controller/AuthController.php::sendCode` (خط ۱۳۶) | **بحرانی** — هر تماس یک پیامک واقعی + هزینه میفرستد |
|
||||
| `POST /api/v1/user/register` | `AuthController::register` (خط ۲۷۶) | ساخت اکانت انبوه |
|
||||
| `POST /api/v1/user/otp-login` | `AuthController::otpLogin` (خط ۳۷۲) | تلاش خودکار ورود |
|
||||
| `POST /api/v1/user/reset-password` | `AuthController::resetPassword` (خط ۳۹۶) | سوءاستفاده از بازیابی |
|
||||
| `POST /api/v1/pre-registration` | `src/Auth/Controller/PreRegistrationController.php` (خط ۴۰) | اسپم درخواست ثبتنام |
|
||||
| `POST /api/v1/rate` | `src/Rating/Controller/RatingController.php` (خط ۶۷) | امتیاز جعلی انبوه |
|
||||
| `POST /api/v1/comment` | `RatingController.php` (خط ۲۱۴) | اسپم نظر |
|
||||
|
||||
> `verify-code` خودش با rate-limit و uuid محافظت است و کپچا اضافه لازم ندارد (تجربهی کاربر را خراب میکند). اگر لازم شد، فقط `send-code` را کپچا بزن — بقیه اختیاری با فلگ.
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
الگوی هر endpoint عمومی امروز: rate-limit با `RateLimiterFactory` (Redis)، سپس `json_decode` بدنه، سپس `$this->error(...)`. نمونهی واقعی از `AuthController::sendCode`:
|
||||
|
||||
```php
|
||||
public function sendCode(Request $request): JsonResponse
|
||||
{
|
||||
$limiter = $this->sendCodeLimiter->create($request->getClientIp() ?? 'unknown');
|
||||
if (!$limiter->consume(1)->isAccepted()) {
|
||||
return $this->error(ErrorCodes::ERR_RATE_LIMIT_001, ErrorCodes::message(ErrorCodes::ERR_RATE_LIMIT_001), 429);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$mobile = trim($data['mobile'] ?? '');
|
||||
// ... اعتبارسنجی موبایل ...
|
||||
$uuid = $this->otpService->sendCode($mobile, $domain ?: null);
|
||||
return new JsonResponse(['uuid' => $uuid, 'message' => 'کد تایید با موفقیت ارسال شد.']);
|
||||
}
|
||||
```
|
||||
|
||||
زیرساخت موجود که باید استفاده شود:
|
||||
- **Redis** آماده است: `config/packages/cache.yaml` → `app: cache.adapter.redis`، `REDIS_URL=redis://redis:6379`. برای store یکبارهی challengeها از یک cache pool اختصاصی استفاده کن (نه ساخت اتصال دستی).
|
||||
- همهی controllerها `extends BaseController` و از `$this->error($code, $message, $status, $field?)` استفاده میکنند.
|
||||
- کدهای خطا در `src/Shared/Constant/ErrorCodes.php` (پیام فارسی).
|
||||
- Webpack Encore با entryهای `admin` و `home` (`webpack.config.js` خطوط ۱۱–۱۳). **AssetMapper در کار نیست** — داراییها با Encore بسته میشوند.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. نصب کتابخانهی رسمی PHP
|
||||
|
||||
```bash
|
||||
ddev exec composer require altcha-org/altcha
|
||||
```
|
||||
اگر نصب پکیج به هر دلیل ممکن نبود، معادل حداقلی طبق مستندات رسمی ALTCHA پیاده کن (HMAC-SHA256 روی `salt+number`، خروجی base64). ولی **اول پکیج رسمی را امتحان کن**.
|
||||
|
||||
### ۲. سرویس مرکزی — `src/Shared/Captcha/AltchaService.php` (namespace جدید `App\Shared\Captcha`)
|
||||
|
||||
مسئولیتها:
|
||||
- `createChallenge(): array` — تولید challenge امضاشده با HMAC-SHA256 و `%env(ALTCHA_HMAC_KEY)%`، `expires`، و `maxNumber = %env(int:ALTCHA_MAX_NUMBER)%`.
|
||||
- `verifySolution(string $payloadBase64): bool` — decode payload، بررسی امضا (`algorithm`, `challenge`, `salt`, `signature`)، بررسی انقضا از داخل `salt` (پارامتر `?expires=`)، سپس **one-time**: کلید `altcha:used:<challenge>` را در cache pool اتمیک `get`/`save` کن؛ اگر قبلاً بوده → `false` (ضدِ replay). TTL = زمان باقیمانده تا انقضا.
|
||||
- `enabled(): bool` — از `%env(bool:ALTCHA_ENABLED)%`.
|
||||
|
||||
تزریق: `CacheItemPoolInterface $altchaPool`, پارامترهای env. از `random_bytes` برای salt، `hash_hmac('sha256', ...)` برای امضا.
|
||||
|
||||
### ۳. Guard قابلاستفادهمجدد — `src/Shared/Captcha/CaptchaGuard.php`
|
||||
|
||||
یک متد کوتاه که در هر controller صدا زده شود:
|
||||
|
||||
```php
|
||||
public function assertValid(Request $request): void
|
||||
{
|
||||
if (!$this->altcha->enabled()) {
|
||||
return; // dev/test یا ALTCHA_ENABLED=false
|
||||
}
|
||||
$payload = (string) (json_decode($request->getContent(), true)['altcha'] ?? '');
|
||||
if ($payload === '' || !$this->altcha->verifySolution($payload)) {
|
||||
throw new AppException(ErrorCodes::ERR_CAPTCHA_001, null, 422);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`AppException` توسط `ExceptionSubscriber` به `$this->error()` تبدیل میشود — پس نیازی به try/catch در controller نیست. یک کد خطای جدید `ERR_CAPTCHA_001` با پیام فارسی («تأیید امنیتی ناموفق بود، صفحه را تازه کنید») به `ErrorCodes.php` اضافه کن.
|
||||
|
||||
### ۴. Endpoint صدور challenge — عمومی
|
||||
|
||||
در یک controller جدید `src/Shared/Captcha/CaptchaController.php`:
|
||||
|
||||
```php
|
||||
#[Route('/api/v1/altcha/challenge', methods: ['GET'])]
|
||||
public function challenge(): JsonResponse
|
||||
{
|
||||
return new JsonResponse($this->altcha->createChallenge());
|
||||
}
|
||||
```
|
||||
|
||||
این مسیر را به `public_endpoints` در `config/packages/security.yaml` (خط ۳۵ pattern) اضافه کن: `api/v1/altcha/challenge`.
|
||||
|
||||
### ۵. اعمال Guard روی endpointها
|
||||
|
||||
در ابتدای هر یک از ۷ متد جدول بالا (بعد از rate-limit موجود، قبل از منطق):
|
||||
|
||||
```php
|
||||
$this->captcha->assertValid($request);
|
||||
```
|
||||
|
||||
`CaptchaGuard` را به constructor آن controllerها inject کن. **rate-limitهای موجود را حذف نکن** — کپچا مکمل آنهاست نه جایگزین.
|
||||
|
||||
### ۶. تنظیمات
|
||||
|
||||
`.env` (مقادیر پیشفرض؛ کلید واقعی در `.env.local`):
|
||||
```dotenv
|
||||
###> altcha ###
|
||||
ALTCHA_ENABLED=false
|
||||
ALTCHA_HMAC_KEY=change-me-in-env-local
|
||||
ALTCHA_MAX_NUMBER=100000
|
||||
ALTCHA_EXPIRE_SECONDS=300
|
||||
###< altcha ###
|
||||
```
|
||||
`config/services.yaml`: bind پارامترها به `AltchaService`. یک cache pool اختصاصی در `config/packages/cache.yaml`:
|
||||
```yaml
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
altcha.pool:
|
||||
adapter: cache.adapter.redis
|
||||
default_lifetime: 600
|
||||
```
|
||||
غیرفعالسازی محیطی: در `config/services_dev.yaml` و `config/packages/test/` مقدار `ALTCHA_ENABLED=false` تضمین شود؛ در `prod` مقدار از `.env.local` سرور `true`. (بهجای اتکا به env، `enabled()` مستقیماً `ALTCHA_ENABLED` را میخواند تا رفتار صریح باشد.)
|
||||
|
||||
### ۷. Frontend پنل ادمین React (`assets/admin/`)
|
||||
|
||||
- نصب web-component محلی: `ddev exec yarn add altcha` (بدون CDN؛ Encore آن را bundle میکند).
|
||||
- یک کامپوننت `assets/admin/components/ui/Altcha.tsx` که `<altcha-widget>` را با `challengeurl="/api/v1/altcha/challenge"` رندر میکند و مقدار حلشده را از event `verified` میگیرد.
|
||||
- در فرمهای عمومی ادمین (صفحهی login عمومی اگر روی همین endpointها میرود) مقدار `altcha` را به بدنهی درخواست در `lib/api.ts` ضمیمه کن. اگر پنل ادمین از این endpointهای عمومی استفاده نمیکند، این بخش را حداقلی نگهدار و در README مسیر افزودن را مستند کن.
|
||||
|
||||
> بیشتر مصرفکنندهی این endpointها `nobat724_front` است؛ سیمکشی کامل widget آنجا در پرامپت همتای frontend انجام میشود.
|
||||
|
||||
### ۸. تست
|
||||
|
||||
- **Unit** `tests/Shared/Captcha/AltchaServiceTest.php`: امضای معتبر تأیید شود؛ امضای دستکاریشده رد؛ challenge منقضی رد؛ استفادهی دوم از همان challenge رد (replay).
|
||||
- **Functional** `tests/Shared/Captcha/CaptchaFlowTest.php`: `GET /api/v1/altcha/challenge` ساختار (`algorithm/challenge/salt/signature/maxnumber`) برگرداند؛ با `ALTCHA_ENABLED=false` (پیشفرض test) endpointهای عمومی بدون `altcha` هم ۲۰۰ بدهند؛ سپس با فعالسازی موقت سرویس (mock/override) نبودِ `altcha` → ۴۲۲ با `ERR_CAPTCHA_001`.
|
||||
- از `ApiTestCase` ارث ببر؛ الگوی موجود `tests/Admin/AdminLogsTest.php` را دنبال کن.
|
||||
|
||||
### ۹. مستندات
|
||||
|
||||
- `docs/api/captcha.md` جدید: مسیر challenge، شکل payload، کد خطای `ERR_CAPTCHA_001`، لیست endpointهای محافظتشده.
|
||||
- در `docs/api/auth.md`، `docs/api/rating.md`، و مستند pre-registration: به هر endpoint یک نکته اضافه کن که در `prod` هدر/فیلد `altcha` الزامی است.
|
||||
- `README` بخش ALTCHA: نصب، env، فعال/غیرفعال، افزودن به endpoint جدید (`$this->captcha->assertValid($request)`)، تغییر difficulty (`ALTCHA_MAX_NUMBER`)، Troubleshooting (کلید HMAC ناهماهنگ بین challenge و verify، ساعت سرور/انقضا، Redis در دسترس نبودن).
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **این پروژه Twig/FormType برای فرمهای کاربری ندارد** — پیادهسازی API-محور است (challenge endpoint + verifier)، نه Form Theme. اگر جایی صفحهی Twig عمومی با فرم واقعی بود (`templates/public/`)، همانجا web-component را مستقیم بگذار؛ ولی فرض پیشفرض API است.
|
||||
- کپچا فقط روی `public_endpoints`؛ **هرگز روی endpointهای پشت JWT/`ROLE_*`** (تجربهی ادمین را خراب و بیفایده است).
|
||||
- One-time بودن challenge **الزامی** است؛ بدون آن replay ممکن میشود. حتماً از Redis pool اتمیک استفاده کن.
|
||||
- Secret/HMAC key هرگز به client نرود؛ فقط challenge امضاشده و salt عمومیاند.
|
||||
- rate-limitهای موجود دستنخورده بمانند؛ کپچا لایهی مکمل است.
|
||||
- envelope پاسخ خطا باید همان `$this->error()` استاندارد BaseController بماند (`{ success:false, errors:[{code,message}] }`).
|
||||
- بعد از تغییر controllerها و افزودن endpoint، طبق قانون ثابت پروژه فایلهای `docs/api/*` را در همین session بهروز کن.
|
||||
- بعد از تغییر کد: `ddev exec php bin/console cache:clear` (route جدید) و `ddev exec php bin/phpunit` و `ddev exec yarn dev`.
|
||||
Reference in New Issue
Block a user