- 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.
180 lines
13 KiB
Markdown
180 lines
13 KiB
Markdown
# یکپارچهسازی 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`.
|