# نکات پیاده‌سازی — تسک ۰۱: راه‌اندازی پروژه ## تفاوت‌های اصلی Drupal vs Symfony ### CSRF Token در Drupal: endpoint مخصوص `GET /session/token` برای دریافت CSRF توکن وجود دارد. در Symfony: چون API کاملاً stateless است و JWT استفاده می‌شود، CSRF Token سنتی **نیاز نیست**. به جای آن، JWT در هر request ارسال می‌شود. → در TASK-02 endpoint ساختگی `/session/token` پیاده‌سازی می‌شود که یک مقدار تصادفی برمی‌گرداند تا کلاینت موجود بدون تغییر کار کند. ### UUID در Drupal: UUID داخلی Drupal مدیریت می‌شود. در Symfony: از `symfony/uid` (built-in) استفاده کن — نه `ramsey/uuid`. → تمام ID های عمومی در API باید UUID باشند، نه auto-increment. --- ## پیکربندی Security (security.yaml) ```yaml # config/packages/security.yaml security: password_hashers: App\Auth\Entity\User: algorithm: bcrypt cost: 12 providers: # تنها provider — username همیشه شماره موبایل است (برای همه نقش‌ها) app_user_provider: entity: class: App\Auth\Entity\User property: mobileNumber firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false health: pattern: ^/health$ security: false # Endpoints کاملاً عمومی (بدون هیچ بررسی) public: pattern: ^/(api/v1/user/send-code|api/v1/user/verify-code|api/v1/user/register|oauth/token|session/token)$ stateless: true security: false api: pattern: ^/(api|oauth)/ stateless: true # ۱) Password login برای doctor/clinic/secretary custom_authenticators: - App\Auth\Security\PasswordAuthenticator # ۲) JWT middleware — Authorization: Bearer را می‌خواند jwt: ~ access_control: # Public endpoints - { path: ^/health$, roles: PUBLIC_ACCESS } - { path: ^/api/v1/user/send-code, roles: PUBLIC_ACCESS } - { path: ^/api/v1/user/verify-code, roles: PUBLIC_ACCESS } - { path: ^/api/v1/user/register, roles: PUBLIC_ACCESS } - { path: ^/api/v1/user/login, roles: PUBLIC_ACCESS } - { path: ^/oauth/token$, roles: PUBLIC_ACCESS } - { path: ^/oauth/token/refresh$, roles: PUBLIC_ACCESS } - { path: ^/session/token, roles: PUBLIC_ACCESS } # Swagger — فقط در dev - { path: ^/api/doc, roles: PUBLIC_ACCESS, env: dev } # Admin-only - { path: ^/api/v1/user/\d+$, methods: [DELETE], roles: ROLE_ADMIN } # Authenticated - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } - { path: ^/oauth/userinfo, roles: IS_AUTHENTICATED_FULLY } - { path: ^/oauth/logout, roles: IS_AUTHENTICATED_FULLY } ``` --- ## ⚠ استاندارد JWT — Access Token در هدر، Refresh Token در Body این مهم‌ترین نکته امنیتی در مدیریت توکن‌هاست. دو توکن دو جای کاملاً متفاوت دارند: ### Access Token → فقط در `Authorization` header ``` Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... ``` **LexikJWTAuthenticationBundle** این هدر را به صورت خودکار در تمام endpoint های محافظت‌شده بررسی می‌کند. در Controller کد اضافه‌ای لازم نیست. **هرگز access_token را اینجا نفرست:** ``` ❌ GET /api/endpoint?token=eyJ... ← URL — در لاگ‌های سرور ذخیره می‌شود ❌ POST body: {"token": "eyJ..."} ← Body — افشا در لاگ‌های request ❌ Cookie: access_token=eyJ... ← Cookie — باید HttpOnly باشد و CSRF لازم دارد ``` ### Refresh Token → فقط در body برای یک endpoint خاص ```json POST /oauth/token/refresh Content-Type: application/json { "refresh_token": "a8f3b2c1d0e4..." } ``` Refresh Token هرگز در `Authorization` header نمی‌رود. این endpoint در `access_control` با `PUBLIC_ACCESS` است — تأیید هویت با خود Refresh Token انجام می‌شود. ### گردش کامل توکن‌ها ``` [ورود — یکبار] POST /api/v1/user/login (password) یا POST /oauth/token (OTP) ← Response: { "access_token": "eyJ..." ← TTL=1h — در RAM/memory ذخیره کن "refresh_token": "a8f3b2..." ← TTL=30d — در HttpOnly Cookie یا secure storage } [هر request محافظت‌شده] Authorization: Bearer eyJ... ← فقط access_token [وقتی access_token منقضی — 401 دریافت شد] POST /oauth/token/refresh { "refresh_token": "a8f3b2..." } ← Response: access_token جدید + refresh_token جدید (Rotation) [خروج] POST /oauth/logout Authorization: Bearer eyJ... { "refresh_token": "a8f3b2..." } ← هر دو توکن باطل می‌شوند ``` --- ## پیکربندی CORS — محدود، نه باز ```yaml # config/packages/nelmio_cors.yaml nelmio_cors: defaults: origin_regex: true allow_origin: - '%env(CORS_ALLOW_ORIGIN)%' allow_methods: ['GET', 'OPTIONS', 'POST', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'Content-Disposition'] expose_headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'] max_age: 3600 allow_credentials: false paths: '^/api/': # ⚠️ هرگز '*' نگذار — فقط دامنه‌های مشخص allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] '^/oauth/': allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] '^/health': allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] ``` ```dotenv # .env.local (production) CORS_ALLOW_ORIGIN=^https://(app\.clinicpro\.ir|admin\.clinicpro\.ir)$ # .env (development) CORS_ALLOW_ORIGIN=^https?://(localhost|.*\.ddev\.site)(:\d+)?$ ``` > **⚠ هشدار:** هرگز `allow_origin: ['*']` در production استفاده نکن. > این اجازه می‌دهد هر وب‌سایت مخرب درخواست‌های authenticated ارسال کند. --- ## Security Headers (EventSubscriber) ```php // src/Shared/EventSubscriber/SecurityHeadersSubscriber.php class SecurityHeadersSubscriber implements EventSubscriberInterface { public function onKernelResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) return; $response = $event->getResponse(); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-Frame-Options', 'DENY'); $response->headers->set('X-XSS-Protection', '1; mode=block'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); if ($event->getRequest()->isSecure()) { $response->headers->set( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains' ); } // برای API responses، Content-Security-Policy محدود if (str_starts_with($event->getRequest()->getPathInfo(), '/api')) { $response->headers->set('Content-Security-Policy', "default-src 'none'"); } } public static function getSubscribedEvents(): array { return [KernelEvents::RESPONSE => 'onKernelResponse']; } } ``` --- ## Swagger UI — فقط در محیط Dev ```yaml # config/packages/nelmio_api_doc.yaml when@prod: nelmio_api_doc: # در production کاملاً غیرفعال می‌شود # route ها به /api/doc باید در routing فقط برای dev تعریف شوند ``` ```yaml # config/routes/nelmio_api_doc.yaml when@dev: app.swagger_ui: path: /api/doc methods: GET defaults: _controller: nelmio_api_doc.controller.swagger_ui app.swagger_json: path: /api/doc.json methods: GET defaults: _controller: nelmio_api_doc.controller.swagger ``` > **⚠** با این روش، در production هیچ route ای برای `/api/doc` وجود ندارد → 404 --- ## Audit Log — جدول security_logs برای رویدادهای امنیتی حساس، یک جدول جداگانه وجود دارد: ```sql CREATE TABLE security_logs ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, event_type VARCHAR(50) NOT NULL, -- 'otp_failed', 'login_success', 'role_changed', 'payment_verified', ... user_id INT NULL, ip_address VARCHAR(45) NOT NULL, user_agent VARCHAR(255) NULL, details JSON NULL, -- اطلاعات اضافه (بدون data حساس!) created_at INT NOT NULL ); CREATE INDEX idx_sec_logs_event ON security_logs(event_type); CREATE INDEX idx_sec_logs_user ON security_logs(user_id); CREATE INDEX idx_sec_logs_created ON security_logs(created_at); ``` **رویدادهایی که باید log شوند:** ``` otp_sent — ارسال OTP (mobile ماسک‌شده: 0912***1713) otp_failed — کد اشتباه otp_expired — کد منقضی login_success — ورود موفق login_failed — ورود ناموفق logout — خروج token_refreshed — Refresh Token استفاده شد role_changed — تغییر نقش کاربر user_deleted — حذف کاربر payment_initiated — شروع پرداخت payment_verified — تأیید پرداخت payment_failed — پرداخت ناموفق file_uploaded — آپلود فایل ``` > **⚠ داده‌های حساس را log نکن:** شماره کامل موبایل، کد OTP، شماره کارت، JWT. --- ## نکات DDEV ```bash ddev exec php bin/console ... ddev composer ... ddev describe # مشاهده آدرس‌ها و پورت‌ها ddev ssh # ورود به container ``` URL پروژه: `https://clinic-pro.ddev.site` --- ## نکات امنیتی Production ``` ✅ هرگز config/jwt/private.pem را در git commit نکن (.gitignore) ✅ JWT_PASSPHRASE را قوی انتخاب کن (حداقل 32 کاراکتر تصادفی) ✅ APP_SECRET را با openssl rand -hex 32 تولید کن ✅ .env.local برای production (نه .env) ✅ CORS_ALLOW_ORIGIN فقط دامنه‌های مشخص (نه *) ✅ Swagger UI فقط در dev فعال است ✅ APP_DEBUG=false در production ```