Files
clinicpro/.claude/prompt/altcha-captcha-integration.md
T
hamed 10b0743d9a 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.
2026-07-10 10:31:59 +03:30

13 KiB
Raw Blame History

یکپارچه‌سازی 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:

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.yamlapp: 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

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 صدا زده شود:

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:

#[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 موجود، قبل از منطق):

$this->captcha->assertValid($request);

CaptchaGuard را به constructor آن controllerها inject کن. rate-limitهای موجود را حذف نکن — کپچا مکمل آن‌هاست نه جایگزین.

۶. تنظیمات

.env (مقادیر پیش‌فرض؛ کلید واقعی در .env.local):

###> 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:

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.