Skip to content

Security Considerations ​

Authentication & Session ​

  • Laravel Fortify provides the authentication backend (login, registration, password reset, 2FA); see config/fortify.php and routes/auth.php.
  • Laravel Sanctum guards SPA sessions & API tokens. Ensure SANCTUM_STATEFUL_DOMAINS is configured for cookie-based sessions in multi-domain setups.

Authorization ​

  • Spatie Permission defines roles & permissions; keep them granular.
  • Policies enforce model-level constraints; validate user context (isShop() / isClient()).
  • EnsureUserTypeShop / EnsureUserTypeClient gate whole route groups. Note they redirect (302) rather than returning 403.
  • Laravel Actions run authorize() before asController() — a failure there is a 403.
  • EnsureShopHasModule (module:<value>) is a third source of 403: the shop's plan does not include the feature, or its monthly quota is used up. It is a plan check, not a user check — see Plans & modules.
  • auth.features is only shared to members. Public {shop} routes (estimate approval, CASL consent) make the URL's shop current for any signed-in user, so HandleInertiaRequests::features() also requires isShop() and hasAccessToShop() before sharing a shop's modules. Being the current shop is not proof of membership.
  • isShop() alone is a tenancy check, not an authorization one: every employee of a shop passes it. Anything carrying payroll or rewriting the ledger needs a policy or a permission on top — see TimeEntryPolicy (stopping a colleague's timer re-books a billed work order) and GetSalesByTechnician (its cost column divides back out to a technician's hourly rate).
  • A nested route parameter is only as safe as its binding. Under ->scopeBindings() a child resolves through its parent, so /work-orders/{A}/{line-of-B} is a 404 (see Multi-tenancy). A route marked ->withoutScopedBindings() loses that for every parameter and must check the child/parent link itself — AssignStorageTicket and ChooseWorkOrderLineVariant do it with abort(404) in asController().
  • User::canManageClockFor() is the single rule behind both ShiftPunchPolicy and TimeEntryPolicy: your own clock is yours, a colleague's is a manager's, and only within the shop you both belong to.
  • Split read from write when two contexts share a model. ShopPolicy::view() passes for shop staff and for a client who is a customer of that shop, because the client portal reads a garage's details through it; ShopPolicy::update() adds $user->isShop(), so being able to see a shop never implies being able to change it. A single belongsToShop() check covering both verbs would have handed customers write access the day the portal shipped.
  • A "current" tenant is a stored preference, not a proof of membership. users.current_id is never revalidated on read, and App\Actions\RemoveUserClient detaches a user without clearing it, so Client::current() can still resolve a household the user has left. Every surface that resolves it without a route-bound model must guard it: abort_if($client === null, 403); then $this->authorize('view', $client);. Policies keyed on the client_user pivot (ClientPolicy, CarPolicy, WorkOrderPolicy, ShopPolicy) are what makes that guard meaningful.

Data Validation ​

  • Always validate incoming data via Form Requests.
  • Use DTOs (Spatie Data) internally to prevent unvalidated arrays propagating.

Sensitive Configuration ​

Store secrets ONLY in .env:

  • APP_KEY, DB_PASSWORD, SENTRY_DSN, RABBITMQ_PASSWORD, COMMS_QUERY_SECRET. Never commit .env.

Logging Hygiene ​

  • Avoid logging PII (personally identifiable information) or credentials.
  • Activity logs should capture actions (who, what, when) without sensitive payloads.

Error Reporting ​

  • Sentry captures exceptions; configure data scrubbing rules if necessary.
  • Avoid exposing stack traces in production responses (exception handling configured by Laravel).

WebSockets Security ​

  • Private & presence channels must implement authorization callbacks (in routes/channels.php).
  • Reuse permission checks; do not duplicate logic.
  • Model broadcasting is intentionally per-instance (models.{tableAlias}.{id}). Do not introduce a class-wide channel: it would authorize via viewAny(), which is not shop-scoped, and leak newly created records across tenants.

Inter-Service Security ​

  • This application exposes no webhook endpoints. Provider webhooks and their signature validation belong to comms-service; no route here is CSRF-exempt.
  • The broker is the trust boundary. RabbitMQ binds to localhost in production and carries the tenant id in every message — a consumer that ignores it would act across tenants.
  • COMMS_QUERY_SECRET authenticates the read-only HTTP queries to comms-service; it must match on both sides and never reach the client.

File & Media Handling ​

  • Use Media Library conversions to sanitize uploads.
  • Validate file types & sizes before attaching.
  • Store sensitive documents on private disks (e.g., S3 with restricted ACL).

Dependencies & Updates ​

  • Monitor Laravel, Fortify, Sanctum, Horizon, Reverb, and Sentry for security patches.
  • Run ./vendor/bin/sail composer audit for known vulnerabilities.
  • The Pulse dashboard carries cards for outdated (pulse-outdated) and vulnerable (hungthai1401/vulnerable) Composer dependencies — check them periodically.

SQL & Query Safety ​

  • Use Eloquent or parameterized queries; avoid raw concatenated SQL.
  • Leverage query scopes for consistent tenancy constraints.

Rate Limiting & Throttling ​

  • Apply throttle middleware to authentication and the ai. endpoints.
  • Consider per-user or per-role limits for resource-intensive operations (file uploads, PDF generation).

Input Sanitization ​

  • Escape dynamic output in Blade (if used) or rely on Vue binding safety.
  • Strip or encode HTML in user-submitted rich text if not required (XSS mitigation).

Password & Credential Management ​

  • Rely on Laravel's hashing configuration (bcrypt/argon2id) for user passwords.
  • Enforce strong password rules in Form Requests.

Transport Security ​

  • Serve over HTTPS; ensure correct APP_URL configured.
  • Use secure cookies in production (SESSION_SECURE_COOKIE=true).

Session & CSRF ​

  • CSRF protection enabled on web routes by default.
  • For APIs consumed by SPAs, ensure Sanctum token / session flow is correctly set.

Backups & Disaster Recovery (Recommendations) ​

  • Schedule DB dumps (encrypted) stored off-site.
  • Maintain media backup lifecycle (versioning & retention).

Monitoring & Alerts ​

  • Configure Sentry alerts for high error rates.
  • Consider Slack/Email notifications for Horizon job failures or queue backlog thresholds.

Least Privilege Principle ​

  • Limit roles to necessary permissions; avoid catch-all roles.
  • Restrict access to administrative dashboards (Horizon, Pulse) via gate checks.

Example Channel Authorization ​

php
Broadcast::channel('models.cars.{id}', function (User $user, int $id) {
    return $user->isShop() && Car::query()
        ->whereKey($id)
        ->where('shop_id', $user->currentShop->id)
        ->exists();
});

Every channel callback must re-assert the tenant, not just authentication.

Consult troubleshooting.md for resolving auth-related issues.